Upstream version 8.37.180.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / extensions / install_verifier.cc
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