| // Copyright 2012 The Chromium Authors |
| // 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_context_menu_model.h" |
| |
| #include <memory> |
| |
| #include "base/containers/contains.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/notreached.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/extensions/chrome_extension_browser_constants.h" |
| #include "chrome/browser/extensions/context_menu_matcher.h" |
| #include "chrome/browser/extensions/extension_management.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/extensions/extension_uninstall_dialog.h" |
| #include "chrome/browser/extensions/extension_util.h" |
| #include "chrome/browser/extensions/menu_manager.h" |
| #include "chrome/browser/extensions/site_permissions_helper.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/chrome_pages.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/toolbar/toolbar_actions_model.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/grit/branded_strings.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/sessions/content/session_tab_helper.h" |
| #include "components/url_formatter/url_formatter.h" |
| #include "components/vector_icons/vector_icons.h" |
| #include "content/public/browser/context_menu_params.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/browser/extension_action.h" |
| #include "extensions/browser/extension_action_manager.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/management_policy.h" |
| #include "extensions/browser/permissions_manager.h" |
| #include "extensions/browser/uninstall_reason.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_features.h" |
| #include "extensions/common/manifest_handlers/options_page_info.h" |
| #include "extensions/common/manifest_url_handlers.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/models/image_model.h" |
| #include "ui/base/models/menu_separator_types.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // Returns true if the given |item| is of the given |type|. |
| bool MenuItemMatchesAction(const absl::optional<ActionInfo::Type> action_type, |
| const MenuItem* item) { |
| if (!action_type) |
| return false; |
| |
| const MenuItem::ContextList& contexts = item->contexts(); |
| |
| if (contexts.Contains(MenuItem::ALL)) |
| return true; |
| if (contexts.Contains(MenuItem::PAGE_ACTION) && |
| (*action_type == ActionInfo::TYPE_PAGE)) { |
| return true; |
| } |
| if (contexts.Contains(MenuItem::BROWSER_ACTION) && |
| (*action_type == ActionInfo::TYPE_BROWSER)) { |
| return true; |
| } |
| if (contexts.Contains(MenuItem::ACTION) && |
| (*action_type == ActionInfo::TYPE_ACTION)) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // Returns true if the given |extension| is required to remain pinned/visible in |
| // the toolbar by policy. |
| bool IsExtensionForcePinned(const Extension& extension, Profile* profile) { |
| auto* management = ExtensionManagementFactory::GetForBrowserContext(profile); |
| return base::Contains(management->GetForcePinnedList(), extension.id()); |
| } |
| |
| // Returns the id for the visibility command for the given |extension|. |
| int GetVisibilityStringId(Profile* profile, |
| const Extension* extension, |
| bool is_pinned) { |
| if (IsExtensionForcePinned(*extension, profile)) { |
| return IDS_EXTENSIONS_PINNED_BY_ADMIN; |
| } |
| return is_pinned ? IDS_EXTENSIONS_UNPIN_FROM_TOOLBAR |
| : IDS_EXTENSIONS_PIN_TO_TOOLBAR; |
| } |
| |
| // Returns true if the given |extension| is required to remain installed by |
| // policy. |
| bool IsExtensionRequiredByPolicy(const Extension* extension, Profile* profile) { |
| ManagementPolicy* policy = ExtensionSystem::Get(profile)->management_policy(); |
| return !policy->UserMayModifySettings(extension, nullptr) || |
| policy->MustRemainInstalled(extension, nullptr); |
| } |
| |
| std::u16string GetCurrentSite(const GURL& url) { |
| return url_formatter::IDNToUnicode(url_formatter::StripWWW(url.host())); |
| } |
| |
| ExtensionContextMenuModel::ContextMenuAction CommandIdToContextMenuAction( |
| int command_id) { |
| using ContextMenuAction = ExtensionContextMenuModel::ContextMenuAction; |
| |
| switch (command_id) { |
| case ExtensionContextMenuModel::HOME_PAGE: |
| return ContextMenuAction::kHomePage; |
| case ExtensionContextMenuModel::OPTIONS: |
| return ContextMenuAction::kOptions; |
| case ExtensionContextMenuModel::TOGGLE_VISIBILITY: |
| return ContextMenuAction::kToggleVisibility; |
| case ExtensionContextMenuModel::UNINSTALL: |
| return ContextMenuAction::kUninstall; |
| case ExtensionContextMenuModel::MANAGE_EXTENSIONS: |
| return ContextMenuAction::kManageExtensions; |
| case ExtensionContextMenuModel::INSPECT_POPUP: |
| return ContextMenuAction::kInspectPopup; |
| case ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_CLICK: |
| return ContextMenuAction::kPageAccessRunOnClick; |
| case ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_SITE: |
| return ContextMenuAction::kPageAccessRunOnSite; |
| case ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_ALL_SITES: |
| return ContextMenuAction::kPageAccessRunOnAllSites; |
| case ExtensionContextMenuModel::PAGE_ACCESS_PERMISSIONS_PAGE: |
| return ContextMenuAction::kPageAccessPermissionsPage; |
| case ExtensionContextMenuModel::PAGE_ACCESS_LEARN_MORE: |
| return ContextMenuAction::kPageAccessLearnMore; |
| case ExtensionContextMenuModel::PAGE_ACCESS_CANT_ACCESS: |
| case ExtensionContextMenuModel::PAGE_ACCESS_SUBMENU: |
| case ExtensionContextMenuModel::PAGE_ACCESS_ALL_EXTENSIONS_GRANTED: |
| case ExtensionContextMenuModel::PAGE_ACCESS_ALL_EXTENSIONS_BLOCKED: |
| NOTREACHED(); |
| break; |
| case ExtensionContextMenuModel::VIEW_WEB_PERMISSIONS: |
| return ContextMenuAction::kViewWebPermissions; |
| case ExtensionContextMenuModel::POLICY_INSTALLED: |
| return ContextMenuAction::kPolicyInstalled; |
| default: |
| break; |
| } |
| NOTREACHED(); |
| return ContextMenuAction::kNoAction; |
| } |
| |
| PermissionsManager::UserSiteAccess CommandIdToSiteAccess(int command_id) { |
| switch (command_id) { |
| case ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_CLICK: |
| return PermissionsManager::UserSiteAccess::kOnClick; |
| case ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_SITE: |
| return PermissionsManager::UserSiteAccess::kOnSite; |
| case ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_ALL_SITES: |
| return PermissionsManager::UserSiteAccess::kOnAllSites; |
| } |
| NOTREACHED(); |
| return PermissionsManager::UserSiteAccess::kOnClick; |
| } |
| |
| // Logs a user action when an option is selected in the page access section of |
| // the context menu. |
| void LogPageAccessAction(int command_id) { |
| switch (command_id) { |
| case ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_CLICK: |
| base::RecordAction(base::UserMetricsAction( |
| "Extensions.ContextMenu.Hosts.OnClickClicked")); |
| break; |
| case ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_SITE: |
| base::RecordAction(base::UserMetricsAction( |
| "Extensions.ContextMenu.Hosts.OnSiteClicked")); |
| break; |
| case ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_ALL_SITES: |
| base::RecordAction(base::UserMetricsAction( |
| "Extensions.ContextMenu.Hosts.OnAllSitesClicked")); |
| break; |
| case ExtensionContextMenuModel::PAGE_ACCESS_PERMISSIONS_PAGE: |
| base::RecordAction(base::UserMetricsAction( |
| "Extensions.ContextMenu.Hosts.PermissionsPageClicked")); |
| break; |
| case ExtensionContextMenuModel::PAGE_ACCESS_LEARN_MORE: |
| base::RecordAction(base::UserMetricsAction( |
| "Extensions.ContextMenu.Hosts.LearnMoreClicked")); |
| break; |
| default: |
| NOTREACHED() << "Unknown option: " << command_id; |
| break; |
| } |
| } |
| |
| // Logs the action's visibility in the toolbar after it was set to `visible`. |
| void LogToggleVisibility(bool visible) { |
| if (visible) { |
| base::RecordAction( |
| base::UserMetricsAction("Extensions.ContextMenu.PinExtension")); |
| } else { |
| base::RecordAction( |
| base::UserMetricsAction("Extensions.ContextMenu.UnpinExtension")); |
| } |
| } |
| |
| void OpenUrl(Browser& browser, const GURL& url) { |
| content::OpenURLParams params( |
| url, content::Referrer(), WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui::PAGE_TRANSITION_LINK, /*is_renderer_initiated=*/false); |
| browser.OpenURL(params); |
| } |
| |
| // A stub for the uninstall dialog. |
| // TODO(devlin): Ideally, we would just have the uninstall dialog take a |
| // base::OnceCallback, but that's a bunch of churn. |
| class UninstallDialogHelper : public ExtensionUninstallDialog::Delegate { |
| public: |
| UninstallDialogHelper(const UninstallDialogHelper&) = delete; |
| UninstallDialogHelper& operator=(const UninstallDialogHelper&) = delete; |
| |
| // Kicks off the asynchronous process to confirm and uninstall the given |
| // |extension|. |
| static void UninstallExtension(Browser* browser, const Extension* extension) { |
| UninstallDialogHelper* helper = new UninstallDialogHelper(); |
| helper->BeginUninstall(browser, extension); |
| } |
| |
| private: |
| // This class handles its own lifetime. |
| UninstallDialogHelper() {} |
| ~UninstallDialogHelper() override {} |
| |
| void BeginUninstall(Browser* browser, const Extension* extension) { |
| uninstall_dialog_ = ExtensionUninstallDialog::Create( |
| browser->profile(), browser->window()->GetNativeWindow(), this); |
| uninstall_dialog_->ConfirmUninstall(extension, |
| UNINSTALL_REASON_USER_INITIATED, |
| UNINSTALL_SOURCE_TOOLBAR_CONTEXT_MENU); |
| } |
| |
| // ExtensionUninstallDialog::Delegate: |
| void OnExtensionUninstallDialogClosed(bool did_start_uninstall, |
| const std::u16string& error) override { |
| delete this; |
| } |
| |
| std::unique_ptr<ExtensionUninstallDialog> uninstall_dialog_; |
| }; |
| |
| } // namespace |
| |
| ExtensionContextMenuModel::ExtensionContextMenuModel( |
| const Extension* extension, |
| Browser* browser, |
| bool is_pinned, |
| PopupDelegate* delegate, |
| bool can_show_icon_in_toolbar, |
| ContextMenuSource source) |
| : SimpleMenuModel(this), |
| extension_id_(extension->id()), |
| is_component_(Manifest::IsComponentLocation(extension->location())), |
| browser_(browser), |
| profile_(browser->profile()), |
| delegate_(delegate), |
| is_pinned_(is_pinned), |
| source_(source) { |
| if (base::FeatureList::IsEnabled( |
| extensions_features::kExtensionsMenuAccessControl)) { |
| InitMenuWithFeature(extension, can_show_icon_in_toolbar); |
| } else { |
| InitMenu(extension, can_show_icon_in_toolbar); |
| } |
| } |
| |
| bool ExtensionContextMenuModel::IsCommandIdChecked(int command_id) const { |
| const Extension* extension = GetExtension(); |
| if (!extension) |
| return false; |
| |
| if (ContextMenuMatcher::IsExtensionsCustomCommandId(command_id)) |
| return extension_items_->IsCommandIdChecked(command_id); |
| |
| if (command_id == PAGE_ACCESS_RUN_ON_CLICK || |
| command_id == PAGE_ACCESS_RUN_ON_SITE || |
| command_id == PAGE_ACCESS_RUN_ON_ALL_SITES) { |
| auto* permissions = PermissionsManager::Get(profile_); |
| PermissionsManager::UserSiteAccess current_access = |
| permissions->GetUserSiteAccess(*extension, origin_.GetURL()); |
| return current_access == CommandIdToSiteAccess(command_id); |
| } |
| |
| return false; |
| } |
| |
| bool ExtensionContextMenuModel::IsCommandIdVisible(int command_id) const { |
| const Extension* extension = GetExtension(); |
| if (!extension) |
| return false; |
| |
| if (ContextMenuMatcher::IsExtensionsCustomCommandId(command_id)) |
| return extension_items_->IsCommandIdVisible(command_id); |
| |
| // Items added by Chrome to the menu are always visible. |
| return true; |
| } |
| |
| bool ExtensionContextMenuModel::IsCommandIdEnabled(int command_id) const { |
| const Extension* extension = GetExtension(); |
| if (!extension) |
| return false; |
| |
| if (ContextMenuMatcher::IsExtensionsCustomCommandId(command_id)) |
| return extension_items_->IsCommandIdEnabled(command_id); |
| |
| switch (command_id) { |
| case HOME_PAGE: |
| // The HOME_PAGE links to the Homepage URL. If the extension doesn't have |
| // a homepage, we just disable this menu item. We also disable for |
| // component extensions, because it doesn't make sense to link to a |
| // webstore page or chrome://extensions. |
| return ManifestURL::GetHomepageURL(extension).is_valid() && |
| !is_component_; |
| case OPTIONS: |
| // Options is always enabled since it will only be visible if it has an |
| // options page. |
| DCHECK(OptionsPageInfo::HasOptionsPage(extension)); |
| return true; |
| case INSPECT_POPUP: { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| return web_contents && extension_action_ && |
| extension_action_->HasPopup( |
| sessions::SessionTabHelper::IdForTab(web_contents).id()); |
| } |
| case UNINSTALL: |
| // Uninstall is always enabled since it will only be visible when the |
| // extension can be removed. |
| return true; |
| case POLICY_INSTALLED: |
| // This option is always disabled since user cannot remove a policy |
| // installed extension. |
| return false; |
| case PAGE_ACCESS_CANT_ACCESS: |
| case PAGE_ACCESS_ALL_EXTENSIONS_GRANTED: |
| case PAGE_ACCESS_ALL_EXTENSIONS_BLOCKED: |
| // When these commands are shown, they are always disabled. |
| return false; |
| case PAGE_ACCESS_SUBMENU: |
| case PAGE_ACCESS_PERMISSIONS_PAGE: |
| case PAGE_ACCESS_LEARN_MORE: |
| // When these commands are shown, they are always enabled. |
| return true; |
| case PAGE_ACCESS_RUN_ON_CLICK: |
| case PAGE_ACCESS_RUN_ON_SITE: |
| case PAGE_ACCESS_RUN_ON_ALL_SITES: |
| return PermissionsManager::Get(profile_)->CanUserSelectSiteAccess( |
| *extension, origin_.GetURL(), CommandIdToSiteAccess(command_id)); |
| // Extension pinning/unpinning is not available for Incognito as this |
| // leaves a trace of user activity. |
| case TOGGLE_VISIBILITY: |
| return !browser_->profile()->IsOffTheRecord() && |
| !IsExtensionForcePinned(*extension, profile_); |
| // Manage extensions and view web permissions are always enabled. |
| case MANAGE_EXTENSIONS: |
| case VIEW_WEB_PERMISSIONS: |
| return true; |
| default: |
| NOTREACHED() << "Unknown command" << command_id; |
| } |
| return true; |
| } |
| |
| void ExtensionContextMenuModel::ExecuteCommand(int command_id, |
| int event_flags) { |
| const Extension* extension = GetExtension(); |
| if (!extension) |
| return; |
| |
| if (ContextMenuMatcher::IsExtensionsCustomCommandId(command_id)) { |
| DCHECK(extension_items_); |
| extension_items_->ExecuteCommand(command_id, GetActiveWebContents(), |
| nullptr, content::ContextMenuParams()); |
| action_taken_ = ContextMenuAction::kCustomCommand; |
| return; |
| } |
| |
| action_taken_ = CommandIdToContextMenuAction(command_id); |
| |
| switch (command_id) { |
| case HOME_PAGE: { |
| OpenUrl(*browser_, ManifestURL::GetHomepageURL(extension)); |
| break; |
| } |
| case OPTIONS: |
| DCHECK(OptionsPageInfo::HasOptionsPage(extension)); |
| ExtensionTabUtil::OpenOptionsPage(extension, browser_); |
| break; |
| case TOGGLE_VISIBILITY: { |
| bool visible = !is_pinned_; |
| ToolbarActionsModel::Get(browser_->profile()) |
| ->SetActionVisibility(extension->id(), visible); |
| LogToggleVisibility(visible); |
| break; |
| } |
| case UNINSTALL: { |
| UninstallDialogHelper::UninstallExtension(browser_, extension); |
| break; |
| } |
| case MANAGE_EXTENSIONS: { |
| chrome::ShowExtensions(browser_, extension->id()); |
| break; |
| } |
| case VIEW_WEB_PERMISSIONS: |
| chrome::ShowSiteSettings(browser_, extension->url()); |
| break; |
| case INSPECT_POPUP: { |
| delegate_->InspectPopup(); |
| break; |
| } |
| case POLICY_INSTALLED: |
| // When visible, this option is always disabled. |
| break; |
| case PAGE_ACCESS_RUN_ON_CLICK: |
| case PAGE_ACCESS_RUN_ON_SITE: |
| case PAGE_ACCESS_RUN_ON_ALL_SITES: { |
| // Do nothing if the web contents have navigated to a different origin. |
| auto* web_contents = GetActiveWebContents(); |
| if (!web_contents || |
| !origin_.IsSameOriginWith(web_contents->GetLastCommittedURL())) { |
| return; |
| } |
| |
| LogPageAccessAction(command_id); |
| |
| // Do nothing if the extension cannot have its site permissions updated. |
| // Page access option should only be enabled when the extension site |
| // permissions can be changed. However, sometimes the command still gets |
| // invoked (crbug.com/1468151). Thus, we exit early to prevent any |
| // crashes. |
| if (!PermissionsManager::Get(profile_)->CanAffectExtension(*extension)) { |
| return; |
| } |
| |
| SitePermissionsHelper permissions(profile_); |
| permissions.UpdateSiteAccess(*extension, web_contents, |
| CommandIdToSiteAccess(command_id)); |
| break; |
| } |
| case PAGE_ACCESS_PERMISSIONS_PAGE: |
| LogPageAccessAction(command_id); |
| OpenUrl(*browser_, |
| GURL(chrome_extension_constants::kExtensionsSitePermissionsURL)); |
| break; |
| case PAGE_ACCESS_LEARN_MORE: |
| LogPageAccessAction(command_id); |
| OpenUrl(*browser_, |
| GURL(chrome_extension_constants::kRuntimeHostPermissionsHelpURL)); |
| |
| break; |
| default: |
| NOTREACHED() << "Unknown option"; |
| break; |
| } |
| } |
| |
| void ExtensionContextMenuModel::OnMenuWillShow(ui::SimpleMenuModel* menu) { |
| action_taken_ = ContextMenuAction::kNoAction; |
| } |
| |
| void ExtensionContextMenuModel::MenuClosed(ui::SimpleMenuModel* menu) { |
| if (action_taken_) { |
| ContextMenuAction action = *action_taken_; |
| UMA_HISTOGRAM_ENUMERATION("Extensions.ContextMenuAction", action); |
| action_taken_ = absl::nullopt; |
| } |
| } |
| |
| ExtensionContextMenuModel::~ExtensionContextMenuModel() {} |
| |
| void ExtensionContextMenuModel::InitMenuWithFeature( |
| const Extension* extension, |
| bool can_show_icon_in_toolbar) { |
| DCHECK(base::FeatureList::IsEnabled( |
| extensions_features::kExtensionsMenuAccessControl)); |
| DCHECK(extension); |
| |
| extension_action_ = |
| ExtensionActionManager::Get(profile_)->GetExtensionAction(*extension); |
| absl::optional<ActionInfo::Type> action_type = |
| extension_action_ |
| ? absl::optional<ActionInfo::Type>(extension_action_->action_type()) |
| : absl::nullopt; |
| |
| extension_items_ = std::make_unique<ContextMenuMatcher>( |
| profile_, this, this, |
| base::BindRepeating(MenuItemMatchesAction, action_type)); |
| |
| // Home page section. |
| 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); |
| AddItem(HOME_PAGE, base::UTF8ToUTF16(extension_name)); |
| AppendExtensionItems(); |
| |
| // Site permissions section. |
| bool policy_entry_in_subpage = false; |
| bool is_required_by_policy = IsExtensionRequiredByPolicy(extension, profile_); |
| |
| // Show section only when the extension requests host permissions. |
| auto* permissions_manager = PermissionsManager::Get(profile_); |
| if (permissions_manager->ExtensionRequestsHostPermissionsOrActiveTab( |
| *extension)) { |
| content::WebContents* web_contents = GetActiveWebContents(); |
| const GURL& url = web_contents->GetLastCommittedURL(); |
| // We store the origin to make sure it's the same when executing page access |
| // commands. |
| origin_ = url::Origin::Create(url); |
| auto site_setting = permissions_manager->GetUserSiteSetting(origin_); |
| |
| if (site_setting == |
| PermissionsManager::UserSiteSetting::kGrantAllExtensions) { |
| AddItem( |
| PAGE_ACCESS_ALL_EXTENSIONS_GRANTED, |
| l10n_util::GetStringFUTF16( |
| IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_ALL_EXTENSIONS_GRANTED, |
| GetCurrentSite(url))); |
| } else if (site_setting == |
| PermissionsManager::UserSiteSetting::kBlockAllExtensions && |
| !is_required_by_policy) { |
| // An extension required by policy can have access when the user |
| // blocked all extensions. Thus, we only show the 'all extensions blocked' |
| // item for extensions not required by policy. |
| AddItem( |
| PAGE_ACCESS_ALL_EXTENSIONS_BLOCKED, |
| l10n_util::GetStringFUTF16( |
| IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_ALL_EXTENSIONS_BLOCKED, |
| GetCurrentSite(url))); |
| } else if (SitePermissionsHelper(profile_).GetSiteInteraction( |
| *extension, web_contents) == |
| SitePermissionsHelper::SiteInteraction::kNone) { |
| // Extensions that don't request site access to this site have no site |
| // interaction. Note: it's important this comes after handling the 'block |
| // all extensions' site settings, since such setting changes all the |
| // extensions site interaction to 'none' even if the extension requested |
| // access to this site. |
| AddItemWithStringId(PAGE_ACCESS_CANT_ACCESS, |
| IDS_EXTENSIONS_CONTEXT_MENU_CANT_ACCESS_PAGE); |
| } else { |
| // The extension wants site access and can run on the page. Add the three |
| // site access options, which may be disabled. |
| static constexpr int kRadioGroup = 0; |
| page_access_submenu_ = std::make_unique<ui::SimpleMenuModel>(this); |
| page_access_submenu_->AddRadioItemWithStringId( |
| PAGE_ACCESS_RUN_ON_CLICK, |
| IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_RUN_ON_CLICK_V2, kRadioGroup); |
| page_access_submenu_->AddRadioItem( |
| PAGE_ACCESS_RUN_ON_SITE, |
| l10n_util::GetStringFUTF16( |
| IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_RUN_ON_SITE_V2, |
| GetCurrentSite(url)), |
| kRadioGroup); |
| page_access_submenu_->AddRadioItemWithStringId( |
| PAGE_ACCESS_RUN_ON_ALL_SITES, |
| IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_RUN_ON_ALL_SITES_V2, |
| kRadioGroup); |
| |
| // When the page access submenu is visible, it holds the policy entry. |
| page_access_submenu_->AddSeparator(ui::NORMAL_SEPARATOR); |
| page_access_submenu_->AddItemWithStringIdAndIcon( |
| POLICY_INSTALLED, IDS_EXTENSIONS_INSTALLED_BY_ADMIN, |
| ui::ImageModel::FromVectorIcon(vector_icons::kBusinessIcon, |
| ui::kColorIcon, 16)); |
| policy_entry_in_subpage = true; |
| |
| AddSubMenuWithStringId(PAGE_ACCESS_SUBMENU, |
| IDS_EXTENSIONS_CONTEXT_MENU_SITE_PERMISSIONS, |
| page_access_submenu_.get()); |
| } |
| |
| // Permissions page is always visible when the extension requests host |
| // permissions. |
| AddItemWithStringId( |
| PAGE_ACCESS_PERMISSIONS_PAGE, |
| IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_PERMISSIONS_PAGE); |
| } |
| |
| // Policy section. |
| if (!is_component_ && is_required_by_policy && !policy_entry_in_subpage) { |
| AddSeparator(ui::NORMAL_SEPARATOR); |
| // TODO (kylixrd): Investigate the usage of the hard-coded color. |
| AddItemWithStringIdAndIcon( |
| POLICY_INSTALLED, IDS_EXTENSIONS_INSTALLED_BY_ADMIN, |
| ui::ImageModel::FromVectorIcon(vector_icons::kBusinessIcon, |
| ui::kColorIcon, 16)); |
| } |
| |
| // Controls section. |
| bool has_options_page = OptionsPageInfo::HasOptionsPage(extension); |
| bool can_uninstall_extension = !is_component_ && is_required_by_policy; |
| if (can_show_icon_in_toolbar || has_options_page || can_uninstall_extension) { |
| AddSeparator(ui::NORMAL_SEPARATOR); |
| } |
| |
| if (can_show_icon_in_toolbar) { |
| if (IsExtensionForcePinned(*extension, profile_)) { |
| AddItemWithStringIdAndIcon( |
| TOGGLE_VISIBILITY, IDS_EXTENSIONS_PINNED_BY_ADMIN, |
| ui::ImageModel::FromVectorIcon(vector_icons::kBusinessIcon, |
| ui::kColorIcon, 16)); |
| } else { |
| int message_id = is_pinned_ |
| ? IDS_EXTENSIONS_CONTEXT_MENU_UNPIN_FROM_TOOLBAR |
| : IDS_EXTENSIONS_CONTEXT_MENU_PIN_TO_TOOLBAR; |
| AddItemWithStringId(TOGGLE_VISIBILITY, message_id); |
| } |
| } |
| |
| if (has_options_page) { |
| AddItemWithStringId(OPTIONS, IDS_EXTENSIONS_OPTIONS_MENU_ITEM); |
| } |
| |
| if (can_uninstall_extension) { |
| AddItemWithStringId(UNINSTALL, IDS_EXTENSIONS_UNINSTALL); |
| } |
| |
| // Settings section. |
| if (!is_component_) { |
| AddSeparator(ui::NORMAL_SEPARATOR); |
| AddItemWithStringId(MANAGE_EXTENSIONS, IDS_MANAGE_EXTENSION); |
| AddItemWithStringId(VIEW_WEB_PERMISSIONS, IDS_VIEW_WEB_PERMISSIONS); |
| } |
| |
| // Developer section. |
| const ActionInfo* action_info = ActionInfo::GetExtensionActionInfo(extension); |
| if (delegate_ && !is_component_ && action_info && !action_info->synthesized && |
| profile_->GetPrefs()->GetBoolean(prefs::kExtensionsUIDeveloperMode)) { |
| AddSeparator(ui::NORMAL_SEPARATOR); |
| AddItemWithStringId(INSPECT_POPUP, IDS_EXTENSION_ACTION_INSPECT_POPUP); |
| } |
| } |
| |
| void ExtensionContextMenuModel::InitMenu(const Extension* extension, |
| bool can_show_icon_in_toolbar) { |
| DCHECK(extension); |
| |
| absl::optional<ActionInfo::Type> action_type; |
| extension_action_ = |
| ExtensionActionManager::Get(profile_)->GetExtensionAction(*extension); |
| if (extension_action_) |
| action_type = extension_action_->action_type(); |
| |
| extension_items_ = std::make_unique<ContextMenuMatcher>( |
| profile_, this, this, |
| base::BindRepeating(MenuItemMatchesAction, action_type)); |
| |
| 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); |
| AddItem(HOME_PAGE, base::UTF8ToUTF16(extension_name)); |
| AppendExtensionItems(); |
| AddSeparator(ui::NORMAL_SEPARATOR); |
| |
| // Add page access items if active web contents exist and the extension |
| // wants site access (either by requesting host permissions or active tab). |
| auto* web_contents = GetActiveWebContents(); |
| auto* permissions_manager = PermissionsManager::Get(profile_); |
| if (web_contents && (permissions_manager->CanAffectExtension(*extension) || |
| permissions_manager->HasActiveTabAndCanAccess( |
| *extension, web_contents->GetLastCommittedURL()))) { |
| CreatePageAccessItems(extension, web_contents); |
| AddSeparator(ui::NORMAL_SEPARATOR); |
| } |
| |
| if (OptionsPageInfo::HasOptionsPage(extension)) |
| AddItemWithStringId(OPTIONS, IDS_EXTENSIONS_OPTIONS_MENU_ITEM); |
| |
| if (!is_component_) { |
| if (IsExtensionRequiredByPolicy(extension, profile_)) { |
| // TODO (kylixrd): Investigate the usage of the hard-coded color. |
| AddItemWithStringIdAndIcon( |
| POLICY_INSTALLED, IDS_EXTENSIONS_INSTALLED_BY_ADMIN, |
| ui::ImageModel::FromVectorIcon(vector_icons::kBusinessIcon, |
| ui::kColorIcon, 16)); |
| |
| } else { |
| AddItemWithStringId(UNINSTALL, IDS_EXTENSIONS_UNINSTALL); |
| } |
| } |
| |
| if (can_show_icon_in_toolbar && |
| source_ == ContextMenuSource::kToolbarAction) { |
| int visibility_string_id = |
| GetVisibilityStringId(profile_, extension, is_pinned_); |
| DCHECK_NE(-1, visibility_string_id); |
| AddItemWithStringId(TOGGLE_VISIBILITY, visibility_string_id); |
| if (IsExtensionForcePinned(*extension, profile_)) { |
| size_t toggle_visibility_index = |
| GetIndexOfCommandId(TOGGLE_VISIBILITY).value(); |
| SetIcon(toggle_visibility_index, |
| ui::ImageModel::FromVectorIcon(vector_icons::kBusinessIcon, |
| ui::kColorIcon, 16)); |
| } |
| } |
| |
| if (!is_component_) { |
| AddSeparator(ui::NORMAL_SEPARATOR); |
| AddItemWithStringId(MANAGE_EXTENSIONS, IDS_MANAGE_EXTENSION); |
| AddItemWithStringId(VIEW_WEB_PERMISSIONS, IDS_VIEW_WEB_PERMISSIONS); |
| } |
| |
| const ActionInfo* action_info = ActionInfo::GetExtensionActionInfo(extension); |
| if (delegate_ && !is_component_ && action_info && !action_info->synthesized && |
| profile_->GetPrefs()->GetBoolean(prefs::kExtensionsUIDeveloperMode)) { |
| AddSeparator(ui::NORMAL_SEPARATOR); |
| AddItemWithStringId(INSPECT_POPUP, IDS_EXTENSION_ACTION_INSPECT_POPUP); |
| } |
| } |
| |
| const Extension* ExtensionContextMenuModel::GetExtension() const { |
| return ExtensionRegistry::Get(profile_)->enabled_extensions().GetByID( |
| extension_id_); |
| } |
| |
| void ExtensionContextMenuModel::AppendExtensionItems() { |
| MenuManager* menu_manager = MenuManager::Get(profile_); |
| if (!menu_manager || // Null in unit tests |
| !menu_manager->MenuItems(MenuItem::ExtensionKey(extension_id_))) |
| return; |
| |
| AddSeparator(ui::NORMAL_SEPARATOR); |
| |
| int index = 0; |
| extension_items_->AppendExtensionItems(MenuItem::ExtensionKey(extension_id_), |
| std::u16string(), &index, |
| true); // is_action_menu |
| } |
| |
| void ExtensionContextMenuModel::CreatePageAccessItems( |
| const Extension* extension, |
| content::WebContents* web_contents) { |
| DCHECK(!base::FeatureList::IsEnabled( |
| extensions_features::kExtensionsMenuAccessControl)); |
| |
| const GURL& url = web_contents->GetLastCommittedURL(); |
| // We store the origin to make sure it's the same when executing page access |
| // commands. |
| origin_ = url::Origin::Create(url); |
| auto* permissions_manager = PermissionsManager::Get(profile_); |
| |
| // The extension wants site access but can't run on the page if it does |
| // not have at least "on click" access. |
| if (!permissions_manager->CanUserSelectSiteAccess( |
| *extension, url, PermissionsManager::UserSiteAccess::kOnClick)) { |
| AddItemWithStringId(PAGE_ACCESS_CANT_ACCESS, |
| IDS_EXTENSIONS_CONTEXT_MENU_CANT_ACCESS_PAGE); |
| return; |
| } |
| |
| // The extension wants site access and can ran on the page. Add the three |
| // options for "on click", "on this site", "on all sites". Though we |
| // always add these three, some may be disabled. |
| static constexpr int kRadioGroup = 0; |
| page_access_submenu_ = std::make_unique<ui::SimpleMenuModel>(this); |
| |
| page_access_submenu_->AddRadioItemWithStringId( |
| PAGE_ACCESS_RUN_ON_CLICK, |
| IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_RUN_ON_CLICK, kRadioGroup); |
| page_access_submenu_->AddRadioItem( |
| PAGE_ACCESS_RUN_ON_SITE, |
| l10n_util::GetStringFUTF16( |
| IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_RUN_ON_SITE, |
| GetCurrentSite(url)), |
| kRadioGroup); |
| page_access_submenu_->AddRadioItemWithStringId( |
| PAGE_ACCESS_RUN_ON_ALL_SITES, |
| IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_RUN_ON_ALL_SITES, kRadioGroup); |
| |
| page_access_submenu_->AddSeparator(ui::NORMAL_SEPARATOR); |
| page_access_submenu_->AddItemWithStringId( |
| PAGE_ACCESS_LEARN_MORE, |
| IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS_LEARN_MORE); |
| |
| AddSubMenuWithStringId(PAGE_ACCESS_SUBMENU, |
| IDS_EXTENSIONS_CONTEXT_MENU_PAGE_ACCESS, |
| page_access_submenu_.get()); |
| } |
| |
| content::WebContents* ExtensionContextMenuModel::GetActiveWebContents() const { |
| return browser_->tab_strip_model()->GetActiveWebContents(); |
| } |
| |
| } // namespace extensions |