1 // Copyright (c) 2012 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 "grit/generated_resources.h"
39 #include "ui/base/l10n/l10n_util.h"
40 #include "ui/gfx/image/image.h"
41 #include "ui/gfx/image/image_skia_operations.h"
43 namespace extensions
{
47 // Whether the external extension can use the streamlined bubble install flow.
48 bool UseBubbleInstall(const Extension
* extension
, bool is_new_profile
) {
49 return ManifestURL::UpdatesFromGallery(extension
) && !is_new_profile
;
54 static const int kMenuCommandId
= IDC_EXTERNAL_EXTENSION_ALERT
;
56 class ExternalInstallGlobalError
;
58 namespace extensions
{
59 class ExtensionRegistry
;
62 // This class is refcounted to stay alive while we try and pull webstore data.
63 class ExternalInstallDialogDelegate
64 : public ExtensionInstallPrompt::Delegate
,
65 public WebstoreDataFetcherDelegate
,
66 public content::NotificationObserver
,
67 public base::RefCountedThreadSafe
<ExternalInstallDialogDelegate
> {
69 ExternalInstallDialogDelegate(Browser
* browser
,
70 ExtensionService
* service
,
71 const Extension
* extension
,
72 bool use_global_error
);
74 Browser
* browser() { return browser_
; }
77 friend class base::RefCountedThreadSafe
<ExternalInstallDialogDelegate
>;
78 friend class ExternalInstallGlobalError
;
80 virtual ~ExternalInstallDialogDelegate();
82 // ExtensionInstallPrompt::Delegate:
83 virtual void InstallUIProceed() OVERRIDE
;
84 virtual void InstallUIAbort(bool user_initiated
) OVERRIDE
;
86 // WebstoreDataFetcherDelegate:
87 virtual void OnWebstoreRequestFailure() OVERRIDE
;
88 virtual void OnWebstoreResponseParseSuccess(
89 scoped_ptr
<base::DictionaryValue
> webstore_data
) OVERRIDE
;
90 virtual void OnWebstoreResponseParseFailure(
91 const std::string
& error
) OVERRIDE
;
93 // content::NotificationObserver:
94 virtual void Observe(int type
,
95 const content::NotificationSource
& source
,
96 const content::NotificationDetails
& details
) OVERRIDE
;
98 // Show the install dialog to the user.
101 // The UI for showing the install dialog when enabling.
102 scoped_ptr
<ExtensionInstallPrompt
> install_ui_
;
103 scoped_ptr
<ExtensionInstallPrompt::Prompt
> prompt_
;
106 base::WeakPtr
<ExtensionService
> service_weak_
;
107 scoped_ptr
<WebstoreDataFetcher
> webstore_data_fetcher_
;
108 content::NotificationRegistrar registrar_
;
109 std::string extension_id_
;
110 bool use_global_error_
;
112 DISALLOW_COPY_AND_ASSIGN(ExternalInstallDialogDelegate
);
115 // Only shows a menu item, no bubble. Clicking the menu item shows
116 // an external install dialog.
117 class ExternalInstallMenuAlert
: public GlobalErrorWithStandardBubble
,
118 public content::NotificationObserver
,
119 public ExtensionRegistryObserver
{
121 ExternalInstallMenuAlert(ExtensionService
* service
,
122 const Extension
* extension
);
123 virtual ~ExternalInstallMenuAlert();
125 // GlobalError implementation.
126 virtual Severity
GetSeverity() OVERRIDE
;
127 virtual bool HasMenuItem() OVERRIDE
;
128 virtual int MenuItemCommandID() OVERRIDE
;
129 virtual base::string16
MenuItemLabel() OVERRIDE
;
130 virtual void ExecuteMenuItem(Browser
* browser
) OVERRIDE
;
131 virtual bool HasBubbleView() OVERRIDE
;
132 virtual base::string16
GetBubbleViewTitle() OVERRIDE
;
133 virtual std::vector
<base::string16
> GetBubbleViewMessages() OVERRIDE
;
134 virtual base::string16
GetBubbleViewAcceptButtonLabel() OVERRIDE
;
135 virtual base::string16
GetBubbleViewCancelButtonLabel() OVERRIDE
;
136 virtual void OnBubbleViewDidClose(Browser
* browser
) OVERRIDE
;
137 virtual void BubbleViewAcceptButtonPressed(Browser
* browser
) OVERRIDE
;
138 virtual void BubbleViewCancelButtonPressed(Browser
* browser
) OVERRIDE
;
141 ExtensionService
* service_
;
142 const Extension
* extension_
;
145 // Delete this instance after cleaning jobs.
148 // content::NotificationObserver implementation.
149 virtual void Observe(int type
,
150 const content::NotificationSource
& source
,
151 const content::NotificationDetails
& details
) OVERRIDE
;
153 // ExtensionRegistryObserver implementation.
154 virtual void OnExtensionLoaded(content::BrowserContext
* browser_context
,
155 const Extension
* extension
) OVERRIDE
;
157 content::NotificationRegistrar registrar_
;
159 // Listen to extension load notifications.
160 ScopedObserver
<ExtensionRegistry
, ExtensionRegistryObserver
>
161 extension_registry_observer_
;
163 DISALLOW_COPY_AND_ASSIGN(ExternalInstallMenuAlert
);
166 // Shows a menu item and a global error bubble, replacing the install dialog.
167 class ExternalInstallGlobalError
: public ExternalInstallMenuAlert
{
169 ExternalInstallGlobalError(ExtensionService
* service
,
170 const Extension
* extension
,
171 ExternalInstallDialogDelegate
* delegate
,
172 const ExtensionInstallPrompt::Prompt
& prompt
);
173 virtual ~ExternalInstallGlobalError();
175 virtual void ExecuteMenuItem(Browser
* browser
) OVERRIDE
;
176 virtual bool HasBubbleView() OVERRIDE
;
177 virtual gfx::Image
GetBubbleViewIcon() OVERRIDE
;
178 virtual base::string16
GetBubbleViewTitle() OVERRIDE
;
179 virtual std::vector
<base::string16
> GetBubbleViewMessages() OVERRIDE
;
180 virtual base::string16
GetBubbleViewAcceptButtonLabel() OVERRIDE
;
181 virtual base::string16
GetBubbleViewCancelButtonLabel() OVERRIDE
;
182 virtual void OnBubbleViewDidClose(Browser
* browser
) OVERRIDE
;
183 virtual void BubbleViewAcceptButtonPressed(Browser
* browser
) OVERRIDE
;
184 virtual void BubbleViewCancelButtonPressed(Browser
* browser
) OVERRIDE
;
187 // Ref-counted, but needs to be disposed of if we are dismissed without
188 // having been clicked (perhaps because the user enabled the extension
190 ExternalInstallDialogDelegate
* delegate_
;
191 const ExtensionInstallPrompt::Prompt
* prompt_
;
194 DISALLOW_COPY_AND_ASSIGN(ExternalInstallGlobalError
);
197 static void CreateExternalInstallGlobalError(
198 base::WeakPtr
<ExtensionService
> service
,
199 const std::string
& extension_id
,
200 const ExtensionInstallPrompt::ShowParams
& show_params
,
201 ExtensionInstallPrompt::Delegate
* prompt_delegate
,
202 const ExtensionInstallPrompt::Prompt
& prompt
) {
205 const Extension
* extension
= service
->GetInstalledExtension(extension_id
);
208 GlobalErrorService
* error_service
=
209 GlobalErrorServiceFactory::GetForProfile(service
->profile());
210 if (error_service
->GetGlobalErrorByMenuItemCommandID(kMenuCommandId
))
213 ExternalInstallDialogDelegate
* delegate
=
214 static_cast<ExternalInstallDialogDelegate
*>(prompt_delegate
);
215 ExternalInstallGlobalError
* error_bubble
= new ExternalInstallGlobalError(
216 service
.get(), extension
, delegate
, prompt
);
217 error_service
->AddGlobalError(error_bubble
);
218 // Show bubble immediately if possible.
219 if (delegate
->browser())
220 error_bubble
->ShowBubbleView(delegate
->browser());
223 static void ShowExternalInstallDialog(
224 ExtensionService
* service
,
226 const Extension
* extension
) {
227 // This object manages its own lifetime.
228 new ExternalInstallDialogDelegate(browser
, service
, extension
, false);
231 // ExternalInstallDialogDelegate --------------------------------------------
233 ExternalInstallDialogDelegate::ExternalInstallDialogDelegate(
235 ExtensionService
* service
,
236 const Extension
* extension
,
237 bool use_global_error
)
239 service_weak_(service
->AsWeakPtr()),
240 extension_id_(extension
->id()),
241 use_global_error_(use_global_error
) {
242 AddRef(); // Balanced in Proceed or Abort.
244 prompt_
.reset(new ExtensionInstallPrompt::Prompt(
245 ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT
));
247 // If we don't have a browser, we can't go to the webstore to fetch data.
248 // This should only happen in tests.
254 // Make sure to be notified if the owning profile is destroyed.
256 chrome::NOTIFICATION_PROFILE_DESTROYED
,
257 content::Source
<Profile
>(browser
->profile()));
259 webstore_data_fetcher_
.reset(new WebstoreDataFetcher(
261 browser
->profile()->GetRequestContext(),
264 webstore_data_fetcher_
->Start();
267 void ExternalInstallDialogDelegate::OnWebstoreRequestFailure() {
271 void ExternalInstallDialogDelegate::OnWebstoreResponseParseSuccess(
272 scoped_ptr
<base::DictionaryValue
> webstore_data
) {
273 std::string localized_user_count
;
274 double average_rating
;
276 if (!webstore_data
->GetString(kUsersKey
, &localized_user_count
) ||
277 !webstore_data
->GetDouble(kAverageRatingKey
, &average_rating
) ||
278 !webstore_data
->GetInteger(kRatingCountKey
, &rating_count
)) {
279 // If we don't get a valid webstore response, short circuit, and continue
280 // to show a prompt without webstore data.
285 bool show_user_count
= true;
286 webstore_data
->GetBoolean(kShowUserCountKey
, &show_user_count
);
288 prompt_
->SetWebstoreData(localized_user_count
,
296 void ExternalInstallDialogDelegate::OnWebstoreResponseParseFailure(
297 const std::string
& error
) {
301 void ExternalInstallDialogDelegate::Observe(
303 const content::NotificationSource
& source
,
304 const content::NotificationDetails
& details
) {
305 DCHECK_EQ(type
, chrome::NOTIFICATION_PROFILE_DESTROYED
);
306 // If the owning profile is destroyed, we need to abort so that we don't leak.
307 InstallUIAbort(false); // Not user initiated.
310 void ExternalInstallDialogDelegate::ShowInstallUI() {
311 const Extension
* extension
= NULL
;
312 if (!service_weak_
.get() ||
313 !(extension
= service_weak_
->GetInstalledExtension(extension_id_
))) {
317 ExtensionInstallUI::CreateInstallPromptWithBrowser(browser_
));
319 const ExtensionInstallPrompt::ShowDialogCallback callback
=
321 base::Bind(&CreateExternalInstallGlobalError
,
324 ExtensionInstallPrompt::GetDefaultShowDialogCallback();
326 install_ui_
->ConfirmExternalInstall(this, extension
, callback
, *prompt_
);
329 ExternalInstallDialogDelegate::~ExternalInstallDialogDelegate() {
332 void ExternalInstallDialogDelegate::InstallUIProceed() {
333 const Extension
* extension
= NULL
;
334 if (service_weak_
.get() &&
335 (extension
= service_weak_
->GetInstalledExtension(extension_id_
))) {
336 service_weak_
->GrantPermissionsAndEnableExtension(extension
);
341 void ExternalInstallDialogDelegate::InstallUIAbort(bool user_initiated
) {
342 const Extension
* extension
= NULL
;
343 if (service_weak_
.get() &&
344 (extension
= service_weak_
->GetInstalledExtension(extension_id_
))) {
345 service_weak_
->UninstallExtension(extension_id_
, false, NULL
);
350 // ExternalInstallMenuAlert -------------------------------------------------
352 ExternalInstallMenuAlert::ExternalInstallMenuAlert(ExtensionService
* service
,
353 const Extension
* extension
)
355 extension_(extension
),
356 extension_registry_observer_(this) {
357 extension_registry_observer_
.Add(ExtensionRegistry::Get(service
->profile()));
358 registrar_
.Add(this, chrome::NOTIFICATION_EXTENSION_REMOVED
,
359 content::Source
<Profile
>(service
->profile()));
362 ExternalInstallMenuAlert::~ExternalInstallMenuAlert() {
365 GlobalError::Severity
ExternalInstallMenuAlert::GetSeverity() {
369 bool ExternalInstallMenuAlert::HasMenuItem() {
373 int ExternalInstallMenuAlert::MenuItemCommandID() {
374 return kMenuCommandId
;
377 base::string16
ExternalInstallMenuAlert::MenuItemLabel() {
379 if (extension_
->is_app())
380 id
= IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_APP
;
381 else if (extension_
->is_theme())
382 id
= IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_THEME
;
384 id
= IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_EXTENSION
;
385 return l10n_util::GetStringFUTF16(id
, base::UTF8ToUTF16(extension_
->name()));
388 void ExternalInstallMenuAlert::ExecuteMenuItem(Browser
* browser
) {
389 ShowExternalInstallDialog(service_
, browser
, extension_
);
392 bool ExternalInstallMenuAlert::HasBubbleView() {
395 base::string16
ExternalInstallMenuAlert::GetBubbleViewTitle() {
396 return base::string16();
399 std::vector
<base::string16
> ExternalInstallMenuAlert::GetBubbleViewMessages() {
400 return std::vector
<base::string16
>();
403 base::string16
ExternalInstallMenuAlert::GetBubbleViewAcceptButtonLabel() {
404 return base::string16();
407 base::string16
ExternalInstallMenuAlert::GetBubbleViewCancelButtonLabel() {
408 return base::string16();
411 void ExternalInstallMenuAlert::OnBubbleViewDidClose(Browser
* browser
) {
415 void ExternalInstallMenuAlert::BubbleViewAcceptButtonPressed(
420 void ExternalInstallMenuAlert::BubbleViewCancelButtonPressed(
425 void ExternalInstallMenuAlert::OnExtensionLoaded(
426 content::BrowserContext
* browser_context
,
427 const Extension
* extension
) {
428 if (extension
== extension_
)
432 void ExternalInstallMenuAlert::Observe(
434 const content::NotificationSource
& source
,
435 const content::NotificationDetails
& details
) {
436 // The error is invalidated if the extension has been loaded or removed.
437 DCHECK_EQ(type
, chrome::NOTIFICATION_EXTENSION_REMOVED
);
438 const Extension
* extension
= content::Details
<const Extension
>(details
).ptr();
439 if (extension
== extension_
)
443 void ExternalInstallMenuAlert::Clean() {
444 GlobalErrorService
* error_service
=
445 GlobalErrorServiceFactory::GetForProfile(service_
->profile());
446 error_service
->RemoveGlobalError(this);
447 service_
->AcknowledgeExternalExtension(extension_
->id());
451 // ExternalInstallGlobalError -----------------------------------------------
453 ExternalInstallGlobalError::ExternalInstallGlobalError(
454 ExtensionService
* service
,
455 const Extension
* extension
,
456 ExternalInstallDialogDelegate
* delegate
,
457 const ExtensionInstallPrompt::Prompt
& prompt
)
458 : ExternalInstallMenuAlert(service
, extension
),
463 ExternalInstallGlobalError::~ExternalInstallGlobalError() {
465 delegate_
->Release();
468 void ExternalInstallGlobalError::ExecuteMenuItem(Browser
* browser
) {
469 ShowBubbleView(browser
);
472 bool ExternalInstallGlobalError::HasBubbleView() {
476 gfx::Image
ExternalInstallGlobalError::GetBubbleViewIcon() {
477 if (prompt_
->icon().IsEmpty())
478 return GlobalErrorWithStandardBubble::GetBubbleViewIcon();
479 // Scale icon to a reasonable size.
480 return gfx::Image(gfx::ImageSkiaOperations::CreateResizedImage(
481 *prompt_
->icon().ToImageSkia(),
482 skia::ImageOperations::RESIZE_BEST
,
483 gfx::Size(extension_misc::EXTENSION_ICON_SMALL
,
484 extension_misc::EXTENSION_ICON_SMALL
)));
487 base::string16
ExternalInstallGlobalError::GetBubbleViewTitle() {
488 return prompt_
->GetDialogTitle();
491 std::vector
<base::string16
>
492 ExternalInstallGlobalError::GetBubbleViewMessages() {
493 std::vector
<base::string16
> messages
;
494 messages
.push_back(prompt_
->GetHeading());
495 if (prompt_
->GetPermissionCount()) {
496 messages
.push_back(prompt_
->GetPermissionsHeading());
497 for (size_t i
= 0; i
< prompt_
->GetPermissionCount(); ++i
) {
498 messages
.push_back(l10n_util::GetStringFUTF16(
499 IDS_EXTENSION_PERMISSION_LINE
,
500 prompt_
->GetPermission(i
)));
503 // TODO(yoz): OAuth issue advice?
507 base::string16
ExternalInstallGlobalError::GetBubbleViewAcceptButtonLabel() {
508 return prompt_
->GetAcceptButtonLabel();
511 base::string16
ExternalInstallGlobalError::GetBubbleViewCancelButtonLabel() {
512 return prompt_
->GetAbortButtonLabel();
515 void ExternalInstallGlobalError::OnBubbleViewDidClose(Browser
* browser
) {
518 void ExternalInstallGlobalError::BubbleViewAcceptButtonPressed(
520 ExternalInstallDialogDelegate
* delegate
= delegate_
;
522 delegate
->InstallUIProceed();
525 void ExternalInstallGlobalError::BubbleViewCancelButtonPressed(
527 ExternalInstallDialogDelegate
* delegate
= delegate_
;
529 delegate
->InstallUIAbort(true);
532 // Public interface ---------------------------------------------------------
534 void AddExternalInstallError(ExtensionService
* service
,
535 const Extension
* extension
,
536 bool is_new_profile
) {
537 GlobalErrorService
* error_service
=
538 GlobalErrorServiceFactory::GetForProfile(service
->profile());
539 if (error_service
->GetGlobalErrorByMenuItemCommandID(kMenuCommandId
))
542 if (UseBubbleInstall(extension
, is_new_profile
)) {
543 Browser
* browser
= NULL
;
544 #if !defined(OS_ANDROID)
545 browser
= chrome::FindTabbedBrowser(service
->profile(),
547 chrome::GetActiveDesktop());
549 new ExternalInstallDialogDelegate(browser
, service
, extension
, true);
551 error_service
->AddGlobalError(
552 new ExternalInstallMenuAlert(service
, extension
));
556 void RemoveExternalInstallError(ExtensionService
* service
) {
557 GlobalErrorService
* error_service
=
558 GlobalErrorServiceFactory::GetForProfile(service
->profile());
559 GlobalError
* error
= error_service
->GetGlobalErrorByMenuItemCommandID(
562 error_service
->RemoveGlobalError(error
);
567 bool HasExternalInstallError(ExtensionService
* service
) {
568 GlobalErrorService
* error_service
=
569 GlobalErrorServiceFactory::GetForProfile(service
->profile());
570 GlobalError
* error
= error_service
->GetGlobalErrorByMenuItemCommandID(
575 bool HasExternalInstallBubble(ExtensionService
* service
) {
576 GlobalErrorService
* error_service
=
577 GlobalErrorServiceFactory::GetForProfile(service
->profile());
578 GlobalError
* error
= error_service
->GetGlobalErrorByMenuItemCommandID(
580 return error
&& error
->HasBubbleView();
583 } // namespace extensions