// Copyright (c) 2012 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/extension_disabled_ui.h" #include #include #include #include "base/bind.h" #include "base/lazy_instance.h" #include "base/location.h" #include "base/macros.h" #include "base/memory/ref_counted.h" #include "base/memory/scoped_ptr.h" #include "base/metrics/histogram.h" #include "base/scoped_observer.h" #include "base/single_thread_task_runner.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "base/thread_task_runner_handle.h" #include "chrome/app/chrome_command_ids.h" #include "chrome/browser/extensions/extension_install_error_menu_item_id_provider.h" #include "chrome/browser/extensions/extension_install_prompt.h" #include "chrome/browser/extensions/extension_service.h" #include "chrome/browser/extensions/extension_uninstall_dialog.h" #include "chrome/browser/extensions/extension_util.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_window.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/browser/ui/tabs/tab_strip_model.h" #include "chrome/grit/chromium_strings.h" #include "chrome/grit/generated_resources.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/browser/extension_util.h" #include "extensions/browser/image_loader.h" #include "extensions/browser/notification_types.h" #include "extensions/browser/uninstall_reason.h" #include "extensions/common/constants.h" #include "extensions/common/extension.h" #include "extensions/common/extension_icon_set.h" #include "extensions/common/manifest_handlers/icons_handler.h" #include "extensions/common/permissions/permission_message.h" #include "extensions/common/permissions/permissions_data.h" #include "grit/components_strings.h" #include "ui/base/l10n/l10n_util.h" #include "ui/gfx/geometry/size.h" #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_skia_operations.h" using extensions::Extension; using extensions::PermissionMessage; using extensions::PermissionMessages; namespace { static const int kIconSize = extension_misc::EXTENSION_ICON_SMALL; } // namespace // ExtensionDisabledDialogDelegate -------------------------------------------- class ExtensionDisabledDialogDelegate { public: ExtensionDisabledDialogDelegate(ExtensionService* service, scoped_ptr install_ui, const Extension* extension); private: ~ExtensionDisabledDialogDelegate(); void InstallPromptDone(ExtensionInstallPrompt::Result result); // The UI for showing the install dialog when enabling. scoped_ptr install_ui_; ExtensionService* service_; const Extension* extension_; DISALLOW_COPY_AND_ASSIGN(ExtensionDisabledDialogDelegate); }; ExtensionDisabledDialogDelegate::ExtensionDisabledDialogDelegate( ExtensionService* service, scoped_ptr install_ui, const Extension* extension) : install_ui_(std::move(install_ui)), service_(service), extension_(extension) { ExtensionInstallPrompt::PromptType type = ExtensionInstallPrompt::GetReEnablePromptTypeForExtension( service_->profile(), extension); // Unretained() is safe since this object manages its own lifetime and deletes // itself only once the prompt finishes. install_ui_->ShowDialog( base::Bind(&ExtensionDisabledDialogDelegate::InstallPromptDone, base::Unretained(this)), extension_, nullptr, make_scoped_ptr(new ExtensionInstallPrompt::Prompt(type)), ExtensionInstallPrompt::GetDefaultShowDialogCallback()); } ExtensionDisabledDialogDelegate::~ExtensionDisabledDialogDelegate() { } void ExtensionDisabledDialogDelegate::InstallPromptDone( ExtensionInstallPrompt::Result result) { if (result == ExtensionInstallPrompt::Result::ACCEPTED) { service_->GrantPermissionsAndEnableExtension(extension_); } else { const char* histogram_name = result == ExtensionInstallPrompt::Result::USER_CANCELED ? "ReEnableCancel" : "ReEnableAbort"; ExtensionService::RecordPermissionMessagesHistogram(extension_, histogram_name); // Do nothing. The extension will remain disabled. } delete this; } // ExtensionDisabledGlobalError ----------------------------------------------- class ExtensionDisabledGlobalError : public GlobalErrorWithStandardBubble, public content::NotificationObserver, public extensions::ExtensionUninstallDialog::Delegate, public extensions::ExtensionRegistryObserver { public: ExtensionDisabledGlobalError(ExtensionService* service, const Extension* extension, bool is_remote_install, const gfx::Image& icon); ~ExtensionDisabledGlobalError() override; // GlobalError: Severity GetSeverity() override; bool HasMenuItem() override; int MenuItemCommandID() override; base::string16 MenuItemLabel() override; void ExecuteMenuItem(Browser* browser) override; gfx::Image GetBubbleViewIcon() override; base::string16 GetBubbleViewTitle() override; std::vector GetBubbleViewMessages() override; base::string16 GetBubbleViewAcceptButtonLabel() override; base::string16 GetBubbleViewCancelButtonLabel() override; void OnBubbleViewDidClose(Browser* browser) override; void BubbleViewAcceptButtonPressed(Browser* browser) override; void BubbleViewCancelButtonPressed(Browser* browser) override; bool ShouldCloseOnDeactivate() const override; bool ShouldShowCloseButton() const override; // ExtensionUninstallDialog::Delegate: void OnExtensionUninstallDialogClosed(bool did_start_uninstall, const base::string16& error) override; private: // content::NotificationObserver: void Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details) override; // ExtensionRegistryObserver: void OnExtensionLoaded(content::BrowserContext* browser_context, const Extension* extension) override; void OnShutdown(extensions::ExtensionRegistry* registry) override; void RemoveGlobalError(); ExtensionService* service_; const Extension* extension_; bool is_remote_install_; gfx::Image icon_; // How the user responded to the error; used for metrics. enum UserResponse { IGNORED, REENABLE, UNINSTALL, EXTENSION_DISABLED_UI_BUCKET_BOUNDARY }; UserResponse user_response_; scoped_ptr uninstall_dialog_; // Helper to get menu command ID assigned for this extension's error. extensions::ExtensionInstallErrorMenuItemIdProvider id_provider_; content::NotificationRegistrar registrar_; ScopedObserver registry_observer_; }; // TODO(yoz): create error at startup for disabled extensions. ExtensionDisabledGlobalError::ExtensionDisabledGlobalError( ExtensionService* service, const Extension* extension, bool is_remote_install, const gfx::Image& icon) : service_(service), extension_(extension), is_remote_install_(is_remote_install), icon_(icon), user_response_(IGNORED), registry_observer_(this) { if (icon_.IsEmpty()) { icon_ = gfx::Image( gfx::ImageSkiaOperations::CreateResizedImage( extension_->is_app() ? extensions::util::GetDefaultAppIcon() : extensions::util::GetDefaultExtensionIcon(), skia::ImageOperations::RESIZE_BEST, gfx::Size(kIconSize, kIconSize))); } registry_observer_.Add( extensions::ExtensionRegistry::Get(service->profile())); registrar_.Add(this, extensions::NOTIFICATION_EXTENSION_REMOVED, content::Source(service->profile())); } ExtensionDisabledGlobalError::~ExtensionDisabledGlobalError() { if (is_remote_install_) { UMA_HISTOGRAM_ENUMERATION("Extensions.DisabledUIUserResponseRemoteInstall", user_response_, EXTENSION_DISABLED_UI_BUCKET_BOUNDARY); } else { UMA_HISTOGRAM_ENUMERATION("Extensions.DisabledUIUserResponse", user_response_, EXTENSION_DISABLED_UI_BUCKET_BOUNDARY); } } GlobalError::Severity ExtensionDisabledGlobalError::GetSeverity() { return SEVERITY_LOW; } bool ExtensionDisabledGlobalError::HasMenuItem() { return true; } int ExtensionDisabledGlobalError::MenuItemCommandID() { return id_provider_.menu_command_id(); } base::string16 ExtensionDisabledGlobalError::MenuItemLabel() { std::string extension_name = extension_->name(); // Ampersands need to be escaped to avoid being treated like // mnemonics in the menu. base::ReplaceChars(extension_name, "&", "&&", &extension_name); if (is_remote_install_) { return l10n_util::GetStringFUTF16( IDS_EXTENSION_DISABLED_REMOTE_INSTALL_ERROR_TITLE, base::UTF8ToUTF16(extension_name)); } else { return l10n_util::GetStringFUTF16(IDS_EXTENSION_DISABLED_ERROR_TITLE, base::UTF8ToUTF16(extension_name)); } } void ExtensionDisabledGlobalError::ExecuteMenuItem(Browser* browser) { ShowBubbleView(browser); } gfx::Image ExtensionDisabledGlobalError::GetBubbleViewIcon() { return icon_; } base::string16 ExtensionDisabledGlobalError::GetBubbleViewTitle() { if (is_remote_install_) { return l10n_util::GetStringFUTF16( IDS_EXTENSION_DISABLED_REMOTE_INSTALL_ERROR_TITLE, base::UTF8ToUTF16(extension_->name())); } else { return l10n_util::GetStringFUTF16(IDS_EXTENSION_DISABLED_ERROR_TITLE, base::UTF8ToUTF16(extension_->name())); } } std::vector ExtensionDisabledGlobalError::GetBubbleViewMessages() { std::vector messages; PermissionMessages permission_warnings = extension_->permissions_data()->GetPermissionMessages(); if (is_remote_install_) { messages.push_back(l10n_util::GetStringFUTF16( extension_->is_app() ? IDS_APP_DISABLED_REMOTE_INSTALL_ERROR_LABEL : IDS_EXTENSION_DISABLED_REMOTE_INSTALL_ERROR_LABEL, base::UTF8ToUTF16(extension_->name()))); if (!permission_warnings.empty()) messages.push_back( l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_WILL_HAVE_ACCESS_TO)); } else { // TODO(treib): If NeedCustodianApprovalForPermissionIncrease, add an extra // message for supervised users. crbug.com/461261 messages.push_back(l10n_util::GetStringFUTF16( extension_->is_app() ? IDS_APP_DISABLED_ERROR_LABEL : IDS_EXTENSION_DISABLED_ERROR_LABEL, base::UTF8ToUTF16(extension_->name()))); messages.push_back(l10n_util::GetStringUTF16( IDS_EXTENSION_PROMPT_WILL_NOW_HAVE_ACCESS_TO)); } for (const PermissionMessage& msg : permission_warnings) { messages.push_back(l10n_util::GetStringFUTF16(IDS_EXTENSION_PERMISSION_LINE, msg.message())); } return messages; } base::string16 ExtensionDisabledGlobalError::GetBubbleViewAcceptButtonLabel() { if (extensions::util::IsExtensionSupervised(extension_, service_->profile()) && extensions::util::NeedCustodianApprovalForPermissionIncrease( service_->profile())) { // TODO(treib): Probably use a new string here once we get UX design. // For now, just use "OK". crbug.com/461261 return l10n_util::GetStringUTF16(IDS_OK); } if (is_remote_install_) { return l10n_util::GetStringUTF16( extension_->is_app() ? IDS_EXTENSION_PROMPT_REMOTE_INSTALL_BUTTON_APP : IDS_EXTENSION_PROMPT_REMOTE_INSTALL_BUTTON_EXTENSION); } return l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_RE_ENABLE_BUTTON); } base::string16 ExtensionDisabledGlobalError::GetBubbleViewCancelButtonLabel() { if (extensions::util::IsExtensionSupervised(extension_, service_->profile())) { if (extensions::util::NeedCustodianApprovalForPermissionIncrease( service_->profile())) { // If the supervised user can't approve the update, then there is no // "cancel" button. return base::string16(); } else { // Supervised users can not remove extensions, so use "cancel" here // instead of "uninstall". return l10n_util::GetStringUTF16(IDS_CANCEL); } } return l10n_util::GetStringUTF16(IDS_EXTENSIONS_UNINSTALL); } void ExtensionDisabledGlobalError::OnBubbleViewDidClose(Browser* browser) { } void ExtensionDisabledGlobalError::BubbleViewAcceptButtonPressed( Browser* browser) { if (extensions::util::IsExtensionSupervised(extension_, service_->profile()) && extensions::util::NeedCustodianApprovalForPermissionIncrease( service_->profile())) { return; } // Delay extension reenabling so this bubble closes properly. base::ThreadTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::Bind(&ExtensionService::GrantPermissionsAndEnableExtension, service_->AsWeakPtr(), extension_)); } void ExtensionDisabledGlobalError::BubbleViewCancelButtonPressed( Browser* browser) { if (extensions::util::IsExtensionSupervised(extension_, service_->profile())) { // For custodian-installed extensions, this button should only exist if the // supervised user can approve the update. Otherwise there is only an "OK" // button. DCHECK(!extensions::util::NeedCustodianApprovalForPermissionIncrease( service_->profile())); // Supervised users may never remove custodian-installed extensions. return; } uninstall_dialog_.reset(extensions::ExtensionUninstallDialog::Create( service_->profile(), browser->window()->GetNativeWindow(), this)); // Delay showing the uninstall dialog, so that this function returns // immediately, to close the bubble properly. See crbug.com/121544. base::ThreadTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::Bind(&extensions::ExtensionUninstallDialog::ConfirmUninstall, uninstall_dialog_->AsWeakPtr(), extension_, extensions::UNINSTALL_REASON_EXTENSION_DISABLED, extensions::UNINSTALL_SOURCE_PERMISSIONS_INCREASE)); } bool ExtensionDisabledGlobalError::ShouldCloseOnDeactivate() const { // Since this indicates that an extension was disabled, we should definitely // have the user acknowledge it, rather than having the bubble disappear when // a new window pops up. return false; } bool ExtensionDisabledGlobalError::ShouldShowCloseButton() const { // As we don't close the bubble on deactivation (see ShouldCloseOnDeactivate), // we add a close button so the user doesn't *need* to act right away. // If the bubble is closed, the error remains in the wrench menu and the user // can address it later. return true; } void ExtensionDisabledGlobalError::OnExtensionUninstallDialogClosed( bool did_start_uninstall, const base::string16& error) { // No need to do anything. } void ExtensionDisabledGlobalError::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(extensions::NOTIFICATION_EXTENSION_REMOVED, type); const Extension* extension = content::Details(details).ptr(); if (extension != extension_) return; user_response_ = UNINSTALL; RemoveGlobalError(); } void ExtensionDisabledGlobalError::OnExtensionLoaded( content::BrowserContext* browser_context, const Extension* extension) { if (extension != extension_) return; user_response_ = REENABLE; RemoveGlobalError(); } void ExtensionDisabledGlobalError::OnShutdown( extensions::ExtensionRegistry* registry) { DCHECK_EQ(extensions::ExtensionRegistry::Get(service_->profile()), registry); registry_observer_.RemoveAll(); } void ExtensionDisabledGlobalError::RemoveGlobalError() { GlobalErrorServiceFactory::GetForProfile(service_->profile()) ->RemoveGlobalError(this); registrar_.RemoveAll(); registry_observer_.RemoveAll(); // Delete this object after any running tasks, so that the extension dialog // still has it as a delegate to finish the current tasks. base::MessageLoop::current()->DeleteSoon(FROM_HERE, this); } // Globals -------------------------------------------------------------------- namespace extensions { void AddExtensionDisabledErrorWithIcon(base::WeakPtr service, const std::string& extension_id, bool is_remote_install, const gfx::Image& icon) { if (!service.get()) return; const Extension* extension = service->GetInstalledExtension(extension_id); if (extension) { GlobalErrorServiceFactory::GetForProfile(service->profile()) ->AddGlobalError(new ExtensionDisabledGlobalError( service.get(), extension, is_remote_install, icon)); } } void AddExtensionDisabledError(ExtensionService* service, const Extension* extension, bool is_remote_install) { extensions::ExtensionResource image = extensions::IconsInfo::GetIconResource( extension, kIconSize, ExtensionIconSet::MATCH_BIGGER); gfx::Size size(kIconSize, kIconSize); ImageLoader::Get(service->profile()) ->LoadImageAsync(extension, image, size, base::Bind(&AddExtensionDisabledErrorWithIcon, service->AsWeakPtr(), extension->id(), is_remote_install)); } void ShowExtensionDisabledDialog(ExtensionService* service, content::WebContents* web_contents, const Extension* extension) { scoped_ptr install_ui( new ExtensionInstallPrompt(web_contents)); // This object manages its own lifetime. new ExtensionDisabledDialogDelegate(service, std::move(install_ui), extension); } } // namespace extensions