blob: d802758f0b38bd8aa542b48f28331eb8d3ab2cad [file] [log] [blame]
// Copyright 2012 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/extensions/api/extension_action/extension_action_api.h"
#include <stddef.h>
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/extensions/extension_action_dispatcher.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_model.h"
#include "content/public/common/color_parser.h"
#include "extensions/browser/api/declarative_net_request/constants.h"
#include "extensions/browser/api/declarative_net_request/prefs_helper.h"
#include "extensions/browser/api/declarative_net_request/utils.h"
#include "extensions/browser/extension_action.h"
#include "extensions/browser/extension_action_manager.h"
#include "extensions/browser/extension_event_histogram_value.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/icon_util.h"
#include "extensions/common/api/extension_action/action_info.h"
#include "extensions/common/constants.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/image_util.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/mojom/view_type.mojom.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "url/origin.h"
using content::WebContents;
namespace extensions {
namespace {
// Errors.
const char kNoExtensionActionError[] =
"This extension has no action specified.";
const char kNoTabError[] = "No tab with id: *.";
bool g_report_error_for_invisible_icon = false;
// Returns true if the color values provided could be parsed into a color
// object out param.
bool ParseColor(const base::Value& color_value, SkColor& color) {
if (color_value.is_string()) {
return content::ParseCssColorString(color_value.GetString(), &color);
}
if (!color_value.is_list()) {
return false;
}
const base::Value::List& color_list = color_value.GetList();
if (color_list.size() != 4 ||
std::ranges::any_of(color_list,
[](const auto& color) { return !color.is_int(); })) {
return false;
}
color = SkColorSetARGB(color_list[3].GetInt(), color_list[0].GetInt(),
color_list[1].GetInt(), color_list[2].GetInt());
return true;
}
} // namespace
//
// ExtensionActionFunction
//
ExtensionActionFunction::ExtensionActionFunction()
: details_(nullptr),
tab_id_(ExtensionAction::kDefaultTabId),
contents_(nullptr),
extension_action_(nullptr) {}
ExtensionActionFunction::~ExtensionActionFunction() = default;
ExtensionFunction::ResponseAction ExtensionActionFunction::Run() {
ExtensionActionManager* manager =
ExtensionActionManager::Get(browser_context());
extension_action_ = manager->GetExtensionAction(*extension());
if (!extension_action_) {
// TODO(kalman): Ideally the browserAction/pageAction APIs wouldn't even
// 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) {
content::WebContents* contents_out_param = nullptr;
ExtensionTabUtil::GetTabById(tab_id_, browser_context(),
include_incognito_information(),
&contents_out_param);
if (!contents_out_param) {
return RespondNow(Error(kNoTabError, base::NumberToString(tab_id_)));
}
contents_ = contents_out_param;
} else {
// Page actions do not have a default tabId.
EXTENSION_FUNCTION_VALIDATE(extension_action_->action_type() !=
ActionInfo::Type::kPage);
}
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.
if (args().empty()) {
return true;
}
const base::Value& first_arg = args()[0];
switch (first_arg.type()) {
case base::Value::Type::INTEGER:
tab_id_ = first_arg.GetInt();
break;
case base::Value::Type::DICT: {
// Found the details argument.
details_ = &first_arg.GetDict();
// Still need to check for the tabId within details.
if (const base::Value* tab_id_value = details_->Find("tabId")) {
switch (tab_id_value->type()) {
case base::Value::Type::NONE:
// OK; tabId is optional, leave it default.
return true;
case base::Value::Type::INTEGER:
tab_id_ = tab_id_value->GetInt();
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() {
ExtensionActionDispatcher::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());
}
ExtensionFunction::ResponseAction
ActionIsEnabledFunction::RunExtensionAction() {
return RespondNow(WithArguments(
extension_action_->GetIsVisibleIgnoringDeclarative(tab_id_)));
}
// 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.
const base::Value::Dict* canvas_set = details_->FindDict("imageData");
if (canvas_set) {
gfx::ImageSkia icon;
extensions::IconParseResult parse_result =
extensions::ParseIconFromCanvasDictionary(*canvas_set, &icon);
EXTENSION_FUNCTION_VALIDATE(parse_result ==
extensions::IconParseResult::kSuccess);
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);
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_->FindInt("iconIndex")) {
// Obsolete argument: ignore it.
return RespondNow(NoArguments());
} else {
EXTENSION_FUNCTION_VALIDATE(false);
}
NotifyChange();
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction
ExtensionActionSetTitleFunction::RunExtensionAction() {
EXTENSION_FUNCTION_VALIDATE(details_);
const std::string* title = details_->FindString("title");
EXTENSION_FUNCTION_VALIDATE(title);
extension_action_->SetTitle(tab_id_, *title);
NotifyChange();
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction
ExtensionActionSetPopupFunction::RunExtensionAction() {
EXTENSION_FUNCTION_VALIDATE(details_);
const std::string* popup_string = details_->FindString("popup");
EXTENSION_FUNCTION_VALIDATE(popup_string);
GURL popup_url;
// If an empty string is passed, remove the explicitly set popup. Setting it
// back to an empty string (URL) will cause it to fall back to the default set
// in the manifest.
if (!popup_string->empty()) {
popup_url = extension()->ResolveExtensionURL(*popup_string);
if (!popup_url.is_valid()) {
return RespondNow(Error(manifest_errors::kInvalidExtensionPopupPath));
}
}
extension_action_->SetPopupUrl(tab_id_, popup_url);
NotifyChange();
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction
ExtensionActionSetBadgeTextFunction::RunExtensionAction() {
EXTENSION_FUNCTION_VALIDATE(details_);
const std::string* badge_text = details_->FindString("text");
if (badge_text) {
extension_action_->SetBadgeText(tab_id_, *badge_text);
} else {
extension_action_->ClearBadgeText(tab_id_);
}
NotifyChange();
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction
ExtensionActionSetBadgeBackgroundColorFunction::RunExtensionAction() {
EXTENSION_FUNCTION_VALIDATE(details_);
const base::Value* color_value = details_->Find("color");
EXTENSION_FUNCTION_VALIDATE(color_value);
SkColor color = 0;
if (!ParseColor(*color_value, color)) {
return RespondNow(Error(extension_misc::kInvalidColorError));
}
extension_action_->SetBadgeBackgroundColor(tab_id_, color);
NotifyChange();
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction
ActionSetBadgeTextColorFunction::RunExtensionAction() {
EXTENSION_FUNCTION_VALIDATE(details_);
const base::Value* color_value = details_->Find("color");
EXTENSION_FUNCTION_VALIDATE(color_value);
SkColor color = 0;
if (!ParseColor(*color_value, color)) {
return RespondNow(Error(extension_misc::kInvalidColorError));
}
if (SkColorGetA(color) == SK_AlphaTRANSPARENT) {
return RespondNow(Error(extension_misc::kInvalidColorError));
}
extension_action_->SetBadgeTextColor(tab_id_, color);
NotifyChange();
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction
ExtensionActionGetTitleFunction::RunExtensionAction() {
return RespondNow(WithArguments(extension_action_->GetTitle(tab_id_)));
}
ExtensionFunction::ResponseAction
ExtensionActionGetPopupFunction::RunExtensionAction() {
return RespondNow(
WithArguments(extension_action_->GetPopupUrl(tab_id_).spec()));
}
ExtensionFunction::ResponseAction
ExtensionActionGetBadgeTextFunction::RunExtensionAction() {
declarative_net_request::PrefsHelper helper(
*ExtensionPrefs::Get(browser_context()));
bool is_dnr_action_count_active =
helper.GetUseActionCountAsBadgeText(extension_id()) &&
!extension_action_->HasBadgeText(tab_id_);
// Ensure that the placeholder string is returned if this extension is
// displaying action counts for the badge labels and the extension doesn't
// have permission to view the action count for this tab. Note that
// tab-specific badge text takes priority over the action count.
if (is_dnr_action_count_active &&
!declarative_net_request::HasDNRFeedbackPermission(extension(),
tab_id_)) {
return RespondNow(WithArguments(
std::move(declarative_net_request::kActionCountPlaceholderBadgeText)));
}
return RespondNow(
WithArguments(extension_action_->GetDisplayBadgeText(tab_id_)));
}
ExtensionFunction::ResponseAction
ExtensionActionGetBadgeBackgroundColorFunction::RunExtensionAction() {
base::Value::List list;
SkColor color = extension_action_->GetBadgeBackgroundColor(tab_id_);
list.Append(static_cast<int>(SkColorGetR(color)));
list.Append(static_cast<int>(SkColorGetG(color)));
list.Append(static_cast<int>(SkColorGetB(color)));
list.Append(static_cast<int>(SkColorGetA(color)));
return RespondNow(WithArguments(std::move(list)));
}
ExtensionFunction::ResponseAction
ActionGetBadgeTextColorFunction::RunExtensionAction() {
base::Value::List list;
SkColor color = extension_action_->GetBadgeTextColor(tab_id_);
list.Append(static_cast<int>(SkColorGetR(color)));
list.Append(static_cast<int>(SkColorGetG(color)));
list.Append(static_cast<int>(SkColorGetB(color)));
list.Append(static_cast<int>(SkColorGetA(color)));
return RespondNow(WithArguments(std::move(list)));
}
ActionGetUserSettingsFunction::ActionGetUserSettingsFunction() = default;
ActionGetUserSettingsFunction::~ActionGetUserSettingsFunction() = default;
ExtensionFunction::ResponseAction ActionGetUserSettingsFunction::Run() {
DCHECK(extension());
ExtensionActionManager* const action_manager =
ExtensionActionManager::Get(browser_context());
ExtensionAction* const action =
action_manager->GetExtensionAction(*extension());
// This API is only available to extensions with the "action" key in the
// manifest, so they should always have an action.
DCHECK(action);
DCHECK_EQ(ActionInfo::Type::kAction, action->action_type());
const bool is_pinned =
ToolbarActionsModel::Get(Profile::FromBrowserContext(browser_context()))
->IsActionPinned(extension_id());
// TODO(crbug.com/360916928): Today, no action APIs are compiled.
// Unfortunately, this means we miss out on the compiled types, which would be
// rather helpful here.
base::Value::Dict ui_settings;
ui_settings.Set("isOnToolbar", is_pinned);
return RespondNow(WithArguments(std::move(ui_settings)));
}
// ActionOpenPopupFunction and BrowserActionOpenPopupFunction have separate
// Android and non-Android implementations. See extension_action_api_android.cc
// and extension_action_api_non_android.cc.
} // namespace extensions