blob: 553d1e4be7770b76cb7cb74381f8da7b2420f06a [file] [log] [blame]
// Copyright 2015 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/ui/extensions/extension_action_view_controller.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/json/json_reader.h"
#include "base/run_loop.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/extensions/api/extension_action/extension_action_api.h"
#include "chrome/browser/extensions/chrome_extensions_browser_client.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.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_util.h"
#include "chrome/browser/extensions/scripting_permissions_modifier.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/browser/ui/extensions/icon_with_badge_image_source.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_bar.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_bar_unittest.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/sessions/content/session_tab_helper.h"
#include "content/public/browser/notification_service.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/notification_types.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/user_script.h"
#include "extensions/test/test_extension_dir.h"
#include "ui/base/l10n/l10n_util.h"
namespace {
// A helper class to create a "main" and "overflow" extension toolbar. This is
// used in tests that are relevant to the overflow behavior, and not valid with
// the new ExtensionsMenu (https://crbug.com/943702).
class LegacyToolbarTestHelper {
public:
explicit LegacyToolbarTestHelper(Browser* browser)
: test_util_(ExtensionActionTestHelper::Create(browser, false)),
overflow_test_util_(test_util_->CreateOverflowBar(browser)),
main_bar_(test_util_->GetToolbarActionsBar()),
overflow_bar_(overflow_test_util_->GetToolbarActionsBar()) {
if (base::FeatureList::IsEnabled(features::kExtensionsToolbarMenu))
ADD_FAILURE();
}
~LegacyToolbarTestHelper() = default;
LegacyToolbarTestHelper(const LegacyToolbarTestHelper& other) = delete;
LegacyToolbarTestHelper& operator=(const LegacyToolbarTestHelper& other) =
delete;
ToolbarActionsBar* main_bar() { return main_bar_; }
ToolbarActionsBar* overflow_bar() { return overflow_bar_; }
private:
std::unique_ptr<ExtensionActionTestHelper> test_util_;
std::unique_ptr<ExtensionActionTestHelper> overflow_test_util_;
ToolbarActionsBar* main_bar_ = nullptr;
ToolbarActionsBar* overflow_bar_ = nullptr;
};
enum class ToolbarType {
kExtensionsMenu,
kLegacyToolbar,
};
} // namespace
class ExtensionActionViewControllerUnitTest
: public BrowserWithTestWindowTest,
public testing::WithParamInterface<ToolbarType> {
public:
ExtensionActionViewControllerUnitTest() {
if (GetParam() == ToolbarType::kExtensionsMenu) {
scoped_feature_list_.InitAndEnableFeature(
features::kExtensionsToolbarMenu);
} else {
DCHECK_EQ(ToolbarType::kLegacyToolbar, GetParam());
scoped_feature_list_.InitAndDisableFeature(
features::kExtensionsToolbarMenu);
}
}
ExtensionActionViewControllerUnitTest(
const ExtensionActionViewControllerUnitTest& other) = delete;
ExtensionActionViewControllerUnitTest& operator=(
const ExtensionActionViewControllerUnitTest& other) = delete;
~ExtensionActionViewControllerUnitTest() override = default;
void SetUp() override {
BrowserWithTestWindowTest::SetUp();
// Initialize the various pieces of the extensions system.
extensions::LoadErrorReporter::Init(false);
extensions::TestExtensionSystem* extension_system =
static_cast<extensions::TestExtensionSystem*>(
extensions::ExtensionSystem::Get(profile()));
extension_system->CreateExtensionService(
base::CommandLine::ForCurrentProcess(), base::FilePath(), false);
toolbar_model_ =
extensions::extension_action_test_util::CreateToolbarModelForProfile(
profile());
extension_service_ =
extensions::ExtensionSystem::Get(profile())->extension_service();
test_util_ = ExtensionActionTestHelper::Create(browser(), false);
view_size_ = test_util_->GetToolbarActionSize();
}
void TearDown() override {
test_util_.reset();
BrowserWithTestWindowTest::TearDown();
}
// Sets whether the given |action| wants to run on the |web_contents|.
void SetActionWantsToRunOnTab(extensions::ExtensionAction* action,
content::WebContents* web_contents,
bool wants_to_run) {
action->SetIsVisible(
sessions::SessionTabHelper::IdForTab(web_contents).id(), wants_to_run);
extensions::ExtensionActionAPI::Get(profile())->NotifyChange(
action, web_contents, profile());
}
// Returns the active WebContents for the primary browser.
content::WebContents* GetActiveWebContents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
ExtensionActionViewController* GetViewControllerForId(
const std::string& action_id) {
// It's safe to static cast here, because these tests only deal with
// extensions.
return static_cast<ExtensionActionViewController*>(
test_util_->GetExtensionsContainer()->GetActionForId(action_id));
}
scoped_refptr<const extensions::Extension> CreateAndAddExtension(
const std::string& name,
extensions::ExtensionBuilder::ActionType action_type) {
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder(name)
.SetAction(action_type)
.SetLocation(extensions::Manifest::INTERNAL)
.Build();
extension_service()->AddExtension(extension.get());
return extension;
}
extensions::ExtensionService* extension_service() {
return extension_service_;
}
ToolbarActionsModel* toolbar_model() { return toolbar_model_; }
ExtensionsContainer* container() {
return test_util_->GetExtensionsContainer();
}
const gfx::Size& view_size() const { return view_size_; }
private:
base::test::ScopedFeatureList scoped_feature_list_;
// The ExtensionService associated with the primary profile.
extensions::ExtensionService* extension_service_ = nullptr;
// ToolbarActionsModel associated with the main profile.
ToolbarActionsModel* toolbar_model_ = nullptr;
std::unique_ptr<ExtensionActionTestHelper> test_util_;
// The standard size associated with a toolbar action view.
gfx::Size view_size_;
};
// Helper classes used to instantiate a test for one version of the toolbar
// only. These are used for behavior that is specific to that version, such as
// overflow behavior.
class LegacyExtensionActionViewControllerUnitTest
: public ExtensionActionViewControllerUnitTest {
public:
LegacyExtensionActionViewControllerUnitTest() {
DCHECK_EQ(ToolbarType::kLegacyToolbar, GetParam());
}
};
class ExtensionsMenuExtensionActionViewControllerUnitTest
: public ExtensionActionViewControllerUnitTest {
public:
ExtensionsMenuExtensionActionViewControllerUnitTest() {
DCHECK_EQ(ToolbarType::kExtensionsMenu, GetParam());
}
};
// Tests the icon appearance of extension actions with the toolbar redesign.
// Extensions that don't want to run should have their icons grayscaled.
TEST_P(ExtensionActionViewControllerUnitTest,
ExtensionActionWantsToRunAppearance) {
const std::string id =
CreateAndAddExtension(
"extension", extensions::ExtensionBuilder::ActionType::PAGE_ACTION)
->id();
AddTab(browser(), GURL("chrome://newtab"));
content::WebContents* web_contents = GetActiveWebContents();
ExtensionActionViewController* const action = GetViewControllerForId(id);
ASSERT_TRUE(action);
std::unique_ptr<IconWithBadgeImageSource> image_source =
action->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_TRUE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
SetActionWantsToRunOnTab(action->extension_action(), web_contents, true);
image_source =
action->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
}
// Tests that overflowed extensions with page actions that want to run have an
// additional decoration.
// The overflow menu is only applicable to the legacy toolbar.
TEST_P(LegacyExtensionActionViewControllerUnitTest,
OverflowedPageActionAppearance) {
CreateAndAddExtension("extension",
extensions::ExtensionBuilder::ActionType::PAGE_ACTION);
LegacyToolbarTestHelper test_helper(browser());
EXPECT_EQ(1u, test_helper.main_bar()->GetIconCount());
EXPECT_EQ(0u, test_helper.overflow_bar()->GetIconCount());
AddTab(browser(), GURL("chrome://newtab"));
content::WebContents* web_contents = GetActiveWebContents();
toolbar_model()->SetVisibleIconCount(0u);
EXPECT_EQ(0u, test_helper.main_bar()->GetIconCount());
ASSERT_EQ(1u, test_helper.overflow_bar()->GetIconCount());
auto* const action = static_cast<ExtensionActionViewController*>(
test_helper.overflow_bar()->GetActions()[0]);
std::unique_ptr<IconWithBadgeImageSource> image_source =
action->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_TRUE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
SetActionWantsToRunOnTab(action->extension_action(), web_contents, true);
image_source =
action->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_TRUE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
}
// Tests the appearance of browser actions with blocked script actions.
TEST_P(ExtensionActionViewControllerUnitTest, BrowserActionBlockedActions) {
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("browser action")
.SetAction(extensions::ExtensionBuilder::ActionType::BROWSER_ACTION)
.SetLocation(extensions::Manifest::INTERNAL)
.AddPermission("https://www.google.com/*")
.Build();
extension_service()->GrantPermissions(extension.get());
extension_service()->AddExtension(extension.get());
extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
extension);
permissions_modifier.SetWithholdHostPermissions(true);
AddTab(browser(), GURL("https://www.google.com/"));
ExtensionActionViewController* const action_controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(action_controller);
EXPECT_EQ(extension.get(), action_controller->extension());
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(web_contents);
std::unique_ptr<IconWithBadgeImageSource> image_source =
action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
ASSERT_TRUE(action_runner);
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::UserScript::DOCUMENT_IDLE,
base::DoNothing());
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_TRUE(image_source->paint_blocked_actions_decoration());
action_runner->RunForTesting(extension.get());
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
}
// Tests the appearance of page actions with blocked script actions.
TEST_P(ExtensionActionViewControllerUnitTest, PageActionBlockedActions) {
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("page action")
.SetAction(extensions::ExtensionBuilder::ActionType::PAGE_ACTION)
.SetLocation(extensions::Manifest::INTERNAL)
.AddPermission("https://www.google.com/*")
.Build();
extension_service()->GrantPermissions(extension.get());
extension_service()->AddExtension(extension.get());
extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
extension);
permissions_modifier.SetWithholdHostPermissions(true);
AddTab(browser(), GURL("https://www.google.com/"));
ExtensionActionViewController* const action_controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(action_controller);
EXPECT_EQ(extension.get(), action_controller->extension());
content::WebContents* web_contents = GetActiveWebContents();
std::unique_ptr<IconWithBadgeImageSource> image_source =
action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::UserScript::DOCUMENT_IDLE,
base::DoNothing());
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_TRUE(image_source->paint_blocked_actions_decoration());
}
// Tests the appearance of extension actions for extensions without a browser or
// page action defined in their manifest, but with host permissions on a page.
TEST_P(ExtensionActionViewControllerUnitTest, OnlyHostPermissionsAppearance) {
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("just hosts")
.SetLocation(extensions::Manifest::INTERNAL)
.AddPermission("https://www.google.com/*")
.Build();
extension_service()->GrantPermissions(extension.get());
extension_service()->AddExtension(extension.get());
extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
extension);
permissions_modifier.SetWithholdHostPermissions(true);
ExtensionActionViewController* const action_controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(action_controller);
EXPECT_EQ(extension.get(), action_controller->extension());
// Initially load on a site that the extension doesn't have permissions to.
AddTab(browser(), GURL("https://www.chromium.org/"));
content::WebContents* web_contents = GetActiveWebContents();
std::unique_ptr<IconWithBadgeImageSource> image_source =
action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_TRUE(image_source->grayscale());
EXPECT_FALSE(action_controller->IsEnabled(web_contents));
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
EXPECT_EQ("just hosts",
base::UTF16ToUTF8(action_controller->GetTooltip(web_contents)));
// Navigate to a url the extension does have permissions to. The extension is
// set to run on click and has the current URL withheld, so it should not be
// grayscaled and should be clickable.
NavigateAndCommitActiveTab(GURL("https://www.google.com/"));
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_TRUE(action_controller->IsEnabled(web_contents));
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
EXPECT_EQ("just hosts\nWants access to this site",
base::UTF16ToUTF8(action_controller->GetTooltip(web_contents)));
// After triggering the action it should have access, which is reflected in
// the tooltip.
action_controller->ExecuteAction(
true, ToolbarActionViewController::InvocationSource::kToolbarButton);
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(action_controller->IsEnabled(web_contents));
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
EXPECT_EQ("just hosts\nHas access to this site",
base::UTF16ToUTF8(action_controller->GetTooltip(web_contents)));
}
// Tests the appearance of page actions with blocked actions in the overflow
// menu.
// The overflow menu is only applicable to the legacy toolbar.
TEST_P(LegacyExtensionActionViewControllerUnitTest,
PageActionBlockedActionsInOverflow) {
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("page action")
.SetAction(extensions::ExtensionBuilder::ActionType::PAGE_ACTION)
.SetLocation(extensions::Manifest::INTERNAL)
.AddPermission("https://www.google.com/*")
.Build();
extension_service()->GrantPermissions(extension.get());
extension_service()->AddExtension(extension.get());
extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
extension);
permissions_modifier.SetWithholdHostPermissions(true);
AddTab(browser(), GURL("https://www.google.com/"));
// Overflow the page action and set the page action as wanting to run. We
// shouldn't show the page action decoration because we are showing the
// blocked action decoration (and should only show one at a time).
toolbar_model()->SetVisibleIconCount(0u);
LegacyToolbarTestHelper test_helper(browser());
EXPECT_EQ(0u, test_helper.main_bar()->GetIconCount());
EXPECT_EQ(1u, test_helper.overflow_bar()->GetIconCount());
auto* const action = static_cast<ExtensionActionViewController*>(
test_helper.overflow_bar()->GetActions()[0]);
content::WebContents* web_contents = GetActiveWebContents();
SetActionWantsToRunOnTab(action->extension_action(), web_contents, true);
std::unique_ptr<IconWithBadgeImageSource> image_source =
action->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_TRUE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::UserScript::DOCUMENT_IDLE,
base::DoNothing());
image_source =
action->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_TRUE(image_source->paint_blocked_actions_decoration());
}
TEST_P(LegacyExtensionActionViewControllerUnitTest,
ExtensionActionContextMenuVisibility) {
std::string id =
CreateAndAddExtension(
"extension", extensions::ExtensionBuilder::ActionType::BROWSER_ACTION)
->id();
// Check that the context menu has the proper string for the action's position
// (in the main toolbar, in the overflow container, or temporarily popped
// out).
auto check_visibility_string = [](ToolbarActionViewController* action,
int expected_visibility_string) {
ui::SimpleMenuModel* context_menu =
static_cast<ui::SimpleMenuModel*>(action->GetContextMenu());
int visibility_index = context_menu->GetIndexOfCommandId(
extensions::ExtensionContextMenuModel::TOGGLE_VISIBILITY);
ASSERT_GE(visibility_index, 0);
base::string16 visibility_label =
context_menu->GetLabelAt(visibility_index);
EXPECT_EQ(l10n_util::GetStringUTF16(expected_visibility_string),
visibility_label);
};
LegacyToolbarTestHelper test_helper(browser());
check_visibility_string(test_helper.main_bar()->GetActions()[0],
IDS_EXTENSIONS_HIDE_BUTTON_IN_MENU);
toolbar_model()->SetVisibleIconCount(0u);
check_visibility_string(test_helper.overflow_bar()->GetActions()[0],
IDS_EXTENSIONS_SHOW_BUTTON_IN_TOOLBAR);
base::RunLoop run_loop;
test_helper.main_bar()->PopOutAction(test_helper.main_bar()->GetActions()[0],
false, run_loop.QuitClosure());
run_loop.Run();
check_visibility_string(test_helper.main_bar()->GetActions()[0],
IDS_EXTENSIONS_KEEP_BUTTON_IN_TOOLBAR);
}
TEST_P(ExtensionsMenuExtensionActionViewControllerUnitTest,
ExtensionActionContextMenuVisibility) {
std::string id =
CreateAndAddExtension(
"extension", extensions::ExtensionBuilder::ActionType::BROWSER_ACTION)
->id();
// Check that the context menu has the proper string for the action's pinned
// state.
auto check_visibility_string = [](ToolbarActionViewController* action,
int expected_visibility_string) {
ui::SimpleMenuModel* context_menu =
static_cast<ui::SimpleMenuModel*>(action->GetContextMenu());
int visibility_index = context_menu->GetIndexOfCommandId(
extensions::ExtensionContextMenuModel::TOGGLE_VISIBILITY);
ASSERT_GE(visibility_index, 0);
base::string16 visibility_label =
context_menu->GetLabelAt(visibility_index);
EXPECT_EQ(l10n_util::GetStringUTF16(expected_visibility_string),
visibility_label);
};
ExtensionActionViewController* const action = GetViewControllerForId(id);
ASSERT_TRUE(action);
// Default state: unpinned.
check_visibility_string(action, IDS_EXTENSIONS_PIN_TO_TOOLBAR);
// Pin the extension; re-check.
toolbar_model()->SetActionVisibility(id, true);
check_visibility_string(action, IDS_EXTENSIONS_UNPIN_FROM_TOOLBAR);
// Unpin the extension and ephemerally pop it out.
toolbar_model()->SetActionVisibility(id, false);
EXPECT_FALSE(container()->IsActionVisibleOnToolbar(action));
base::RunLoop run_loop;
container()->PopOutAction(action, false, run_loop.QuitClosure());
EXPECT_TRUE(container()->IsActionVisibleOnToolbar(action));
// The string should still just be "pin".
check_visibility_string(action, IDS_EXTENSIONS_PIN_TO_TOOLBAR);
}
class ExtensionActionViewControllerGrayscaleTest
: public ExtensionActionViewControllerUnitTest {
public:
enum class PermissionType {
kScriptableHost,
kExplicitHost,
};
ExtensionActionViewControllerGrayscaleTest() {}
~ExtensionActionViewControllerGrayscaleTest() override = default;
void RunGrayscaleTest(PermissionType permission_type);
private:
scoped_refptr<const extensions::Extension> CreateExtension(
PermissionType permission_type);
extensions::PermissionsData::PageAccess GetPageAccess(
content::WebContents* web_contents,
scoped_refptr<const extensions::Extension> extensions,
PermissionType permission_type);
DISALLOW_COPY_AND_ASSIGN(ExtensionActionViewControllerGrayscaleTest);
};
void ExtensionActionViewControllerGrayscaleTest::RunGrayscaleTest(
PermissionType permission_type) {
// Create an extension with google.com as either an explicit or scriptable
// host permission.
scoped_refptr<const extensions::Extension> extension =
CreateExtension(permission_type);
extension_service()->GrantPermissions(extension.get());
extension_service()->AddExtension(extension.get());
extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
extension);
permissions_modifier.SetWithholdHostPermissions(true);
const GURL kHasPermissionUrl("https://www.google.com/");
const GURL kNoPermissionsUrl("https://www.chromium.org/");
// Make sure UserScriptListener doesn't hold up the navigation.
extensions::ExtensionsBrowserClient::Get()
->GetUserScriptListener()
->TriggerUserScriptsReadyForTesting(browser()->profile());
// Load up a page that we will navigate for the different test cases.
AddTab(browser(), GURL("about:blank"));
enum class ActionState {
kEnabled,
kDisabled,
};
enum class PageAccessStatus {
// The extension has been granted permission to the host.
kGranted,
// The extension had the host withheld and it has not tried to access the
// page.
kWithheld,
// The extension had the host withheld and it has been blocked when trying
// to access the page.
kBlocked,
// The extension has not been granted permissions to the host, nor was it
// withheld.
kNone,
};
enum class Coloring {
kGrayscale,
kFull,
};
enum class BlockedDecoration {
kPainted,
kNotPainted,
};
struct {
ActionState action_state;
PageAccessStatus page_access;
Coloring expected_coloring;
BlockedDecoration expected_blocked_decoration;
} test_cases[] = {
{ActionState::kEnabled, PageAccessStatus::kNone, Coloring::kFull,
BlockedDecoration::kNotPainted},
{ActionState::kEnabled, PageAccessStatus::kWithheld, Coloring::kFull,
BlockedDecoration::kNotPainted},
{ActionState::kEnabled, PageAccessStatus::kBlocked, Coloring::kFull,
BlockedDecoration::kPainted},
{ActionState::kEnabled, PageAccessStatus::kGranted, Coloring::kFull,
BlockedDecoration::kNotPainted},
{ActionState::kDisabled, PageAccessStatus::kNone, Coloring::kGrayscale,
BlockedDecoration::kNotPainted},
{ActionState::kDisabled, PageAccessStatus::kWithheld, Coloring::kFull,
BlockedDecoration::kNotPainted},
{ActionState::kDisabled, PageAccessStatus::kBlocked, Coloring::kFull,
BlockedDecoration::kPainted},
{ActionState::kDisabled, PageAccessStatus::kGranted, Coloring::kFull,
BlockedDecoration::kNotPainted},
};
ExtensionActionViewController* const controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(controller);
content::WebContents* web_contents = GetActiveWebContents();
extensions::ExtensionAction* extension_action =
extensions::ExtensionActionManager::Get(profile())->GetExtensionAction(
*extension);
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
for (size_t i = 0; i < base::size(test_cases); ++i) {
SCOPED_TRACE(
base::StringPrintf("Running test case %d", static_cast<int>(i)));
const auto& test_case = test_cases[i];
// Set up the proper state for the test case.
switch (test_case.page_access) {
case PageAccessStatus::kNone: {
NavigateAndCommitActiveTab(kNoPermissionsUrl);
// Page access should be denied; verify.
extensions::PermissionsData::PageAccess page_access =
GetPageAccess(web_contents, extension, permission_type);
EXPECT_EQ(extensions::PermissionsData::PageAccess::kDenied,
page_access);
break;
}
case PageAccessStatus::kWithheld: {
NavigateAndCommitActiveTab(kHasPermissionUrl);
// Page access should already be withheld; verify.
extensions::PermissionsData::PageAccess page_access =
GetPageAccess(web_contents, extension, permission_type);
EXPECT_EQ(extensions::PermissionsData::PageAccess::kWithheld,
page_access);
break;
}
case PageAccessStatus::kBlocked:
// Navigate to a page where the permission is currently withheld and try
// to inject a script.
NavigateAndCommitActiveTab(kHasPermissionUrl);
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::UserScript::DOCUMENT_IDLE,
base::DoNothing());
break;
case PageAccessStatus::kGranted:
// Grant the withheld requested permission and navigate there.
permissions_modifier.GrantHostPermission(kHasPermissionUrl);
NavigateAndCommitActiveTab(kHasPermissionUrl);
break;
}
// Enable or disable the action based on the test case.
extension_action->SetIsVisible(
tab_id, test_case.action_state == ActionState::kEnabled);
std::unique_ptr<IconWithBadgeImageSource> image_source =
controller->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_EQ(test_case.expected_coloring == Coloring::kGrayscale,
image_source->grayscale());
EXPECT_EQ(
test_case.expected_blocked_decoration == BlockedDecoration::kPainted,
image_source->paint_blocked_actions_decoration());
// Clean up permissions state.
if (test_case.page_access == PageAccessStatus::kGranted)
permissions_modifier.RemoveGrantedHostPermission(kHasPermissionUrl);
action_runner->ClearInjectionsForTesting(*extension);
}
}
scoped_refptr<const extensions::Extension>
ExtensionActionViewControllerGrayscaleTest::CreateExtension(
PermissionType permission_type) {
extensions::ExtensionBuilder builder("extension");
builder.SetAction(extensions::ExtensionBuilder::ActionType::BROWSER_ACTION)
.SetLocation(extensions::Manifest::INTERNAL);
constexpr char kHostGoogle[] = "https://www.google.com/*";
switch (permission_type) {
case PermissionType::kScriptableHost: {
builder.AddContentScript("script.js", {kHostGoogle});
break;
}
case PermissionType::kExplicitHost:
builder.AddPermission(kHostGoogle);
break;
}
return builder.Build();
}
extensions::PermissionsData::PageAccess
ExtensionActionViewControllerGrayscaleTest::GetPageAccess(
content::WebContents* web_contents,
scoped_refptr<const extensions::Extension> extension,
PermissionType permission_type) {
int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
GURL url = web_contents->GetLastCommittedURL();
switch (permission_type) {
case PermissionType::kExplicitHost:
return extension->permissions_data()->GetPageAccess(url, tab_id,
/*error=*/nullptr);
case PermissionType::kScriptableHost:
return extension->permissions_data()->GetContentScriptAccess(
url, tab_id, /*error=*/nullptr);
}
}
// Tests the behavior for icon grayscaling. Ideally, these would be a single
// parameterized test, but toolbar tests are already parameterized with the UI
// mode.
TEST_P(ExtensionActionViewControllerGrayscaleTest,
GrayscaleIcon_ExplicitHosts) {
RunGrayscaleTest(PermissionType::kExplicitHost);
}
TEST_P(ExtensionActionViewControllerGrayscaleTest,
GrayscaleIcon_ScriptableHosts) {
RunGrayscaleTest(PermissionType::kScriptableHost);
}
INSTANTIATE_TEST_SUITE_P(All,
ExtensionActionViewControllerGrayscaleTest,
testing::Values(ToolbarType::kExtensionsMenu,
ToolbarType::kLegacyToolbar));
TEST_P(ExtensionActionViewControllerUnitTest, RuntimeHostsTooltip) {
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("extension name")
.SetAction(extensions::ExtensionBuilder::ActionType::BROWSER_ACTION)
.SetLocation(extensions::Manifest::INTERNAL)
.AddPermission("https://www.google.com/*")
.Build();
extension_service()->GrantPermissions(extension.get());
extension_service()->AddExtension(extension.get());
extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
extension);
permissions_modifier.SetWithholdHostPermissions(true);
const GURL kUrl("https://www.google.com/");
AddTab(browser(), kUrl);
ExtensionActionViewController* const controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(controller);
content::WebContents* web_contents = GetActiveWebContents();
int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
// Page access should already be withheld.
EXPECT_EQ(extensions::PermissionsData::PageAccess::kWithheld,
extension->permissions_data()->GetPageAccess(kUrl, tab_id,
/*error=*/nullptr));
EXPECT_EQ("extension name\nWants access to this site",
base::UTF16ToUTF8(controller->GetTooltip(web_contents)));
// Request access.
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::UserScript::DOCUMENT_IDLE,
base::DoNothing());
EXPECT_EQ("extension name\nWants access to this site",
base::UTF16ToUTF8(controller->GetTooltip(web_contents)));
// Grant access.
action_runner->ClearInjectionsForTesting(*extension);
permissions_modifier.GrantHostPermission(kUrl);
EXPECT_EQ("extension name\nHas access to this site",
base::UTF16ToUTF8(controller->GetTooltip(web_contents)));
}
// Tests the appearance of extension actions for an extension with the activeTab
// permission and no browser or page action defined in their manifest.
TEST_P(ExtensionActionViewControllerUnitTest, ActiveTabIconAppearance) {
const GURL kUnlistedHost("https://www.example.com");
const GURL kGrantedHost("https://www.google.com");
const GURL kRestrictedHost("chrome://extensions");
const std::string kWantsAccessTooltip(
"active tab\nWants access to this site");
const std::string kHasAccessTooltip("active tab\nHas access to this site");
const std::string kNoAccessTooltip("active tab");
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("active tab")
.AddPermission("activeTab")
.AddPermission(kGrantedHost.spec())
.Build();
extension_service()->GrantPermissions(extension.get());
extension_service()->AddExtension(extension.get());
// Navigate the browser to a site the extension doesn't have explicit access
// to and verify the expected appearance.
AddTab(browser(), kUnlistedHost);
ExtensionActionViewController* const controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(controller);
content::WebContents* web_contents = GetActiveWebContents();
{
EXPECT_EQ(ExtensionActionViewController::PageInteractionStatus::kPending,
controller->GetPageInteractionStatus(web_contents));
EXPECT_TRUE(controller->IsEnabled(web_contents));
std::unique_ptr<IconWithBadgeImageSource> image_source =
controller->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
EXPECT_EQ(kWantsAccessTooltip,
base::UTF16ToUTF8(controller->GetTooltip(web_contents)));
}
// Navigate to a site which the extension does have explicit host access to
// and verify the expected appearance.
NavigateAndCommitActiveTab(kGrantedHost);
{
EXPECT_EQ(ExtensionActionViewController::PageInteractionStatus::kActive,
controller->GetPageInteractionStatus(web_contents));
// This is a little unintuitive, but if an extension is using a page action
// and has not specified any declarative rules or manually changed it's
// enabled state, it can have access to a page but be in the disabled state.
// The icon will still be colored however.
EXPECT_FALSE(controller->IsEnabled(web_contents));
std::unique_ptr<IconWithBadgeImageSource> image_source =
controller->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
EXPECT_EQ(kHasAccessTooltip,
base::UTF16ToUTF8(controller->GetTooltip(web_contents)));
}
// Navigate to a restricted URL and verify the expected appearance.
NavigateAndCommitActiveTab(kRestrictedHost);
{
EXPECT_EQ(ExtensionActionViewController::PageInteractionStatus::kNone,
controller->GetPageInteractionStatus(web_contents));
EXPECT_FALSE(controller->IsEnabled(web_contents));
std::unique_ptr<IconWithBadgeImageSource> image_source =
controller->GetIconImageSourceForTesting(web_contents, view_size());
EXPECT_TRUE(image_source->grayscale());
EXPECT_FALSE(image_source->paint_page_action_decoration());
EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
EXPECT_EQ(kNoAccessTooltip,
base::UTF16ToUTF8(controller->GetTooltip(web_contents)));
}
}
// Tests that an extension with the activeTab permission is shown to be pending
// user approval for normal web pages, but not for restricted URLs.
TEST_P(ExtensionActionViewControllerUnitTest,
GetPageInteractionStatusWithActiveTab) {
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("active tab")
.SetAction(extensions::ExtensionBuilder::ActionType::BROWSER_ACTION)
.AddPermission("activeTab")
.Build();
extension_service()->AddExtension(extension.get());
// Navigate the browser to google.com. Since clicking the extension would
// grant access to the page, the page interaction status should show as
// "pending".
AddTab(browser(), GURL("https://www.google.com/"));
ExtensionActionViewController* const controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(controller);
content::WebContents* web_contents = GetActiveWebContents();
EXPECT_EQ(ExtensionActionViewController::PageInteractionStatus::kPending,
controller->GetPageInteractionStatus(web_contents));
// Click on the action, which grants activeTab and allows the extension to
// access the page. This changes the page interaction status to "active".
controller->ExecuteAction(
true, ToolbarActionViewController::InvocationSource::kToolbarButton);
EXPECT_EQ(ExtensionActionViewController::PageInteractionStatus::kActive,
controller->GetPageInteractionStatus(web_contents));
// Now navigate to a restricted URL. Clicking the extension won't give access
// here, so the page interaction status should be "none".
NavigateAndCommitActiveTab(GURL("chrome://extensions"));
EXPECT_EQ(ExtensionActionViewController::PageInteractionStatus::kNone,
controller->GetPageInteractionStatus(web_contents));
controller->ExecuteAction(
true, ToolbarActionViewController::InvocationSource::kToolbarButton);
EXPECT_EQ(ExtensionActionViewController::PageInteractionStatus::kNone,
controller->GetPageInteractionStatus(web_contents));
}
// Tests that file URLs only show as pending user approval for activeTab
// extensions if the extension has file URL access.
TEST_P(ExtensionActionViewControllerUnitTest,
GetPageInteractionStatusActiveTabWithFileURL) {
// We need to use a TestExtensionDir here to allow for the reload when giving
// an extension file URL access.
extensions::TestExtensionDir test_dir;
test_dir.WriteManifest(R"(
{
"name": "Active Tab Page Interaction with File URLs",
"description": "Testing PageInteractionStatus and ActiveTab on file URLs",
"version": "0.1",
"manifest_version": 2,
"browser_action": {},
"permissions": ["activeTab"]
})");
extensions::ChromeTestExtensionLoader loader(profile());
loader.set_allow_file_access(false);
scoped_refptr<const extensions::Extension> extension =
loader.LoadExtension(test_dir.UnpackedPath());
// Navigate to a file URL. The page interaction status should be "none", as
// the extension doesn't have file URL access granted. Clicking it should
// result in no change.
AddTab(browser(), GURL("file://foo"));
ExtensionActionViewController* controller =
GetViewControllerForId(extension->id());
ASSERT_TRUE(controller);
content::WebContents* web_contents = GetActiveWebContents();
EXPECT_EQ(ExtensionActionViewController::PageInteractionStatus::kNone,
controller->GetPageInteractionStatus(web_contents));
controller->ExecuteAction(
true, ToolbarActionViewController::InvocationSource::kToolbarButton);
EXPECT_EQ(ExtensionActionViewController::PageInteractionStatus::kNone,
controller->GetPageInteractionStatus(web_contents));
// After being granted access to file URLs the page interaction status should
// show as "pending". A click will grant activeTab, giving access to the page
// and will change the page interaction status to "active".
extensions::TestExtensionRegistryObserver observer(
extensions::ExtensionRegistry::Get(profile()), extension->id());
extensions::util::SetAllowFileAccess(extension->id(), profile(),
true /*allow*/);
extension = observer.WaitForExtensionLoaded();
ASSERT_TRUE(extension);
// Refresh the controller as the extension has been reloaded.
controller = GetViewControllerForId(extension->id());
EXPECT_EQ(ExtensionActionViewController::PageInteractionStatus::kPending,
controller->GetPageInteractionStatus(web_contents));
controller->ExecuteAction(
true, ToolbarActionViewController::InvocationSource::kToolbarButton);
EXPECT_EQ(ExtensionActionViewController::PageInteractionStatus::kActive,
controller->GetPageInteractionStatus(web_contents));
}
// ExtensionActionViewController::GetIcon() can potentially be called with a
// null web contents if the tab strip model doesn't know of an active tab
// (though it's a bit unclear when this is the case).
// See https://crbug.com/888121
TEST_P(ExtensionActionViewControllerUnitTest, TestGetIconWithNullWebContents) {
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("extension name")
.SetAction(extensions::ExtensionBuilder::ActionType::BROWSER_ACTION)
.AddPermission("https://example.com/")
.Build();
extension_service()->GrantPermissions(extension.get());
extension_service()->AddExtension(extension.get());
extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
extension);
permissions_modifier.SetWithholdHostPermissions(true);
// Try getting an icon with no active web contents. Nothing should crash, and
// a non-empty icon should be returned.
ExtensionActionViewController* const controller =
GetViewControllerForId(extension->id());
gfx::Image icon = controller->GetIcon(nullptr, view_size());
EXPECT_FALSE(icon.IsEmpty());
}
INSTANTIATE_TEST_SUITE_P(All,
ExtensionActionViewControllerUnitTest,
testing::Values(ToolbarType::kExtensionsMenu,
ToolbarType::kLegacyToolbar));
INSTANTIATE_TEST_SUITE_P(,
LegacyExtensionActionViewControllerUnitTest,
testing::Values(ToolbarType::kLegacyToolbar));
INSTANTIATE_TEST_SUITE_P(,
ExtensionsMenuExtensionActionViewControllerUnitTest,
testing::Values(ToolbarType::kExtensionsMenu));