blob: cbff2eaa60dd9da7ef591dda6a929c7d3b4be97c [file] [log] [blame]
// Copyright 2020 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/scripting/scripting_api.h"
#include <algorithm>
#include <utility>
#include "base/check.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/common/extensions/api/scripting.h"
#include "extensions/browser/extension_api_frame_id_map.h"
#include "extensions/browser/load_and_localize_file.h"
#include "extensions/browser/script_executor.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/mojom/action_type.mojom-shared.h"
#include "extensions/common/mojom/css_origin.mojom-shared.h"
#include "extensions/common/mojom/host_id.mojom.h"
#include "extensions/common/mojom/run_location.mojom-shared.h"
#include "extensions/common/permissions/api_permission.h"
#include "extensions/common/permissions/permissions_data.h"
namespace extensions {
namespace {
constexpr char kCouldNotLoadFileError[] = "Could not load file: '*'.";
constexpr char kExactlyOneOfCssAndFilesError[] =
"Exactly one of 'css' and 'files' must be specified.";
// Note: CSS always injects as soon as possible, so we default to
// document_start. Because of tab loading, there's no guarantee this will
// *actually* inject before page load, but it will at least inject "soon".
constexpr mojom::RunLocation kCSSRunLocation =
mojom::RunLocation::kDocumentStart;
// Converts the given `style_origin` to a CSSOrigin.
mojom::CSSOrigin ConvertStyleOriginToCSSOrigin(
api::scripting::StyleOrigin style_origin) {
mojom::CSSOrigin css_origin = mojom::CSSOrigin::kAuthor;
switch (style_origin) {
case api::scripting::STYLE_ORIGIN_NONE:
case api::scripting::STYLE_ORIGIN_AUTHOR:
css_origin = mojom::CSSOrigin::kAuthor;
break;
case api::scripting::STYLE_ORIGIN_USER:
css_origin = mojom::CSSOrigin::kUser;
break;
}
return css_origin;
}
// Checks `files` and populates `resource_out` with the appropriate extension
// resource. Returns true on success; on failure, populates `error_out`.
bool GetFileResource(const std::vector<std::string>& files,
const Extension& extension,
ExtensionResource* resource_out,
std::string* error_out) {
if (files.size() != 1) {
constexpr char kExactlyOneFileError[] =
"Exactly one file must be specified.";
*error_out = kExactlyOneFileError;
return false;
}
ExtensionResource resource = extension.GetResource(files[0]);
if (resource.extension_root().empty() || resource.relative_path().empty()) {
*error_out =
ErrorUtils::FormatErrorMessage(kCouldNotLoadFileError, files[0]);
return false;
}
*resource_out = std::move(resource);
return true;
}
// Returns true if the `permissions` allow for injection into the given `frame`.
// If false, populates `error`.
bool HasPermissionToInjectIntoFrame(const PermissionsData& permissions,
int tab_id,
content::RenderFrameHost* frame,
std::string* error) {
GURL url = frame->GetLastCommittedURL();
// TODO(devlin): Add more schemes here, in line with
// https://crbug.com/55084.
if (url.SchemeIs(url::kAboutScheme) || url.SchemeIs(url::kDataScheme)) {
url::Origin origin = frame->GetLastCommittedOrigin();
const url::SchemeHostPort& tuple_or_precursor_tuple =
origin.GetTupleOrPrecursorTupleIfOpaque();
if (!tuple_or_precursor_tuple.IsValid()) {
if (permissions.HasAPIPermission(mojom::APIPermissionID::kTab)) {
*error = ErrorUtils::FormatErrorMessage(
manifest_errors::kCannotAccessPageWithUrl, url.spec());
} else {
*error = manifest_errors::kCannotAccessPage;
}
return false;
}
url = tuple_or_precursor_tuple.GetURL();
}
return permissions.CanAccessPage(url, tab_id, error);
}
// Returns true if the `target` can be accessed with the given `permissions`.
// If the target can be accessed, populates `script_executor_out`,
// `frame_scope_out`, and `frame_ids_out` with the appropriate values;
// if the target cannot be accessed, populates `error_out`.
bool CanAccessTarget(const PermissionsData& permissions,
const api::scripting::InjectionTarget& target,
content::BrowserContext* browser_context,
bool include_incognito_information,
ScriptExecutor** script_executor_out,
ScriptExecutor::FrameScope* frame_scope_out,
std::set<int>* frame_ids_out,
std::string* error_out) {
content::WebContents* tab = nullptr;
TabHelper* tab_helper = nullptr;
if (!ExtensionTabUtil::GetTabById(target.tab_id, browser_context,
include_incognito_information, &tab) ||
!(tab_helper = TabHelper::FromWebContents(tab))) {
// TODO(devlin): Add a constant for this in a centrally-consumable location.
*error_out = base::StringPrintf("No tab with id: %d", target.tab_id);
return false;
}
if ((target.all_frames && *target.all_frames == true) && target.frame_ids) {
*error_out = "Cannot specify both 'allFrames' and 'frameIds'.";
return false;
}
ScriptExecutor* script_executor = tab_helper->script_executor();
DCHECK(script_executor);
ScriptExecutor::FrameScope frame_scope =
target.all_frames && *target.all_frames == true
? ScriptExecutor::INCLUDE_SUB_FRAMES
: ScriptExecutor::SPECIFIED_FRAMES;
std::set<int> frame_ids;
if (target.frame_ids) {
frame_ids.insert(target.frame_ids->begin(), target.frame_ids->end());
} else {
frame_ids.insert(ExtensionApiFrameIdMap::kTopFrameId);
}
// TODO(devlin): If `allFrames` is true, we error out if the extension
// doesn't have access to the top frame (even if it may inject in child
// frames). This is inconsistent with content scripts (which can execute on
// child frames), but consistent with the old tabs.executeScript() API.
for (int frame_id : frame_ids) {
content::RenderFrameHost* frame =
ExtensionApiFrameIdMap::GetRenderFrameHostById(tab, frame_id);
if (!frame) {
*error_out = base::StringPrintf("No frame with id %d in tab with id %d",
frame_id, target.tab_id);
return false;
}
DCHECK_EQ(content::WebContents::FromRenderFrameHost(frame), tab);
if (!HasPermissionToInjectIntoFrame(permissions, target.tab_id, frame,
error_out)) {
return false;
}
}
*frame_ids_out = std::move(frame_ids);
*frame_scope_out = frame_scope;
*script_executor_out = script_executor;
return true;
}
// Returns true if the loaded resource is valid for injection.
bool CheckLoadedResource(bool success,
std::string* data,
const std::string& file_name,
std::string* error) {
if (!success) {
*error = ErrorUtils::FormatErrorMessage(kCouldNotLoadFileError, file_name);
return false;
}
DCHECK(data);
// TODO(devlin): What necessitates this encoding requirement? Is it needed for
// blink injection?
if (!base::IsStringUTF8(*data)) {
constexpr char kBadFileEncodingError[] =
"Could not load file '*'. It isn't UTF-8 encoded.";
*error = ErrorUtils::FormatErrorMessage(kBadFileEncodingError, file_name);
return false;
}
return true;
}
// Checks the specified `files` for validity, and attempts to load and localize
// them, invoking `callback` with the result. Returns true on success; on
// failure, populates `error`.
bool CheckAndLoadFiles(const std::vector<std::string>& files,
const Extension& extension,
bool requires_localization,
LoadAndLocalizeResourceCallback callback,
std::string* error) {
ExtensionResource resource;
if (!GetFileResource(files, extension, &resource, error))
return false;
LoadAndLocalizeResource(extension, resource, requires_localization,
std::move(callback));
return true;
}
} // namespace
ScriptingExecuteScriptFunction::ScriptingExecuteScriptFunction() = default;
ScriptingExecuteScriptFunction::~ScriptingExecuteScriptFunction() = default;
ExtensionFunction::ResponseAction ScriptingExecuteScriptFunction::Run() {
std::unique_ptr<api::scripting::ExecuteScript::Params> params(
api::scripting::ExecuteScript::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params);
injection_ = std::move(params->injection);
if ((injection_.files && injection_.function) ||
(!injection_.files && !injection_.function)) {
return RespondNow(
Error("Exactly one of 'function' and 'files' must be specified"));
}
if (injection_.files) {
// JS files don't require localization.
constexpr bool kRequiresLocalization = false;
std::string error;
if (!CheckAndLoadFiles(
*injection_.files, *extension(), kRequiresLocalization,
base::BindOnce(&ScriptingExecuteScriptFunction::DidLoadResource,
this),
&error)) {
return RespondNow(Error(std::move(error)));
}
return RespondLater();
}
DCHECK(injection_.function);
// TODO(devlin): This (wrapping a function to create an IIFE) is pretty hacky,
// and won't work well when we support currying arguments. Add support to the
// ScriptExecutor to better support this case.
std::string code_to_execute =
base::StringPrintf("(%s)()", injection_.function->c_str());
std::string error;
if (!Execute(std::move(code_to_execute), /*script_src=*/GURL(), &error))
return RespondNow(Error(std::move(error)));
return RespondLater();
}
void ScriptingExecuteScriptFunction::DidLoadResource(
bool success,
std::unique_ptr<std::string> data) {
DCHECK(injection_.files);
DCHECK_EQ(1u, injection_.files->size());
std::string error;
if (!CheckLoadedResource(success, data.get(), injection_.files->at(0),
&error)) {
Respond(Error(std::move(error)));
return;
}
GURL script_url = extension()->GetResourceURL(injection_.files->at(0));
if (!Execute(std::move(*data), std::move(script_url), &error))
Respond(Error(std::move(error)));
}
bool ScriptingExecuteScriptFunction::Execute(std::string code_to_execute,
GURL script_url,
std::string* error) {
ScriptExecutor* script_executor = nullptr;
ScriptExecutor::FrameScope frame_scope = ScriptExecutor::SPECIFIED_FRAMES;
std::set<int> frame_ids;
if (!CanAccessTarget(*extension()->permissions_data(), injection_.target,
browser_context(), include_incognito_information(),
&script_executor, &frame_scope, &frame_ids, error)) {
return false;
}
script_executor->ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension()->id()),
mojom::ActionType::kAddJavascript, std::move(code_to_execute),
frame_scope, frame_ids, ScriptExecutor::MATCH_ABOUT_BLANK,
mojom::RunLocation::kDocumentIdle, ScriptExecutor::DEFAULT_PROCESS,
/* webview_src */ GURL(), std::move(script_url), user_gesture(),
mojom::CSSOrigin::kAuthor, ScriptExecutor::JSON_SERIALIZED_RESULT,
base::BindOnce(&ScriptingExecuteScriptFunction::OnScriptExecuted, this));
return true;
}
void ScriptingExecuteScriptFunction::OnScriptExecuted(
std::vector<ScriptExecutor::FrameResult> frame_results) {
// If only a single frame was included and the injection failed, respond with
// an error.
if (frame_results.size() == 1 && !frame_results[0].error.empty()) {
Respond(Error(std::move(frame_results[0].error)));
return;
}
// Otherwise, respond successfully. We currently just skip over individual
// frames that failed. In the future, we can bubble up these error messages
// to the extension.
std::vector<api::scripting::InjectionResult> injection_results;
for (auto& result : frame_results) {
if (!result.error.empty())
continue;
api::scripting::InjectionResult injection_result;
injection_result.result =
base::Value::ToUniquePtrValue(std::move(result.value));
injection_result.frame_id = result.frame_id;
// Put the top frame first; otherwise, any order.
if (result.frame_id == ExtensionApiFrameIdMap::kTopFrameId) {
injection_results.insert(injection_results.begin(),
std::move(injection_result));
} else {
injection_results.push_back(std::move(injection_result));
}
}
Respond(ArgumentList(
api::scripting::ExecuteScript::Results::Create(injection_results)));
}
ScriptingInsertCSSFunction::ScriptingInsertCSSFunction() = default;
ScriptingInsertCSSFunction::~ScriptingInsertCSSFunction() = default;
ExtensionFunction::ResponseAction ScriptingInsertCSSFunction::Run() {
std::unique_ptr<api::scripting::InsertCSS::Params> params(
api::scripting::InsertCSS::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params);
injection_ = std::move(params->injection);
if ((injection_.files && injection_.css) ||
(!injection_.files && !injection_.css)) {
return RespondNow(Error(kExactlyOneOfCssAndFilesError));
}
if (injection_.files) {
// CSS files require localization.
constexpr bool kRequiresLocalization = true;
std::string error;
if (!CheckAndLoadFiles(
*injection_.files, *extension(), kRequiresLocalization,
base::BindOnce(&ScriptingInsertCSSFunction::DidLoadResource, this),
&error)) {
return RespondNow(Error(std::move(error)));
}
return RespondLater();
}
DCHECK(injection_.css);
std::string error;
if (!Execute(std::move(*injection_.css), /*script_url=*/GURL(), &error)) {
return RespondNow(Error(std::move(error)));
}
return RespondLater();
}
void ScriptingInsertCSSFunction::DidLoadResource(
bool success,
std::unique_ptr<std::string> data) {
DCHECK(injection_.files);
DCHECK_EQ(1u, injection_.files->size());
std::string error;
if (!CheckLoadedResource(success, data.get(), injection_.files->at(0),
&error)) {
Respond(Error(std::move(error)));
return;
}
GURL script_url = extension()->GetResourceURL(injection_.files->at(0));
if (!Execute(std::move(*data), std::move(script_url), &error))
Respond(Error(std::move(error)));
}
bool ScriptingInsertCSSFunction::Execute(std::string code_to_execute,
GURL script_url,
std::string* error) {
ScriptExecutor* script_executor = nullptr;
ScriptExecutor::FrameScope frame_scope = ScriptExecutor::SPECIFIED_FRAMES;
std::set<int> frame_ids;
if (!CanAccessTarget(*extension()->permissions_data(), injection_.target,
browser_context(), include_incognito_information(),
&script_executor, &frame_scope, &frame_ids, error)) {
return false;
}
DCHECK(script_executor);
script_executor->ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension()->id()),
mojom::ActionType::kAddCss, std::move(code_to_execute), frame_scope,
frame_ids, ScriptExecutor::MATCH_ABOUT_BLANK, kCSSRunLocation,
ScriptExecutor::DEFAULT_PROCESS,
/* webview_src */ GURL(), std::move(script_url), user_gesture(),
ConvertStyleOriginToCSSOrigin(injection_.origin),
ScriptExecutor::NO_RESULT,
base::BindOnce(&ScriptingInsertCSSFunction::OnCSSInserted, this));
return true;
}
void ScriptingInsertCSSFunction::OnCSSInserted(
std::vector<ScriptExecutor::FrameResult> results) {
// If only a single frame was included and the injection failed, respond with
// an error.
if (results.size() == 1 && !results[0].error.empty()) {
Respond(Error(std::move(results[0].error)));
return;
}
Respond(NoArguments());
}
ScriptingRemoveCSSFunction::ScriptingRemoveCSSFunction() = default;
ScriptingRemoveCSSFunction::~ScriptingRemoveCSSFunction() = default;
ExtensionFunction::ResponseAction ScriptingRemoveCSSFunction::Run() {
std::unique_ptr<api::scripting::RemoveCSS::Params> params(
api::scripting::RemoveCSS::Params::Create(*args_));
EXTENSION_FUNCTION_VALIDATE(params);
api::scripting::CSSInjection& injection = params->injection;
if ((injection.files && injection.css) ||
(!injection.files && !injection.css)) {
return RespondNow(Error(kExactlyOneOfCssAndFilesError));
}
GURL script_url;
std::string error;
std::string code;
if (injection.files) {
// Note: Since we're just removing the CSS, we don't actually need to load
// the file here. It's okay for `code` to be empty in this case.
ExtensionResource resource;
if (!GetFileResource(*injection.files, *extension(), &resource, &error))
return RespondNow(Error(std::move(error)));
script_url = extension()->GetResourceURL(injection.files->at(0));
} else {
DCHECK(injection.css);
code = std::move(*injection.css);
}
ScriptExecutor* script_executor = nullptr;
ScriptExecutor::FrameScope frame_scope = ScriptExecutor::SPECIFIED_FRAMES;
std::set<int> frame_ids;
if (!CanAccessTarget(*extension()->permissions_data(), injection.target,
browser_context(), include_incognito_information(),
&script_executor, &frame_scope, &frame_ids, &error)) {
return RespondNow(Error(std::move(error)));
}
DCHECK(script_executor);
DCHECK(code.empty() || !script_url.is_valid());
script_executor->ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension()->id()),
mojom::ActionType::kRemoveCss, std::move(code), frame_scope, frame_ids,
ScriptExecutor::MATCH_ABOUT_BLANK, kCSSRunLocation,
ScriptExecutor::DEFAULT_PROCESS,
/* webview_src */ GURL(), std::move(script_url), user_gesture(),
ConvertStyleOriginToCSSOrigin(injection.origin),
ScriptExecutor::NO_RESULT,
base::BindOnce(&ScriptingRemoveCSSFunction::OnCSSRemoved, this));
return RespondLater();
}
void ScriptingRemoveCSSFunction::OnCSSRemoved(
std::vector<ScriptExecutor::FrameResult> results) {
// If only a single frame was included and the injection failed, respond with
// an error.
if (results.size() == 1 && !results[0].error.empty()) {
Respond(Error(std::move(results[0].error)));
return;
}
Respond(NoArguments());
}
} // namespace extensions