blob: b8614c7c9c441876fcdeb1bf1e0c0cce6f83da3e [file] [log] [blame]
// Copyright 2014 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 <memory>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/api/commands/command_service.h"
#include "chrome/browser/extensions/extension_action_runner.h"
#include "chrome/browser/extensions/extension_view.h"
#include "chrome/browser/extensions/extension_view_host.h"
#include "chrome/browser/extensions/extension_view_host_factory.h"
#include "chrome/browser/extensions/site_permissions_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/extensions/extension_action_platform_delegate.h"
#include "chrome/browser/ui/extensions/extension_popup_types.h"
#include "chrome/browser/ui/extensions/extensions_container.h"
#include "chrome/browser/ui/extensions/icon_with_badge_image_source.h"
#include "chrome/browser/ui/toolbar/toolbar_action_view_delegate.h"
#include "chrome/grit/generated_resources.h"
#include "components/sessions/content/session_tab_helper.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_action.h"
#include "extensions/browser/extension_action_manager.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/api/extension_action/action_info.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/manifest_constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/color/color_provider_manager.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/native_theme/native_theme.h"
using extensions::ActionInfo;
using extensions::CommandService;
using extensions::ExtensionActionRunner;
using extensions::PermissionsManager;
namespace {
void RecordInvocationSource(
ToolbarActionViewController::InvocationSource source) {
base::UmaHistogramEnumeration("Extensions.Toolbar.InvocationSource", source);
}
// Computes hover card site access status based on:
// 1. Extension wants site access: user site settings takes precedence
// over the extension's site access.
// 2. Extension does not want access: if all extensions are blocked display
// such message because a) user could wrongly infer that an extension that
// does not want access has access if we only show the blocked message for
// extensions that want access; and b) it helps us work around tricky
// calculations where we get into collisions between withheld and denied
// permission. Otherwise, it should display "does not want access".
ExtensionActionViewController::HoverCardState::SiteAccess
GetHoverCardSiteAccessState(
extensions::PermissionsManager::UserSiteSetting site_setting,
extensions::SitePermissionsHelper::SiteInteraction site_interaction) {
switch (site_interaction) {
case extensions::SitePermissionsHelper::SiteInteraction::kGranted:
return site_setting == extensions::PermissionsManager::UserSiteSetting::
kGrantAllExtensions
? ExtensionActionViewController::HoverCardState::SiteAccess::
kAllExtensionsAllowed
: ExtensionActionViewController::HoverCardState::SiteAccess::
kExtensionHasAccess;
case extensions::SitePermissionsHelper::SiteInteraction::kWithheld:
case extensions::SitePermissionsHelper::SiteInteraction::kActiveTab:
return site_setting == extensions::PermissionsManager::UserSiteSetting::
kBlockAllExtensions
? ExtensionActionViewController::HoverCardState::SiteAccess::
kAllExtensionsBlocked
: ExtensionActionViewController::HoverCardState::SiteAccess::
kExtensionRequestsAccess;
case extensions::SitePermissionsHelper::SiteInteraction::kNone:
// kNone site interaction includes extensions that don't want access when
// user site setting is "block all extensions".
return site_setting == extensions::PermissionsManager::UserSiteSetting::
kBlockAllExtensions
? ExtensionActionViewController::HoverCardState::SiteAccess::
kAllExtensionsBlocked
: ExtensionActionViewController::HoverCardState::SiteAccess::
kExtensionDoesNotWantAccess;
}
}
// Computes hover card policy status based on admin policy. Note that an
// extension pinned by admin is also installed by admin. Thus, "pinned by admin"
// has preference.
ExtensionActionViewController::HoverCardState::AdminPolicy
GetHoverCardPolicyState(Browser* browser,
const extensions::ExtensionId& extension_id) {
auto* const model = ToolbarActionsModel::Get(browser->profile());
if (model->IsActionForcePinned(extension_id))
return ExtensionActionViewController::HoverCardState::AdminPolicy::
kPinnedByAdmin;
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionRegistry::Get(browser->profile())
->enabled_extensions()
.GetByID(extension_id);
if (extensions::Manifest::IsPolicyLocation(extension->location()))
return ExtensionActionViewController::HoverCardState::AdminPolicy::
kInstalledByAdmin;
return ExtensionActionViewController::HoverCardState::AdminPolicy::kNone;
}
} // namespace
// static
std::unique_ptr<ExtensionActionViewController>
ExtensionActionViewController::Create(
const extensions::ExtensionId& extension_id,
Browser* browser,
ExtensionsContainer* extensions_container) {
DCHECK(browser);
DCHECK(extensions_container);
auto* registry = extensions::ExtensionRegistry::Get(browser->profile());
scoped_refptr<const extensions::Extension> extension =
registry->enabled_extensions().GetByID(extension_id);
DCHECK(extension);
extensions::ExtensionAction* extension_action =
extensions::ExtensionActionManager::Get(browser->profile())
->GetExtensionAction(*extension);
DCHECK(extension_action);
// WrapUnique() because the constructor is private.
return base::WrapUnique(new ExtensionActionViewController(
std::move(extension), browser, extension_action, registry,
extensions_container));
}
// static
bool ExtensionActionViewController::AnyActionHasCurrentSiteAccess(
const std::vector<std::unique_ptr<ToolbarActionViewController>>& actions,
content::WebContents* web_contents) {
for (const auto& action : actions) {
if (action->GetSiteInteraction(web_contents) ==
extensions::SitePermissionsHelper::SiteInteraction::kGranted) {
return true;
}
}
return false;
}
ExtensionActionViewController::ExtensionActionViewController(
scoped_refptr<const extensions::Extension> extension,
Browser* browser,
extensions::ExtensionAction* extension_action,
extensions::ExtensionRegistry* extension_registry,
ExtensionsContainer* extensions_container)
: extension_(std::move(extension)),
browser_(browser),
extension_action_(extension_action),
extensions_container_(extensions_container),
popup_host_(nullptr),
view_delegate_(nullptr),
platform_delegate_(ExtensionActionPlatformDelegate::Create(this)),
icon_factory_(browser->profile(),
extension_.get(),
extension_action,
this),
extension_registry_(extension_registry) {}
ExtensionActionViewController::~ExtensionActionViewController() {
DCHECK(!IsShowingPopup());
}
std::string ExtensionActionViewController::GetId() const {
return extension_->id();
}
void ExtensionActionViewController::SetDelegate(
ToolbarActionViewDelegate* delegate) {
DCHECK((delegate == nullptr) ^ (view_delegate_ == nullptr));
if (delegate) {
view_delegate_ = delegate;
} else {
HidePopup();
platform_delegate_.reset();
view_delegate_ = nullptr;
}
}
gfx::Image ExtensionActionViewController::GetIcon(
content::WebContents* web_contents,
const gfx::Size& size) {
if (!ExtensionIsValid())
return gfx::Image();
return gfx::Image(
gfx::ImageSkia(GetIconImageSource(web_contents, size), size));
}
std::u16string ExtensionActionViewController::GetActionName() const {
if (!ExtensionIsValid())
return std::u16string();
return base::UTF8ToUTF16(extension_->name());
}
std::u16string ExtensionActionViewController::GetAccessibleName(
content::WebContents* web_contents) const {
if (!ExtensionIsValid())
return std::u16string();
// GetAccessibleName() can (surprisingly) be called during browser
// teardown. Handle this gracefully.
if (!web_contents)
return base::UTF8ToUTF16(extension()->name());
std::string title = extension_action()->GetTitle(
sessions::SessionTabHelper::IdForTab(web_contents).id());
std::u16string title_utf16 =
base::UTF8ToUTF16(title.empty() ? extension()->name() : title);
// Include a "host access" portion of the tooltip if the extension has active
// or pending interaction with the site.
auto site_interaction = GetSiteInteraction(web_contents);
int site_interaction_description_id = -1;
switch (site_interaction) {
case extensions::SitePermissionsHelper::SiteInteraction::kNone:
// No string for neither having nor wanting access.
break;
case extensions::SitePermissionsHelper::SiteInteraction::kWithheld:
case extensions::SitePermissionsHelper::SiteInteraction::kActiveTab:
site_interaction_description_id = IDS_EXTENSIONS_WANTS_ACCESS_TO_SITE;
break;
case extensions::SitePermissionsHelper::SiteInteraction::kGranted:
site_interaction_description_id = IDS_EXTENSIONS_HAS_ACCESS_TO_SITE;
break;
}
if (site_interaction_description_id != -1) {
title_utf16 = base::StrCat(
{title_utf16, u"\n",
l10n_util::GetStringUTF16(site_interaction_description_id)});
}
return title_utf16;
}
std::u16string ExtensionActionViewController::GetTooltip(
content::WebContents* web_contents) const {
return GetAccessibleName(web_contents);
}
bool ExtensionActionViewController::IsEnabled(
content::WebContents* web_contents) const {
if (!ExtensionIsValid())
return false;
extensions::SitePermissionsHelper::SiteInteraction site_interaction =
GetSiteInteraction(web_contents);
return extension_action_->GetIsVisible(
sessions::SessionTabHelper::IdForTab(web_contents).id()) ||
site_interaction ==
extensions::SitePermissionsHelper::SiteInteraction::kWithheld ||
site_interaction ==
extensions::SitePermissionsHelper::SiteInteraction::kActiveTab;
}
bool ExtensionActionViewController::IsShowingPopup() const {
return popup_host_ != nullptr;
}
bool ExtensionActionViewController::IsRequestingSiteAccess(
content::WebContents* web_contents) const {
return GetSiteInteraction(web_contents) ==
extensions::SitePermissionsHelper::SiteInteraction::kWithheld;
}
void ExtensionActionViewController::HidePopup() {
if (IsShowingPopup()) {
// Only call Close() on the popup if it's been shown; otherwise, the popup
// will be cleaned up in ShowPopup().
if (has_opened_popup_)
popup_host_->Close();
// We need to do these actions synchronously (instead of closing and then
// performing the rest of the cleanup in OnExtensionHostDestroyed()) because
// the extension host may close asynchronously, and we need to keep the view
// delegate up to date.
if (popup_host_)
OnPopupClosed();
}
}
gfx::NativeView ExtensionActionViewController::GetPopupNativeView() {
return popup_host_ ? popup_host_->view()->GetNativeView() : nullptr;
}
ui::MenuModel* ExtensionActionViewController::GetContextMenu(
extensions::ExtensionContextMenuModel::ContextMenuSource
context_menu_source) {
if (!ExtensionIsValid())
return nullptr;
ToolbarActionViewController* const action =
extensions_container_->GetActionForId(GetId());
extensions::ExtensionContextMenuModel::ButtonVisibility visibility =
extensions_container_->GetActionVisibility(action);
// Reconstruct the menu every time because the menu's contents are dynamic.
context_menu_model_ = std::make_unique<extensions::ExtensionContextMenuModel>(
extension(), browser_, visibility, this,
view_delegate_->CanShowIconInToolbar(), context_menu_source);
return context_menu_model_.get();
}
void ExtensionActionViewController::OnContextMenuShown() {
extensions_container_->OnContextMenuShown(this);
}
void ExtensionActionViewController::OnContextMenuClosed() {
extensions_container_->OnContextMenuClosed(this);
}
void ExtensionActionViewController::ExecuteUserAction(InvocationSource source) {
if (!ExtensionIsValid())
return;
if (!IsEnabled(view_delegate_->GetCurrentWebContents())) {
GetPreferredPopupViewController()
->view_delegate_->ShowContextMenuAsFallback();
return;
}
content::WebContents* const web_contents =
view_delegate_->GetCurrentWebContents();
ExtensionActionRunner* action_runner =
ExtensionActionRunner::GetForWebContents(web_contents);
if (!action_runner)
return;
RecordInvocationSource(source);
extensions_container_->CloseOverflowMenuIfOpen();
// This method is only called to execute an action by the user, so we can
// always grant tab permissions.
constexpr bool kGrantTabPermissions = true;
if (action_runner->RunAction(extension(), kGrantTabPermissions) ==
extensions::ExtensionAction::ACTION_SHOW_POPUP) {
constexpr bool kByUser = true;
GetPreferredPopupViewController()->TriggerPopup(
PopupShowAction::kShow, kByUser, ShowPopupCallback());
}
}
void ExtensionActionViewController::TriggerPopupForAPI(
ShowPopupCallback callback) {
RecordInvocationSource(InvocationSource::kApi);
// This method is called programmatically by an API; it should never be
// considered a user action.
constexpr bool kByUser = false;
TriggerPopup(PopupShowAction::kShow, kByUser, std::move(callback));
}
void ExtensionActionViewController::UpdateState() {
if (!ExtensionIsValid())
return;
view_delegate_->UpdateState();
}
void ExtensionActionViewController::UpdateHoverCard(
ToolbarActionView* action_view,
ToolbarActionHoverCardUpdateType update_type) {
if (!ExtensionIsValid())
return;
extensions_container_->UpdateToolbarActionHoverCard(action_view, update_type);
}
void ExtensionActionViewController::RegisterCommand() {
if (!ExtensionIsValid())
return;
platform_delegate_->RegisterCommand();
}
void ExtensionActionViewController::UnregisterCommand() {
platform_delegate_->UnregisterCommand();
}
void ExtensionActionViewController::InspectPopup() {
// This method is only triggered through user action (clicking on the context
// menu entry).
constexpr bool kByUser = true;
GetPreferredPopupViewController()->TriggerPopup(
PopupShowAction::kShowAndInspect, kByUser, ShowPopupCallback());
}
void ExtensionActionViewController::OnIconUpdated() {
// We update the view first, so that if the observer relies on its UI it can
// be ready.
if (view_delegate_)
view_delegate_->UpdateState();
}
void ExtensionActionViewController::OnExtensionHostDestroyed(
extensions::ExtensionHost* host) {
OnPopupClosed();
}
extensions::SitePermissionsHelper::SiteInteraction
ExtensionActionViewController::GetSiteInteraction(
content::WebContents* web_contents) const {
return extensions::SitePermissionsHelper(browser_->profile())
.GetSiteInteraction(*extension(), web_contents);
}
bool ExtensionActionViewController::ExtensionIsValid() const {
return extension_registry_->enabled_extensions().Contains(extension_->id());
}
bool ExtensionActionViewController::GetExtensionCommand(
extensions::Command* command) const {
DCHECK(command);
if (!ExtensionIsValid())
return false;
CommandService* command_service = CommandService::Get(browser_->profile());
return command_service->GetExtensionActionCommand(
extension_->id(), extension_action_->action_type(),
CommandService::ACTIVE, command, nullptr);
}
ToolbarActionViewController::HoverCardState
ExtensionActionViewController::GetHoverCardState(
content::WebContents* web_contents) const {
DCHECK(ExtensionIsValid());
DCHECK(web_contents);
url::Origin origin =
web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin();
extensions::PermissionsManager::UserSiteSetting site_setting =
extensions::PermissionsManager::Get(browser_->profile())
->GetUserSiteSetting(origin);
auto site_interaction = GetSiteInteraction(web_contents);
HoverCardState state;
state.site_access =
GetHoverCardSiteAccessState(site_setting, site_interaction);
state.policy = GetHoverCardPolicyState(browser_, GetId());
return state;
}
bool ExtensionActionViewController::CanHandleAccelerators() const {
if (!ExtensionIsValid())
return false;
#if DCHECK_IS_ON()
{
extensions::Command command;
DCHECK(GetExtensionCommand(&command));
}
#endif
// Page action accelerators are enabled if and only if the page action is
// enabled ("visible" in legacy terms) on the given tab. Other actions can
// always accept accelerators.
// TODO(devlin): Have all actions behave similarly; this should likely mean
// always checking IsEnabled(). It's weird to use a keyboard shortcut on a
// disabled action (in most cases, this will result in opening the context
// menu).
if (extension_action_->action_type() == extensions::ActionInfo::TYPE_PAGE)
return IsEnabled(view_delegate_->GetCurrentWebContents());
return true;
}
std::unique_ptr<IconWithBadgeImageSource>
ExtensionActionViewController::GetIconImageSourceForTesting(
content::WebContents* web_contents,
const gfx::Size& size) {
return GetIconImageSource(web_contents, size);
}
ExtensionActionViewController*
ExtensionActionViewController::GetPreferredPopupViewController() {
return static_cast<ExtensionActionViewController*>(
extensions_container_->GetActionForId(GetId()));
}
void ExtensionActionViewController::TriggerPopup(PopupShowAction show_action,
bool by_user,
ShowPopupCallback callback) {
DCHECK(ExtensionIsValid());
DCHECK_EQ(this, GetPreferredPopupViewController());
content::WebContents* const web_contents =
view_delegate_->GetCurrentWebContents();
const int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
DCHECK(extension_action_->GetIsVisible(tab_id));
DCHECK(extension_action_->HasPopup(tab_id));
const GURL popup_url = extension_action_->GetPopupUrl(tab_id);
std::unique_ptr<extensions::ExtensionViewHost> host =
extensions::ExtensionViewHostFactory::CreatePopupHost(popup_url,
browser_);
// Creating a host should never fail in this case, since the extension is
// valid and has a valid popup URL.
CHECK(host);
// Always hide the current popup, even if it's not owned by this extension.
// Only one popup should be visible at a time.
extensions_container_->HideActivePopup();
extensions_container_->CloseOverflowMenuIfOpen();
popup_host_ = host.get();
popup_host_observation_.Observe(popup_host_.get());
extensions_container_->SetPopupOwner(this);
extensions_container_->PopOutAction(
this, base::BindOnce(&ExtensionActionViewController::ShowPopup,
weak_factory_.GetWeakPtr(), std::move(host), by_user,
show_action, std::move(callback)));
}
void ExtensionActionViewController::ShowPopup(
std::unique_ptr<extensions::ExtensionViewHost> popup_host,
bool grant_tab_permissions,
PopupShowAction show_action,
ShowPopupCallback callback) {
// It's possible that the popup should be closed before it finishes opening
// (since it can open asynchronously). Check before proceeding.
if (!popup_host_) {
if (callback)
std::move(callback).Run(nullptr);
return;
}
// NOTE: Today, ShowPopup() always synchronously creates the platform-specific
// popup class, which is what we care most about (since `has_opened_popup_`
// is used to determine whether we need to manually close the
// ExtensionViewHost). This doesn't necessarily mean that the popup has
// completed rendering on the screen.
has_opened_popup_ = true;
platform_delegate_->ShowPopup(std::move(popup_host), show_action,
std::move(callback));
view_delegate_->OnPopupShown(grant_tab_permissions);
}
void ExtensionActionViewController::OnPopupClosed() {
DCHECK(popup_host_observation_.IsObservingSource(popup_host_.get()));
popup_host_observation_.Reset();
popup_host_ = nullptr;
has_opened_popup_ = false;
extensions_container_->SetPopupOwner(nullptr);
if (extensions_container_->GetPoppedOutAction() == this)
extensions_container_->UndoPopOut();
view_delegate_->OnPopupClosed();
}
std::unique_ptr<IconWithBadgeImageSource>
ExtensionActionViewController::GetIconImageSource(
content::WebContents* web_contents,
const gfx::Size& size) {
// `web_contents` may be null during tab closure or in tests. Fall back on a
// generic color provider.
auto get_color_provider_callback = base::BindRepeating(
[](base::WeakPtr<content::WebContents> weak_web_contents) {
return weak_web_contents
? &weak_web_contents->GetColorProvider()
: ui::ColorProviderManager::Get().GetColorProviderFor(
ui::NativeTheme::GetInstanceForNativeUi()
->GetColorProviderKey(nullptr));
},
web_contents ? web_contents->GetWeakPtr()
: base::WeakPtr<content::WebContents>());
auto image_source = std::make_unique<IconWithBadgeImageSource>(
size, std::move(get_color_provider_callback));
int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
image_source->SetIcon(icon_factory_.GetIcon(tab_id));
std::unique_ptr<IconWithBadgeImageSource::Badge> badge;
std::string badge_text = extension_action_->GetDisplayBadgeText(tab_id);
if (!badge_text.empty()) {
badge = std::make_unique<IconWithBadgeImageSource::Badge>(
badge_text, extension_action_->GetBadgeTextColor(tab_id),
extension_action_->GetBadgeBackgroundColor(tab_id));
}
image_source->SetBadge(std::move(badge));
// We only grayscale the icon if it cannot interact with the page and the icon
// is disabled.
bool action_is_visible = extension_action_->GetIsVisible(tab_id);
bool grayscale =
GetSiteInteraction(web_contents) ==
extensions::SitePermissionsHelper::SiteInteraction::kNone &&
!action_is_visible;
image_source->set_grayscale(grayscale);
if (base::FeatureList::IsEnabled(
extensions_features::kExtensionsMenuAccessControl)) {
return image_source;
}
bool was_blocked = extensions::SitePermissionsHelper(browser_->profile())
.HasBeenBlocked(*extension(), web_contents);
image_source->set_paint_blocked_actions_decoration(was_blocked);
return image_source;
}