|  | // 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_context_menu_model.h" | 
|  |  | 
|  | #include <memory> | 
|  | #include <utility> | 
|  |  | 
|  | #include "base/bind.h" | 
|  | #include "base/json/json_reader.h" | 
|  | #include "base/macros.h" | 
|  | #include "base/memory/ptr_util.h" | 
|  | #include "base/strings/stringprintf.h" | 
|  | #include "base/strings/utf_string_conversions.h" | 
|  | #include "base/test/metrics/histogram_tester.h" | 
|  | #include "base/test/metrics/user_action_tester.h" | 
|  | #include "build/chromeos_buildflags.h" | 
|  | #include "chrome/app/chrome_command_ids.h" | 
|  | #include "chrome/browser/extensions/chrome_extension_browser_constants.h" | 
|  | #include "chrome/browser/extensions/context_menu_matcher.h" | 
|  | #include "chrome/browser/extensions/extension_action_runner.h" | 
|  | #include "chrome/browser/extensions/extension_action_test_util.h" | 
|  | #include "chrome/browser/extensions/extension_service.h" | 
|  | #include "chrome/browser/extensions/extension_service_test_base.h" | 
|  | #include "chrome/browser/extensions/menu_manager.h" | 
|  | #include "chrome/browser/extensions/menu_manager_factory.h" | 
|  | #include "chrome/browser/extensions/permissions_test_util.h" | 
|  | #include "chrome/browser/extensions/permissions_updater.h" | 
|  | #include "chrome/browser/extensions/scripting_permissions_modifier.h" | 
|  | #include "chrome/browser/ui/browser.h" | 
|  | #include "chrome/browser/ui/tabs/tab_strip_model.h" | 
|  | #include "chrome/browser/ui/toolbar/toolbar_actions_model.h" | 
|  | #include "chrome/common/extensions/api/context_menus.h" | 
|  | #include "chrome/grit/chromium_strings.h" | 
|  | #include "chrome/grit/generated_resources.h" | 
|  | #include "chrome/test/base/test_browser_window.h" | 
|  | #include "chrome/test/base/testing_profile.h" | 
|  | #include "components/crx_file/id_util.h" | 
|  | #include "content/public/browser/navigation_controller.h" | 
|  | #include "content/public/test/test_renderer_host.h" | 
|  | #include "content/public/test/web_contents_tester.h" | 
|  | #include "extensions/browser/extension_dialog_auto_confirm.h" | 
|  | #include "extensions/browser/extension_registry.h" | 
|  | #include "extensions/browser/extension_system.h" | 
|  | #include "extensions/browser/test_extension_registry_observer.h" | 
|  | #include "extensions/browser/test_management_policy.h" | 
|  | #include "extensions/common/api/extension_action/action_info.h" | 
|  | #include "extensions/common/api/extension_action/action_info_test_util.h" | 
|  | #include "extensions/common/extension_builder.h" | 
|  | #include "extensions/common/extension_features.h" | 
|  | #include "extensions/common/extensions_client.h" | 
|  | #include "extensions/common/manifest.h" | 
|  | #include "extensions/common/manifest_constants.h" | 
|  | #include "extensions/common/manifest_handlers/options_page_info.h" | 
|  | #include "extensions/common/mojom/run_location.mojom-shared.h" | 
|  | #include "extensions/common/permissions/permissions_data.h" | 
|  | #include "extensions/common/value_builder.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  | #include "ui/base/l10n/l10n_util.h" | 
|  | #include "ui/display/test/scoped_screen_override.h" | 
|  | #include "ui/display/test/test_screen.h" | 
|  | #include "ui/gfx/image/image.h" | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS_ASH) | 
|  | #include "chrome/browser/ash/app_mode/kiosk_app_manager.h" | 
|  | #endif | 
|  |  | 
|  | namespace extensions { | 
|  |  | 
|  | using display::test::ScopedScreenOverride; | 
|  | using mojom::ManifestLocation; | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | void Increment(int* i, bool granted) { | 
|  | if (!granted) | 
|  | return; | 
|  | CHECK(i); | 
|  | ++(*i); | 
|  | } | 
|  |  | 
|  | MenuItem::Context MenuItemContextForActionType(ActionInfo::Type type) { | 
|  | MenuItem::Context context = MenuItem::ALL; | 
|  | switch (type) { | 
|  | case ActionInfo::TYPE_BROWSER: | 
|  | context = MenuItem::BROWSER_ACTION; | 
|  | break; | 
|  | case ActionInfo::TYPE_PAGE: | 
|  | context = MenuItem::PAGE_ACTION; | 
|  | break; | 
|  | case ActionInfo::TYPE_ACTION: | 
|  | context = MenuItem::ACTION; | 
|  | break; | 
|  | } | 
|  |  | 
|  | return context; | 
|  | } | 
|  |  | 
|  | scoped_refptr<const Extension> BuildExtensionWithActionType( | 
|  | ActionInfo::Type type) { | 
|  | ExtensionBuilder builder("extension"); | 
|  | switch (type) { | 
|  | case ActionInfo::TYPE_BROWSER: | 
|  | builder.SetAction(ExtensionBuilder::ActionType::BROWSER_ACTION); | 
|  | break; | 
|  | case ActionInfo::TYPE_PAGE: | 
|  | builder.SetAction(ExtensionBuilder::ActionType::PAGE_ACTION); | 
|  | break; | 
|  | case ActionInfo::TYPE_ACTION: | 
|  | builder.SetAction(ExtensionBuilder::ActionType::ACTION); | 
|  | break; | 
|  | } | 
|  |  | 
|  | builder.SetManifestKey("manifest_version", | 
|  | GetManifestVersionForActionType(type)); | 
|  |  | 
|  | return builder.Build(); | 
|  | } | 
|  |  | 
|  | // Label for test extension menu item. | 
|  | const char* kTestExtensionItemLabel = "test-ext-item"; | 
|  |  | 
|  | std::string item_label() { | 
|  | return kTestExtensionItemLabel; | 
|  | } | 
|  |  | 
|  | class MenuBuilder { | 
|  | public: | 
|  | MenuBuilder(scoped_refptr<const Extension> extension, | 
|  | Browser* browser, | 
|  | MenuManager* menu_manager) | 
|  | : extension_(extension), | 
|  | browser_(browser), | 
|  | menu_manager_(menu_manager), | 
|  | cur_id_(0) {} | 
|  | ~MenuBuilder() {} | 
|  |  | 
|  | std::unique_ptr<ExtensionContextMenuModel> BuildMenu() { | 
|  | return std::make_unique<ExtensionContextMenuModel>( | 
|  | extension_.get(), browser_, ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true /* can_show_icon_in_toolbar */); | 
|  | } | 
|  |  | 
|  | void AddContextItem(MenuItem::Context context) { | 
|  | MenuItem::Id id(false /* not incognito */, | 
|  | MenuItem::ExtensionKey(extension_->id())); | 
|  | id.uid = ++cur_id_; | 
|  | menu_manager_->AddContextItem( | 
|  | extension_.get(), | 
|  | std::make_unique<MenuItem>(id, kTestExtensionItemLabel, | 
|  | false,  // check`ed | 
|  | true,   // visible | 
|  | true,   // enabled | 
|  | MenuItem::NORMAL, | 
|  | MenuItem::ContextList(context))); | 
|  | } | 
|  |  | 
|  | void SetItemVisibility(int item_id, bool visible) { | 
|  | MenuItem::Id id(false /* not incognito */, | 
|  | MenuItem::ExtensionKey(extension_->id())); | 
|  | id.uid = item_id; | 
|  |  | 
|  | menu_manager_->GetItemById(id)->set_visible(visible); | 
|  | } | 
|  |  | 
|  | void SetItemTitle(int item_id, const std::string& title) { | 
|  | MenuItem::Id id(false /* not incognito */, | 
|  | MenuItem::ExtensionKey(extension_->id())); | 
|  | id.uid = item_id; | 
|  |  | 
|  | menu_manager_->GetItemById(id)->set_title(title); | 
|  | } | 
|  |  | 
|  | private: | 
|  | scoped_refptr<const Extension> extension_; | 
|  | Browser* browser_; | 
|  | MenuManager* menu_manager_; | 
|  | int cur_id_; | 
|  |  | 
|  | DISALLOW_COPY_AND_ASSIGN(MenuBuilder); | 
|  | }; | 
|  |  | 
|  | // Returns the number of extension menu items that show up in |model|. | 
|  | // For this test, all the extension items have same label | 
|  | // |kTestExtensionItemLabel|. | 
|  | int CountExtensionItems(const ExtensionContextMenuModel& model) { | 
|  | std::u16string expected_label = base::ASCIIToUTF16(kTestExtensionItemLabel); | 
|  | int num_items_found = 0; | 
|  | int num_custom_found = 0; | 
|  | for (int i = 0; i < model.GetItemCount(); ++i) { | 
|  | std::u16string actual_label = model.GetLabelAt(i); | 
|  | int command_id = model.GetCommandIdAt(i); | 
|  | // If the command id is not visible, it should not be counted. | 
|  | if (model.IsCommandIdVisible(command_id)) { | 
|  | // The last character of |expected_label| can be the item number (e.g | 
|  | // "test-ext-item" -> "test-ext-item1"). In checking that extensions items | 
|  | // have the same label |kTestExtensionItemLabel|, the specific item number | 
|  | // is ignored, [0, expected_label.size). | 
|  | if (base::StartsWith(actual_label, expected_label, | 
|  | base::CompareCase::SENSITIVE)) | 
|  | ++num_items_found; | 
|  | if (ContextMenuMatcher::IsExtensionsCustomCommandId(command_id)) | 
|  | ++num_custom_found; | 
|  | } | 
|  | } | 
|  | // The only custom extension items present on the menu should be those we | 
|  | // added in the test. | 
|  | EXPECT_EQ(num_items_found, num_custom_found); | 
|  | return num_items_found; | 
|  | } | 
|  |  | 
|  | // Checks that the model has the extension items in the exact order specified by | 
|  | // |item_number|. | 
|  | void VerifyItems(const ExtensionContextMenuModel& model, | 
|  | std::vector<std::string> item_number) { | 
|  | size_t j = 0; | 
|  | for (int i = 0; i < model.GetItemCount(); i++) { | 
|  | int command_id = model.GetCommandIdAt(i); | 
|  | if (ContextMenuMatcher::IsExtensionsCustomCommandId(command_id) && | 
|  | model.IsCommandIdVisible(command_id)) { | 
|  | ASSERT_LT(j, item_number.size()); | 
|  | EXPECT_EQ(base::ASCIIToUTF16(item_label() + item_number[j]), | 
|  | model.GetLabelAt(i)); | 
|  | j++; | 
|  | } | 
|  | } | 
|  | EXPECT_EQ(item_number.size(), j); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | class ExtensionContextMenuModelTest : public ExtensionServiceTestBase { | 
|  | public: | 
|  | enum class CommandState { | 
|  | kAbsent,    // The command is not present in the menu. | 
|  | kEnabled,   // The command is present, and enabled. | 
|  | kDisabled,  // The command is present, and disabled. | 
|  | }; | 
|  |  | 
|  | ExtensionContextMenuModelTest(); | 
|  |  | 
|  | // Build an extension to pass to the menu constructor, with the action | 
|  | // specified by |action_key|. | 
|  | const Extension* AddExtension(const std::string& name, | 
|  | const char* action_key, | 
|  | ManifestLocation location); | 
|  | const Extension* AddExtensionWithHostPermission( | 
|  | const std::string& name, | 
|  | const char* action_key, | 
|  | ManifestLocation location, | 
|  | const std::string& host_permission); | 
|  | // TODO(devlin): Consolidate this with the methods above. | 
|  | void InitializeAndAddExtension(const Extension& extension); | 
|  |  | 
|  | Browser* GetBrowser(); | 
|  |  | 
|  | MenuManager* CreateMenuManager(); | 
|  |  | 
|  | // Adds a new tab with |url| to the tab strip, and returns the WebContents | 
|  | // associated with it. | 
|  | content::WebContents* AddTab(const GURL& url); | 
|  |  | 
|  | // Returns the current state for the specified page access |command|. | 
|  | CommandState GetPageAccessCommandState(const ExtensionContextMenuModel& menu, | 
|  | int command) const; | 
|  |  | 
|  | // Returns true if the |menu| has the page access submenu at all. | 
|  | bool HasPageAccessSubmenu(const ExtensionContextMenuModel& menu) const; | 
|  |  | 
|  | // Returns true if the |menu| has a valid entry for the "can't access page" | 
|  | // item. | 
|  | bool HasCantAccessPageEntry(const ExtensionContextMenuModel& menu) const; | 
|  |  | 
|  | void SetUp() override; | 
|  | void TearDown() override; | 
|  |  | 
|  | private: | 
|  | std::unique_ptr<TestBrowserWindow> test_window_; | 
|  | std::unique_ptr<Browser> browser_; | 
|  | display::test::TestScreen test_screen_; | 
|  | std::unique_ptr<ScopedScreenOverride> scoped_screen_override_; | 
|  |  | 
|  | DISALLOW_COPY_AND_ASSIGN(ExtensionContextMenuModelTest); | 
|  | }; | 
|  |  | 
|  | ExtensionContextMenuModelTest::ExtensionContextMenuModelTest() {} | 
|  |  | 
|  | const Extension* ExtensionContextMenuModelTest::AddExtension( | 
|  | const std::string& name, | 
|  | const char* action_key, | 
|  | ManifestLocation location) { | 
|  | return AddExtensionWithHostPermission(name, action_key, location, | 
|  | std::string()); | 
|  | } | 
|  |  | 
|  | const Extension* ExtensionContextMenuModelTest::AddExtensionWithHostPermission( | 
|  | const std::string& name, | 
|  | const char* action_key, | 
|  | ManifestLocation location, | 
|  | const std::string& host_permission) { | 
|  | DictionaryBuilder manifest; | 
|  | manifest.Set("name", name) | 
|  | .Set("version", "1") | 
|  | .Set("manifest_version", 2); | 
|  | if (action_key) | 
|  | manifest.Set(action_key, DictionaryBuilder().Build()); | 
|  | if (!host_permission.empty()) | 
|  | manifest.Set("permissions", ListBuilder().Append(host_permission).Build()); | 
|  | scoped_refptr<const Extension> extension = | 
|  | ExtensionBuilder() | 
|  | .SetManifest(manifest.Build()) | 
|  | .SetID(crx_file::id_util::GenerateId(name)) | 
|  | .SetLocation(location) | 
|  | .Build(); | 
|  | if (!extension.get()) | 
|  | ADD_FAILURE(); | 
|  | service()->GrantPermissions(extension.get()); | 
|  | service()->AddExtension(extension.get()); | 
|  | return extension.get(); | 
|  | } | 
|  |  | 
|  | void ExtensionContextMenuModelTest::InitializeAndAddExtension( | 
|  | const Extension& extension) { | 
|  | PermissionsUpdater updater(profile()); | 
|  | updater.InitializePermissions(&extension); | 
|  | updater.GrantActivePermissions(&extension); | 
|  | service()->AddExtension(&extension); | 
|  | } | 
|  |  | 
|  | Browser* ExtensionContextMenuModelTest::GetBrowser() { | 
|  | if (!browser_) { | 
|  | Browser::CreateParams params(profile(), true); | 
|  | test_window_ = std::make_unique<TestBrowserWindow>(); | 
|  | params.window = test_window_.get(); | 
|  | browser_.reset(Browser::Create(params)); | 
|  | } | 
|  | return browser_.get(); | 
|  | } | 
|  |  | 
|  | MenuManager* ExtensionContextMenuModelTest::CreateMenuManager() { | 
|  | return static_cast<MenuManager*>( | 
|  | MenuManagerFactory::GetInstance()->SetTestingFactoryAndUse( | 
|  | profile(), base::BindRepeating( | 
|  | &MenuManagerFactory::BuildServiceInstanceForTesting))); | 
|  | } | 
|  |  | 
|  | content::WebContents* ExtensionContextMenuModelTest::AddTab(const GURL& url) { | 
|  | std::unique_ptr<content::WebContents> contents( | 
|  | content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); | 
|  | content::WebContents* raw_contents = contents.get(); | 
|  | Browser* browser = GetBrowser(); | 
|  | browser->tab_strip_model()->AppendWebContents(std::move(contents), true); | 
|  | EXPECT_EQ(browser->tab_strip_model()->GetActiveWebContents(), raw_contents); | 
|  | content::WebContentsTester* web_contents_tester = | 
|  | content::WebContentsTester::For(raw_contents); | 
|  | web_contents_tester->NavigateAndCommit(url); | 
|  | return raw_contents; | 
|  | } | 
|  |  | 
|  | ExtensionContextMenuModelTest::CommandState | 
|  | ExtensionContextMenuModelTest::GetPageAccessCommandState( | 
|  | const ExtensionContextMenuModel& menu, | 
|  | int command) const { | 
|  | int submenu_index = | 
|  | menu.GetIndexOfCommandId(ExtensionContextMenuModel::PAGE_ACCESS_SUBMENU); | 
|  | if (submenu_index == -1) | 
|  | return CommandState::kAbsent; | 
|  |  | 
|  | ui::MenuModel* submenu = menu.GetSubmenuModelAt(submenu_index); | 
|  | DCHECK(submenu); | 
|  |  | 
|  | ui::MenuModel** menu_to_search = &submenu; | 
|  | int index_unused = 0; | 
|  | if (!ui::MenuModel::GetModelAndIndexForCommandId(command, menu_to_search, | 
|  | &index_unused)) { | 
|  | return CommandState::kAbsent; | 
|  | } | 
|  |  | 
|  | // The command is present; determine if it's enabled. | 
|  | return menu.IsCommandIdEnabled(command) ? CommandState::kEnabled | 
|  | : CommandState::kDisabled; | 
|  | } | 
|  |  | 
|  | bool ExtensionContextMenuModelTest::HasPageAccessSubmenu( | 
|  | const ExtensionContextMenuModel& menu) const { | 
|  | return menu.GetIndexOfCommandId( | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_SUBMENU) != -1; | 
|  | } | 
|  |  | 
|  | bool ExtensionContextMenuModelTest::HasCantAccessPageEntry( | 
|  | const ExtensionContextMenuModel& menu) const { | 
|  | if (menu.GetIndexOfCommandId( | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_CANT_ACCESS) == -1) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // The "Can't access this page" entry, if present, should always be disabled. | 
|  | EXPECT_FALSE(menu.IsCommandIdEnabled( | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_CANT_ACCESS)); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | void ExtensionContextMenuModelTest::SetUp() { | 
|  | ExtensionServiceTestBase::SetUp(); | 
|  | scoped_screen_override_ = | 
|  | std::make_unique<ScopedScreenOverride>(&test_screen_); | 
|  | } | 
|  |  | 
|  | void ExtensionContextMenuModelTest::TearDown() { | 
|  | // Remove any tabs in the tab strip; else the test crashes. | 
|  | if (browser_) { | 
|  | while (!browser_->tab_strip_model()->empty()) | 
|  | browser_->tab_strip_model()->DetachWebContentsAt(0); | 
|  | } | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS_ASH) | 
|  | // The ash::KioskAppManager, if initialized, needs to be cleaned up. | 
|  | // TODO(devlin): This should probably go somewhere more central, like | 
|  | // chromeos::ScopedCrosSettingsTestHelper. | 
|  | ash::KioskAppManager::Shutdown(); | 
|  | #endif | 
|  |  | 
|  | ExtensionServiceTestBase::TearDown(); | 
|  | } | 
|  |  | 
|  | // Tests that applicable menu items are disabled when a ManagementPolicy | 
|  | // prohibits them. | 
|  | TEST_F(ExtensionContextMenuModelTest, RequiredInstallationsDisablesItems) { | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | // Test that management policy can determine whether or not policy-installed | 
|  | // extensions can be installed/uninstalled. | 
|  | const Extension* extension = | 
|  | AddExtension("extension", manifest_keys::kPageAction, | 
|  | ManifestLocation::kExternalPolicy); | 
|  |  | 
|  | ExtensionContextMenuModel menu(extension, GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  |  | 
|  | ExtensionSystem* system = ExtensionSystem::Get(profile()); | 
|  | system->management_policy()->UnregisterAllProviders(); | 
|  |  | 
|  | // Uninstallation should be, by default, enabled. | 
|  | EXPECT_TRUE(menu.IsCommandIdEnabled(ExtensionContextMenuModel::UNINSTALL)); | 
|  | // Uninstallation should always be visible. | 
|  | EXPECT_TRUE(menu.IsCommandIdVisible(ExtensionContextMenuModel::UNINSTALL)); | 
|  |  | 
|  | TestManagementPolicyProvider policy_provider( | 
|  | TestManagementPolicyProvider::PROHIBIT_MODIFY_STATUS); | 
|  | system->management_policy()->RegisterProvider(&policy_provider); | 
|  |  | 
|  | // If there's a policy provider that requires the extension stay enabled, then | 
|  | // uninstallation should be disabled. | 
|  | EXPECT_FALSE(menu.IsCommandIdEnabled(ExtensionContextMenuModel::UNINSTALL)); | 
|  | int uninstall_index = | 
|  | menu.GetIndexOfCommandId(ExtensionContextMenuModel::UNINSTALL); | 
|  | // There should also be an icon to visually indicate why uninstallation is | 
|  | // forbidden. | 
|  | ui::ImageModel icon = menu.GetIconAt(uninstall_index); | 
|  | EXPECT_FALSE(icon.IsEmpty()); | 
|  |  | 
|  | // Don't leave |policy_provider| dangling. | 
|  | system->management_policy()->UnregisterProvider(&policy_provider); | 
|  | } | 
|  |  | 
|  | // Tests the context menu for a component extension. | 
|  | TEST_F(ExtensionContextMenuModelTest, ComponentExtensionContextMenu) { | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | std::string name("component"); | 
|  | std::unique_ptr<base::DictionaryValue> manifest = | 
|  | DictionaryBuilder() | 
|  | .Set("name", name) | 
|  | .Set("version", "1") | 
|  | .Set("manifest_version", 2) | 
|  | .Set("browser_action", DictionaryBuilder().Build()) | 
|  | .Build(); | 
|  |  | 
|  | { | 
|  | scoped_refptr<const Extension> extension = | 
|  | ExtensionBuilder() | 
|  | .SetManifest(base::WrapUnique(manifest->DeepCopy())) | 
|  | .SetID(crx_file::id_util::GenerateId("component")) | 
|  | .SetLocation(ManifestLocation::kComponent) | 
|  | .Build(); | 
|  | service()->AddExtension(extension.get()); | 
|  |  | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  |  | 
|  | // A component extension's context menu should not include options for | 
|  | // managing extensions or removing it, and should only include an option for | 
|  | // the options page if the extension has one (which this one doesn't). | 
|  | EXPECT_EQ(-1, menu.GetIndexOfCommandId(ExtensionContextMenuModel::OPTIONS)); | 
|  | EXPECT_EQ(-1, | 
|  | menu.GetIndexOfCommandId(ExtensionContextMenuModel::UNINSTALL)); | 
|  | EXPECT_EQ(-1, menu.GetIndexOfCommandId( | 
|  | ExtensionContextMenuModel::MANAGE_EXTENSIONS)); | 
|  | // The "name" option should be present, but not enabled for component | 
|  | // extensions. | 
|  | EXPECT_NE(-1, | 
|  | menu.GetIndexOfCommandId(ExtensionContextMenuModel::HOME_PAGE)); | 
|  | EXPECT_FALSE(menu.IsCommandIdEnabled(ExtensionContextMenuModel::HOME_PAGE)); | 
|  | } | 
|  |  | 
|  | { | 
|  | // Check that a component extension with an options page does have the | 
|  | // options | 
|  | // menu item, and it is enabled. | 
|  | manifest->SetString("options_page", "options_page.html"); | 
|  | scoped_refptr<const Extension> extension = | 
|  | ExtensionBuilder() | 
|  | .SetManifest(std::move(manifest)) | 
|  | .SetID(crx_file::id_util::GenerateId("component_opts")) | 
|  | .SetLocation(ManifestLocation::kComponent) | 
|  | .Build(); | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | service()->AddExtension(extension.get()); | 
|  | EXPECT_TRUE(extensions::OptionsPageInfo::HasOptionsPage(extension.get())); | 
|  | EXPECT_NE(-1, menu.GetIndexOfCommandId(ExtensionContextMenuModel::OPTIONS)); | 
|  | EXPECT_TRUE(menu.IsCommandIdEnabled(ExtensionContextMenuModel::OPTIONS)); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Tests that the standard menu items (e.g. uninstall, manage) are always | 
|  | // visible. | 
|  | TEST_F(ExtensionContextMenuModelTest, | 
|  | ExtensionContextMenuStandardItemsAlwaysVisible) { | 
|  | InitializeEmptyExtensionService(); | 
|  | const Extension* extension = AddExtension( | 
|  | "extension", manifest_keys::kPageAction, ManifestLocation::kInternal); | 
|  |  | 
|  | ExtensionContextMenuModel menu(extension, GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | EXPECT_TRUE(menu.IsCommandIdVisible(ExtensionContextMenuModel::HOME_PAGE)); | 
|  | EXPECT_TRUE(menu.IsCommandIdVisible(ExtensionContextMenuModel::OPTIONS)); | 
|  | EXPECT_TRUE( | 
|  | menu.IsCommandIdVisible(ExtensionContextMenuModel::TOGGLE_VISIBILITY)); | 
|  | EXPECT_TRUE(menu.IsCommandIdVisible(ExtensionContextMenuModel::UNINSTALL)); | 
|  | EXPECT_TRUE( | 
|  | menu.IsCommandIdVisible(ExtensionContextMenuModel::MANAGE_EXTENSIONS)); | 
|  | EXPECT_TRUE( | 
|  | menu.IsCommandIdVisible(ExtensionContextMenuModel::INSPECT_POPUP)); | 
|  | EXPECT_TRUE(menu.IsCommandIdVisible( | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_CLICK)); | 
|  | EXPECT_TRUE(menu.IsCommandIdVisible( | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_SITE)); | 
|  | EXPECT_TRUE(menu.IsCommandIdVisible( | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_ALL_SITES)); | 
|  | } | 
|  |  | 
|  | // Test that the "pin" and "unpin" menu items appear correctly in the extension | 
|  | // context menu. | 
|  | TEST_F(ExtensionContextMenuModelTest, ExtensionContextMenuShowAndHide) { | 
|  | InitializeEmptyExtensionService(); | 
|  | Browser* browser = GetBrowser(); | 
|  | extension_action_test_util::CreateToolbarModelForProfile(profile()); | 
|  | const Extension* page_action = | 
|  | AddExtension("page_action_extension", manifest_keys::kPageAction, | 
|  | ManifestLocation::kInternal); | 
|  | const Extension* browser_action = | 
|  | AddExtension("browser_action_extension", manifest_keys::kBrowserAction, | 
|  | ManifestLocation::kInternal); | 
|  |  | 
|  | // For laziness. | 
|  | const ExtensionContextMenuModel::MenuEntries visibility_command = | 
|  | ExtensionContextMenuModel::TOGGLE_VISIBILITY; | 
|  | const std::u16string pin_string = | 
|  | l10n_util::GetStringUTF16(IDS_EXTENSIONS_PIN_TO_TOOLBAR); | 
|  | const std::u16string unpin_string = | 
|  | l10n_util::GetStringUTF16(IDS_EXTENSIONS_UNPIN_FROM_TOOLBAR); | 
|  |  | 
|  | { | 
|  | // Even page actions should have a visibility option. | 
|  | ExtensionContextMenuModel menu( | 
|  | page_action, browser, ExtensionContextMenuModel::PINNED, nullptr, true); | 
|  | int index = menu.GetIndexOfCommandId(visibility_command); | 
|  | EXPECT_NE(-1, index); | 
|  | EXPECT_EQ(unpin_string, menu.GetLabelAt(index)); | 
|  | } | 
|  |  | 
|  | { | 
|  | ExtensionContextMenuModel menu(browser_action, browser, | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | int index = menu.GetIndexOfCommandId(visibility_command); | 
|  | EXPECT_NE(-1, index); | 
|  | EXPECT_EQ(unpin_string, menu.GetLabelAt(index)); | 
|  |  | 
|  | // Pin before unpinning. | 
|  | ToolbarActionsModel::Get(profile())->SetActionVisibility( | 
|  | browser_action->id(), true); | 
|  | menu.ExecuteCommand(visibility_command, 0); | 
|  | } | 
|  |  | 
|  | { | 
|  | // If the action is unpinned, it should have the "Pin" string. | 
|  | ExtensionContextMenuModel menu(browser_action, browser, | 
|  | ExtensionContextMenuModel::UNPINNED, nullptr, | 
|  | true); | 
|  | int index = menu.GetIndexOfCommandId(visibility_command); | 
|  | EXPECT_NE(-1, index); | 
|  | EXPECT_EQ(pin_string, menu.GetLabelAt(index)); | 
|  | } | 
|  |  | 
|  | { | 
|  | // If the action is transitively visible, as happens when it is showing a | 
|  | // popup, we should use the same "Pin" string. | 
|  | ExtensionContextMenuModel menu( | 
|  | browser_action, browser, | 
|  | ExtensionContextMenuModel::TRANSITIVELY_VISIBLE, nullptr, true); | 
|  | int index = menu.GetIndexOfCommandId(visibility_command); | 
|  | EXPECT_NE(-1, index); | 
|  | EXPECT_EQ(pin_string, menu.GetLabelAt(index)); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Test that the "pin" and "unpin" menu items is disabled when the extension is | 
|  | // force-pinned via ExtensionSettings. | 
|  | TEST_F(ExtensionContextMenuModelTest, ExtensionContextMenuForcePinned) { | 
|  | InitializeEmptyExtensionService(); | 
|  | Browser* browser = GetBrowser(); | 
|  | extension_action_test_util::CreateToolbarModelForProfile(profile()); | 
|  | const Extension* extension = AddExtension( | 
|  | "extension", manifest_keys::kBrowserAction, ManifestLocation::kInternal); | 
|  | const Extension* force_pinned_extension = | 
|  | AddExtension("force_pinned_extension", manifest_keys::kBrowserAction, | 
|  | ManifestLocation::kInternal); | 
|  |  | 
|  | std::string json = base::StringPrintf( | 
|  | R"({ | 
|  | "%s": { | 
|  | "toolbar_pin": "force_pinned" | 
|  | } | 
|  | })", | 
|  | force_pinned_extension->id().c_str()); | 
|  | base::Optional<base::Value> parsed = base::JSONReader::Read(json); | 
|  | policy::PolicyMap map; | 
|  | map.Set("ExtensionSettings", policy::POLICY_LEVEL_MANDATORY, | 
|  | policy::POLICY_SCOPE_USER, policy::POLICY_SOURCE_PLATFORM, | 
|  | std::move(parsed), nullptr); | 
|  | policy_provider()->UpdateChromePolicy(map); | 
|  |  | 
|  | // For laziness. | 
|  | const ExtensionContextMenuModel::MenuEntries visibility_command = | 
|  | ExtensionContextMenuModel::TOGGLE_VISIBILITY; | 
|  | const std::u16string unpin_string = | 
|  | l10n_util::GetStringUTF16(IDS_EXTENSIONS_UNPIN_FROM_TOOLBAR); | 
|  | const std::u16string force_pinned_string = | 
|  | l10n_util::GetStringUTF16(IDS_EXTENSIONS_PINNED_BY_ADMIN); | 
|  |  | 
|  | { | 
|  | // Not force-pinned. | 
|  | ExtensionContextMenuModel menu( | 
|  | extension, browser, ExtensionContextMenuModel::PINNED, nullptr, true); | 
|  | int index = menu.GetIndexOfCommandId(visibility_command); | 
|  | EXPECT_NE(-1, index); | 
|  | EXPECT_TRUE(menu.IsEnabledAt(index)); | 
|  | EXPECT_EQ(unpin_string, menu.GetLabelAt(index)); | 
|  | } | 
|  |  | 
|  | { | 
|  | // Force-pinned. | 
|  | ExtensionContextMenuModel menu(force_pinned_extension, browser, | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | int index = menu.GetIndexOfCommandId(visibility_command); | 
|  | EXPECT_NE(-1, index); | 
|  | EXPECT_FALSE(menu.IsEnabledAt(index)); | 
|  | EXPECT_EQ(force_pinned_string, menu.GetLabelAt(index)); | 
|  | } | 
|  | } | 
|  |  | 
|  | TEST_F(ExtensionContextMenuModelTest, ExtensionContextUninstall) { | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | const Extension* extension = AddExtension( | 
|  | "extension", manifest_keys::kBrowserAction, ManifestLocation::kInternal); | 
|  | const std::string extension_id = extension->id(); | 
|  | ASSERT_TRUE(registry()->enabled_extensions().GetByID(extension_id)); | 
|  |  | 
|  | ScopedTestDialogAutoConfirm auto_confirm(ScopedTestDialogAutoConfirm::ACCEPT); | 
|  | TestExtensionRegistryObserver uninstalled_observer(registry()); | 
|  | { | 
|  | // Scope the menu so that it's destroyed during the uninstall process. This | 
|  | // reflects what normally happens (Chrome closes the menu when the uninstall | 
|  | // dialog shows up). | 
|  | ExtensionContextMenuModel menu(extension, GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | menu.ExecuteCommand(ExtensionContextMenuModel::UNINSTALL, 0); | 
|  | } | 
|  | uninstalled_observer.WaitForExtensionUninstalled(); | 
|  | EXPECT_FALSE(registry()->GetExtensionById(extension_id, | 
|  | ExtensionRegistry::EVERYTHING)); | 
|  | } | 
|  |  | 
|  | TEST_F(ExtensionContextMenuModelTest, TestPageAccessSubmenu) { | 
|  | base::UserActionTester user_action_tester; | 
|  | constexpr char kOnClickAction[] = | 
|  | "Extensions.ContextMenu.Hosts.OnClickClicked"; | 
|  | constexpr char kOnSiteAction[] = "Extensions.ContextMenu.Hosts.OnSiteClicked"; | 
|  | constexpr char kOnAllSitesAction[] = | 
|  | "Extensions.ContextMenu.Hosts.OnAllSitesClicked"; | 
|  |  | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | // Add an extension with all urls, and withhold permission. | 
|  | const Extension* extension = | 
|  | AddExtensionWithHostPermission("extension", manifest_keys::kBrowserAction, | 
|  | ManifestLocation::kInternal, "*://*/*"); | 
|  | ScriptingPermissionsModifier(profile(), extension) | 
|  | .SetWithholdHostPermissions(true); | 
|  | EXPECT_TRUE(registry()->enabled_extensions().Contains(extension->id())); | 
|  |  | 
|  | const GURL kActiveUrl("http://www.example.com/"); | 
|  | const GURL kOtherUrl("http://www.google.com/"); | 
|  |  | 
|  | // Add a tab to the browser. | 
|  | content::WebContents* web_contents = AddTab(kActiveUrl); | 
|  |  | 
|  | ExtensionActionRunner* action_runner = | 
|  | ExtensionActionRunner::GetForWebContents(web_contents); | 
|  | ASSERT_TRUE(action_runner); | 
|  |  | 
|  | // Pretend the extension wants to run. | 
|  | int run_count = 0; | 
|  | auto increment_run_count_1 = base::BindOnce(&Increment, &run_count); | 
|  | action_runner->RequestScriptInjectionForTesting( | 
|  | extension, mojom::RunLocation::kDocumentIdle, | 
|  | std::move(increment_run_count_1)); | 
|  |  | 
|  | ExtensionContextMenuModel menu(extension, GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  |  | 
|  | EXPECT_NE(-1, menu.GetIndexOfCommandId( | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_SUBMENU)); | 
|  |  | 
|  | // For laziness. | 
|  | const ExtensionContextMenuModel::MenuEntries kRunOnClick = | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_CLICK; | 
|  | const ExtensionContextMenuModel::MenuEntries kRunOnSite = | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_SITE; | 
|  | const ExtensionContextMenuModel::MenuEntries kRunOnAllSites = | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_RUN_ON_ALL_SITES; | 
|  |  | 
|  | // Initial state: The extension should be in "run on click" mode. | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kRunOnClick)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnSite)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnAllSites)); | 
|  |  | 
|  | // Initial state: The extension should have all permissions withheld, so | 
|  | // shouldn't be allowed to run on the active url or another arbitrary url, and | 
|  | // should have withheld permissions. | 
|  | ScriptingPermissionsModifier permissions_modifier(profile(), extension); | 
|  | EXPECT_FALSE(permissions_modifier.HasGrantedHostPermission(kActiveUrl)); | 
|  | EXPECT_FALSE(permissions_modifier.HasGrantedHostPermission(kOtherUrl)); | 
|  | const PermissionsData* permissions = extension->permissions_data(); | 
|  | EXPECT_FALSE(permissions->withheld_permissions().IsEmpty()); | 
|  |  | 
|  | EXPECT_EQ(0, user_action_tester.GetActionCount(kOnClickAction)); | 
|  | EXPECT_EQ(0, user_action_tester.GetActionCount(kOnSiteAction)); | 
|  | EXPECT_EQ(0, user_action_tester.GetActionCount(kOnAllSitesAction)); | 
|  |  | 
|  | // Change the mode to be "Run on site". | 
|  | menu.ExecuteCommand(kRunOnSite, 0); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnClick)); | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kRunOnSite)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnAllSites)); | 
|  | EXPECT_EQ(0, user_action_tester.GetActionCount(kOnClickAction)); | 
|  | EXPECT_EQ(1, user_action_tester.GetActionCount(kOnSiteAction)); | 
|  | EXPECT_EQ(0, user_action_tester.GetActionCount(kOnAllSitesAction)); | 
|  |  | 
|  | // The extension should have access to the active url, but not to another | 
|  | // arbitrary url, and the extension should still have withheld permissions. | 
|  | EXPECT_TRUE(permissions_modifier.HasGrantedHostPermission(kActiveUrl)); | 
|  | EXPECT_FALSE(permissions_modifier.HasGrantedHostPermission(kOtherUrl)); | 
|  | EXPECT_FALSE(permissions->withheld_permissions().IsEmpty()); | 
|  |  | 
|  | // Since the extension has permission, it should have ran. | 
|  | EXPECT_EQ(1, run_count); | 
|  | EXPECT_FALSE(action_runner->WantsToRun(extension)); | 
|  |  | 
|  | // On another url, the mode should still be run on click. | 
|  | content::WebContentsTester* web_contents_tester = | 
|  | content::WebContentsTester::For(web_contents); | 
|  | web_contents_tester->NavigateAndCommit(kOtherUrl); | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kRunOnClick)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnSite)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnAllSites)); | 
|  |  | 
|  | // And returning to the first url should return the mode to run on site. | 
|  | web_contents_tester->NavigateAndCommit(kActiveUrl); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnClick)); | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kRunOnSite)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnAllSites)); | 
|  |  | 
|  | // Request another run. | 
|  | auto increment_run_count_2 = base::BindOnce(&Increment, &run_count); | 
|  | action_runner->RequestScriptInjectionForTesting( | 
|  | extension, mojom::RunLocation::kDocumentIdle, | 
|  | std::move(increment_run_count_2)); | 
|  |  | 
|  | // Change the mode to be "Run on all sites". | 
|  | menu.ExecuteCommand(kRunOnAllSites, 0); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnClick)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnSite)); | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kRunOnAllSites)); | 
|  | EXPECT_EQ(0, user_action_tester.GetActionCount(kOnClickAction)); | 
|  | EXPECT_EQ(1, user_action_tester.GetActionCount(kOnSiteAction)); | 
|  | EXPECT_EQ(1, user_action_tester.GetActionCount(kOnAllSitesAction)); | 
|  |  | 
|  | // The extension should be able to run on any url, and shouldn't have any | 
|  | // withheld permissions. | 
|  | EXPECT_TRUE(permissions_modifier.HasGrantedHostPermission(kActiveUrl)); | 
|  | EXPECT_TRUE(permissions_modifier.HasGrantedHostPermission(kOtherUrl)); | 
|  | EXPECT_TRUE(permissions->withheld_permissions().IsEmpty()); | 
|  |  | 
|  | // It should have ran again. | 
|  | EXPECT_EQ(2, run_count); | 
|  | EXPECT_FALSE(action_runner->WantsToRun(extension)); | 
|  |  | 
|  | // On another url, the mode should also be run on all sites. | 
|  | web_contents_tester->NavigateAndCommit(kOtherUrl); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnClick)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnSite)); | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kRunOnAllSites)); | 
|  |  | 
|  | web_contents_tester->NavigateAndCommit(kActiveUrl); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnClick)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnSite)); | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kRunOnAllSites)); | 
|  |  | 
|  | auto increment_run_count_3 = base::BindOnce(&Increment, &run_count); | 
|  | action_runner->RequestScriptInjectionForTesting( | 
|  | extension, mojom::RunLocation::kDocumentIdle, | 
|  | std::move(increment_run_count_3)); | 
|  |  | 
|  | // Return the mode to "Run on click". | 
|  | menu.ExecuteCommand(kRunOnClick, 0); | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kRunOnClick)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnSite)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kRunOnAllSites)); | 
|  | EXPECT_EQ(1, user_action_tester.GetActionCount(kOnClickAction)); | 
|  | EXPECT_EQ(1, user_action_tester.GetActionCount(kOnSiteAction)); | 
|  | EXPECT_EQ(1, user_action_tester.GetActionCount(kOnAllSitesAction)); | 
|  |  | 
|  | // We should return to the initial state - no access. | 
|  | EXPECT_FALSE(permissions_modifier.HasGrantedHostPermission(kActiveUrl)); | 
|  | EXPECT_FALSE(permissions_modifier.HasGrantedHostPermission(kOtherUrl)); | 
|  | EXPECT_FALSE(permissions->withheld_permissions().IsEmpty()); | 
|  |  | 
|  | // And the extension shouldn't have ran. | 
|  | EXPECT_EQ(2, run_count); | 
|  | EXPECT_TRUE(action_runner->WantsToRun(extension)); | 
|  |  | 
|  | // Install an extension requesting a single host. The page access submenu | 
|  | // should still be present. | 
|  | const Extension* single_host_extension = AddExtensionWithHostPermission( | 
|  | "single_host_extension", manifest_keys::kBrowserAction, | 
|  | ManifestLocation::kInternal, "http://www.example.com/*"); | 
|  | ExtensionContextMenuModel single_host_menu( | 
|  | single_host_extension, GetBrowser(), ExtensionContextMenuModel::PINNED, | 
|  | nullptr, true); | 
|  | EXPECT_NE(-1, single_host_menu.GetIndexOfCommandId( | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_SUBMENU)); | 
|  | } | 
|  |  | 
|  | TEST_F(ExtensionContextMenuModelTest, TestInspectPopupPresence) { | 
|  | InitializeEmptyExtensionService(); | 
|  | { | 
|  | const Extension* page_action = AddExtension( | 
|  | "page_action", manifest_keys::kPageAction, ManifestLocation::kInternal); | 
|  | ASSERT_TRUE(page_action); | 
|  | ExtensionContextMenuModel menu(page_action, GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | int inspect_popup_index = | 
|  | menu.GetIndexOfCommandId(ExtensionContextMenuModel::INSPECT_POPUP); | 
|  | EXPECT_GE(0, inspect_popup_index); | 
|  | } | 
|  | { | 
|  | const Extension* browser_action = | 
|  | AddExtension("browser_action", manifest_keys::kBrowserAction, | 
|  | ManifestLocation::kInternal); | 
|  | ExtensionContextMenuModel menu(browser_action, GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | int inspect_popup_index = | 
|  | menu.GetIndexOfCommandId(ExtensionContextMenuModel::INSPECT_POPUP); | 
|  | EXPECT_GE(0, inspect_popup_index); | 
|  | } | 
|  | { | 
|  | // An extension with no specified action has one synthesized. However, | 
|  | // there will never be a popup to inspect, so we shouldn't add a menu item. | 
|  | const Extension* no_action = | 
|  | AddExtension("no_action", nullptr, ManifestLocation::kInternal); | 
|  | ExtensionContextMenuModel menu(no_action, GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | int inspect_popup_index = | 
|  | menu.GetIndexOfCommandId(ExtensionContextMenuModel::INSPECT_POPUP); | 
|  | EXPECT_EQ(-1, inspect_popup_index); | 
|  | } | 
|  | } | 
|  |  | 
|  | TEST_F(ExtensionContextMenuModelTest, PageAccessMenuOptions) { | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | // For laziness. | 
|  | // TODO(devlin): Hoist these up to ExtensionContextMenuModelTest; enough | 
|  | // tests use them. | 
|  | using Entries = ExtensionContextMenuModel::MenuEntries; | 
|  | const Entries kOnClick = Entries::PAGE_ACCESS_RUN_ON_CLICK; | 
|  | const Entries kOnSite = Entries::PAGE_ACCESS_RUN_ON_SITE; | 
|  | const Entries kOnAllSites = Entries::PAGE_ACCESS_RUN_ON_ALL_SITES; | 
|  | const Entries kLearnMore = Entries::PAGE_ACCESS_LEARN_MORE; | 
|  |  | 
|  | struct { | 
|  | // The pattern requested by the extension. | 
|  | std::string requested_pattern; | 
|  | // The pattern that's granted to the extension, if any. This may be | 
|  | // significantly different than the requested pattern. | 
|  | base::Optional<std::string> granted_pattern; | 
|  | // The current URL the context menu will be used on. | 
|  | GURL current_url; | 
|  | // The set of page access menu entries that should be present. | 
|  | std::set<Entries> expected_entries; | 
|  | // The set of page access menu entries that should be enabled. | 
|  | std::set<Entries> enabled_entries; | 
|  | // The selected page access menu entry. | 
|  | base::Optional<Entries> selected_entry; | 
|  | } test_cases[] = { | 
|  | // Easy cases: site the extension wants to run on, with or without | 
|  | // permission granted. | 
|  | {"https://google.com/maps", | 
|  | "https://google.com/maps", | 
|  | GURL("https://google.com/maps"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite}, | 
|  | kOnSite}, | 
|  | {"https://google.com/maps", | 
|  | base::nullopt, | 
|  | GURL("https://google.com/maps"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite}, | 
|  | kOnClick}, | 
|  | // We should display the page access controls if the extension wants to | 
|  | // run on the specified origin, even if not on the exact site itself. | 
|  | {"https://google.com/maps", | 
|  | "https://google.com/maps", | 
|  | GURL("https://google.com"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite}, | 
|  | kOnSite}, | 
|  | // The menu should be hidden if the extension cannot run on the origin. | 
|  | {"https://google.com/maps", | 
|  | "https://google.com/maps", | 
|  | GURL("https://mail.google.com"), | 
|  | {}, | 
|  | {}}, | 
|  | // An extension with all hosts granted should display the all sites | 
|  | // controls, even if it didn't request all sites. | 
|  | {"https://google.com/maps", | 
|  | "*://*/*", | 
|  | GURL("https://mail.google.com"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | kOnAllSites}, | 
|  | // Subdomain pattern tests. | 
|  | {"https://*.google.com/*", | 
|  | "https://*.google.com/*", | 
|  | GURL("https://google.com"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite}, | 
|  | kOnSite}, | 
|  | {"https://*.google.com/*", | 
|  | base::nullopt, | 
|  | GURL("https://google.com"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite}, | 
|  | kOnClick}, | 
|  | {"https://*.google.com/*", | 
|  | "https://*.google.com/*", | 
|  | GURL("https://mail.google.com"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite}, | 
|  | kOnSite}, | 
|  | {"https://*.google.com/*", | 
|  | "https://google.com/*", | 
|  | GURL("https://mail.google.com"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite}, | 
|  | kOnClick}, | 
|  | // On sites the extension doesn't want to run on, no controls should be | 
|  | // shown... | 
|  | {"https://*.google.com/*", | 
|  | base::nullopt, | 
|  | GURL("https://example.com"), | 
|  | {}}, | 
|  | // ...unless the extension has access to the page, in which case we should | 
|  | // display the controls. | 
|  | {"https://*.google.com/*", | 
|  | "https://*.example.com/*", | 
|  | GURL("https://example.com"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite}, | 
|  | kOnSite}, | 
|  | // All-hosts like permissions should be treated as if the extension | 
|  | // requested access to all urls. | 
|  | {"https://*/maps", | 
|  | "https://*/maps", | 
|  | GURL("https://google.com/maps"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | kOnAllSites}, | 
|  | {"https://*/maps", | 
|  | "https://google.com/*", | 
|  | GURL("https://google.com/maps"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | kOnSite}, | 
|  | {"https://*/maps", | 
|  | "https://*/maps", | 
|  | GURL("https://google.com"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | kOnAllSites}, | 
|  | {"https://*/maps", | 
|  | "https://*/maps", | 
|  | GURL("https://chromium.org"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | kOnAllSites}, | 
|  | {"https://*.com/*", | 
|  | "https://*.com/*", | 
|  | GURL("https://google.com"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | kOnAllSites}, | 
|  | {"https://*.com/*", | 
|  | "https://*.com/*", | 
|  | GURL("https://maps.google.com"), | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | {kOnClick, kOnSite, kOnAllSites}, | 
|  | kOnAllSites}, | 
|  | // Even with an all-hosts like pattern, we shouldn't show access controls | 
|  | // if the extension can't run on the origin (though we show the learn more | 
|  | // option). | 
|  | {"https://*.com/*", | 
|  | "https://*.com/*", | 
|  | GURL("https://chromium.org"), | 
|  | {}, | 
|  | {}}, | 
|  | // No access controls should ever show for restricted pages, like | 
|  | // chrome:-scheme pages or the webstore. | 
|  | {"<all_urls>", "<all_urls>", GURL("chrome://extensions"), {}, {}}, | 
|  | {"<all_urls>", | 
|  | "<all_urls>", | 
|  | ExtensionsClient::Get()->GetWebstoreBaseURL(), | 
|  | {}, | 
|  | {}}, | 
|  | }; | 
|  |  | 
|  | // Add a web contents to the browser. | 
|  | content::WebContents* web_contents = AddTab(GURL("about:blank")); | 
|  | content::WebContentsTester* web_contents_tester = | 
|  | content::WebContentsTester::For(web_contents); | 
|  |  | 
|  | for (const auto& test_case : test_cases) { | 
|  | SCOPED_TRACE(base::StringPrintf( | 
|  | "Request: '%s'; Granted: %s; URL: %s", | 
|  | test_case.requested_pattern.c_str(), | 
|  | test_case.granted_pattern.value_or(std::string()).c_str(), | 
|  | test_case.current_url.spec().c_str())); | 
|  |  | 
|  | // Install an extension with the specified permission. | 
|  | scoped_refptr<const Extension> extension = | 
|  | ExtensionBuilder("test") | 
|  | .AddContentScript("script.js", {test_case.requested_pattern}) | 
|  | .Build(); | 
|  | InitializeAndAddExtension(*extension); | 
|  |  | 
|  | ScriptingPermissionsModifier(profile(), extension) | 
|  | .SetWithholdHostPermissions(true); | 
|  | if (test_case.granted_pattern) { | 
|  | URLPattern pattern(UserScript::ValidUserScriptSchemes(false), | 
|  | *test_case.granted_pattern); | 
|  | permissions_test_util::GrantRuntimePermissionsAndWaitForCompletion( | 
|  | profile(), *extension, | 
|  | PermissionSet(APIPermissionSet(), ManifestPermissionSet(), | 
|  | URLPatternSet(), URLPatternSet({pattern}))); | 
|  | } | 
|  |  | 
|  | web_contents_tester->NavigateAndCommit(test_case.current_url); | 
|  |  | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  |  | 
|  | EXPECT_EQ(test_case.selected_entry.has_value(), | 
|  | !test_case.expected_entries.empty()) | 
|  | << "If any entries are available, one should be selected."; | 
|  |  | 
|  | if (test_case.expected_entries.empty()) { | 
|  | // If there are no expected entries (i.e., the extension can't run on the | 
|  | // page), there should be no submenu and instead there should be a | 
|  | // disabled label. | 
|  | int label_index = menu.GetIndexOfCommandId( | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_CANT_ACCESS); | 
|  | EXPECT_NE(-1, label_index); | 
|  | EXPECT_FALSE(menu.IsCommandIdEnabled( | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_CANT_ACCESS)); | 
|  | EXPECT_FALSE(HasPageAccessSubmenu(menu)); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | // The learn more option should be visible whenever the menu is. | 
|  | EXPECT_EQ(CommandState::kEnabled, | 
|  | GetPageAccessCommandState(menu, kLearnMore)); | 
|  |  | 
|  | auto get_expected_state = [test_case](Entries command) { | 
|  | if (!test_case.expected_entries.count(command)) | 
|  | return CommandState::kAbsent; | 
|  | return test_case.enabled_entries.count(command) ? CommandState::kEnabled | 
|  | : CommandState::kDisabled; | 
|  | }; | 
|  |  | 
|  | // Verify the menu options are what we expect. | 
|  | EXPECT_EQ(get_expected_state(kOnClick), | 
|  | GetPageAccessCommandState(menu, kOnClick)); | 
|  | EXPECT_EQ(get_expected_state(kOnSite), | 
|  | GetPageAccessCommandState(menu, kOnSite)); | 
|  | EXPECT_EQ(get_expected_state(kOnAllSites), | 
|  | GetPageAccessCommandState(menu, kOnAllSites)); | 
|  |  | 
|  | auto should_command_be_checked = [test_case](int command) { | 
|  | return test_case.selected_entry && *test_case.selected_entry == command; | 
|  | }; | 
|  |  | 
|  | if (test_case.expected_entries.count(kOnClick)) { | 
|  | EXPECT_EQ(should_command_be_checked(kOnClick), | 
|  | menu.IsCommandIdChecked(kOnClick)); | 
|  | } | 
|  | if (test_case.expected_entries.count(kOnSite)) { | 
|  | EXPECT_EQ(should_command_be_checked(kOnSite), | 
|  | menu.IsCommandIdChecked(kOnSite)); | 
|  | } | 
|  | if (test_case.expected_entries.count(kOnAllSites)) { | 
|  | EXPECT_EQ(should_command_be_checked(kOnAllSites), | 
|  | menu.IsCommandIdChecked(kOnAllSites)); | 
|  | } | 
|  |  | 
|  | // Uninstall the extension so as not to conflict with more additions. | 
|  | std::u16string error; | 
|  | EXPECT_TRUE(service()->UninstallExtension( | 
|  | extension->id(), UNINSTALL_REASON_FOR_TESTING, &error)); | 
|  | EXPECT_TRUE(error.empty()) << error; | 
|  | EXPECT_EQ(nullptr, registry()->GetInstalledExtension(extension->id())); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Test that changing to 'run on site' while already having an all_url like | 
|  | // pattern actually removes the broad pattern to restrict to the site. | 
|  | TEST_F(ExtensionContextMenuModelTest, PageAccessSubmenu_OnSiteWithAllURLs) { | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | // For laziness. | 
|  | using Entries = ExtensionContextMenuModel::MenuEntries; | 
|  | const Entries kOnClick = Entries::PAGE_ACCESS_RUN_ON_CLICK; | 
|  | const Entries kOnSite = Entries::PAGE_ACCESS_RUN_ON_SITE; | 
|  | const Entries kOnAllSites = Entries::PAGE_ACCESS_RUN_ON_ALL_SITES; | 
|  |  | 
|  | // Add an extension with all urls, and withhold permissions. | 
|  | const Extension* extension = | 
|  | AddExtensionWithHostPermission("extension", manifest_keys::kBrowserAction, | 
|  | ManifestLocation::kInternal, "<all_urls>"); | 
|  | ScriptingPermissionsModifier(profile(), extension) | 
|  | .SetWithholdHostPermissions(true); | 
|  |  | 
|  | // Grant the extension the all_urls pattern. | 
|  | URLPattern pattern(UserScript::ValidUserScriptSchemes(false), "<all_urls>"); | 
|  | permissions_test_util::GrantRuntimePermissionsAndWaitForCompletion( | 
|  | profile(), *extension, | 
|  | PermissionSet(APIPermissionSet(), ManifestPermissionSet(), | 
|  | URLPatternSet({pattern}), URLPatternSet())); | 
|  | ScriptingPermissionsModifier modifier(profile(), extension); | 
|  | EXPECT_TRUE(modifier.HasWithheldHostPermissions()); | 
|  |  | 
|  | const GURL kActiveUrl("http://www.example.com/"); | 
|  | const GURL kOtherUrl("http://www.google.com/"); | 
|  |  | 
|  | // Now it should look like it can run on all sites and have access to both | 
|  | // urls. | 
|  | AddTab(kActiveUrl); | 
|  | ExtensionContextMenuModel menu(extension, GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | EXPECT_TRUE(HasPageAccessSubmenu(menu)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kOnClick)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kOnSite)); | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kOnAllSites)); | 
|  |  | 
|  | EXPECT_TRUE(modifier.HasGrantedHostPermission(kActiveUrl)); | 
|  | EXPECT_TRUE(modifier.HasGrantedHostPermission(kOtherUrl)); | 
|  |  | 
|  | // Change mode to "Run on site". | 
|  | menu.ExecuteCommand(kOnSite, 0); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kOnClick)); | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kOnSite)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kOnAllSites)); | 
|  |  | 
|  | // The extension should have access to the active url, but not to another | 
|  | // arbitrary url. | 
|  | EXPECT_TRUE(modifier.HasGrantedHostPermission(kActiveUrl)); | 
|  | EXPECT_FALSE(modifier.HasGrantedHostPermission(kOtherUrl)); | 
|  | } | 
|  |  | 
|  | // Test that changing to 'run on click' while having a broad pattern which | 
|  | // doesn't actually overlap the current url, still actually removes that broad | 
|  | // pattern and stops showing that the extension can run on all sites. | 
|  | // TODO(tjudkins): This test is kind of bizarre in that it highlights a case | 
|  | // where the submenu is displaying that extension can read data on all sites, | 
|  | // when it can't actually read the site it is currently on. We should revisit | 
|  | // what exactly the submenu should be conveying to the user about the current | 
|  | // page and how that relates to the similar set of information on the Extension | 
|  | // Settings page. | 
|  | TEST_F(ExtensionContextMenuModelTest, | 
|  | PageAccessSubmenu_OnClickWithBroadPattern) { | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | // For laziness. | 
|  | using Entries = ExtensionContextMenuModel::MenuEntries; | 
|  | const Entries kOnClick = Entries::PAGE_ACCESS_RUN_ON_CLICK; | 
|  | const Entries kOnSite = Entries::PAGE_ACCESS_RUN_ON_SITE; | 
|  | const Entries kOnAllSites = Entries::PAGE_ACCESS_RUN_ON_ALL_SITES; | 
|  |  | 
|  | // Add an extension with all urls, and withhold permissions. | 
|  | const Extension* extension = | 
|  | AddExtensionWithHostPermission("extension", manifest_keys::kBrowserAction, | 
|  | ManifestLocation::kInternal, "<all_urls>"); | 
|  | ScriptingPermissionsModifier modifier(profile(), extension); | 
|  | modifier.SetWithholdHostPermissions(true); | 
|  |  | 
|  | // Grant the extension a broad pattern which doesn't overlap the active url. | 
|  | URLPattern pattern(UserScript::ValidUserScriptSchemes(false), "*://*.org/*"); | 
|  | permissions_test_util::GrantRuntimePermissionsAndWaitForCompletion( | 
|  | profile(), *extension, | 
|  | PermissionSet(APIPermissionSet(), ManifestPermissionSet(), | 
|  | URLPatternSet({pattern}), URLPatternSet())); | 
|  |  | 
|  | const GURL kActiveUrl("http://www.example.com/"); | 
|  | const GURL kOrgUrl("http://chromium.org/"); | 
|  | const GURL kOtherUrl("http://www.google.com/"); | 
|  |  | 
|  | // Also explicitly grant google.com. | 
|  | modifier.GrantHostPermission(kOtherUrl); | 
|  | EXPECT_TRUE(modifier.HasWithheldHostPermissions()); | 
|  |  | 
|  | // The extension will now look like it can run on all sites even though it | 
|  | // can't access the active url. | 
|  | AddTab(kActiveUrl); | 
|  | ExtensionContextMenuModel menu(extension, GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | EXPECT_TRUE(HasPageAccessSubmenu(menu)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kOnClick)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kOnSite)); | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kOnAllSites)); | 
|  |  | 
|  | EXPECT_FALSE(modifier.HasGrantedHostPermission(kActiveUrl)); | 
|  | EXPECT_TRUE(modifier.HasGrantedHostPermission(kOrgUrl)); | 
|  | EXPECT_TRUE(modifier.HasGrantedHostPermission(kOtherUrl)); | 
|  |  | 
|  | // Change mode to "Run on click". | 
|  | menu.ExecuteCommand(kOnClick, 0); | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kOnClick)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kOnSite)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kOnAllSites)); | 
|  |  | 
|  | // The broad org pattern should have been removed, but the explicit google | 
|  | // pattern should still remain. | 
|  | EXPECT_FALSE(modifier.HasGrantedHostPermission(kActiveUrl)); | 
|  | EXPECT_FALSE(modifier.HasGrantedHostPermission(kOrgUrl)); | 
|  | EXPECT_TRUE(modifier.HasGrantedHostPermission(kOtherUrl)); | 
|  | } | 
|  |  | 
|  | TEST_F(ExtensionContextMenuModelTest, PageAccessWithActiveTab) { | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | // For laziness. | 
|  | using Entries = ExtensionContextMenuModel::MenuEntries; | 
|  | const Entries kOnClick = Entries::PAGE_ACCESS_RUN_ON_CLICK; | 
|  | const Entries kOnSite = Entries::PAGE_ACCESS_RUN_ON_SITE; | 
|  | const Entries kOnAllSites = Entries::PAGE_ACCESS_RUN_ON_ALL_SITES; | 
|  | const Entries kLearnMore = Entries::PAGE_ACCESS_LEARN_MORE; | 
|  |  | 
|  | // Add an extension that has activeTab. Note: we add permission for b.com so | 
|  | // that the extension is seen as affectable by the runtime host permissions | 
|  | // feature; otherwise the page access menu entry is omitted entirely. | 
|  | // TODO(devlin): Should we change that? | 
|  | scoped_refptr<const Extension> extension = | 
|  | ExtensionBuilder("extension") | 
|  | .AddPermissions({"activeTab", "http://b.com/*"}) | 
|  | .Build(); | 
|  | InitializeAndAddExtension(*extension); | 
|  |  | 
|  | AddTab(GURL("https://a.com")); | 
|  |  | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | EXPECT_EQ(CommandState::kEnabled, GetPageAccessCommandState(menu, kOnClick)); | 
|  | EXPECT_EQ(CommandState::kDisabled, GetPageAccessCommandState(menu, kOnSite)); | 
|  | EXPECT_EQ(CommandState::kDisabled, | 
|  | GetPageAccessCommandState(menu, kOnAllSites)); | 
|  | EXPECT_EQ(CommandState::kEnabled, | 
|  | GetPageAccessCommandState(menu, kLearnMore)); | 
|  | } | 
|  |  | 
|  | TEST_F(ExtensionContextMenuModelTest, | 
|  | TestTogglingAccessWithSpecificSitesWithUnrequestedUrl) { | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | // For laziness. | 
|  | using Entries = ExtensionContextMenuModel::MenuEntries; | 
|  | const Entries kOnClick = Entries::PAGE_ACCESS_RUN_ON_CLICK; | 
|  | const Entries kOnSite = Entries::PAGE_ACCESS_RUN_ON_SITE; | 
|  | const Entries kOnAllSites = Entries::PAGE_ACCESS_RUN_ON_ALL_SITES; | 
|  | const Entries kLearnMore = Entries::PAGE_ACCESS_LEARN_MORE; | 
|  |  | 
|  | // Add an extension that wants access to a.com. | 
|  | scoped_refptr<const Extension> extension = | 
|  | ExtensionBuilder("extension").AddPermission("*://a.com/*").Build(); | 
|  | InitializeAndAddExtension(*extension); | 
|  |  | 
|  | // Additionally, grant it the (unrequested) access to b.com. | 
|  | ExtensionPrefs* prefs = ExtensionPrefs::Get(profile()); | 
|  | URLPattern b_com_pattern(Extension::kValidHostPermissionSchemes, | 
|  | "*://b.com/*"); | 
|  | PermissionSet b_com_permissions(APIPermissionSet(), ManifestPermissionSet(), | 
|  | URLPatternSet({b_com_pattern}), | 
|  | URLPatternSet()); | 
|  | prefs->AddGrantedPermissions(extension->id(), b_com_permissions); | 
|  |  | 
|  | ScriptingPermissionsModifier modifier(profile(), extension); | 
|  | EXPECT_FALSE(modifier.HasWithheldHostPermissions()); | 
|  |  | 
|  | const GURL a_com("https://a.com"); | 
|  | content::WebContents* web_contents = AddTab(a_com); | 
|  |  | 
|  | { | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  |  | 
|  | // Without withholding host permissions, the menu should be visible on | 
|  | // a.com... | 
|  | EXPECT_TRUE(HasPageAccessSubmenu(menu)); | 
|  | EXPECT_FALSE(HasCantAccessPageEntry(menu)); | 
|  |  | 
|  | EXPECT_EQ(CommandState::kEnabled, | 
|  | GetPageAccessCommandState(menu, kOnClick)); | 
|  | EXPECT_EQ(CommandState::kEnabled, GetPageAccessCommandState(menu, kOnSite)); | 
|  | EXPECT_EQ(CommandState::kDisabled, | 
|  | GetPageAccessCommandState(menu, kOnAllSites)); | 
|  | EXPECT_EQ(CommandState::kEnabled, | 
|  | GetPageAccessCommandState(menu, kLearnMore)); | 
|  |  | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kOnSite)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kOnClick)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kOnAllSites)); | 
|  | } | 
|  |  | 
|  | const GURL b_com("https://b.com"); | 
|  | content::WebContentsTester* web_contents_tester = | 
|  | content::WebContentsTester::For(web_contents); | 
|  | web_contents_tester->NavigateAndCommit(b_com); | 
|  |  | 
|  | { | 
|  | // ... but not on b.com, where it doesn't want to run. | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | EXPECT_FALSE(HasPageAccessSubmenu(menu)); | 
|  | EXPECT_TRUE(HasCantAccessPageEntry(menu)); | 
|  | } | 
|  |  | 
|  | modifier.SetWithholdHostPermissions(true); | 
|  |  | 
|  | // However, if the extension has runtime-granted permissions to b.com, we | 
|  | // *should* display them in the menu. | 
|  | permissions_test_util::GrantRuntimePermissionsAndWaitForCompletion( | 
|  | profile(), *extension, b_com_permissions); | 
|  |  | 
|  | { | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | EXPECT_TRUE(HasPageAccessSubmenu(menu)); | 
|  | EXPECT_FALSE(HasCantAccessPageEntry(menu)); | 
|  | EXPECT_EQ(CommandState::kEnabled, | 
|  | GetPageAccessCommandState(menu, kOnClick)); | 
|  | EXPECT_EQ(CommandState::kEnabled, GetPageAccessCommandState(menu, kOnSite)); | 
|  | EXPECT_EQ(CommandState::kDisabled, | 
|  | GetPageAccessCommandState(menu, kOnAllSites)); | 
|  | EXPECT_EQ(CommandState::kEnabled, | 
|  | GetPageAccessCommandState(menu, kLearnMore)); | 
|  |  | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kOnSite)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kOnClick)); | 
|  |  | 
|  | // Set the extension to run on click. This revokes b.com permissions. | 
|  | menu.ExecuteCommand(kOnClick, 0); | 
|  | } | 
|  |  | 
|  | { | 
|  | ScriptingPermissionsModifier::SiteAccess site_access = | 
|  | modifier.GetSiteAccess(b_com); | 
|  | EXPECT_FALSE(site_access.has_site_access); | 
|  | EXPECT_FALSE(site_access.withheld_site_access); | 
|  | } | 
|  |  | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | // Somewhat strangely, this also removes the access controls, because we don't | 
|  | // show it for sites the extension doesn't want to run on. | 
|  | EXPECT_FALSE(HasPageAccessSubmenu(menu)); | 
|  | EXPECT_TRUE(HasCantAccessPageEntry(menu)); | 
|  | } | 
|  |  | 
|  | TEST_F(ExtensionContextMenuModelTest, | 
|  | TestTogglingAccessWithSpecificSitesWithRequestedSites) { | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | // For laziness. | 
|  | using Entries = ExtensionContextMenuModel::MenuEntries; | 
|  | const Entries kOnClick = Entries::PAGE_ACCESS_RUN_ON_CLICK; | 
|  | const Entries kOnSite = Entries::PAGE_ACCESS_RUN_ON_SITE; | 
|  | const Entries kOnAllSites = Entries::PAGE_ACCESS_RUN_ON_ALL_SITES; | 
|  |  | 
|  | // Add an extension that wants access to a.com and b.com. | 
|  | scoped_refptr<const Extension> extension = | 
|  | ExtensionBuilder("extension") | 
|  | .AddPermissions({"*://a.com/*", "*://b.com/*"}) | 
|  | .Build(); | 
|  | InitializeAndAddExtension(*extension); | 
|  |  | 
|  | ScriptingPermissionsModifier modifier(profile(), extension); | 
|  | EXPECT_FALSE(modifier.HasWithheldHostPermissions()); | 
|  |  | 
|  | const GURL a_com("https://a.com"); | 
|  | AddTab(a_com); | 
|  |  | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  |  | 
|  | EXPECT_EQ(CommandState::kEnabled, GetPageAccessCommandState(menu, kOnClick)); | 
|  | EXPECT_EQ(CommandState::kEnabled, GetPageAccessCommandState(menu, kOnSite)); | 
|  | EXPECT_EQ(CommandState::kDisabled, | 
|  | GetPageAccessCommandState(menu, kOnAllSites)); | 
|  |  | 
|  | EXPECT_TRUE(menu.IsCommandIdChecked(kOnSite)); | 
|  | EXPECT_FALSE(menu.IsCommandIdChecked(kOnClick)); | 
|  |  | 
|  | // Withhold access on a.com by setting the extension to on-click. | 
|  | menu.ExecuteCommand(kOnClick, 0); | 
|  |  | 
|  | // This, sadly, removes access for the extension on b.com as well. :( This | 
|  | // is because we revoke all host permissions when transitioning from "don't | 
|  | // withhold" to "do withhold". | 
|  | // TODO(devlin): We should fix that, so that toggling access on a.com doesn't | 
|  | // revoke access on b.com. | 
|  | const GURL b_com("https://b.com"); | 
|  | ScriptingPermissionsModifier::SiteAccess site_access = | 
|  | modifier.GetSiteAccess(b_com); | 
|  | EXPECT_FALSE(site_access.has_site_access); | 
|  | EXPECT_TRUE(site_access.withheld_site_access); | 
|  | } | 
|  |  | 
|  | TEST_F(ExtensionContextMenuModelTest, TestClickingPageAccessLearnMore) { | 
|  | base::UserActionTester user_action_tester; | 
|  | constexpr char kLearnMoreAction[] = | 
|  | "Extensions.ContextMenu.Hosts.LearnMoreClicked"; | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | // Add an extension that wants access to a.com. | 
|  | scoped_refptr<const Extension> extension = | 
|  | ExtensionBuilder("extension").AddPermission("*://a.com/*").Build(); | 
|  | InitializeAndAddExtension(*extension); | 
|  |  | 
|  | ScriptingPermissionsModifier modifier(profile(), extension); | 
|  | EXPECT_FALSE(modifier.HasWithheldHostPermissions()); | 
|  |  | 
|  | const GURL a_com("https://a.com"); | 
|  | AddTab(a_com); | 
|  |  | 
|  | Browser* browser = GetBrowser(); | 
|  | ExtensionContextMenuModel menu(extension.get(), browser, | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | EXPECT_EQ(0, user_action_tester.GetActionCount(kLearnMoreAction)); | 
|  |  | 
|  | const ExtensionContextMenuModel::MenuEntries kLearnMore = | 
|  | ExtensionContextMenuModel::PAGE_ACCESS_LEARN_MORE; | 
|  | EXPECT_EQ(CommandState::kEnabled, | 
|  | GetPageAccessCommandState(menu, kLearnMore)); | 
|  | menu.ExecuteCommand(kLearnMore, 0); | 
|  |  | 
|  | EXPECT_EQ(2, browser->tab_strip_model()->count()); | 
|  | content::WebContents* web_contents = | 
|  | browser->tab_strip_model()->GetActiveWebContents(); | 
|  | EXPECT_EQ(1, user_action_tester.GetActionCount(kLearnMoreAction)); | 
|  |  | 
|  | // Test web contents need a poke to commit. | 
|  | content::NavigationController& controller = web_contents->GetController(); | 
|  | content::RenderFrameHostTester::CommitPendingLoad(&controller); | 
|  |  | 
|  | EXPECT_EQ(GURL(chrome_extension_constants::kRuntimeHostPermissionsHelpURL), | 
|  | web_contents->GetLastCommittedURL()); | 
|  | } | 
|  |  | 
|  | TEST_F(ExtensionContextMenuModelTest, HistogramTest_Basic) { | 
|  | InitializeEmptyExtensionService(); | 
|  | scoped_refptr<const Extension> extension = | 
|  | ExtensionBuilder("extension").Build(); | 
|  | InitializeAndAddExtension(*extension); | 
|  | constexpr char kHistogramName[] = "Extensions.ContextMenuAction"; | 
|  | { | 
|  | base::HistogramTester tester; | 
|  | { | 
|  | // The menu is constructed, but never shown. | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | } | 
|  | tester.ExpectTotalCount(kHistogramName, 0); | 
|  | } | 
|  |  | 
|  | { | 
|  | base::HistogramTester tester; | 
|  | { | 
|  | // The menu is constructed and shown, but no action is taken. | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | menu.OnMenuWillShow(&menu); | 
|  | menu.MenuClosed(&menu); | 
|  | } | 
|  | tester.ExpectUniqueSample( | 
|  | kHistogramName, ExtensionContextMenuModel::ContextMenuAction::kNoAction, | 
|  | 1 /* expected_count */); | 
|  | } | 
|  |  | 
|  | { | 
|  | base::HistogramTester tester; | 
|  | { | 
|  | // The menu is constructed, shown, and an action taken. | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true); | 
|  | menu.OnMenuWillShow(&menu); | 
|  | menu.ExecuteCommand(ExtensionContextMenuModel::MANAGE_EXTENSIONS, 0); | 
|  | menu.MenuClosed(&menu); | 
|  | } | 
|  |  | 
|  | tester.ExpectUniqueSample( | 
|  | kHistogramName, | 
|  | ExtensionContextMenuModel::ContextMenuAction::kManageExtensions, | 
|  | 1 /* expected_count */); | 
|  | } | 
|  | } | 
|  |  | 
|  | TEST_F(ExtensionContextMenuModelTest, HistogramTest_CustomCommand) { | 
|  | constexpr char kHistogramName[] = "Extensions.ContextMenuAction"; | 
|  |  | 
|  | InitializeEmptyExtensionService(); | 
|  | scoped_refptr<const Extension> extension = | 
|  | ExtensionBuilder("extension") | 
|  | .SetAction(ExtensionBuilder::ActionType::BROWSER_ACTION) | 
|  | .Build(); | 
|  | InitializeAndAddExtension(*extension); | 
|  |  | 
|  | MenuManager* const manager = CreateMenuManager(); | 
|  | ASSERT_TRUE(manager); | 
|  |  | 
|  | MenuBuilder builder(extension, GetBrowser(), manager); | 
|  | builder.AddContextItem(MenuItem::BROWSER_ACTION); | 
|  | std::unique_ptr<ExtensionContextMenuModel> menu = builder.BuildMenu(); | 
|  | EXPECT_EQ(1, CountExtensionItems(*menu)); | 
|  |  | 
|  | base::HistogramTester tester; | 
|  | menu->OnMenuWillShow(menu.get()); | 
|  | menu->ExecuteCommand( | 
|  | ContextMenuMatcher::ConvertToExtensionsCustomCommandId(0), 0); | 
|  | menu->MenuClosed(menu.get()); | 
|  |  | 
|  | tester.ExpectUniqueSample( | 
|  | kHistogramName, | 
|  | ExtensionContextMenuModel::ContextMenuAction::kCustomCommand, | 
|  | 1 /* expected_count */); | 
|  | } | 
|  |  | 
|  | TEST_F(ExtensionContextMenuModelTest, HideToggleVisibility) { | 
|  | InitializeEmptyExtensionService(); | 
|  | scoped_refptr<const Extension> extension = | 
|  | ExtensionBuilder("extension").Build(); | 
|  | InitializeAndAddExtension(*extension); | 
|  | { | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | true /* can_show_icon_in_toolbar */); | 
|  | EXPECT_TRUE( | 
|  | menu.IsCommandIdVisible(ExtensionContextMenuModel::TOGGLE_VISIBILITY)); | 
|  | } | 
|  | { | 
|  | ExtensionContextMenuModel menu(extension.get(), GetBrowser(), | 
|  | ExtensionContextMenuModel::PINNED, nullptr, | 
|  | false /* can_show_icon_in_toolbar */); | 
|  | EXPECT_FALSE( | 
|  | menu.IsCommandIdVisible(ExtensionContextMenuModel::TOGGLE_VISIBILITY)); | 
|  | } | 
|  | } | 
|  |  | 
|  | class ExtensionActionContextMenuModelTest | 
|  | : public ExtensionContextMenuModelTest, | 
|  | public testing::WithParamInterface<ActionInfo::Type> {}; | 
|  |  | 
|  | TEST_P(ExtensionActionContextMenuModelTest, | 
|  | MenuItemShowsOnlyForAppropriateActionType) { | 
|  | const ActionInfo::Type action_type = GetParam(); | 
|  |  | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | scoped_refptr<const Extension> extension = | 
|  | BuildExtensionWithActionType(action_type); | 
|  | service()->AddExtension(extension.get()); | 
|  |  | 
|  | MenuManager* const manager = CreateMenuManager(); | 
|  |  | 
|  | std::set<ActionInfo::Type> mismatched_types = { | 
|  | ActionInfo::TYPE_PAGE, ActionInfo::TYPE_BROWSER, ActionInfo::TYPE_ACTION}; | 
|  | mismatched_types.erase(GetParam()); | 
|  |  | 
|  | // Currently, there are no associated context menu items. | 
|  | MenuBuilder builder(extension, GetBrowser(), manager); | 
|  | EXPECT_EQ(0, CountExtensionItems(*builder.BuildMenu())); | 
|  |  | 
|  | for (ActionInfo::Type type : mismatched_types) { | 
|  | builder.AddContextItem(MenuItemContextForActionType(type)); | 
|  | // Adding a menu item for an invalid type shouldn't result in a visible | 
|  | // menu item. | 
|  | EXPECT_EQ(0, CountExtensionItems(*builder.BuildMenu())); | 
|  | } | 
|  |  | 
|  | builder.AddContextItem(MenuItemContextForActionType(action_type)); | 
|  | EXPECT_EQ(1, CountExtensionItems(*builder.BuildMenu())); | 
|  | } | 
|  |  | 
|  | TEST_P(ExtensionActionContextMenuModelTest, ActionMenuItemsAreLimited) { | 
|  | const ActionInfo::Type action_type = GetParam(); | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | scoped_refptr<const Extension> extension = | 
|  | BuildExtensionWithActionType(action_type); | 
|  | service()->AddExtension(extension.get()); | 
|  |  | 
|  | MenuManager* const manager = CreateMenuManager(); | 
|  |  | 
|  | MenuBuilder builder(extension, GetBrowser(), manager); | 
|  | EXPECT_EQ(0, CountExtensionItems(*builder.BuildMenu())); | 
|  |  | 
|  | const MenuItem::Context context_type = | 
|  | MenuItemContextForActionType(action_type); | 
|  | for (int i = 0; i < api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT + 1; ++i) | 
|  | builder.AddContextItem(context_type); | 
|  |  | 
|  | // Even though LIMIT + 1 items were added, only LIMIT should be displayed. | 
|  | EXPECT_EQ(api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT, | 
|  | CountExtensionItems(*builder.BuildMenu())); | 
|  | } | 
|  |  | 
|  | // Tests that top-level items adjust according to the visibility of others | 
|  | // in the list. | 
|  | TEST_P(ExtensionActionContextMenuModelTest, | 
|  | ActionItemsOverTheLimitAreShownIfSomeOthersAreHidden) { | 
|  | // This test uses hard-coded assumptions about the value of the top-level | 
|  | // limit in order to aid in readability. Assert that the value is expected. | 
|  | // Note: This can't be a static_assert() because the LIMIT is defined in a | 
|  | // different object file. | 
|  | ASSERT_EQ(6, api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT); | 
|  | const ActionInfo::Type action_type = GetParam(); | 
|  | InitializeEmptyExtensionService(); | 
|  |  | 
|  | scoped_refptr<const Extension> extension = | 
|  | BuildExtensionWithActionType(action_type); | 
|  | service()->AddExtension(extension.get()); | 
|  |  | 
|  | MenuManager* const manager = CreateMenuManager(); | 
|  |  | 
|  | MenuBuilder builder(extension, GetBrowser(), manager); | 
|  | EXPECT_EQ(0, CountExtensionItems(*builder.BuildMenu())); | 
|  |  | 
|  | const MenuItem::Context context_type = | 
|  | MenuItemContextForActionType(action_type); | 
|  | constexpr int kNumItemsToAdd = 9;  // 3 over the limit. | 
|  |  | 
|  | // Note: One-indexed; add exactly kNumItemsToAdd (9) items. | 
|  | for (int i = 1; i <= kNumItemsToAdd; ++i) { | 
|  | builder.AddContextItem(context_type); | 
|  | builder.SetItemTitle(i, | 
|  | base::StringPrintf("%s%d", item_label().c_str(), i)); | 
|  | } | 
|  |  | 
|  | // We should cap the visible actions. | 
|  | EXPECT_EQ(api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT, | 
|  | CountExtensionItems(*builder.BuildMenu())); | 
|  |  | 
|  | // By default, the additional action items have their visibility set to true. | 
|  | // Explicitly hide the eighth. | 
|  | builder.SetItemVisibility(8, false); | 
|  |  | 
|  | { | 
|  | auto model = builder.BuildMenu(); | 
|  |  | 
|  | // The limit is still obeyed, so items 7 through 9 should not be visible. | 
|  | EXPECT_EQ(api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT, | 
|  | CountExtensionItems(*model)); | 
|  | VerifyItems(*model, {"1", "2", "3", "4", "5", "6"}); | 
|  | } | 
|  |  | 
|  | // Hide the first two items. | 
|  | builder.SetItemVisibility(1, false); | 
|  | builder.SetItemVisibility(2, false); | 
|  |  | 
|  | { | 
|  | auto model = builder.BuildMenu(); | 
|  | // Hiding the first two items in the model should make visible the "extra" | 
|  | // items -- items 7 and 9. Note, item 8 was set to hidden, so it should not | 
|  | // show in the model. | 
|  | EXPECT_EQ(api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT, | 
|  | CountExtensionItems(*model)); | 
|  | VerifyItems(*model, {"3", "4", "5", "6", "7", "9"}); | 
|  | } | 
|  |  | 
|  | // Unhide the eighth item. | 
|  | builder.SetItemVisibility(8, true); | 
|  |  | 
|  | { | 
|  | auto model = builder.BuildMenu(); | 
|  | // The ninth item should be replaced with the eighth. | 
|  | EXPECT_EQ(api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT, | 
|  | CountExtensionItems(*model)); | 
|  | VerifyItems(*model, {"3", "4", "5", "6", "7", "8"}); | 
|  | } | 
|  |  | 
|  | // Unhide the first two items. | 
|  | builder.SetItemVisibility(1, true); | 
|  | builder.SetItemVisibility(2, true); | 
|  |  | 
|  | { | 
|  | auto model = builder.BuildMenu(); | 
|  | // Unhiding the first two items should put us back into the original state, | 
|  | // with only the first items displayed. | 
|  | EXPECT_EQ(api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT, | 
|  | CountExtensionItems(*model)); | 
|  | VerifyItems(*model, {"1", "2", "3", "4", "5", "6"}); | 
|  | } | 
|  | } | 
|  |  | 
|  | INSTANTIATE_TEST_SUITE_P(All, | 
|  | ExtensionActionContextMenuModelTest, | 
|  | testing::Values(ActionInfo::TYPE_PAGE, | 
|  | ActionInfo::TYPE_BROWSER, | 
|  | ActionInfo::TYPE_ACTION)); | 
|  |  | 
|  | }  // namespace extensions |