blob: d27bdd46af8e9500b3f3802f34561113fc94a2ef [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/extensions/api/extension_action/extension_action_api.h"
#include <stddef.h>
#include <memory>
#include <utility>
#include "base/lazy_instance.h"
#include "base/location.h"
#include "base/metrics/histogram_macros.h"
#include "base/single_thread_task_runner.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_action_manager.h"
#include "chrome/browser/extensions/extension_action_runner.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/extensions/extension_ui_util.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_tab_helper.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_bar.h"
#include "chrome/common/extensions/api/extension_action/action_info.h"
#include "content/public/browser/notification_service.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_function_registry.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/notification_types.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/feature_switch.h"
#include "extensions/common/image_util.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
using content::WebContents;
namespace extensions {
namespace {
// Whether the browser action is visible in the toolbar.
const char kBrowserActionVisible[] = "browser_action_visible";
// Errors.
const char kNoExtensionActionError[] =
"This extension has no action specified.";
const char kNoTabError[] = "No tab with id: *.";
const char kOpenPopupError[] =
"Failed to show popup either because there is an existing popup or another "
"error occurred.";
const char kInvalidColorError[] =
"The color specification could not be parsed.";
bool g_report_error_for_invisible_icon = false;
} // namespace
//
// ExtensionActionAPI::Observer
//
void ExtensionActionAPI::Observer::OnExtensionActionUpdated(
ExtensionAction* extension_action,
content::WebContents* web_contents,
content::BrowserContext* browser_context) {
}
void ExtensionActionAPI::Observer::OnExtensionActionVisibilityChanged(
const std::string& extension_id,
bool is_now_visible) {
}
void ExtensionActionAPI::Observer::OnPageActionsUpdated(
content::WebContents* web_contents) {
}
void ExtensionActionAPI::Observer::OnExtensionActionAPIShuttingDown() {
}
ExtensionActionAPI::Observer::~Observer() {
}
//
// ExtensionActionAPI
//
static base::LazyInstance<BrowserContextKeyedAPIFactory<ExtensionActionAPI>>::
DestructorAtExit g_extension_action_api_factory = LAZY_INSTANCE_INITIALIZER;
ExtensionActionAPI::ExtensionActionAPI(content::BrowserContext* context)
: browser_context_(context),
extension_prefs_(nullptr) {
ExtensionFunctionRegistry& registry =
ExtensionFunctionRegistry::GetInstance();
// Browser Actions
registry.RegisterFunction<BrowserActionSetIconFunction>();
registry.RegisterFunction<BrowserActionSetTitleFunction>();
registry.RegisterFunction<BrowserActionSetBadgeTextFunction>();
registry.RegisterFunction<BrowserActionSetBadgeBackgroundColorFunction>();
registry.RegisterFunction<BrowserActionSetPopupFunction>();
registry.RegisterFunction<BrowserActionGetTitleFunction>();
registry.RegisterFunction<BrowserActionGetBadgeTextFunction>();
registry.RegisterFunction<BrowserActionGetBadgeBackgroundColorFunction>();
registry.RegisterFunction<BrowserActionGetPopupFunction>();
registry.RegisterFunction<BrowserActionEnableFunction>();
registry.RegisterFunction<BrowserActionDisableFunction>();
registry.RegisterFunction<BrowserActionOpenPopupFunction>();
// Page Actions
registry.RegisterFunction<PageActionShowFunction>();
registry.RegisterFunction<PageActionHideFunction>();
registry.RegisterFunction<PageActionSetIconFunction>();
registry.RegisterFunction<PageActionSetTitleFunction>();
registry.RegisterFunction<PageActionSetPopupFunction>();
registry.RegisterFunction<PageActionGetTitleFunction>();
registry.RegisterFunction<PageActionGetPopupFunction>();
}
ExtensionActionAPI::~ExtensionActionAPI() {
}
// static
BrowserContextKeyedAPIFactory<ExtensionActionAPI>*
ExtensionActionAPI::GetFactoryInstance() {
return g_extension_action_api_factory.Pointer();
}
// static
ExtensionActionAPI* ExtensionActionAPI::Get(content::BrowserContext* context) {
return BrowserContextKeyedAPIFactory<ExtensionActionAPI>::Get(context);
}
void ExtensionActionAPI::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void ExtensionActionAPI::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
bool ExtensionActionAPI::GetBrowserActionVisibility(
const std::string& extension_id) {
bool visible = false;
ExtensionPrefs* prefs = GetExtensionPrefs();
if (!prefs || !prefs->ReadPrefAsBoolean(extension_id,
kBrowserActionVisible,
&visible)) {
return true;
}
return visible;
}
void ExtensionActionAPI::SetBrowserActionVisibility(
const std::string& extension_id,
bool visible) {
if (GetBrowserActionVisibility(extension_id) == visible)
return;
GetExtensionPrefs()->UpdateExtensionPref(
extension_id, kBrowserActionVisible,
std::make_unique<base::Value>(visible));
for (auto& observer : observers_)
observer.OnExtensionActionVisibilityChanged(extension_id, visible);
}
bool ExtensionActionAPI::ShowExtensionActionPopup(
const Extension* extension,
Browser* browser,
bool grant_active_tab_permissions) {
ExtensionAction* extension_action =
ExtensionActionManager::Get(browser_context_)->GetExtensionAction(
*extension);
if (!extension_action)
return false;
// Don't support showing action popups in a popup window.
if (!browser->SupportsWindowFeature(Browser::FEATURE_TOOLBAR))
return false;
ToolbarActionsBar* toolbar_actions_bar =
browser->window()->GetToolbarActionsBar();
// ToolbarActionsBar could be null if, e.g., this is a popup window with no
// toolbar.
return toolbar_actions_bar &&
toolbar_actions_bar->ShowToolbarActionPopup(
extension->id(), grant_active_tab_permissions);
}
void ExtensionActionAPI::NotifyChange(ExtensionAction* extension_action,
content::WebContents* web_contents,
content::BrowserContext* context) {
for (auto& observer : observers_)
observer.OnExtensionActionUpdated(extension_action, web_contents, context);
if (extension_action->action_type() == ActionInfo::TYPE_PAGE)
NotifyPageActionsChanged(web_contents);
}
void ExtensionActionAPI::DispatchExtensionActionClicked(
const ExtensionAction& extension_action,
WebContents* web_contents,
const Extension* extension) {
events::HistogramValue histogram_value = events::UNKNOWN;
const char* event_name = NULL;
switch (extension_action.action_type()) {
case ActionInfo::TYPE_BROWSER:
histogram_value = events::BROWSER_ACTION_ON_CLICKED;
event_name = "browserAction.onClicked";
break;
case ActionInfo::TYPE_PAGE:
histogram_value = events::PAGE_ACTION_ON_CLICKED;
event_name = "pageAction.onClicked";
break;
case ActionInfo::TYPE_SYSTEM_INDICATOR:
// The System Indicator handles its own clicks.
NOTREACHED();
break;
}
if (event_name) {
std::unique_ptr<base::ListValue> args(new base::ListValue());
args->Append(ExtensionTabUtil::CreateTabObject(
web_contents, ExtensionTabUtil::kScrubTab, extension)
->ToValue());
DispatchEventToExtension(web_contents->GetBrowserContext(),
extension_action.extension_id(), histogram_value,
event_name, std::move(args));
}
}
void ExtensionActionAPI::ClearAllValuesForTab(
content::WebContents* web_contents) {
DCHECK(web_contents);
const SessionID tab_id = SessionTabHelper::IdForTab(web_contents);
content::BrowserContext* browser_context = web_contents->GetBrowserContext();
const ExtensionSet& enabled_extensions =
ExtensionRegistry::Get(browser_context_)->enabled_extensions();
ExtensionActionManager* action_manager =
ExtensionActionManager::Get(browser_context_);
for (ExtensionSet::const_iterator iter = enabled_extensions.begin();
iter != enabled_extensions.end(); ++iter) {
ExtensionAction* extension_action =
action_manager->GetExtensionAction(**iter);
if (extension_action) {
extension_action->ClearAllValuesForTab(tab_id.id());
NotifyChange(extension_action, web_contents, browser_context);
}
}
}
ExtensionPrefs* ExtensionActionAPI::GetExtensionPrefs() {
// This lazy initialization is more than just an optimization, because it
// allows tests to associate a new ExtensionPrefs with the browser context
// before we access it.
if (!extension_prefs_)
extension_prefs_ = ExtensionPrefs::Get(browser_context_);
return extension_prefs_;
}
void ExtensionActionAPI::DispatchEventToExtension(
content::BrowserContext* context,
const std::string& extension_id,
events::HistogramValue histogram_value,
const std::string& event_name,
std::unique_ptr<base::ListValue> event_args) {
if (!EventRouter::Get(context))
return;
auto event = std::make_unique<Event>(histogram_value, event_name,
std::move(event_args), context);
event->user_gesture = EventRouter::USER_GESTURE_ENABLED;
EventRouter::Get(context)
->DispatchEventToExtension(extension_id, std::move(event));
}
void ExtensionActionAPI::NotifyPageActionsChanged(
content::WebContents* web_contents) {
Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
if (!browser)
return;
for (auto& observer : observers_)
observer.OnPageActionsUpdated(web_contents);
}
void ExtensionActionAPI::Shutdown() {
for (auto& observer : observers_)
observer.OnExtensionActionAPIShuttingDown();
}
//
// ExtensionActionFunction
//
ExtensionActionFunction::ExtensionActionFunction()
: details_(NULL),
tab_id_(ExtensionAction::kDefaultTabId),
contents_(NULL),
extension_action_(NULL) {
}
ExtensionActionFunction::~ExtensionActionFunction() {
}
ExtensionFunction::ResponseAction ExtensionActionFunction::Run() {
ExtensionActionManager* manager =
ExtensionActionManager::Get(browser_context());
if (base::StartsWith(name(), "systemIndicator.",
base::CompareCase::INSENSITIVE_ASCII)) {
extension_action_ = manager->GetSystemIndicator(*extension());
} else {
extension_action_ = manager->GetBrowserAction(*extension());
if (!extension_action_) {
extension_action_ = manager->GetPageAction(*extension());
}
}
if (!extension_action_) {
// TODO(kalman): ideally the browserAction/pageAction APIs wouldn't event
// exist for extensions that don't have one declared. This should come as
// part of the Feature system.
return RespondNow(Error(kNoExtensionActionError));
}
// Populates the tab_id_ and details_ members.
EXTENSION_FUNCTION_VALIDATE(ExtractDataFromArguments());
// Find the WebContents that contains this tab id if one is required.
if (tab_id_ != ExtensionAction::kDefaultTabId) {
ExtensionTabUtil::GetTabById(tab_id_, browser_context(),
include_incognito_information(), nullptr,
nullptr, &contents_, nullptr);
if (!contents_)
return RespondNow(Error(kNoTabError, base::IntToString(tab_id_)));
} else {
// Only browser actions and system indicators have a default tabId.
ActionInfo::Type action_type = extension_action_->action_type();
EXTENSION_FUNCTION_VALIDATE(
action_type == ActionInfo::TYPE_BROWSER ||
action_type == ActionInfo::TYPE_SYSTEM_INDICATOR);
}
return RunExtensionAction();
}
bool ExtensionActionFunction::ExtractDataFromArguments() {
// There may or may not be details (depends on the function).
// The tabId might appear in details (if it exists), as the first
// argument besides the action type (depends on the function), or be omitted
// entirely.
base::Value* first_arg = NULL;
if (!args_->Get(0, &first_arg))
return true;
switch (first_arg->type()) {
case base::Value::Type::INTEGER:
CHECK(first_arg->GetAsInteger(&tab_id_));
break;
case base::Value::Type::DICTIONARY: {
// Found the details argument.
details_ = static_cast<base::DictionaryValue*>(first_arg);
// Still need to check for the tabId within details.
base::Value* tab_id_value = NULL;
if (details_->Get("tabId", &tab_id_value)) {
switch (tab_id_value->type()) {
case base::Value::Type::NONE:
// OK; tabId is optional, leave it default.
return true;
case base::Value::Type::INTEGER:
CHECK(tab_id_value->GetAsInteger(&tab_id_));
return true;
default:
// Boom.
return false;
}
}
// Not found; tabId is optional, leave it default.
break;
}
case base::Value::Type::NONE:
// The tabId might be an optional argument.
break;
default:
return false;
}
return true;
}
void ExtensionActionFunction::NotifyChange() {
ExtensionActionAPI::Get(browser_context())
->NotifyChange(extension_action_, contents_, browser_context());
}
void ExtensionActionFunction::SetVisible(bool visible) {
if (extension_action_->GetIsVisible(tab_id_) == visible)
return;
extension_action_->SetIsVisible(tab_id_, visible);
NotifyChange();
}
ExtensionFunction::ResponseAction
ExtensionActionShowFunction::RunExtensionAction() {
SetVisible(true);
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction
ExtensionActionHideFunction::RunExtensionAction() {
SetVisible(false);
return RespondNow(NoArguments());
}
// static
void ExtensionActionSetIconFunction::SetReportErrorForInvisibleIconForTesting(
bool value) {
g_report_error_for_invisible_icon = value;
}
ExtensionFunction::ResponseAction
ExtensionActionSetIconFunction::RunExtensionAction() {
EXTENSION_FUNCTION_VALIDATE(details_);
// setIcon can take a variant argument: either a dictionary of canvas
// ImageData, or an icon index.
base::DictionaryValue* canvas_set = NULL;
int icon_index;
if (details_->GetDictionary("imageData", &canvas_set)) {
gfx::ImageSkia icon;
EXTENSION_FUNCTION_VALIDATE(
ExtensionAction::ParseIconFromCanvasDictionary(*canvas_set, &icon));
if (icon.isNull())
return RespondNow(Error("Icon invalid."));
gfx::Image icon_image(icon);
const SkBitmap bitmap = icon_image.AsBitmap();
const bool is_visible = image_util::IsIconSufficientlyVisible(bitmap);
UMA_HISTOGRAM_BOOLEAN("Extensions.DynamicExtensionActionIconWasVisible",
is_visible);
const bool is_visible_rendered =
extensions::ui_util::IsRenderedIconSufficientlyVisibleForBrowserContext(
bitmap, browser_context());
UMA_HISTOGRAM_BOOLEAN(
"Extensions.DynamicExtensionActionIconWasVisibleRendered",
is_visible_rendered);
if (!is_visible && g_report_error_for_invisible_icon)
return RespondNow(Error("Icon not sufficiently visible."));
extension_action_->SetIcon(tab_id_, icon_image);
} else if (details_->GetInteger("iconIndex", &icon_index)) {
// Obsolete argument: ignore it.
return RespondNow(NoArguments());
} else {
EXTENSION_FUNCTION_VALIDATE(false);
}
NotifyChange();
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction
ExtensionActionSetTitleFunction::RunExtensionAction() {
EXTENSION_FUNCTION_VALIDATE(details_);
std::string title;
EXTENSION_FUNCTION_VALIDATE(details_->GetString("title", &title));
extension_action_->SetTitle(tab_id_, title);
NotifyChange();
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction
ExtensionActionSetPopupFunction::RunExtensionAction() {
EXTENSION_FUNCTION_VALIDATE(details_);
std::string popup_string;
EXTENSION_FUNCTION_VALIDATE(details_->GetString("popup", &popup_string));
GURL popup_url;
if (!popup_string.empty())
popup_url = extension()->GetResourceURL(popup_string);
extension_action_->SetPopupUrl(tab_id_, popup_url);
NotifyChange();
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction
ExtensionActionSetBadgeTextFunction::RunExtensionAction() {
EXTENSION_FUNCTION_VALIDATE(details_);
std::string badge_text;
EXTENSION_FUNCTION_VALIDATE(details_->GetString("text", &badge_text));
extension_action_->SetBadgeText(tab_id_, badge_text);
NotifyChange();
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction
ExtensionActionSetBadgeBackgroundColorFunction::RunExtensionAction() {
EXTENSION_FUNCTION_VALIDATE(details_);
base::Value* color_value = NULL;
EXTENSION_FUNCTION_VALIDATE(details_->Get("color", &color_value));
SkColor color = 0;
if (color_value->is_list()) {
base::ListValue* list = NULL;
EXTENSION_FUNCTION_VALIDATE(details_->GetList("color", &list));
EXTENSION_FUNCTION_VALIDATE(list->GetSize() == 4);
int color_array[4] = {0};
for (size_t i = 0; i < base::size(color_array); ++i) {
EXTENSION_FUNCTION_VALIDATE(list->GetInteger(i, &color_array[i]));
}
color = SkColorSetARGB(color_array[3], color_array[0],
color_array[1], color_array[2]);
} else if (color_value->is_string()) {
std::string color_string;
EXTENSION_FUNCTION_VALIDATE(details_->GetString("color", &color_string));
if (!image_util::ParseCssColorString(color_string, &color))
return RespondNow(Error(kInvalidColorError));
}
extension_action_->SetBadgeBackgroundColor(tab_id_, color);
NotifyChange();
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction
ExtensionActionGetTitleFunction::RunExtensionAction() {
return RespondNow(OneArgument(
std::make_unique<base::Value>(extension_action_->GetTitle(tab_id_))));
}
ExtensionFunction::ResponseAction
ExtensionActionGetPopupFunction::RunExtensionAction() {
return RespondNow(OneArgument(std::make_unique<base::Value>(
extension_action_->GetPopupUrl(tab_id_).spec())));
}
ExtensionFunction::ResponseAction
ExtensionActionGetBadgeTextFunction::RunExtensionAction() {
return RespondNow(OneArgument(
std::make_unique<base::Value>(extension_action_->GetBadgeText(tab_id_))));
}
ExtensionFunction::ResponseAction
ExtensionActionGetBadgeBackgroundColorFunction::RunExtensionAction() {
std::unique_ptr<base::ListValue> list(new base::ListValue());
SkColor color = extension_action_->GetBadgeBackgroundColor(tab_id_);
list->AppendInteger(static_cast<int>(SkColorGetR(color)));
list->AppendInteger(static_cast<int>(SkColorGetG(color)));
list->AppendInteger(static_cast<int>(SkColorGetB(color)));
list->AppendInteger(static_cast<int>(SkColorGetA(color)));
return RespondNow(OneArgument(std::move(list)));
}
BrowserActionOpenPopupFunction::BrowserActionOpenPopupFunction() = default;
ExtensionFunction::ResponseAction BrowserActionOpenPopupFunction::Run() {
// We only allow the popup in the active window.
Profile* profile = Profile::FromBrowserContext(browser_context());
Browser* browser = chrome::FindLastActiveWithProfile(profile);
// It's possible that the last active browser actually corresponds to the
// associated incognito profile, and this won't be returned by
// FindLastActiveWithProfile. If the browser we found isn't active and the
// extension can operate incognito, then check the last active incognito, too.
if ((!browser || !browser->window()->IsActive()) &&
util::IsIncognitoEnabled(extension()->id(), profile) &&
profile->HasOffTheRecordProfile()) {
browser =
chrome::FindLastActiveWithProfile(profile->GetOffTheRecordProfile());
}
// If there's no active browser, or the Toolbar isn't visible, abort.
// Otherwise, try to open a popup in the active browser.
// TODO(justinlin): Remove toolbar check when http://crbug.com/308645 is
// fixed.
if (!browser || !browser->window()->IsActive() ||
!browser->window()->IsToolbarVisible() ||
!ExtensionActionAPI::Get(profile)->ShowExtensionActionPopup(
extension_.get(), browser, false)) {
return RespondNow(Error(kOpenPopupError));
}
// Even if this is for an incognito window, we want to use the normal profile.
// If the extension is spanning, then extension hosts are created with the
// original profile, and if it's split, then we know the api call came from
// the right profile.
registrar_.Add(this, NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD,
content::Source<Profile>(profile));
// Set a timeout for waiting for the notification that the popup is loaded.
// Waiting is required so that the popup view can be retrieved by the custom
// bindings for the response callback. It's also needed to keep this function
// instance around until a notification is observed.
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&BrowserActionOpenPopupFunction::OpenPopupTimedOut, this),
base::TimeDelta::FromSeconds(10));
return RespondLater();
}
void BrowserActionOpenPopupFunction::OpenPopupTimedOut() {
if (did_respond())
return;
DVLOG(1) << "chrome.browserAction.openPopup did not show a popup.";
Respond(Error(kOpenPopupError));
}
void BrowserActionOpenPopupFunction::Observe(
int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
DCHECK_EQ(NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD, type);
if (did_respond())
return;
ExtensionHost* host = content::Details<ExtensionHost>(details).ptr();
if (host->extension_host_type() != VIEW_TYPE_EXTENSION_POPUP ||
host->extension()->id() != extension_->id())
return;
Respond(NoArguments());
registrar_.RemoveAll();
}
} // namespace extensions