| // 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 <memory> |
| #include <string> |
| |
| #include "base/bind.h" |
| #include "base/location.h" |
| #include "base/macros.h" |
| #include "base/metrics/histogram_macros.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/threading/thread_task_runner_handle.h" |
| #include "chrome/browser/extensions/extension_install_error_menu_item_id_provider.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/generated_resources.h" |
| #include "components/strings/grit/components_strings.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 "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" |
| |
| namespace { |
| |
| static const int kIconSize = extension_misc::EXTENSION_ICON_SMALL; |
| |
| } // namespace |
| |
| // ExtensionDisabledGlobalError ----------------------------------------------- |
| |
| namespace extensions { |
| |
| class ExtensionDisabledGlobalError : public GlobalErrorWithStandardBubble, |
| public content::NotificationObserver, |
| public ExtensionUninstallDialog::Delegate, |
| public 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<base::string16> 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(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_; |
| |
| std::unique_ptr<ExtensionUninstallDialog> uninstall_dialog_; |
| |
| // Helper to get menu command ID assigned for this extension's error. |
| ExtensionInstallErrorMenuItemIdProvider id_provider_; |
| |
| content::NotificationRegistrar registrar_; |
| |
| ScopedObserver<ExtensionRegistry, ExtensionRegistryObserver> |
| 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() ? util::GetDefaultAppIcon() |
| : util::GetDefaultExtensionIcon(), |
| skia::ImageOperations::RESIZE_BEST, gfx::Size(kIconSize, kIconSize))); |
| } |
| registry_observer_.Add(ExtensionRegistry::Get(service->profile())); |
| registrar_.Add(this, NOTIFICATION_EXTENSION_REMOVED, |
| content::Source<Profile>(service->profile())); |
| } |
| |
| ExtensionDisabledGlobalError::~ExtensionDisabledGlobalError() {} |
| |
| 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<base::string16> |
| ExtensionDisabledGlobalError::GetBubbleViewMessages() { |
| std::vector<base::string16> messages; |
| |
| std::unique_ptr<const PermissionSet> granted_permissions = |
| ExtensionPrefs::Get(service_->GetBrowserContext()) |
| ->GetGrantedPermissions(extension_->id()); |
| |
| PermissionMessages permission_warnings = |
| extension_->permissions_data()->GetNewPermissionMessages( |
| *granted_permissions); |
| |
| if (is_remote_install_) { |
| if (!permission_warnings.empty()) |
| messages.push_back( |
| l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_WILL_HAVE_ACCESS_TO)); |
| } else { |
| // TODO(crbug.com/461261): If NeedCustodianApprovalForPermissionIncrease, |
| // add an extra message for supervised users. |
| messages.push_back( |
| l10n_util::GetStringUTF16(IDS_EXTENSION_DISABLED_ERROR_LABEL)); |
| } |
| 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 (util::IsExtensionSupervised(extension_, service_->profile())) { |
| // TODO(crbug.com/461261): Probably use a new string here once we get UX |
| // design. For now, just use "OK". |
| 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_PERMISSIONS_ACCEPT_BUTTON); |
| } |
| |
| base::string16 ExtensionDisabledGlobalError::GetBubbleViewCancelButtonLabel() { |
| if (util::IsExtensionSupervised(extension_, service_->profile())) { |
| // The supervised user can't approve the update, and hence there is no |
| // "cancel" button. Return an empty string such that the "cancel" button |
| // is not shown in the dialog. |
| return base::string16(); |
| } |
| return l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_UNINSTALL_BUTTON); |
| } |
| |
| void ExtensionDisabledGlobalError::OnBubbleViewDidClose(Browser* browser) { |
| // If the user takes an action, |user_response_| is set in |
| // BubbleView[Cancel|Accept]Pressed(). Otherwise, the IGNORE value set in the |
| // constructor is correct. |
| UMA_HISTOGRAM_ENUMERATION("Extensions.DisabledUIUserResponseRemoteInstall2", |
| user_response_, |
| EXTENSION_DISABLED_UI_BUCKET_BOUNDARY); |
| UMA_HISTOGRAM_ENUMERATION("Extensions.DisabledUIUserResponse2", |
| user_response_, |
| EXTENSION_DISABLED_UI_BUCKET_BOUNDARY); |
| // Reset in case the user does not follow through on subsequent dialogs to |
| // confirm removal decision, in which case the bubble can be shown again |
| // when the user clicks on the global error in the menu. |
| user_response_ = IGNORED; |
| } |
| |
| void ExtensionDisabledGlobalError::BubbleViewAcceptButtonPressed( |
| Browser* browser) { |
| if (util::IsExtensionSupervised(extension_, service_->profile())) { |
| return; |
| } |
| user_response_ = REENABLE; |
| // Delay extension reenabling so this bubble closes properly. |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&ExtensionService::GrantPermissionsAndEnableExtension, |
| service_->AsWeakPtr(), base::RetainedRef(extension_))); |
| } |
| |
| void ExtensionDisabledGlobalError::BubbleViewCancelButtonPressed( |
| Browser* browser) { |
| // For custodian-installed extensions, this button should not exist because |
| // there is only an "OK" button. |
| // Supervised users may never remove custodian-installed extensions. |
| DCHECK(!util::IsExtensionSupervised(extension_, service_->profile())); |
| uninstall_dialog_ = ExtensionUninstallDialog::Create( |
| service_->profile(), browser->window()->GetNativeWindow(), this); |
| user_response_ = UNINSTALL; |
| // 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::BindOnce(&ExtensionUninstallDialog::ConfirmUninstall, |
| uninstall_dialog_->AsWeakPtr(), |
| base::RetainedRef(extension_), |
| UNINSTALL_REASON_EXTENSION_DISABLED, |
| 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(NOTIFICATION_EXTENSION_REMOVED, type); |
| const Extension* extension = content::Details<const Extension>(details).ptr(); |
| if (extension != extension_) |
| return; |
| RemoveGlobalError(); |
| } |
| |
| void ExtensionDisabledGlobalError::OnExtensionLoaded( |
| content::BrowserContext* browser_context, |
| const Extension* extension) { |
| if (extension != extension_) |
| return; |
| RemoveGlobalError(); |
| } |
| |
| void ExtensionDisabledGlobalError::OnShutdown(ExtensionRegistry* registry) { |
| DCHECK_EQ(ExtensionRegistry::Get(service_->profile()), registry); |
| registry_observer_.RemoveAll(); |
| } |
| |
| void ExtensionDisabledGlobalError::RemoveGlobalError() { |
| std::unique_ptr<GlobalError> ptr = |
| 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::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, ptr.release()); |
| } |
| |
| // Globals -------------------------------------------------------------------- |
| |
| void AddExtensionDisabledErrorWithIcon(base::WeakPtr<ExtensionService> 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(std::make_unique<ExtensionDisabledGlobalError>( |
| service.get(), extension, is_remote_install, icon)); |
| } |
| } |
| |
| void AddExtensionDisabledError(ExtensionService* service, |
| const Extension* extension, |
| bool is_remote_install) { |
| ExtensionResource image = IconsInfo::GetIconResource( |
| extension, kIconSize, ExtensionIconSet::MATCH_BIGGER); |
| gfx::Size size(kIconSize, kIconSize); |
| ImageLoader::Get(service->profile()) |
| ->LoadImageAsync(extension, image, size, |
| base::BindOnce(&AddExtensionDisabledErrorWithIcon, |
| service->AsWeakPtr(), extension->id(), |
| is_remote_install)); |
| } |
| |
| } // namespace extensions |