blob: 8a446d611f0571922326eb218273e184f10a68d3 [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/declarative_content/content_action.h"
#include <map>
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/escape.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_action_dispatcher.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/extensions/extension_ui_util.h"
#include "chrome/browser/profiles/profile.h"
#include "components/sessions/content/session_tab_helper.h"
#include "content/public/browser/invalidate_type.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_system.h"
#include "extensions/browser/extension_user_script_loader.h"
#include "extensions/browser/extension_web_contents_observer.h"
#include "extensions/browser/icon_util.h"
#include "extensions/browser/script_injection_tracker.h"
#include "extensions/browser/user_script_manager.h"
#include "extensions/common/api/declarative/declarative_constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/image_util.h"
#include "extensions/common/mojom/host_id.mojom.h"
#include "extensions/common/mojom/match_origin_as_fallback.mojom-shared.h"
#include "extensions/common/mojom/run_location.mojom-shared.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
namespace extensions {
namespace {
// Error messages.
const char kInvalidIconDictionary[] =
"Icon dictionary must be of the form {\"19\": ImageData1, \"38\": "
"ImageData2}";
const char kInvalidInstanceTypeError[] =
"An action has an invalid instanceType: %s";
const char kMissingInstanceTypeError[] = "Action is missing instanceType";
const char kMissingParameter[] = "Missing parameter is required: %s";
const char kNoAction[] =
"Can't use declarativeContent.ShowAction without an action";
const char kNoPageOrBrowserAction[] =
"Can't use declarativeContent.SetIcon without a page or browser action";
const char kIconNotSufficientlyVisible[] =
"The specified icon is not sufficiently visible";
bool g_allow_invisible_icons_content_action = true;
void RecordContentActionCreated(
declarative_content_constants::ContentActionType type) {
base::UmaHistogramEnumeration("Extensions.DeclarativeContentActionCreated",
type);
}
//
// The following are concrete actions.
//
// Action that instructs to show an extension's page action.
class ShowExtensionAction : public ContentAction {
public:
ShowExtensionAction() = default;
ShowExtensionAction(const ShowExtensionAction&) = delete;
ShowExtensionAction& operator=(const ShowExtensionAction&) = delete;
~ShowExtensionAction() override = default;
static std::unique_ptr<ContentAction> Create(
content::BrowserContext* browser_context,
const Extension* extension,
const base::Value::Dict* dict,
std::string* error) {
// TODO(devlin): We should probably throw an error if the extension has no
// action specified in the manifest. Currently, this is allowed since
// extensions will have a synthesized page action.
if (!ActionInfo::GetExtensionActionInfo(extension)) {
*error = kNoAction;
return nullptr;
}
RecordContentActionCreated(
declarative_content_constants::ContentActionType::kShowAction);
return std::make_unique<ShowExtensionAction>();
}
// Implementation of ContentAction:
void Apply(const ApplyInfo& apply_info) const override {
ExtensionAction* action =
GetAction(apply_info.browser_context, apply_info.extension);
action->DeclarativeShow(ExtensionTabUtil::GetTabId(apply_info.tab));
ExtensionActionDispatcher::Get(apply_info.browser_context)
->NotifyChange(action, apply_info.tab, apply_info.browser_context);
}
// The page action is already showing, so nothing needs to be done here.
void Reapply(const ApplyInfo& apply_info) const override {}
void Revert(const ApplyInfo& apply_info) const override {
if (ExtensionAction* action =
GetAction(apply_info.browser_context, apply_info.extension)) {
action->UndoDeclarativeShow(ExtensionTabUtil::GetTabId(apply_info.tab));
ExtensionActionDispatcher::Get(apply_info.browser_context)
->NotifyChange(action, apply_info.tab, apply_info.browser_context);
}
}
private:
static ExtensionAction* GetAction(content::BrowserContext* browser_context,
const Extension* extension) {
return ExtensionActionManager::Get(browser_context)
->GetExtensionAction(*extension);
}
};
// Action that sets an extension's action icon.
class SetIcon : public ContentAction {
public:
explicit SetIcon(const gfx::Image& icon) : icon_(icon) {}
SetIcon(const SetIcon&) = delete;
SetIcon& operator=(const SetIcon&) = delete;
~SetIcon() override = default;
static std::unique_ptr<ContentAction> Create(
content::BrowserContext* browser_context,
const Extension* extension,
const base::Value::Dict* dict,
std::string* error);
// Implementation of ContentAction:
void Apply(const ApplyInfo& apply_info) const override {
Profile* profile = Profile::FromBrowserContext(apply_info.browser_context);
ExtensionAction* action = GetExtensionAction(profile,
apply_info.extension);
if (action) {
action->DeclarativeSetIcon(ExtensionTabUtil::GetTabId(apply_info.tab),
apply_info.priority,
icon_);
ExtensionActionDispatcher::Get(profile)->NotifyChange(
action, apply_info.tab, profile);
}
}
void Reapply(const ApplyInfo& apply_info) const override {}
void Revert(const ApplyInfo& apply_info) const override {
Profile* profile = Profile::FromBrowserContext(apply_info.browser_context);
ExtensionAction* action = GetExtensionAction(profile,
apply_info.extension);
if (action) {
action->UndoDeclarativeSetIcon(
ExtensionTabUtil::GetTabId(apply_info.tab),
apply_info.priority,
icon_);
ExtensionActionDispatcher::Get(apply_info.browser_context)
->NotifyChange(action, apply_info.tab, profile);
}
}
private:
ExtensionAction* GetExtensionAction(Profile* profile,
const Extension* extension) const {
return ExtensionActionManager::Get(profile)->GetExtensionAction(*extension);
}
gfx::Image icon_;
};
// Helper for getting JS collections into C++.
static bool AppendJSStringsToCPPStrings(const base::Value::List& append_strings,
std::vector<std::string>* append_to) {
for (const auto& entry : append_strings) {
if (entry.is_string()) {
append_to->push_back(entry.GetString());
} else {
return false;
}
}
return true;
}
struct ContentActionFactory {
// Factory methods for ContentAction instances. |extension| is the extension
// for which the action is being created. |dict| contains the json dictionary
// that describes the action. |error| is used to return error messages.
using FactoryMethod = std::unique_ptr<ContentAction> (*)(
content::BrowserContext* /* browser_context */,
const Extension* /* extension */,
const base::Value::Dict* /* dict */,
std::string* /* error */);
// Maps the name of a declarativeContent action type to the factory
// function creating it.
std::map<std::string, FactoryMethod> factory_methods;
ContentActionFactory() {
factory_methods[declarative_content_constants::kShowAction] =
&ShowExtensionAction::Create;
factory_methods[declarative_content_constants::kRequestContentScript] =
&RequestContentScript::Create;
factory_methods[declarative_content_constants::kSetIcon] = &SetIcon::Create;
}
};
ContentActionFactory& GetContentActionFactory() {
static base::NoDestructor<ContentActionFactory> content_action_factory;
return *content_action_factory;
}
} // namespace
//
// RequestContentScript
//
struct RequestContentScript::ScriptData {
ScriptData();
~ScriptData();
std::vector<std::string> css_file_names;
std::vector<std::string> js_file_names;
bool all_frames;
bool match_about_blank;
};
RequestContentScript::ScriptData::ScriptData()
: all_frames(false),
match_about_blank(false) {}
RequestContentScript::ScriptData::~ScriptData() = default;
// static
std::unique_ptr<ContentAction> RequestContentScript::Create(
content::BrowserContext* browser_context,
const Extension* extension,
const base::Value::Dict* dict,
std::string* error) {
ScriptData script_data;
if (!InitScriptData(dict, error, &script_data))
return nullptr;
RecordContentActionCreated(
declarative_content_constants::ContentActionType::kRequestContentScript);
return base::WrapUnique(
new RequestContentScript(browser_context, extension, script_data));
}
// static
bool RequestContentScript::InitScriptData(const base::Value::Dict* dict,
std::string* error,
ScriptData* script_data) {
const base::Value* css = dict->Find(declarative_content_constants::kCss);
const base::Value* js = dict->Find(declarative_content_constants::kJs);
if (!css && !js) {
*error = base::StringPrintf(kMissingParameter, "css or js");
return false;
}
if (css) {
if (!css->is_list() || !AppendJSStringsToCPPStrings(
css->GetList(), &script_data->css_file_names)) {
return false;
}
}
if (js) {
if (!js->is_list() || !AppendJSStringsToCPPStrings(
js->GetList(), &script_data->js_file_names)) {
return false;
}
}
if (const base::Value* all_frames_val =
dict->Find(declarative_content_constants::kAllFrames)) {
if (!all_frames_val->is_bool())
return false;
script_data->all_frames = all_frames_val->GetBool();
}
if (const base::Value* match_about_blank_val =
dict->Find(declarative_content_constants::kMatchAboutBlank)) {
if (!match_about_blank_val->is_bool())
return false;
script_data->match_about_blank = match_about_blank_val->GetBool();
}
return true;
}
RequestContentScript::RequestContentScript(
content::BrowserContext* browser_context,
const Extension* extension,
const ScriptData& script_data) {
mojom::HostID host_id(mojom::HostID::HostType::kExtensions, extension->id());
InitScript(host_id, extension, script_data);
script_loader_ = ExtensionSystem::Get(browser_context)
->user_script_manager()
->GetUserScriptLoaderForExtension(extension->id());
scoped_observation_.Observe(script_loader_.get());
AddScript();
}
RequestContentScript::~RequestContentScript() {
// This can occur either if this RequestContentScript action is removed via an
// API call or if its extension is unloaded. If the extension is unloaded, the
// associated `script_loader_` may have been deleted before this object which
// means the loader has already removed `script_`.
if (script_loader_) {
script_loader_->RemoveScripts({script_.id()},
UserScriptLoader::ScriptsLoadedCallback());
}
}
void RequestContentScript::InitScript(const mojom::HostID& host_id,
const Extension* extension,
const ScriptData& script_data) {
script_.set_id(UserScript::GenerateUserScriptID());
script_.set_host_id(host_id);
script_.set_run_location(mojom::RunLocation::kBrowserDriven);
script_.set_match_all_frames(script_data.all_frames);
script_.set_match_origin_as_fallback(
script_data.match_about_blank
? mojom::MatchOriginAsFallbackBehavior::
kMatchForAboutSchemeAndClimbTree
: mojom::MatchOriginAsFallbackBehavior::kNever);
for (const auto& css_file_name : script_data.css_file_names) {
GURL url = extension->GetResourceURL(base::EscapePath(css_file_name));
ExtensionResource resource = extension->GetResource(css_file_name);
script_.css_scripts().push_back(UserScript::Content::CreateFile(
resource.extension_root(), resource.relative_path(), url));
}
for (const auto& js_file_name : script_data.js_file_names) {
GURL url = extension->GetResourceURL(base::EscapePath(js_file_name));
ExtensionResource resource = extension->GetResource(js_file_name);
script_.js_scripts().push_back(UserScript::Content::CreateFile(
resource.extension_root(), resource.relative_path(), url));
}
}
void RequestContentScript::AddScript() {
DCHECK(script_loader_);
UserScriptList scripts;
scripts.push_back(UserScript::CopyMetadataFrom(script_));
script_loader_->AddScripts(std::move(scripts),
UserScriptLoader::ScriptsLoadedCallback());
}
void RequestContentScript::Apply(const ApplyInfo& apply_info) const {
InstructRenderProcessToInject(apply_info.tab, apply_info.extension);
}
void RequestContentScript::Reapply(const ApplyInfo& apply_info) const {
InstructRenderProcessToInject(apply_info.tab, apply_info.extension);
}
void RequestContentScript::Revert(const ApplyInfo& apply_info) const {}
void RequestContentScript::InstructRenderProcessToInject(
content::WebContents* contents,
const Extension* extension) const {
ScriptInjectionTracker::WillExecuteCode(base::PassKey<RequestContentScript>(),
contents->GetPrimaryMainFrame(),
*extension);
mojom::LocalFrame* local_frame =
ExtensionWebContentsObserver::GetForWebContents(contents)->GetLocalFrame(
contents->GetPrimaryMainFrame());
if (!local_frame) {
// TODO(crbug.com/40763607): Need to review when this method is
// called with non-live frame.
return;
}
local_frame->ExecuteDeclarativeScript(
sessions::SessionTabHelper::IdForTab(contents).id(), extension->id(),
script_.id(), contents->GetLastCommittedURL());
}
void RequestContentScript::OnScriptsLoaded(
UserScriptLoader* loader,
content::BrowserContext* browser_context) {}
void RequestContentScript::OnUserScriptLoaderDestroyed(
UserScriptLoader* loader) {
DCHECK_EQ(script_loader_, loader);
scoped_observation_.Reset();
script_loader_ = nullptr;
}
// static
std::unique_ptr<ContentAction> SetIcon::Create(
content::BrowserContext* browser_context,
const Extension* extension,
const base::Value::Dict* dict,
std::string* error) {
// We can't set a page or action's icon if the extension doesn't have one.
if (!ActionInfo::GetExtensionActionInfo(extension)) {
*error = kNoPageOrBrowserAction;
return nullptr;
}
gfx::ImageSkia icon;
const base::Value::Dict* canvas_set = dict->FindDict("imageData");
if (canvas_set &&
extensions::ParseIconFromCanvasDictionary(*canvas_set, &icon) !=
extensions::IconParseResult::kSuccess) {
*error = kInvalidIconDictionary;
return nullptr;
}
gfx::Image image(icon);
const SkBitmap bitmap = image.AsBitmap();
const bool is_sufficiently_visible =
extensions::image_util::IsIconSufficientlyVisible(bitmap);
if (!is_sufficiently_visible && !g_allow_invisible_icons_content_action) {
*error = kIconNotSufficientlyVisible;
return nullptr;
}
RecordContentActionCreated(
declarative_content_constants::ContentActionType::kSetIcon);
return std::make_unique<SetIcon>(image);
}
//
// ContentAction
//
ContentAction::~ContentAction() = default;
// static
std::unique_ptr<ContentAction> ContentAction::Create(
content::BrowserContext* browser_context,
const Extension* extension,
const base::Value::Dict& json_action_dict,
std::string* error) {
error->clear();
const std::string* instance_type = nullptr;
if (!(instance_type = json_action_dict.FindString(
declarative_content_constants::kInstanceType))) {
*error = kMissingInstanceTypeError;
return nullptr;
}
ContentActionFactory& factory = GetContentActionFactory();
auto factory_method_iter = factory.factory_methods.find(*instance_type);
if (factory_method_iter != factory.factory_methods.end())
return (*factory_method_iter->second)(browser_context, extension,
&json_action_dict, error);
*error =
base::StringPrintf(kInvalidInstanceTypeError, instance_type->c_str());
return nullptr;
}
// static
void ContentAction::SetAllowInvisibleIconsForTest(bool value) {
g_allow_invisible_icons_content_action = value;
}
ContentAction::ContentAction() = default;
} // namespace extensions