blob: b9a2a304e80e296599c9055f63098ea16da5668c [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 <utility>
#include "base/lazy_instance.h"
#include "base/location.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/single_thread_task_runner.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_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/location_bar/location_bar.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/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.";
} // 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> >
g_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_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,
new base::FundamentalValue(visible));
FOR_EACH_OBSERVER(Observer, observers_, 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;
if (extension_action->action_type() == ActionInfo::TYPE_PAGE &&
!FeatureSwitch::extension_action_redesign()->IsEnabled()) {
// We show page actions in the location bar unless the new toolbar is
// enabled.
return browser->window()->GetLocationBar()->ShowPageActionPopup(
extension, grant_active_tab_permissions);
}
// 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_EACH_OBSERVER(
Observer,
observers_,
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) {
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)->ToValue().release());
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);
int 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);
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;
std::unique_ptr<Event> event(
new Event(histogram_value, event_name, std::move(event_args)));
event->restrict_to_browser_context = 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;
LocationBar* location_bar =
browser->window() ? browser->window()->GetLocationBar() : NULL;
if (!location_bar)
return;
location_bar->UpdatePageActions();
FOR_EACH_OBSERVER(Observer, observers_, OnPageActionsUpdated(web_contents));
}
void ExtensionActionAPI::Shutdown() {
FOR_EACH_OBSERVER(Observer, observers_, OnExtensionActionAPIShuttingDown());
}
//
// ExtensionActionFunction
//
ExtensionActionFunction::ExtensionActionFunction()
: details_(NULL),
tab_id_(ExtensionAction::kDefaultTabId),
contents_(NULL),
extension_action_(NULL) {
}
ExtensionActionFunction::~ExtensionActionFunction() {
}
bool ExtensionActionFunction::RunSync() {
ExtensionActionManager* manager = ExtensionActionManager::Get(GetProfile());
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.
error_ = kNoExtensionActionError;
return false;
}
// 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_,
GetProfile(),
include_incognito(),
NULL,
NULL,
&contents_,
NULL);
if (!contents_) {
error_ = ErrorUtils::FormatErrorMessage(
kNoTabError, base::IntToString(tab_id_));
return false;
}
} 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->GetType()) {
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->GetType()) {
case base::Value::TYPE_NULL:
// 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_NULL:
// The tabId might be an optional argument.
break;
default:
return false;
}
return true;
}
void ExtensionActionFunction::NotifyChange() {
ExtensionActionAPI::Get(GetProfile())->NotifyChange(
extension_action_, contents_, GetProfile());
}
bool ExtensionActionFunction::SetVisible(bool visible) {
if (extension_action_->GetIsVisible(tab_id_) == visible)
return true;
extension_action_->SetIsVisible(tab_id_, visible);
NotifyChange();
return true;
}
bool ExtensionActionShowFunction::RunExtensionAction() {
return SetVisible(true);
}
bool ExtensionActionHideFunction::RunExtensionAction() {
return SetVisible(false);
}
bool 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()) {
error_ = "Icon invalid.";
return false;
}
extension_action_->SetIcon(tab_id_, gfx::Image(icon));
} else if (details_->GetInteger("iconIndex", &icon_index)) {
// Obsolete argument: ignore it.
return true;
} else {
EXTENSION_FUNCTION_VALIDATE(false);
}
NotifyChange();
return true;
}
bool ExtensionActionSetTitleFunction::RunExtensionAction() {
EXTENSION_FUNCTION_VALIDATE(details_);
std::string title;
EXTENSION_FUNCTION_VALIDATE(details_->GetString("title", &title));
extension_action_->SetTitle(tab_id_, title);
NotifyChange();
return true;
}
bool 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 true;
}
bool 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 true;
}
bool 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->IsType(base::Value::TYPE_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 < arraysize(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->IsType(base::Value::TYPE_STRING)) {
std::string color_string;
EXTENSION_FUNCTION_VALIDATE(details_->GetString("color", &color_string));
if (!image_util::ParseCssColorString(color_string, &color)) {
error_ = kInvalidColorError;
return false;
}
}
extension_action_->SetBadgeBackgroundColor(tab_id_, color);
NotifyChange();
return true;
}
bool ExtensionActionGetTitleFunction::RunExtensionAction() {
SetResult(base::MakeUnique<base::StringValue>(
extension_action_->GetTitle(tab_id_)));
return true;
}
bool ExtensionActionGetPopupFunction::RunExtensionAction() {
SetResult(base::MakeUnique<base::StringValue>(
extension_action_->GetPopupUrl(tab_id_).spec()));
return true;
}
bool ExtensionActionGetBadgeTextFunction::RunExtensionAction() {
SetResult(base::MakeUnique<base::StringValue>(
extension_action_->GetBadgeText(tab_id_)));
return true;
}
bool 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)));
SetResult(std::move(list));
return true;
}
BrowserActionOpenPopupFunction::BrowserActionOpenPopupFunction()
: response_sent_(false) {
}
bool BrowserActionOpenPopupFunction::RunAsync() {
// We only allow the popup in the active window.
Profile* profile = GetProfile();
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(GetProfile())->ShowExtensionActionPopup(
extension_.get(), browser, false)) {
error_ = kOpenPopupError;
return false;
}
// 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::Bind(&BrowserActionOpenPopupFunction::OpenPopupTimedOut, this),
base::TimeDelta::FromSeconds(10));
return true;
}
void BrowserActionOpenPopupFunction::OpenPopupTimedOut() {
if (response_sent_)
return;
DVLOG(1) << "chrome.browserAction.openPopup did not show a popup.";
error_ = kOpenPopupError;
SendResponse(false);
response_sent_ = true;
}
void BrowserActionOpenPopupFunction::Observe(
int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
DCHECK_EQ(NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD, type);
if (response_sent_)
return;
ExtensionHost* host = content::Details<ExtensionHost>(details).ptr();
if (host->extension_host_type() != VIEW_TYPE_EXTENSION_POPUP ||
host->extension()->id() != extension_->id())
return;
SendResponse(true);
response_sent_ = true;
registrar_.RemoveAll();
}
} // namespace extensions