1 // Copyright 2014 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.
5 #include "chrome/browser/extensions/external_install_ui.h"
10 #include "base/lazy_instance.h"
11 #include "base/memory/ref_counted.h"
12 #include "base/memory/scoped_ptr.h"
13 #include "base/message_loop/message_loop.h"
14 #include "base/metrics/histogram.h"
15 #include "base/scoped_observer.h"
16 #include "base/strings/utf_string_conversions.h"
17 #include "chrome/app/chrome_command_ids.h"
18 #include "chrome/browser/chrome_notification_types.h"
19 #include "chrome/browser/extensions/extension_install_prompt.h"
20 #include "chrome/browser/extensions/extension_install_ui.h"
21 #include "chrome/browser/extensions/extension_service.h"
22 #include "chrome/browser/extensions/extension_uninstall_dialog.h"
23 #include "chrome/browser/extensions/webstore_data_fetcher.h"
24 #include "chrome/browser/extensions/webstore_data_fetcher_delegate.h"
25 #include "chrome/browser/profiles/profile.h"
26 #include "chrome/browser/ui/browser.h"
27 #include "chrome/browser/ui/browser_finder.h"
28 #include "chrome/browser/ui/global_error/global_error.h"
29 #include "chrome/browser/ui/global_error/global_error_service.h"
30 #include "chrome/browser/ui/global_error/global_error_service_factory.h"
31 #include "chrome/common/extensions/manifest_url_handler.h"
32 #include "content/public/browser/notification_details.h"
33 #include "content/public/browser/notification_observer.h"
34 #include "content/public/browser/notification_registrar.h"
35 #include "content/public/browser/notification_source.h"
36 #include "extensions/browser/extension_registry.h"
37 #include "extensions/browser/extension_registry_observer.h"
38 #include "extensions/common/constants.h"
39 #include "grit/generated_resources.h"
40 #include "ui/base/l10n/l10n_util.h"
41 #include "ui/gfx/image/image.h"
42 #include "ui/gfx/image/image_skia_operations.h"
44 namespace extensions {
48 // Whether the external extension can use the streamlined bubble install flow.
49 bool UseBubbleInstall(const Extension* extension, bool is_new_profile) {
50 return ManifestURL::UpdatesFromGallery(extension) && !is_new_profile;
55 static const int kMenuCommandId = IDC_EXTERNAL_EXTENSION_ALERT;
57 class ExternalInstallGlobalError;
59 namespace extensions {
60 class ExtensionRegistry;
63 // This class is refcounted to stay alive while we try and pull webstore data.
64 class ExternalInstallDialogDelegate
65 : public ExtensionInstallPrompt::Delegate,
66 public WebstoreDataFetcherDelegate,
67 public content::NotificationObserver,
68 public base::RefCountedThreadSafe<ExternalInstallDialogDelegate> {
70 ExternalInstallDialogDelegate(Browser* browser,
71 ExtensionService* service,
72 const Extension* extension,
73 bool use_global_error);
75 Browser* browser() { return browser_; }
78 friend class base::RefCountedThreadSafe<ExternalInstallDialogDelegate>;
79 friend class ExternalInstallGlobalError;
81 virtual ~ExternalInstallDialogDelegate();
83 // ExtensionInstallPrompt::Delegate:
84 virtual void InstallUIProceed() OVERRIDE;
85 virtual void InstallUIAbort(bool user_initiated) OVERRIDE;
87 // WebstoreDataFetcherDelegate:
88 virtual void OnWebstoreRequestFailure() OVERRIDE;
89 virtual void OnWebstoreResponseParseSuccess(
90 scoped_ptr<base::DictionaryValue> webstore_data) OVERRIDE;
91 virtual void OnWebstoreResponseParseFailure(
92 const std::string& error) OVERRIDE;
94 // content::NotificationObserver:
95 virtual void Observe(int type,
96 const content::NotificationSource& source,
97 const content::NotificationDetails& details) OVERRIDE;
99 // Show the install dialog to the user.
100 void ShowInstallUI();
102 // The UI for showing the install dialog when enabling.
103 scoped_ptr<ExtensionInstallPrompt> install_ui_;
104 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt_;
107 base::WeakPtr<ExtensionService> service_weak_;
108 scoped_ptr<WebstoreDataFetcher> webstore_data_fetcher_;
109 content::NotificationRegistrar registrar_;
110 std::string extension_id_;
111 bool use_global_error_;
113 DISALLOW_COPY_AND_ASSIGN(ExternalInstallDialogDelegate);
116 // Only shows a menu item, no bubble. Clicking the menu item shows
117 // an external install dialog.
118 class ExternalInstallMenuAlert : public GlobalErrorWithStandardBubble,
119 public content::NotificationObserver,
120 public ExtensionRegistryObserver {
122 ExternalInstallMenuAlert(ExtensionService* service,
123 const Extension* extension);
124 virtual ~ExternalInstallMenuAlert();
126 // GlobalError implementation.
127 virtual Severity GetSeverity() OVERRIDE;
128 virtual bool HasMenuItem() OVERRIDE;
129 virtual int MenuItemCommandID() OVERRIDE;
130 virtual base::string16 MenuItemLabel() OVERRIDE;
131 virtual void ExecuteMenuItem(Browser* browser) OVERRIDE;
132 virtual bool HasBubbleView() OVERRIDE;
133 virtual base::string16 GetBubbleViewTitle() OVERRIDE;
134 virtual std::vector<base::string16> GetBubbleViewMessages() OVERRIDE;
135 virtual base::string16 GetBubbleViewAcceptButtonLabel() OVERRIDE;
136 virtual base::string16 GetBubbleViewCancelButtonLabel() OVERRIDE;
137 virtual void OnBubbleViewDidClose(Browser* browser) OVERRIDE;
138 virtual void BubbleViewAcceptButtonPressed(Browser* browser) OVERRIDE;
139 virtual void BubbleViewCancelButtonPressed(Browser* browser) OVERRIDE;
142 ExtensionService* service_;
143 const Extension* extension_;
146 // Delete this instance after cleaning jobs.
149 // content::NotificationObserver implementation.
150 virtual void Observe(int type,
151 const content::NotificationSource& source,
152 const content::NotificationDetails& details) OVERRIDE;
154 // ExtensionRegistryObserver implementation.
155 virtual void OnExtensionLoaded(content::BrowserContext* browser_context,
156 const Extension* extension) OVERRIDE;
158 content::NotificationRegistrar registrar_;
160 // Listen to extension load notifications.
161 ScopedObserver<ExtensionRegistry, ExtensionRegistryObserver>
162 extension_registry_observer_;
164 DISALLOW_COPY_AND_ASSIGN(ExternalInstallMenuAlert);
167 // Shows a menu item and a global error bubble, replacing the install dialog.
168 class ExternalInstallGlobalError : public ExternalInstallMenuAlert {
170 ExternalInstallGlobalError(
171 ExtensionService* service,
172 const Extension* extension,
173 ExternalInstallDialogDelegate* delegate,
174 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt);
175 virtual ~ExternalInstallGlobalError();
177 virtual void ExecuteMenuItem(Browser* browser) OVERRIDE;
178 virtual bool HasBubbleView() OVERRIDE;
179 virtual gfx::Image GetBubbleViewIcon() OVERRIDE;
180 virtual base::string16 GetBubbleViewTitle() OVERRIDE;
181 virtual std::vector<base::string16> GetBubbleViewMessages() OVERRIDE;
182 virtual base::string16 GetBubbleViewAcceptButtonLabel() OVERRIDE;
183 virtual base::string16 GetBubbleViewCancelButtonLabel() OVERRIDE;
184 virtual void OnBubbleViewDidClose(Browser* browser) OVERRIDE;
185 virtual void BubbleViewAcceptButtonPressed(Browser* browser) OVERRIDE;
186 virtual void BubbleViewCancelButtonPressed(Browser* browser) OVERRIDE;
189 // Ref-counted, but needs to be disposed of if we are dismissed without
190 // having been clicked (perhaps because the user enabled the extension
192 ExternalInstallDialogDelegate* delegate_;
193 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt_;
196 DISALLOW_COPY_AND_ASSIGN(ExternalInstallGlobalError);
199 static void CreateExternalInstallGlobalError(
200 base::WeakPtr<ExtensionService> service,
201 const std::string& extension_id,
202 const ExtensionInstallPrompt::ShowParams& show_params,
203 ExtensionInstallPrompt::Delegate* prompt_delegate,
204 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt) {
207 const Extension* extension = service->GetInstalledExtension(extension_id);
210 GlobalErrorService* error_service =
211 GlobalErrorServiceFactory::GetForProfile(service->profile());
212 if (error_service->GetGlobalErrorByMenuItemCommandID(kMenuCommandId))
215 ExternalInstallDialogDelegate* delegate =
216 static_cast<ExternalInstallDialogDelegate*>(prompt_delegate);
217 ExternalInstallGlobalError* error_bubble = new ExternalInstallGlobalError(
218 service.get(), extension, delegate, prompt);
219 error_service->AddGlobalError(error_bubble);
220 // Show bubble immediately if possible.
221 if (delegate->browser())
222 error_bubble->ShowBubbleView(delegate->browser());
225 static void ShowExternalInstallDialog(
226 ExtensionService* service,
228 const Extension* extension) {
229 // This object manages its own lifetime.
230 new ExternalInstallDialogDelegate(browser, service, extension, false);
233 // ExternalInstallDialogDelegate --------------------------------------------
235 ExternalInstallDialogDelegate::ExternalInstallDialogDelegate(
237 ExtensionService* service,
238 const Extension* extension,
239 bool use_global_error)
241 service_weak_(service->AsWeakPtr()),
242 extension_id_(extension->id()),
243 use_global_error_(use_global_error) {
244 AddRef(); // Balanced in Proceed or Abort.
246 prompt_ = new ExtensionInstallPrompt::Prompt(
247 ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT);
249 // If we don't have a browser, we can't go to the webstore to fetch data.
250 // This should only happen in tests.
256 // Make sure to be notified if the owning profile is destroyed.
258 chrome::NOTIFICATION_PROFILE_DESTROYED,
259 content::Source<Profile>(browser->profile()));
261 webstore_data_fetcher_.reset(new WebstoreDataFetcher(
263 browser->profile()->GetRequestContext(),
266 webstore_data_fetcher_->Start();
269 void ExternalInstallDialogDelegate::OnWebstoreRequestFailure() {
273 void ExternalInstallDialogDelegate::OnWebstoreResponseParseSuccess(
274 scoped_ptr<base::DictionaryValue> webstore_data) {
275 std::string localized_user_count;
276 double average_rating;
278 if (!webstore_data->GetString(kUsersKey, &localized_user_count) ||
279 !webstore_data->GetDouble(kAverageRatingKey, &average_rating) ||
280 !webstore_data->GetInteger(kRatingCountKey, &rating_count)) {
281 // If we don't get a valid webstore response, short circuit, and continue
282 // to show a prompt without webstore data.
287 bool show_user_count = true;
288 webstore_data->GetBoolean(kShowUserCountKey, &show_user_count);
290 prompt_->SetWebstoreData(localized_user_count,
298 void ExternalInstallDialogDelegate::OnWebstoreResponseParseFailure(
299 const std::string& error) {
303 void ExternalInstallDialogDelegate::Observe(
305 const content::NotificationSource& source,
306 const content::NotificationDetails& details) {
307 DCHECK_EQ(type, chrome::NOTIFICATION_PROFILE_DESTROYED);
308 // If the owning profile is destroyed, we need to abort so that we don't leak.
309 InstallUIAbort(false); // Not user initiated.
312 void ExternalInstallDialogDelegate::ShowInstallUI() {
313 const Extension* extension = NULL;
314 if (!service_weak_.get() ||
315 !(extension = service_weak_->GetInstalledExtension(extension_id_))) {
319 ExtensionInstallUI::CreateInstallPromptWithBrowser(browser_));
321 const ExtensionInstallPrompt::ShowDialogCallback callback =
323 base::Bind(&CreateExternalInstallGlobalError,
326 ExtensionInstallPrompt::GetDefaultShowDialogCallback();
328 install_ui_->ConfirmExternalInstall(this, extension, callback, prompt_);
331 ExternalInstallDialogDelegate::~ExternalInstallDialogDelegate() {
334 void ExternalInstallDialogDelegate::InstallUIProceed() {
335 const Extension* extension = NULL;
336 if (service_weak_.get() &&
337 (extension = service_weak_->GetInstalledExtension(extension_id_))) {
338 service_weak_->GrantPermissionsAndEnableExtension(extension);
343 void ExternalInstallDialogDelegate::InstallUIAbort(bool user_initiated) {
344 const Extension* extension = NULL;
346 // Uninstall the extension if the abort was user initiated (and not, e.g., the
347 // result of the window closing).
348 // Otherwise, the extension will remain installed, but unacknowledged, so it
349 // will be prompted again.
350 if (user_initiated &&
351 service_weak_.get() &&
352 (extension = service_weak_->GetInstalledExtension(extension_id_))) {
353 service_weak_->UninstallExtension(extension_id_, false, NULL);
358 // ExternalInstallMenuAlert -------------------------------------------------
360 ExternalInstallMenuAlert::ExternalInstallMenuAlert(ExtensionService* service,
361 const Extension* extension)
363 extension_(extension),
364 extension_registry_observer_(this) {
365 extension_registry_observer_.Add(ExtensionRegistry::Get(service->profile()));
366 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_REMOVED,
367 content::Source<Profile>(service->profile()));
370 ExternalInstallMenuAlert::~ExternalInstallMenuAlert() {
373 GlobalError::Severity ExternalInstallMenuAlert::GetSeverity() {
377 bool ExternalInstallMenuAlert::HasMenuItem() {
381 int ExternalInstallMenuAlert::MenuItemCommandID() {
382 return kMenuCommandId;
385 base::string16 ExternalInstallMenuAlert::MenuItemLabel() {
387 if (extension_->is_app())
388 id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_APP;
389 else if (extension_->is_theme())
390 id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_THEME;
392 id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_EXTENSION;
393 return l10n_util::GetStringFUTF16(id, base::UTF8ToUTF16(extension_->name()));
396 void ExternalInstallMenuAlert::ExecuteMenuItem(Browser* browser) {
397 ShowExternalInstallDialog(service_, browser, extension_);
400 bool ExternalInstallMenuAlert::HasBubbleView() {
403 base::string16 ExternalInstallMenuAlert::GetBubbleViewTitle() {
404 return base::string16();
407 std::vector<base::string16> ExternalInstallMenuAlert::GetBubbleViewMessages() {
408 return std::vector<base::string16>();
411 base::string16 ExternalInstallMenuAlert::GetBubbleViewAcceptButtonLabel() {
412 return base::string16();
415 base::string16 ExternalInstallMenuAlert::GetBubbleViewCancelButtonLabel() {
416 return base::string16();
419 void ExternalInstallMenuAlert::OnBubbleViewDidClose(Browser* browser) {
423 void ExternalInstallMenuAlert::BubbleViewAcceptButtonPressed(
428 void ExternalInstallMenuAlert::BubbleViewCancelButtonPressed(
433 void ExternalInstallMenuAlert::OnExtensionLoaded(
434 content::BrowserContext* browser_context,
435 const Extension* extension) {
436 if (extension == extension_)
440 void ExternalInstallMenuAlert::Observe(
442 const content::NotificationSource& source,
443 const content::NotificationDetails& details) {
444 // The error is invalidated if the extension has been loaded or removed.
445 DCHECK_EQ(type, chrome::NOTIFICATION_EXTENSION_REMOVED);
446 const Extension* extension = content::Details<const Extension>(details).ptr();
447 if (extension == extension_)
451 void ExternalInstallMenuAlert::Clean() {
452 GlobalErrorService* error_service =
453 GlobalErrorServiceFactory::GetForProfile(service_->profile());
454 error_service->RemoveGlobalError(this);
455 service_->AcknowledgeExternalExtension(extension_->id());
459 // ExternalInstallGlobalError -----------------------------------------------
461 ExternalInstallGlobalError::ExternalInstallGlobalError(
462 ExtensionService* service,
463 const Extension* extension,
464 ExternalInstallDialogDelegate* delegate,
465 scoped_refptr<ExtensionInstallPrompt::Prompt> prompt)
466 : ExternalInstallMenuAlert(service, extension),
471 ExternalInstallGlobalError::~ExternalInstallGlobalError() {
473 delegate_->Release();
476 void ExternalInstallGlobalError::ExecuteMenuItem(Browser* browser) {
477 ShowBubbleView(browser);
480 bool ExternalInstallGlobalError::HasBubbleView() {
484 gfx::Image ExternalInstallGlobalError::GetBubbleViewIcon() {
485 if (prompt_->icon().IsEmpty())
486 return GlobalErrorWithStandardBubble::GetBubbleViewIcon();
487 // Scale icon to a reasonable size.
488 return gfx::Image(gfx::ImageSkiaOperations::CreateResizedImage(
489 *prompt_->icon().ToImageSkia(),
490 skia::ImageOperations::RESIZE_BEST,
491 gfx::Size(extension_misc::EXTENSION_ICON_SMALL,
492 extension_misc::EXTENSION_ICON_SMALL)));
495 base::string16 ExternalInstallGlobalError::GetBubbleViewTitle() {
496 return prompt_->GetDialogTitle();
499 std::vector<base::string16>
500 ExternalInstallGlobalError::GetBubbleViewMessages() {
501 std::vector<base::string16> messages;
502 messages.push_back(prompt_->GetHeading());
503 if (prompt_->GetPermissionCount()) {
504 messages.push_back(prompt_->GetPermissionsHeading());
505 for (size_t i = 0; i < prompt_->GetPermissionCount(); ++i) {
506 messages.push_back(l10n_util::GetStringFUTF16(
507 IDS_EXTENSION_PERMISSION_LINE,
508 prompt_->GetPermission(i)));
511 // TODO(yoz): OAuth issue advice?
515 base::string16 ExternalInstallGlobalError::GetBubbleViewAcceptButtonLabel() {
516 return prompt_->GetAcceptButtonLabel();
519 base::string16 ExternalInstallGlobalError::GetBubbleViewCancelButtonLabel() {
520 return prompt_->GetAbortButtonLabel();
523 void ExternalInstallGlobalError::OnBubbleViewDidClose(Browser* browser) {
526 void ExternalInstallGlobalError::BubbleViewAcceptButtonPressed(
528 ExternalInstallDialogDelegate* delegate = delegate_;
530 delegate->InstallUIProceed();
533 void ExternalInstallGlobalError::BubbleViewCancelButtonPressed(
535 ExternalInstallDialogDelegate* delegate = delegate_;
537 delegate->InstallUIAbort(true);
540 // Public interface ---------------------------------------------------------
542 void AddExternalInstallError(ExtensionService* service,
543 const Extension* extension,
544 bool is_new_profile) {
545 GlobalErrorService* error_service =
546 GlobalErrorServiceFactory::GetForProfile(service->profile());
547 if (error_service->GetGlobalErrorByMenuItemCommandID(kMenuCommandId))
550 if (UseBubbleInstall(extension, is_new_profile)) {
551 Browser* browser = NULL;
552 #if !defined(OS_ANDROID)
553 browser = chrome::FindTabbedBrowser(service->profile(),
555 chrome::GetActiveDesktop());
557 new ExternalInstallDialogDelegate(browser, service, extension, true);
559 error_service->AddGlobalError(
560 new ExternalInstallMenuAlert(service, extension));
564 void RemoveExternalInstallError(ExtensionService* service) {
565 GlobalErrorService* error_service =
566 GlobalErrorServiceFactory::GetForProfile(service->profile());
567 GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID(
570 error_service->RemoveGlobalError(error);
575 bool HasExternalInstallError(ExtensionService* service) {
576 GlobalErrorService* error_service =
577 GlobalErrorServiceFactory::GetForProfile(service->profile());
578 GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID(
583 bool HasExternalInstallBubble(ExtensionService* service) {
584 GlobalErrorService* error_service =
585 GlobalErrorServiceFactory::GetForProfile(service->profile());
586 GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID(
588 return error && error->HasBubbleView();
591 } // namespace extensions