1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/extensions/install_verifier.h" 6 7 #include <algorithm> 8 #include <string> 9 10 #include "base/bind.h" 11 #include "base/command_line.h" 12 #include "base/metrics/field_trial.h" 13 #include "base/metrics/histogram.h" 14 #include "base/prefs/pref_service.h" 15 #include "base/stl_util.h" 16 #include "chrome/browser/extensions/extension_service.h" 17 #include "chrome/browser/extensions/install_signer.h" 18 #include "chrome/common/chrome_switches.h" 19 #include "chrome/common/extensions/manifest_url_handler.h" 20 #include "chrome/common/pref_names.h" 21 #include "content/public/browser/browser_context.h" 22 #include "content/public/common/content_switches.h" 23 #include "extensions/browser/extension_prefs.h" 24 #include "extensions/browser/extension_registry.h" 25 #include "extensions/browser/extension_system.h" 26 #include "extensions/browser/pref_names.h" 27 #include "extensions/common/extension_set.h" 28 #include "extensions/common/manifest.h" 29 #include "extensions/common/one_shot_event.h" 30 #include "grit/generated_resources.h" 31 #include "ui/base/l10n/l10n_util.h" 32 33 namespace extensions { 34 35 namespace { 36 37 enum VerifyStatus { 38 NONE = 0, // Do not request install signatures, and do not enforce them. 39 BOOTSTRAP, // Request install signatures, but do not enforce them. 40 ENFORCE, // Request install signatures, and enforce them. 41 ENFORCE_STRICT, // Same as ENFORCE, but hard fail if we can't fetch 42 // signatures. 43 44 // This is used in histograms - do not remove or reorder entries above! Also 45 // the "MAX" item below should always be the last element. 46 VERIFY_STATUS_MAX 47 }; 48 49 #if defined(GOOGLE_CHROME_BUILD) 50 const char kExperimentName[] = "ExtensionInstallVerification"; 51 #endif // defined(GOOGLE_CHROME_BUILD) 52 53 VerifyStatus GetExperimentStatus() { 54 #if defined(GOOGLE_CHROME_BUILD) 55 const std::string group = base::FieldTrialList::FindFullName( 56 kExperimentName); 57 58 std::string forced_trials = CommandLine::ForCurrentProcess()-> 59 GetSwitchValueASCII(switches::kForceFieldTrials); 60 if (forced_trials.find(kExperimentName) != std::string::npos) { 61 // We don't want to allow turning off enforcement by forcing the field 62 // trial group to something other than enforcement. 63 return ENFORCE_STRICT; 64 } 65 66 VerifyStatus default_status = NONE; 67 68 if (group == "EnforceStrict") 69 return ENFORCE_STRICT; 70 else if (group == "Enforce") 71 return ENFORCE; 72 else if (group == "Bootstrap") 73 return BOOTSTRAP; 74 else if (group == "None" || group == "Control") 75 return NONE; 76 else 77 return default_status; 78 #endif // defined(GOOGLE_CHROME_BUILD) 79 80 return NONE; 81 } 82 83 VerifyStatus GetCommandLineStatus() { 84 const CommandLine* cmdline = CommandLine::ForCurrentProcess(); 85 if (!InstallSigner::GetForcedNotFromWebstore().empty()) 86 return ENFORCE; 87 88 if (cmdline->HasSwitch(switches::kExtensionsInstallVerification)) { 89 std::string value = cmdline->GetSwitchValueASCII( 90 switches::kExtensionsInstallVerification); 91 if (value == "bootstrap") 92 return BOOTSTRAP; 93 else if (value == "enforce_strict") 94 return ENFORCE_STRICT; 95 else 96 return ENFORCE; 97 } 98 99 return NONE; 100 } 101 102 VerifyStatus GetStatus() { 103 return std::max(GetExperimentStatus(), GetCommandLineStatus()); 104 } 105 106 bool ShouldFetchSignature() { 107 return GetStatus() >= BOOTSTRAP; 108 } 109 110 bool ShouldEnforce() { 111 return GetStatus() >= ENFORCE; 112 } 113 114 enum InitResult { 115 INIT_NO_PREF = 0, 116 INIT_UNPARSEABLE_PREF, 117 INIT_INVALID_SIGNATURE, 118 INIT_VALID_SIGNATURE, 119 120 // This is used in histograms - do not remove or reorder entries above! Also 121 // the "MAX" item below should always be the last element. 122 123 INIT_RESULT_MAX 124 }; 125 126 void LogInitResultHistogram(InitResult result) { 127 UMA_HISTOGRAM_ENUMERATION("ExtensionInstallVerifier.InitResult", 128 result, INIT_RESULT_MAX); 129 } 130 131 bool FromStore(const Extension& extension) { 132 if (extension.from_webstore() || ManifestURL::UpdatesFromGallery(&extension)) 133 return true; 134 135 // If an extension has no update url, our autoupdate code will ask the 136 // webstore about it (to aid in migrating to the webstore from self-hosting 137 // or sideloading based installs). So we want to do verification checks on 138 // such extensions too so that we don't accidentally disable old installs of 139 // extensions that did migrate to the webstore. 140 return (ManifestURL::GetUpdateURL(&extension).is_empty() && 141 Manifest::IsAutoUpdateableLocation(extension.location())); 142 } 143 144 bool CanUseExtensionApis(const Extension& extension) { 145 return extension.is_extension() || extension.is_legacy_packaged_app(); 146 } 147 148 enum VerifyAllSuccess { 149 VERIFY_ALL_BOOTSTRAP_SUCCESS = 0, 150 VERIFY_ALL_BOOTSTRAP_FAILURE, 151 VERIFY_ALL_NON_BOOTSTRAP_SUCCESS, 152 VERIFY_ALL_NON_BOOTSTRAP_FAILURE, 153 154 // Used in histograms. Do not remove/reorder any entries above, and the below 155 // MAX entry should always come last. 156 VERIFY_ALL_SUCCESS_MAX 157 }; 158 159 // Record the success or failure of verifying all extensions, and whether or 160 // not it was a bootstrapping. 161 void LogVerifyAllSuccessHistogram(bool bootstrap, bool success) { 162 VerifyAllSuccess result; 163 if (bootstrap && success) 164 result = VERIFY_ALL_BOOTSTRAP_SUCCESS; 165 else if (bootstrap && !success) 166 result = VERIFY_ALL_BOOTSTRAP_FAILURE; 167 else if (!bootstrap && success) 168 result = VERIFY_ALL_NON_BOOTSTRAP_SUCCESS; 169 else 170 result = VERIFY_ALL_NON_BOOTSTRAP_FAILURE; 171 172 // This used to be part of ExtensionService, but moved here. In order to keep 173 // our histograms accurate, the name is unchanged. 174 UMA_HISTOGRAM_ENUMERATION( 175 "ExtensionService.VerifyAllSuccess", result, VERIFY_ALL_SUCCESS_MAX); 176 } 177 178 // Record the success or failure of a single verification. 179 void LogAddVerifiedSuccess(bool success) { 180 // This used to be part of ExtensionService, but moved here. In order to keep 181 // our histograms accurate, the name is unchanged. 182 UMA_HISTOGRAM_BOOLEAN("ExtensionService.AddVerified", success); 183 } 184 185 } // namespace 186 187 InstallVerifier::InstallVerifier(ExtensionPrefs* prefs, 188 content::BrowserContext* context) 189 : prefs_(prefs), 190 context_(context), 191 bootstrap_check_complete_(false), 192 weak_factory_(this) { 193 } 194 195 InstallVerifier::~InstallVerifier() {} 196 197 // static 198 bool InstallVerifier::NeedsVerification(const Extension& extension) { 199 return FromStore(extension) && CanUseExtensionApis(extension); 200 } 201 202 void InstallVerifier::Init() { 203 UMA_HISTOGRAM_ENUMERATION("ExtensionInstallVerifier.ExperimentStatus", 204 GetExperimentStatus(), VERIFY_STATUS_MAX); 205 UMA_HISTOGRAM_ENUMERATION("ExtensionInstallVerifier.ActualStatus", 206 GetStatus(), VERIFY_STATUS_MAX); 207 208 const base::DictionaryValue* pref = prefs_->GetInstallSignature(); 209 if (pref) { 210 scoped_ptr<InstallSignature> signature_from_prefs = 211 InstallSignature::FromValue(*pref); 212 if (!signature_from_prefs.get()) { 213 LogInitResultHistogram(INIT_UNPARSEABLE_PREF); 214 } else if (!InstallSigner::VerifySignature(*signature_from_prefs.get())) { 215 LogInitResultHistogram(INIT_INVALID_SIGNATURE); 216 DVLOG(1) << "Init - ignoring invalid signature"; 217 } else { 218 signature_ = signature_from_prefs.Pass(); 219 LogInitResultHistogram(INIT_VALID_SIGNATURE); 220 UMA_HISTOGRAM_COUNTS_100("ExtensionInstallVerifier.InitSignatureCount", 221 signature_->ids.size()); 222 GarbageCollect(); 223 } 224 } else { 225 LogInitResultHistogram(INIT_NO_PREF); 226 } 227 228 ExtensionSystem::Get(context_)->ready().Post( 229 FROM_HERE, 230 base::Bind(&InstallVerifier::MaybeBootstrapSelf, 231 weak_factory_.GetWeakPtr())); 232 } 233 234 void InstallVerifier::VerifyAllExtensions() { 235 AddMany(GetExtensionsToVerify(), ADD_ALL); 236 } 237 238 base::Time InstallVerifier::SignatureTimestamp() { 239 if (signature_.get()) 240 return signature_->timestamp; 241 else 242 return base::Time(); 243 } 244 245 bool InstallVerifier::IsKnownId(const std::string& id) { 246 return signature_.get() && (ContainsKey(signature_->ids, id) || 247 ContainsKey(signature_->invalid_ids, id)); 248 } 249 250 void InstallVerifier::VerifyExtension(const std::string& extension_id) { 251 ExtensionIdSet ids; 252 ids.insert(extension_id); 253 AddMany(ids, ADD_SINGLE); 254 } 255 256 void InstallVerifier::AddMany(const ExtensionIdSet& ids, OperationType type) { 257 if (!ShouldFetchSignature()) { 258 OnVerificationComplete(true, type); // considered successful. 259 return; 260 } 261 262 if (signature_.get()) { 263 ExtensionIdSet not_allowed_yet = 264 base::STLSetDifference<ExtensionIdSet>(ids, signature_->ids); 265 if (not_allowed_yet.empty()) { 266 OnVerificationComplete(true, type); // considered successful. 267 return; 268 } 269 } 270 271 InstallVerifier::PendingOperation* operation = 272 new InstallVerifier::PendingOperation(type); 273 operation->ids.insert(ids.begin(), ids.end()); 274 275 operation_queue_.push(linked_ptr<PendingOperation>(operation)); 276 277 // If there are no ongoing pending requests, we need to kick one off. 278 if (operation_queue_.size() == 1) 279 BeginFetch(); 280 } 281 282 void InstallVerifier::AddProvisional(const ExtensionIdSet& ids) { 283 provisional_.insert(ids.begin(), ids.end()); 284 AddMany(ids, ADD_PROVISIONAL); 285 } 286 287 void InstallVerifier::Remove(const std::string& id) { 288 ExtensionIdSet ids; 289 ids.insert(id); 290 RemoveMany(ids); 291 } 292 293 void InstallVerifier::RemoveMany(const ExtensionIdSet& ids) { 294 if (!signature_.get() || !ShouldFetchSignature()) 295 return; 296 297 bool found_any = false; 298 for (ExtensionIdSet::const_iterator i = ids.begin(); i != ids.end(); ++i) { 299 if (ContainsKey(signature_->ids, *i) || 300 ContainsKey(signature_->invalid_ids, *i)) { 301 found_any = true; 302 break; 303 } 304 } 305 if (!found_any) 306 return; 307 308 InstallVerifier::PendingOperation* operation = 309 new InstallVerifier::PendingOperation(InstallVerifier::REMOVE); 310 operation->ids = ids; 311 312 operation_queue_.push(linked_ptr<PendingOperation>(operation)); 313 if (operation_queue_.size() == 1) 314 BeginFetch(); 315 } 316 317 std::string InstallVerifier::GetDebugPolicyProviderName() const { 318 return std::string("InstallVerifier"); 319 } 320 321 namespace { 322 323 enum MustRemainDisabledOutcome { 324 VERIFIED = 0, 325 NOT_EXTENSION, 326 UNPACKED, 327 ENTERPRISE_POLICY_ALLOWED, 328 FORCED_NOT_VERIFIED, 329 NOT_FROM_STORE, 330 NO_SIGNATURE, 331 NOT_VERIFIED_BUT_NOT_ENFORCING, 332 NOT_VERIFIED, 333 NOT_VERIFIED_BUT_INSTALL_TIME_NEWER_THAN_SIGNATURE, 334 NOT_VERIFIED_BUT_UNKNOWN_ID, 335 COMPONENT, 336 337 // This is used in histograms - do not remove or reorder entries above! Also 338 // the "MAX" item below should always be the last element. 339 MUST_REMAIN_DISABLED_OUTCOME_MAX 340 }; 341 342 void MustRemainDisabledHistogram(MustRemainDisabledOutcome outcome) { 343 UMA_HISTOGRAM_ENUMERATION("ExtensionInstallVerifier.MustRemainDisabled", 344 outcome, MUST_REMAIN_DISABLED_OUTCOME_MAX); 345 } 346 347 } // namespace 348 349 bool InstallVerifier::MustRemainDisabled(const Extension* extension, 350 Extension::DisableReason* reason, 351 base::string16* error) const { 352 CHECK(extension); 353 if (!CanUseExtensionApis(*extension)) { 354 MustRemainDisabledHistogram(NOT_EXTENSION); 355 return false; 356 } 357 if (Manifest::IsUnpackedLocation(extension->location())) { 358 MustRemainDisabledHistogram(UNPACKED); 359 return false; 360 } 361 if (extension->location() == Manifest::COMPONENT) { 362 MustRemainDisabledHistogram(COMPONENT); 363 return false; 364 } 365 if (AllowedByEnterprisePolicy(extension->id())) { 366 MustRemainDisabledHistogram(ENTERPRISE_POLICY_ALLOWED); 367 return false; 368 } 369 370 bool verified = true; 371 MustRemainDisabledOutcome outcome = VERIFIED; 372 if (ContainsKey(InstallSigner::GetForcedNotFromWebstore(), extension->id())) { 373 verified = false; 374 outcome = FORCED_NOT_VERIFIED; 375 } else if (!FromStore(*extension)) { 376 verified = false; 377 outcome = NOT_FROM_STORE; 378 } else if (signature_.get() == NULL && 379 (!bootstrap_check_complete_ || GetStatus() < ENFORCE_STRICT)) { 380 // If we don't have a signature yet, we'll temporarily consider every 381 // extension from the webstore verified to avoid false positives on existing 382 // profiles hitting this code for the first time. The InstallVerifier 383 // will bootstrap itself once the ExtensionsSystem is ready. 384 outcome = NO_SIGNATURE; 385 } else if (!IsVerified(extension->id())) { 386 if (signature_.get() && 387 !ContainsKey(signature_->invalid_ids, extension->id())) { 388 outcome = NOT_VERIFIED_BUT_UNKNOWN_ID; 389 } else { 390 verified = false; 391 outcome = NOT_VERIFIED; 392 } 393 } 394 if (!verified && !ShouldEnforce()) { 395 verified = true; 396 outcome = NOT_VERIFIED_BUT_NOT_ENFORCING; 397 } 398 MustRemainDisabledHistogram(outcome); 399 400 if (!verified) { 401 if (reason) 402 *reason = Extension::DISABLE_NOT_VERIFIED; 403 if (error) 404 *error = l10n_util::GetStringFUTF16( 405 IDS_EXTENSIONS_ADDED_WITHOUT_KNOWLEDGE, 406 l10n_util::GetStringUTF16(IDS_EXTENSION_WEB_STORE_TITLE)); 407 } 408 return !verified; 409 } 410 411 InstallVerifier::PendingOperation::PendingOperation(OperationType type) 412 : type(type) {} 413 414 InstallVerifier::PendingOperation::~PendingOperation() { 415 } 416 417 ExtensionIdSet InstallVerifier::GetExtensionsToVerify() const { 418 ExtensionIdSet result; 419 scoped_ptr<ExtensionSet> extensions = 420 ExtensionRegistry::Get(context_)->GenerateInstalledExtensionsSet(); 421 for (ExtensionSet::const_iterator iter = extensions->begin(); 422 iter != extensions->end(); 423 ++iter) { 424 if (NeedsVerification(**iter)) 425 result.insert((*iter)->id()); 426 } 427 return result; 428 } 429 430 void InstallVerifier::MaybeBootstrapSelf() { 431 bool needs_bootstrap = false; 432 433 ExtensionIdSet extension_ids = GetExtensionsToVerify(); 434 if (signature_.get() == NULL && ShouldFetchSignature()) { 435 needs_bootstrap = true; 436 } else { 437 for (ExtensionIdSet::const_iterator iter = extension_ids.begin(); 438 iter != extension_ids.end(); 439 ++iter) { 440 if (!IsKnownId(*iter)) { 441 needs_bootstrap = true; 442 break; 443 } 444 } 445 } 446 447 if (needs_bootstrap) 448 AddMany(extension_ids, ADD_ALL_BOOTSTRAP); 449 else 450 bootstrap_check_complete_ = true; 451 } 452 453 void InstallVerifier::OnVerificationComplete(bool success, OperationType type) { 454 switch (type) { 455 case ADD_SINGLE: 456 LogAddVerifiedSuccess(success); 457 break; 458 case ADD_ALL: 459 case ADD_ALL_BOOTSTRAP: 460 LogVerifyAllSuccessHistogram(type == ADD_ALL_BOOTSTRAP, success); 461 bootstrap_check_complete_ = true; 462 if (success) { 463 // Iterate through the extensions and, if any are newly-verified and 464 // should have the DISABLE_NOT_VERIFIED reason lifted, do so. 465 const ExtensionSet& disabled_extensions = 466 ExtensionRegistry::Get(context_)->disabled_extensions(); 467 for (ExtensionSet::const_iterator iter = disabled_extensions.begin(); 468 iter != disabled_extensions.end(); 469 ++iter) { 470 int disable_reasons = prefs_->GetDisableReasons((*iter)->id()); 471 if (disable_reasons & Extension::DISABLE_NOT_VERIFIED && 472 !MustRemainDisabled(*iter, NULL, NULL)) { 473 prefs_->RemoveDisableReason((*iter)->id(), 474 Extension::DISABLE_NOT_VERIFIED); 475 } 476 } 477 } 478 if (success || GetStatus() == ENFORCE_STRICT) { 479 ExtensionSystem::Get(context_) 480 ->extension_service() 481 ->CheckManagementPolicy(); 482 } 483 break; 484 // We don't need to check disable reasons or report UMA stats for 485 // provisional adds or removals. 486 case ADD_PROVISIONAL: 487 case REMOVE: 488 break; 489 } 490 } 491 492 void InstallVerifier::GarbageCollect() { 493 if (!ShouldFetchSignature()) { 494 return; 495 } 496 CHECK(signature_.get()); 497 ExtensionIdSet leftovers = signature_->ids; 498 leftovers.insert(signature_->invalid_ids.begin(), 499 signature_->invalid_ids.end()); 500 ExtensionIdList all_ids; 501 prefs_->GetExtensions(&all_ids); 502 for (ExtensionIdList::const_iterator i = all_ids.begin(); 503 i != all_ids.end(); ++i) { 504 ExtensionIdSet::iterator found = leftovers.find(*i); 505 if (found != leftovers.end()) 506 leftovers.erase(found); 507 } 508 if (!leftovers.empty()) { 509 RemoveMany(leftovers); 510 } 511 } 512 513 bool InstallVerifier::AllowedByEnterprisePolicy(const std::string& id) const { 514 PrefService* pref_service = prefs_->pref_service(); 515 if (pref_service->IsManagedPreference(pref_names::kInstallAllowList)) { 516 const base::ListValue* whitelist = 517 pref_service->GetList(pref_names::kInstallAllowList); 518 base::StringValue id_value(id); 519 if (whitelist && whitelist->Find(id_value) != whitelist->end()) 520 return true; 521 } 522 if (pref_service->IsManagedPreference(pref_names::kInstallForceList)) { 523 const base::DictionaryValue* forcelist = 524 pref_service->GetDictionary(pref_names::kInstallForceList); 525 if (forcelist && forcelist->HasKey(id)) 526 return true; 527 } 528 return false; 529 } 530 531 bool InstallVerifier::IsVerified(const std::string& id) const { 532 return ((signature_.get() && ContainsKey(signature_->ids, id)) || 533 ContainsKey(provisional_, id)); 534 } 535 536 void InstallVerifier::BeginFetch() { 537 DCHECK(ShouldFetchSignature()); 538 539 // TODO(asargent) - It would be possible to coalesce all operations in the 540 // queue into one fetch - we'd probably just need to change the queue to 541 // hold (set of ids, list of operation type) pairs. 542 CHECK(!operation_queue_.empty()); 543 const PendingOperation& operation = *operation_queue_.front(); 544 545 ExtensionIdSet ids_to_sign; 546 if (signature_.get()) { 547 ids_to_sign.insert(signature_->ids.begin(), signature_->ids.end()); 548 } 549 if (operation.type == InstallVerifier::REMOVE) { 550 for (ExtensionIdSet::const_iterator i = operation.ids.begin(); 551 i != operation.ids.end(); ++i) { 552 if (ContainsKey(ids_to_sign, *i)) 553 ids_to_sign.erase(*i); 554 } 555 } else { // All other operation types are some form of "ADD". 556 ids_to_sign.insert(operation.ids.begin(), operation.ids.end()); 557 } 558 559 signer_.reset(new InstallSigner(context_->GetRequestContext(), ids_to_sign)); 560 signer_->GetSignature(base::Bind(&InstallVerifier::SignatureCallback, 561 weak_factory_.GetWeakPtr())); 562 } 563 564 void InstallVerifier::SaveToPrefs() { 565 if (signature_.get()) 566 DCHECK(InstallSigner::VerifySignature(*signature_)); 567 568 if (!signature_.get() || signature_->ids.empty()) { 569 DVLOG(1) << "SaveToPrefs - saving NULL"; 570 prefs_->SetInstallSignature(NULL); 571 } else { 572 base::DictionaryValue pref; 573 signature_->ToValue(&pref); 574 if (VLOG_IS_ON(1)) { 575 DVLOG(1) << "SaveToPrefs - saving"; 576 577 DCHECK(InstallSigner::VerifySignature(*signature_.get())); 578 scoped_ptr<InstallSignature> rehydrated = 579 InstallSignature::FromValue(pref); 580 DCHECK(InstallSigner::VerifySignature(*rehydrated.get())); 581 } 582 prefs_->SetInstallSignature(&pref); 583 } 584 } 585 586 namespace { 587 588 enum CallbackResult { 589 CALLBACK_NO_SIGNATURE = 0, 590 CALLBACK_INVALID_SIGNATURE, 591 CALLBACK_VALID_SIGNATURE, 592 593 // This is used in histograms - do not remove or reorder entries above! Also 594 // the "MAX" item below should always be the last element. 595 596 CALLBACK_RESULT_MAX 597 }; 598 599 void GetSignatureResultHistogram(CallbackResult result) { 600 UMA_HISTOGRAM_ENUMERATION("ExtensionInstallVerifier.GetSignatureResult", 601 result, CALLBACK_RESULT_MAX); 602 } 603 604 } // namespace 605 606 void InstallVerifier::SignatureCallback( 607 scoped_ptr<InstallSignature> signature) { 608 609 linked_ptr<PendingOperation> operation = operation_queue_.front(); 610 operation_queue_.pop(); 611 612 bool success = false; 613 if (!signature.get()) { 614 GetSignatureResultHistogram(CALLBACK_NO_SIGNATURE); 615 } else if (!InstallSigner::VerifySignature(*signature)) { 616 GetSignatureResultHistogram(CALLBACK_INVALID_SIGNATURE); 617 } else { 618 GetSignatureResultHistogram(CALLBACK_VALID_SIGNATURE); 619 success = true; 620 } 621 622 if (!success) { 623 OnVerificationComplete(false, operation->type); 624 625 // TODO(asargent) - if this was something like a network error, we need to 626 // do retries with exponential back off. 627 } else { 628 signature_ = signature.Pass(); 629 SaveToPrefs(); 630 631 if (!provisional_.empty()) { 632 // Update |provisional_| to remove ids that were successfully signed. 633 provisional_ = base::STLSetDifference<ExtensionIdSet>( 634 provisional_, signature_->ids); 635 } 636 637 OnVerificationComplete(success, operation->type); 638 } 639 640 if (!operation_queue_.empty()) 641 BeginFetch(); 642 } 643 644 } // namespace extensions 645