blob: 9b51684b3e087f45933fb8353a128f0c0c640cd6 [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/extensions/extension_action_view_controller.h"
#include <stddef.h>
#include <memory>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/command_line.h"
#include "base/json/json_reader.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.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/load_error_reporter.h"
#include "chrome/browser/extensions/scripting_permissions_modifier.h"
#include "chrome/browser/extensions/site_permissions_helper.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/browser/ui/extensions/extension_action_test_helper.h"
#include "chrome/browser/ui/extensions/extensions_container.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_model.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/browser_with_test_window_test.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/permissions_manager.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/common/api/extension_action/action_info.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/mojom/run_location.mojom-shared.h"
#include "extensions/common/user_script.h"
#include "extensions/test/permissions_manager_waiter.h"
#include "extensions/test/test_extension_dir.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/image/image_skia_rep.h"
#include "ui/native_theme/native_theme.h"
using extensions::mojom::ManifestLocation;
using SiteInteraction = extensions::SitePermissionsHelper::SiteInteraction;
using UserSiteSetting = extensions::PermissionsManager::UserSiteSetting;
using HoverCardState = ToolbarActionViewController::HoverCardState;
class ExtensionActionViewControllerUnitTest : public BrowserWithTestWindowTest {
public:
ExtensionActionViewControllerUnitTest() = default;
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::ActionInfo::Type action_type) {
return CreateAndAddExtensionWithGrantedHostPermissions(name, action_type,
{});
}
scoped_refptr<const extensions::Extension>
CreateAndAddExtensionWithGrantedHostPermissions(
const std::string& name,
extensions::ActionInfo::Type action_type,
const std::vector<std::string>& permissions) {
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder(name)
.SetAction(action_type)
.SetLocation(ManifestLocation::kInternal)
.AddPermissions(permissions)
.Build();
if (!permissions.empty())
extension_service()->GrantPermissions(extension.get());
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:
// The ExtensionService associated with the primary profile.
raw_ptr<extensions::ExtensionService> extension_service_ = nullptr;
// ToolbarActionsModel associated with the main profile.
raw_ptr<ToolbarActionsModel> toolbar_model_ = nullptr;
std::unique_ptr<ExtensionActionTestHelper> test_util_;
// The standard size associated with a toolbar action view.
gfx::Size view_size_;
};
// Tests the icon appearance of extension actions in the toolbar.
// Extensions that don't want to run should have their icons grayscaled.
TEST_F(ExtensionActionViewControllerUnitTest,
ExtensionActionWantsToRunAppearance) {
const std::string id =
CreateAndAddExtension("extension", extensions::ActionInfo::TYPE_PAGE)
->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_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_blocked_actions_decoration());
}
// Tests the appearance of browser actions with blocked script actions.
TEST_F(ExtensionActionViewControllerUnitTest, BrowserActionBlockedActions) {
auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
"browser_action", extensions::ActionInfo::TYPE_BROWSER,
{"https://www.google.com/*"});
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_blocked_actions_decoration());
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
ASSERT_TRUE(action_runner);
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::mojom::RunLocation::kDocumentIdle,
base::DoNothing());
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
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_blocked_actions_decoration());
}
// Tests the appearance of page actions with blocked script actions.
TEST_F(ExtensionActionViewControllerUnitTest, PageActionBlockedActions) {
auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
"page_action", extensions::ActionInfo::TYPE_PAGE,
{"https://www.google.com/*"});
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_blocked_actions_decoration());
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::mojom::RunLocation::kDocumentIdle,
base::DoNothing());
image_source = action_controller->GetIconImageSourceForTesting(web_contents,
view_size());
EXPECT_FALSE(image_source->grayscale());
EXPECT_TRUE(image_source->paint_blocked_actions_decoration());
// Simulate NativeTheme update after `image_source` is created.
// `image_source` should paint fine without hitting use-after-free in such
// case. See http://crbug.com/1315967
ui::NativeTheme* theme = ui::NativeTheme::GetInstanceForNativeUi();
theme->NotifyOnNativeThemeUpdated();
image_source->GetImageForScale(1.0f);
}
// 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_F(ExtensionActionViewControllerUnitTest, OnlyHostPermissionsAppearance) {
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder("just hosts")
.SetLocation(ManifestLocation::kInternal)
.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_blocked_actions_decoration());
EXPECT_EQ(u"just hosts", 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_blocked_actions_decoration());
EXPECT_EQ(u"just hosts\nWants access to this site",
action_controller->GetTooltip(web_contents));
// After triggering the action it should have access, which is reflected in
// the tooltip.
action_controller->ExecuteUserAction(
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_blocked_actions_decoration());
EXPECT_EQ(u"just hosts\nHas access to this site",
action_controller->GetTooltip(web_contents));
}
TEST_F(ExtensionActionViewControllerUnitTest,
ExtensionActionContextMenuVisibility) {
std::string id =
CreateAndAddExtension("extension", extensions::ActionInfo::TYPE_BROWSER)
->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(extensions::ExtensionContextMenuModel::
ContextMenuSource::kToolbarAction));
absl::optional<size_t> visibility_index = context_menu->GetIndexOfCommandId(
extensions::ExtensionContextMenuModel::TOGGLE_VISIBILITY);
ASSERT_TRUE(visibility_index.has_value());
std::u16string visibility_label =
context_menu->GetLabelAt(visibility_index.value());
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, run_loop.QuitClosure());
EXPECT_TRUE(container()->IsActionVisibleOnToolbar(action));
// The string should still just be "pin".
check_visibility_string(action, IDS_EXTENSIONS_PIN_TO_TOOLBAR);
}
// TODO(devlin): Now that this is only parameterized in one way, it could be a
// TestWithParamInterface<PermissionType>.
class ExtensionActionViewControllerGrayscaleTest
: public ExtensionActionViewControllerUnitTest {
public:
enum class PermissionType {
kScriptableHost,
kExplicitHost,
};
ExtensionActionViewControllerGrayscaleTest() {}
ExtensionActionViewControllerGrayscaleTest(
const ExtensionActionViewControllerGrayscaleTest&) = delete;
ExtensionActionViewControllerGrayscaleTest& operator=(
const ExtensionActionViewControllerGrayscaleTest&) = delete;
~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);
};
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 < std::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::mojom::RunLocation::kDocumentIdle,
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::ActionInfo::TYPE_BROWSER)
.SetLocation(ManifestLocation::kInternal);
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_F(ExtensionActionViewControllerGrayscaleTest,
GrayscaleIcon_ExplicitHosts) {
RunGrayscaleTest(PermissionType::kExplicitHost);
}
TEST_F(ExtensionActionViewControllerGrayscaleTest,
GrayscaleIcon_ScriptableHosts) {
RunGrayscaleTest(PermissionType::kScriptableHost);
}
TEST_F(ExtensionActionViewControllerUnitTest, RuntimeHostsTooltip) {
auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
"extension name", extensions::ActionInfo::TYPE_BROWSER,
{"https://www.google.com/*"});
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(u"extension name\nWants access to this site",
controller->GetTooltip(web_contents));
// Request access.
extensions::ExtensionActionRunner* action_runner =
extensions::ExtensionActionRunner::GetForWebContents(web_contents);
action_runner->RequestScriptInjectionForTesting(
extension.get(), extensions::mojom::RunLocation::kDocumentIdle,
base::DoNothing());
EXPECT_EQ(u"extension name\nWants access to this site",
controller->GetTooltip(web_contents));
// Grant access.
action_runner->ClearInjectionsForTesting(*extension);
permissions_modifier.GrantHostPermission(kUrl);
EXPECT_EQ(u"extension name\nHas access to this site",
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_F(ExtensionActionViewControllerUnitTest, ActiveTabIconAppearance) {
const GURL kUnlistedHost("https://www.example.com");
const GURL kGrantedHost("https://www.google.com");
const GURL kRestrictedHost("chrome://extensions");
static constexpr char16_t kWantsAccessTooltip[] =
u"active tab\nWants access to this site";
static constexpr char16_t kHasAccessTooltip[] =
u"active tab\nHas access to this site";
static constexpr char16_t kNoAccessTooltip[] = u"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(SiteInteraction::kActiveTab,
controller->GetSiteInteraction(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_blocked_actions_decoration());
EXPECT_EQ(kWantsAccessTooltip, 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(SiteInteraction::kGranted,
controller->GetSiteInteraction(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_blocked_actions_decoration());
EXPECT_EQ(kHasAccessTooltip, controller->GetTooltip(web_contents));
}
// Navigate to a restricted URL and verify the expected appearance.
NavigateAndCommitActiveTab(kRestrictedHost);
{
EXPECT_EQ(SiteInteraction::kNone,
controller->GetSiteInteraction(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_blocked_actions_decoration());
EXPECT_EQ(kNoAccessTooltip, controller->GetTooltip(web_contents));
}
}
// Tests that an extension with the activeTab permission has active tab site
// interaction except for restricted URLs.
TEST_F(ExtensionActionViewControllerUnitTest, GetSiteInteractionWithActiveTab) {
auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
"active tab", extensions::ActionInfo::TYPE_BROWSER, {"activeTab"});
// 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(SiteInteraction::kActiveTab,
controller->GetSiteInteraction(web_contents));
// Click on the action, which grants activeTab and allows the extension to
// access the page. This changes the page interaction status to "granted".
controller->ExecuteUserAction(
ToolbarActionViewController::InvocationSource::kToolbarButton);
EXPECT_EQ(SiteInteraction::kGranted,
controller->GetSiteInteraction(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(SiteInteraction::kNone,
controller->GetSiteInteraction(web_contents));
controller->ExecuteUserAction(
ToolbarActionViewController::InvocationSource::kToolbarButton);
EXPECT_EQ(SiteInteraction::kNone,
controller->GetSiteInteraction(web_contents));
}
// Tests that file URLs only have active tab site interaction if the extension
// has active tab permission and file URL access.
TEST_F(ExtensionActionViewControllerUnitTest,
GetSiteInteractionActiveTabWithFileURL) {
// 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 SiteInteraction 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(SiteInteraction::kNone,
controller->GetSiteInteraction(web_contents));
controller->ExecuteUserAction(
ToolbarActionViewController::InvocationSource::kToolbarButton);
EXPECT_EQ(SiteInteraction::kNone,
controller->GetSiteInteraction(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(SiteInteraction::kActiveTab,
controller->GetSiteInteraction(web_contents));
controller->ExecuteUserAction(
ToolbarActionViewController::InvocationSource::kToolbarButton);
EXPECT_EQ(SiteInteraction::kGranted,
controller->GetSiteInteraction(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_F(ExtensionActionViewControllerUnitTest, TestGetIconWithNullWebContents) {
auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
"extension name", extensions::ActionInfo::TYPE_BROWSER,
{"https://example.com/"});
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());
}
class ExtensionActionViewControllerFeatureUnitTest
: public ExtensionActionViewControllerUnitTest {
public:
ExtensionActionViewControllerFeatureUnitTest() {
scoped_feature_list_.InitAndEnableFeature(
extensions_features::kExtensionsMenuAccessControl);
}
~ExtensionActionViewControllerFeatureUnitTest() override = default;
ExtensionActionViewControllerFeatureUnitTest(
const ExtensionActionViewControllerFeatureUnitTest&) = delete;
ExtensionActionViewControllerFeatureUnitTest& operator=(
const ExtensionActionViewControllerFeatureUnitTest&) = delete;
HoverCardState::SiteAccess GetHoverCardSiteAccessState(
ExtensionActionViewController* controller,
content::WebContents* web_contents) {
return controller->GetHoverCardState(web_contents).site_access;
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Tests hover card status after changing user site settings and site access.
TEST_F(ExtensionActionViewControllerFeatureUnitTest, GetHoverCardStatus) {
std::string url_string = "https://example.com/";
auto extensionA =
CreateAndAddExtension("Extension A", extensions::ActionInfo::TYPE_ACTION);
auto extensionB = CreateAndAddExtensionWithGrantedHostPermissions(
"Extension B", extensions::ActionInfo::TYPE_ACTION, {url_string});
auto extensionC = CreateAndAddExtensionWithGrantedHostPermissions(
"Extension c", extensions::ActionInfo::TYPE_ACTION, {url_string});
AddTab(browser(), GURL(url_string));
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(web_contents);
auto url = url::Origin::Create(web_contents->GetLastCommittedURL());
ExtensionActionViewController* const controllerA =
GetViewControllerForId(extensionA->id());
ASSERT_TRUE(controllerA);
ExtensionActionViewController* const controllerB =
GetViewControllerForId(extensionB->id());
ASSERT_TRUE(controllerB);
ExtensionActionViewController* const controllerC =
GetViewControllerForId(extensionC->id());
ASSERT_TRUE(controllerC);
// By default, user site setting is "customize by extension" and site access
// is granted to every extension that requests them. Thus, verify extension A
// hover card state is "does not want access" and the rest is "have access".
auto* permissions_manager =
extensions::PermissionsManager::Get(browser()->profile());
ASSERT_EQ(permissions_manager->GetUserSiteSetting(url),
UserSiteSetting::kCustomizeByExtension);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerA, web_contents),
HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerB, web_contents),
HoverCardState::SiteAccess::kExtensionHasAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerC, web_contents),
HoverCardState::SiteAccess::kExtensionHasAccess);
// Withhold extension C host permissions. Verify only extension C changed
// hover card state to "requests access".
extensions::ScriptingPermissionsModifier(profile(), extensionC)
.SetWithholdHostPermissions(true);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerA, web_contents),
HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerB, web_contents),
HoverCardState::SiteAccess::kExtensionHasAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerC, web_contents),
HoverCardState::SiteAccess::kExtensionRequestsAccess);
// Grant all extensions site access. Verify extension A hover card state is
// "does not want access" and extensions B and C is "all extensions allowed".
permissions_manager->UpdateUserSiteSetting(
url, UserSiteSetting::kGrantAllExtensions);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerA, web_contents),
HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerB, web_contents),
HoverCardState::SiteAccess::kAllExtensionsAllowed);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerC, web_contents),
HoverCardState::SiteAccess::kAllExtensionsAllowed);
// Block all extensions site access. Verify all extensions appear as "all
// extensions blocked" (even though extension A never requested access).
permissions_manager->UpdateUserSiteSetting(
url, UserSiteSetting::kBlockAllExtensions);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerA, web_contents),
HoverCardState::SiteAccess::kAllExtensionsBlocked);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerB, web_contents),
HoverCardState::SiteAccess::kAllExtensionsBlocked);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerC, web_contents),
HoverCardState::SiteAccess::kAllExtensionsBlocked);
// Change back to customize site access by extension. Verify extension A
// hover card state is "does not want access", extension B is "has access" and
// extension C is "requests access".
permissions_manager->UpdateUserSiteSetting(
url, UserSiteSetting::kCustomizeByExtension);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerA, web_contents),
HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerB, web_contents),
HoverCardState::SiteAccess::kExtensionHasAccess);
EXPECT_EQ(GetHoverCardSiteAccessState(controllerC, web_contents),
HoverCardState::SiteAccess::kExtensionRequestsAccess);
}