// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/extensions/external_install_ui.h" #include #include "base/bind.h" #include "base/lazy_instance.h" #include "base/memory/ref_counted.h" #include "base/memory/scoped_ptr.h" #include "base/message_loop/message_loop.h" #include "base/metrics/histogram.h" #include "base/scoped_observer.h" #include "base/strings/utf_string_conversions.h" #include "chrome/app/chrome_command_ids.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/extensions/extension_install_prompt.h" #include "chrome/browser/extensions/extension_install_ui.h" #include "chrome/browser/extensions/extension_service.h" #include "chrome/browser/extensions/extension_uninstall_dialog.h" #include "chrome/browser/extensions/webstore_data_fetcher.h" #include "chrome/browser/extensions/webstore_data_fetcher_delegate.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_finder.h" #include "chrome/browser/ui/global_error/global_error.h" #include "chrome/browser/ui/global_error/global_error_service.h" #include "chrome/browser/ui/global_error/global_error_service_factory.h" #include "chrome/common/extensions/manifest_url_handler.h" #include "content/public/browser/notification_details.h" #include "content/public/browser/notification_observer.h" #include "content/public/browser/notification_registrar.h" #include "content/public/browser/notification_source.h" #include "extensions/browser/extension_registry.h" #include "extensions/browser/extension_registry_observer.h" #include "extensions/common/constants.h" #include "grit/generated_resources.h" #include "ui/base/l10n/l10n_util.h" #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_skia_operations.h" namespace extensions { namespace { // Whether the external extension can use the streamlined bubble install flow. bool UseBubbleInstall(const Extension* extension, bool is_new_profile) { return ManifestURL::UpdatesFromGallery(extension) && !is_new_profile; } } // namespace static const int kMenuCommandId = IDC_EXTERNAL_EXTENSION_ALERT; class ExternalInstallGlobalError; namespace extensions { class ExtensionRegistry; } // This class is refcounted to stay alive while we try and pull webstore data. class ExternalInstallDialogDelegate : public ExtensionInstallPrompt::Delegate, public WebstoreDataFetcherDelegate, public content::NotificationObserver, public base::RefCountedThreadSafe { public: ExternalInstallDialogDelegate(Browser* browser, ExtensionService* service, const Extension* extension, bool use_global_error); Browser* browser() { return browser_; } private: friend class base::RefCountedThreadSafe; friend class ExternalInstallGlobalError; virtual ~ExternalInstallDialogDelegate(); // ExtensionInstallPrompt::Delegate: virtual void InstallUIProceed() OVERRIDE; virtual void InstallUIAbort(bool user_initiated) OVERRIDE; // WebstoreDataFetcherDelegate: virtual void OnWebstoreRequestFailure() OVERRIDE; virtual void OnWebstoreResponseParseSuccess( scoped_ptr webstore_data) OVERRIDE; virtual void OnWebstoreResponseParseFailure( const std::string& error) OVERRIDE; // content::NotificationObserver: virtual void Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details) OVERRIDE; // Show the install dialog to the user. void ShowInstallUI(); // The UI for showing the install dialog when enabling. scoped_ptr install_ui_; scoped_refptr prompt_; Browser* browser_; base::WeakPtr service_weak_; scoped_ptr webstore_data_fetcher_; content::NotificationRegistrar registrar_; std::string extension_id_; bool use_global_error_; DISALLOW_COPY_AND_ASSIGN(ExternalInstallDialogDelegate); }; // Only shows a menu item, no bubble. Clicking the menu item shows // an external install dialog. class ExternalInstallMenuAlert : public GlobalErrorWithStandardBubble, public content::NotificationObserver, public ExtensionRegistryObserver { public: ExternalInstallMenuAlert(ExtensionService* service, const Extension* extension); virtual ~ExternalInstallMenuAlert(); // GlobalError implementation. virtual Severity GetSeverity() OVERRIDE; virtual bool HasMenuItem() OVERRIDE; virtual int MenuItemCommandID() OVERRIDE; virtual base::string16 MenuItemLabel() OVERRIDE; virtual void ExecuteMenuItem(Browser* browser) OVERRIDE; virtual bool HasBubbleView() OVERRIDE; virtual base::string16 GetBubbleViewTitle() OVERRIDE; virtual std::vector GetBubbleViewMessages() OVERRIDE; virtual base::string16 GetBubbleViewAcceptButtonLabel() OVERRIDE; virtual base::string16 GetBubbleViewCancelButtonLabel() OVERRIDE; virtual void OnBubbleViewDidClose(Browser* browser) OVERRIDE; virtual void BubbleViewAcceptButtonPressed(Browser* browser) OVERRIDE; virtual void BubbleViewCancelButtonPressed(Browser* browser) OVERRIDE; protected: ExtensionService* service_; const Extension* extension_; private: // Delete this instance after cleaning jobs. void Clean(); // content::NotificationObserver implementation. virtual void Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details) OVERRIDE; // ExtensionRegistryObserver implementation. virtual void OnExtensionLoaded(content::BrowserContext* browser_context, const Extension* extension) OVERRIDE; content::NotificationRegistrar registrar_; // Listen to extension load notifications. ScopedObserver extension_registry_observer_; DISALLOW_COPY_AND_ASSIGN(ExternalInstallMenuAlert); }; // Shows a menu item and a global error bubble, replacing the install dialog. class ExternalInstallGlobalError : public ExternalInstallMenuAlert { public: ExternalInstallGlobalError( ExtensionService* service, const Extension* extension, ExternalInstallDialogDelegate* delegate, scoped_refptr prompt); virtual ~ExternalInstallGlobalError(); virtual void ExecuteMenuItem(Browser* browser) OVERRIDE; virtual bool HasBubbleView() OVERRIDE; virtual gfx::Image GetBubbleViewIcon() OVERRIDE; virtual base::string16 GetBubbleViewTitle() OVERRIDE; virtual std::vector GetBubbleViewMessages() OVERRIDE; virtual base::string16 GetBubbleViewAcceptButtonLabel() OVERRIDE; virtual base::string16 GetBubbleViewCancelButtonLabel() OVERRIDE; virtual void OnBubbleViewDidClose(Browser* browser) OVERRIDE; virtual void BubbleViewAcceptButtonPressed(Browser* browser) OVERRIDE; virtual void BubbleViewCancelButtonPressed(Browser* browser) OVERRIDE; protected: // Ref-counted, but needs to be disposed of if we are dismissed without // having been clicked (perhaps because the user enabled the extension // manually). ExternalInstallDialogDelegate* delegate_; scoped_refptr prompt_; private: DISALLOW_COPY_AND_ASSIGN(ExternalInstallGlobalError); }; static void CreateExternalInstallGlobalError( base::WeakPtr service, const std::string& extension_id, const ExtensionInstallPrompt::ShowParams& show_params, ExtensionInstallPrompt::Delegate* prompt_delegate, scoped_refptr prompt) { if (!service.get()) return; const Extension* extension = service->GetInstalledExtension(extension_id); if (!extension) return; GlobalErrorService* error_service = GlobalErrorServiceFactory::GetForProfile(service->profile()); if (error_service->GetGlobalErrorByMenuItemCommandID(kMenuCommandId)) return; ExternalInstallDialogDelegate* delegate = static_cast(prompt_delegate); ExternalInstallGlobalError* error_bubble = new ExternalInstallGlobalError( service.get(), extension, delegate, prompt); error_service->AddGlobalError(error_bubble); // Show bubble immediately if possible. if (delegate->browser()) error_bubble->ShowBubbleView(delegate->browser()); } static void ShowExternalInstallDialog( ExtensionService* service, Browser* browser, const Extension* extension) { // This object manages its own lifetime. new ExternalInstallDialogDelegate(browser, service, extension, false); } // ExternalInstallDialogDelegate -------------------------------------------- ExternalInstallDialogDelegate::ExternalInstallDialogDelegate( Browser* browser, ExtensionService* service, const Extension* extension, bool use_global_error) : browser_(browser), service_weak_(service->AsWeakPtr()), extension_id_(extension->id()), use_global_error_(use_global_error) { AddRef(); // Balanced in Proceed or Abort. prompt_ = new ExtensionInstallPrompt::Prompt( ExtensionInstallPrompt::EXTERNAL_INSTALL_PROMPT); // If we don't have a browser, we can't go to the webstore to fetch data. // This should only happen in tests. if (!browser) { ShowInstallUI(); return; } // Make sure to be notified if the owning profile is destroyed. registrar_.Add(this, chrome::NOTIFICATION_PROFILE_DESTROYED, content::Source(browser->profile())); webstore_data_fetcher_.reset(new WebstoreDataFetcher( this, browser->profile()->GetRequestContext(), GURL::EmptyGURL(), extension->id())); webstore_data_fetcher_->Start(); } void ExternalInstallDialogDelegate::OnWebstoreRequestFailure() { ShowInstallUI(); } void ExternalInstallDialogDelegate::OnWebstoreResponseParseSuccess( scoped_ptr webstore_data) { std::string localized_user_count; double average_rating; int rating_count; if (!webstore_data->GetString(kUsersKey, &localized_user_count) || !webstore_data->GetDouble(kAverageRatingKey, &average_rating) || !webstore_data->GetInteger(kRatingCountKey, &rating_count)) { // If we don't get a valid webstore response, short circuit, and continue // to show a prompt without webstore data. ShowInstallUI(); return; } bool show_user_count = true; webstore_data->GetBoolean(kShowUserCountKey, &show_user_count); prompt_->SetWebstoreData(localized_user_count, show_user_count, average_rating, rating_count); ShowInstallUI(); } void ExternalInstallDialogDelegate::OnWebstoreResponseParseFailure( const std::string& error) { ShowInstallUI(); } void ExternalInstallDialogDelegate::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { DCHECK_EQ(type, chrome::NOTIFICATION_PROFILE_DESTROYED); // If the owning profile is destroyed, we need to abort so that we don't leak. InstallUIAbort(false); // Not user initiated. } void ExternalInstallDialogDelegate::ShowInstallUI() { const Extension* extension = NULL; if (!service_weak_.get() || !(extension = service_weak_->GetInstalledExtension(extension_id_))) { return; } install_ui_.reset( ExtensionInstallUI::CreateInstallPromptWithBrowser(browser_)); const ExtensionInstallPrompt::ShowDialogCallback callback = use_global_error_ ? base::Bind(&CreateExternalInstallGlobalError, service_weak_, extension_id_) : ExtensionInstallPrompt::GetDefaultShowDialogCallback(); install_ui_->ConfirmExternalInstall(this, extension, callback, prompt_); } ExternalInstallDialogDelegate::~ExternalInstallDialogDelegate() { } void ExternalInstallDialogDelegate::InstallUIProceed() { const Extension* extension = NULL; if (service_weak_.get() && (extension = service_weak_->GetInstalledExtension(extension_id_))) { service_weak_->GrantPermissionsAndEnableExtension(extension); } Release(); } void ExternalInstallDialogDelegate::InstallUIAbort(bool user_initiated) { const Extension* extension = NULL; // Uninstall the extension if the abort was user initiated (and not, e.g., the // result of the window closing). // Otherwise, the extension will remain installed, but unacknowledged, so it // will be prompted again. if (user_initiated && service_weak_.get() && (extension = service_weak_->GetInstalledExtension(extension_id_))) { service_weak_->UninstallExtension(extension_id_, false, NULL); } Release(); } // ExternalInstallMenuAlert ------------------------------------------------- ExternalInstallMenuAlert::ExternalInstallMenuAlert(ExtensionService* service, const Extension* extension) : service_(service), extension_(extension), extension_registry_observer_(this) { extension_registry_observer_.Add(ExtensionRegistry::Get(service->profile())); registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_REMOVED, content::Source(service->profile())); } ExternalInstallMenuAlert::~ExternalInstallMenuAlert() { } GlobalError::Severity ExternalInstallMenuAlert::GetSeverity() { return SEVERITY_LOW; } bool ExternalInstallMenuAlert::HasMenuItem() { return true; } int ExternalInstallMenuAlert::MenuItemCommandID() { return kMenuCommandId; } base::string16 ExternalInstallMenuAlert::MenuItemLabel() { int id = -1; if (extension_->is_app()) id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_APP; else if (extension_->is_theme()) id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_THEME; else id = IDS_EXTENSION_EXTERNAL_INSTALL_ALERT_EXTENSION; return l10n_util::GetStringFUTF16(id, base::UTF8ToUTF16(extension_->name())); } void ExternalInstallMenuAlert::ExecuteMenuItem(Browser* browser) { ShowExternalInstallDialog(service_, browser, extension_); } bool ExternalInstallMenuAlert::HasBubbleView() { return false; } base::string16 ExternalInstallMenuAlert::GetBubbleViewTitle() { return base::string16(); } std::vector ExternalInstallMenuAlert::GetBubbleViewMessages() { return std::vector(); } base::string16 ExternalInstallMenuAlert::GetBubbleViewAcceptButtonLabel() { return base::string16(); } base::string16 ExternalInstallMenuAlert::GetBubbleViewCancelButtonLabel() { return base::string16(); } void ExternalInstallMenuAlert::OnBubbleViewDidClose(Browser* browser) { NOTREACHED(); } void ExternalInstallMenuAlert::BubbleViewAcceptButtonPressed( Browser* browser) { NOTREACHED(); } void ExternalInstallMenuAlert::BubbleViewCancelButtonPressed( Browser* browser) { NOTREACHED(); } void ExternalInstallMenuAlert::OnExtensionLoaded( content::BrowserContext* browser_context, const Extension* extension) { if (extension == extension_) Clean(); } void ExternalInstallMenuAlert::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { // The error is invalidated if the extension has been loaded or removed. DCHECK_EQ(type, chrome::NOTIFICATION_EXTENSION_REMOVED); const Extension* extension = content::Details(details).ptr(); if (extension == extension_) Clean(); } void ExternalInstallMenuAlert::Clean() { GlobalErrorService* error_service = GlobalErrorServiceFactory::GetForProfile(service_->profile()); error_service->RemoveGlobalError(this); service_->AcknowledgeExternalExtension(extension_->id()); delete this; } // ExternalInstallGlobalError ----------------------------------------------- ExternalInstallGlobalError::ExternalInstallGlobalError( ExtensionService* service, const Extension* extension, ExternalInstallDialogDelegate* delegate, scoped_refptr prompt) : ExternalInstallMenuAlert(service, extension), delegate_(delegate), prompt_(prompt) { } ExternalInstallGlobalError::~ExternalInstallGlobalError() { if (delegate_) delegate_->Release(); } void ExternalInstallGlobalError::ExecuteMenuItem(Browser* browser) { ShowBubbleView(browser); } bool ExternalInstallGlobalError::HasBubbleView() { return true; } gfx::Image ExternalInstallGlobalError::GetBubbleViewIcon() { if (prompt_->icon().IsEmpty()) return GlobalErrorWithStandardBubble::GetBubbleViewIcon(); // Scale icon to a reasonable size. return gfx::Image(gfx::ImageSkiaOperations::CreateResizedImage( *prompt_->icon().ToImageSkia(), skia::ImageOperations::RESIZE_BEST, gfx::Size(extension_misc::EXTENSION_ICON_SMALL, extension_misc::EXTENSION_ICON_SMALL))); } base::string16 ExternalInstallGlobalError::GetBubbleViewTitle() { return prompt_->GetDialogTitle(); } std::vector ExternalInstallGlobalError::GetBubbleViewMessages() { std::vector messages; messages.push_back(prompt_->GetHeading()); if (prompt_->GetPermissionCount()) { messages.push_back(prompt_->GetPermissionsHeading()); for (size_t i = 0; i < prompt_->GetPermissionCount(); ++i) { messages.push_back(l10n_util::GetStringFUTF16( IDS_EXTENSION_PERMISSION_LINE, prompt_->GetPermission(i))); } } // TODO(yoz): OAuth issue advice? return messages; } base::string16 ExternalInstallGlobalError::GetBubbleViewAcceptButtonLabel() { return prompt_->GetAcceptButtonLabel(); } base::string16 ExternalInstallGlobalError::GetBubbleViewCancelButtonLabel() { return prompt_->GetAbortButtonLabel(); } void ExternalInstallGlobalError::OnBubbleViewDidClose(Browser* browser) { } void ExternalInstallGlobalError::BubbleViewAcceptButtonPressed( Browser* browser) { ExternalInstallDialogDelegate* delegate = delegate_; delegate_ = NULL; delegate->InstallUIProceed(); } void ExternalInstallGlobalError::BubbleViewCancelButtonPressed( Browser* browser) { ExternalInstallDialogDelegate* delegate = delegate_; delegate_ = NULL; delegate->InstallUIAbort(true); } // Public interface --------------------------------------------------------- void AddExternalInstallError(ExtensionService* service, const Extension* extension, bool is_new_profile) { GlobalErrorService* error_service = GlobalErrorServiceFactory::GetForProfile(service->profile()); if (error_service->GetGlobalErrorByMenuItemCommandID(kMenuCommandId)) return; if (UseBubbleInstall(extension, is_new_profile)) { Browser* browser = NULL; #if !defined(OS_ANDROID) browser = chrome::FindTabbedBrowser(service->profile(), true, chrome::GetActiveDesktop()); #endif new ExternalInstallDialogDelegate(browser, service, extension, true); } else { error_service->AddGlobalError( new ExternalInstallMenuAlert(service, extension)); } } void RemoveExternalInstallError(ExtensionService* service) { GlobalErrorService* error_service = GlobalErrorServiceFactory::GetForProfile(service->profile()); GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID( kMenuCommandId); if (error) { error_service->RemoveGlobalError(error); delete error; } } bool HasExternalInstallError(ExtensionService* service) { GlobalErrorService* error_service = GlobalErrorServiceFactory::GetForProfile(service->profile()); GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID( kMenuCommandId); return !!error; } bool HasExternalInstallBubble(ExtensionService* service) { GlobalErrorService* error_service = GlobalErrorServiceFactory::GetForProfile(service->profile()); GlobalError* error = error_service->GetGlobalErrorByMenuItemCommandID( kMenuCommandId); return error && error->HasBubbleView(); } } // namespace extensions