blob: 894d8654150109b51d78ecf7a2a589fd1dbe9927 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/api/user_scripts/user_scripts_api.h"
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "base/format_macros.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/strings/escape.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/types/optional_util.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extension_user_script_loader.h"
#include "extensions/browser/scripting_constants.h"
#include "extensions/browser/scripting_utils.h"
#include "extensions/browser/user_script_manager.h"
#include "extensions/browser/user_script_world_configuration_manager.h"
#include "extensions/common/api/extension_types.h"
#include "extensions/common/api/scripts_internal/script_serialization.h"
#include "extensions/common/api/user_scripts.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/mojom/code_injection.mojom-forward.h"
#include "extensions/common/mojom/execution_world.mojom-shared.h"
#include "extensions/common/user_script.h"
#include "extensions/common/utils/content_script_utils.h"
namespace extensions {
namespace {
constexpr char kEmptySourceError[] =
"User script with ID '*' must specify at least one js source.";
constexpr char kEmptySourceErrorWithoutIdError[] =
"User script must specify at least one js source.";
constexpr char kInvalidSourceError[] =
"User script with ID '*' must specify exactly one of 'code' or 'file' as a "
"js source.";
constexpr char kInvalidSourceWithoutIdError[] =
"User script must specify exactly one of 'code' or 'file' as a js source.";
constexpr char kMatchesMissingError[] =
"User script with ID '*' must specify 'matches'.";
// Sanitizes the given `world_id`, updating it if necessary.
// Returns true on success; on failure, returns false and populates `error_out`.
bool IsValidWorldId(api::user_scripts::ExecutionWorld world,
std::optional<std::string>& world_id,
std::string* error_out) {
if (!world_id) {
// Omitting world ID is valid.
return true;
}
if (world != api::user_scripts::ExecutionWorld::kNone &&
world != api::user_scripts::ExecutionWorld::kUserScript) {
*error_out = "World ID can only be specified for USER_SCRIPT worlds.";
return false;
}
if (world_id->empty()) {
// Specifying an empty-string world ID is valid, and will use the default
// user script world. This is represented by nullopt elsewhere, so we update
// the world ID value.
world_id = std::nullopt;
return true;
}
if (world_id->at(0) == '_') {
*error_out = "World IDs beginning with '_' are reserved.";
return false;
}
static constexpr size_t kMaxWorldIdLength = 256;
if (world_id->length() > kMaxWorldIdLength) {
*error_out = "World IDs must be at most 256 characters.";
return false;
}
// Valid world ID!
return true;
}
mojom::ExecutionWorld ConvertExecutionWorld(
api::user_scripts::ExecutionWorld world) {
switch (world) {
// Execution world defaults to `kUserScript` when it's not provided.
case api::user_scripts::ExecutionWorld::kNone:
case api::user_scripts::ExecutionWorld::kUserScript:
return mojom::ExecutionWorld::kUserScript;
case api::user_scripts::ExecutionWorld::kMain:
return mojom::ExecutionWorld::kMain;
}
}
scripting::InjectionTarget ConvertToInternalInjectionTarget(
api::user_scripts::InjectionTarget injection_target) {
scripting::InjectionTarget internal_injection_target;
internal_injection_target.all_frames = injection_target.all_frames;
internal_injection_target.document_ids =
std::move(injection_target.document_ids);
internal_injection_target.frame_ids = std::move(injection_target.frame_ids);
internal_injection_target.tab_id = std::move(injection_target.tab_id);
return internal_injection_target;
}
api::scripts_internal::SerializedUserScript
ConvertRegisteredUserScriptToSerializedUserScript(
api::user_scripts::RegisteredUserScript user_script) {
auto user_script_sources_to_serialized_sources =
[](std::vector<api::user_scripts::ScriptSource> sources) {
std::vector<api::scripts_internal::ScriptSource> result;
result.reserve(sources.size());
for (auto& source : sources) {
api::scripts_internal::ScriptSource converted_source;
converted_source.code = std::move(source.code);
converted_source.file = std::move(source.file);
result.push_back(std::move(converted_source));
}
return result;
};
auto convert_execution_world = [](api::user_scripts::ExecutionWorld world) {
switch (world) {
// Execution world defaults to `kUserScript` when it's not provided.
case api::user_scripts::ExecutionWorld::kNone:
case api::user_scripts::ExecutionWorld::kUserScript:
return api::extension_types::ExecutionWorld::kUserScript;
case api::user_scripts::ExecutionWorld::kMain:
return api::extension_types::ExecutionWorld::kMain;
}
};
api::scripts_internal::SerializedUserScript serialized_script;
serialized_script.source = api::scripts_internal::Source::kDynamicUserScript;
serialized_script.all_frames = user_script.all_frames;
serialized_script.exclude_matches = std::move(user_script.exclude_matches);
// Note: IDs have already been prefixed appropriately.
serialized_script.id = std::move(user_script.id);
serialized_script.include_globs = std::move(user_script.include_globs);
serialized_script.exclude_globs = std::move(user_script.exclude_globs);
serialized_script.js =
user_script_sources_to_serialized_sources(std::move(*user_script.js));
serialized_script.matches = std::move(*user_script.matches);
serialized_script.run_at = std::move(user_script.run_at);
serialized_script.world = convert_execution_world(user_script.world);
if (base::FeatureList::IsEnabled(
extensions_features::kApiUserScriptsMultipleWorlds)) {
serialized_script.world_id = std::move(user_script.world_id);
}
return serialized_script;
}
std::unique_ptr<UserScript> ParseUserScript(
const Extension& extension,
api::user_scripts::RegisteredUserScript user_script,
bool allowed_in_incognito,
std::u16string* error) {
// Custom validation unique to user scripts.
// `matches` must be specified for newly-registered scripts, despite being
// an optional argument.
if (!user_script.matches) {
*error = ErrorUtils::FormatErrorMessageUTF16(
kMatchesMissingError,
UserScript::TrimPrefixFromScriptID(user_script.id));
return nullptr;
}
// `js` must be existent and not empty.
if (!user_script.js || user_script.js.value().empty()) {
*error = ErrorUtils::FormatErrorMessageUTF16(
kEmptySourceError, UserScript::TrimPrefixFromScriptID(user_script.id));
return nullptr;
}
for (const api::user_scripts::ScriptSource& source : *user_script.js) {
if ((source.code && source.file) || (!source.code && !source.file)) {
*error = ErrorUtils::FormatErrorMessageUTF16(
kInvalidSourceError,
UserScript::TrimPrefixFromScriptID(user_script.id));
return nullptr;
}
}
std::string utf8_error;
if (!IsValidWorldId(user_script.world, user_script.world_id, &utf8_error)) {
*error = base::UTF8ToUTF16(utf8_error);
return nullptr;
}
// After this, we can just convert to our internal type and rely on our
// typical parsing to a `UserScript`.
api::scripts_internal::SerializedUserScript serialized_script =
ConvertRegisteredUserScriptToSerializedUserScript(std::move(user_script));
return script_serialization::ParseSerializedUserScript(
serialized_script, extension, allowed_in_incognito, error);
}
// Converts a UserScript object to a api::user_scripts::RegisteredUserScript
// object, used for getScripts.
api::user_scripts::RegisteredUserScript CreateRegisteredUserScriptInfo(
const UserScript& script) {
CHECK_EQ(UserScript::Source::kDynamicUserScript, script.GetSource());
// To convert a `UserScript`, we first go through our script_internal
// serialization; this allows us to do simple conversions and avoid any
// complex logic.
api::scripts_internal::SerializedUserScript serialized_script =
script_serialization::SerializeUserScript(script);
auto convert_serialized_script_sources =
[](std::vector<api::scripts_internal::ScriptSource> sources) {
std::vector<api::user_scripts::ScriptSource> converted;
converted.reserve(sources.size());
for (auto& source : sources) {
api::user_scripts::ScriptSource converted_source;
converted_source.code = std::move(source.code);
converted_source.file = std::move(source.file);
converted.push_back(std::move(converted_source));
}
return converted;
};
auto convert_execution_world =
[](api::extension_types::ExecutionWorld world) {
switch (world) {
case api::extension_types::ExecutionWorld::kNone:
NOTREACHED()
<< "Execution world should always be present in serialization.";
case api::extension_types::ExecutionWorld::kIsolated:
NOTREACHED() << "ISOLATED worlds are not supported in this API.";
case api::extension_types::ExecutionWorld::kUserScript:
return api::user_scripts::ExecutionWorld::kUserScript;
case api::extension_types::ExecutionWorld::kMain:
return api::user_scripts::ExecutionWorld::kMain;
}
};
api::user_scripts::RegisteredUserScript result;
result.all_frames = serialized_script.all_frames;
result.exclude_matches = std::move(serialized_script.exclude_matches);
result.id = std::move(serialized_script.id);
result.include_globs = std::move(serialized_script.include_globs);
result.exclude_globs = std::move(serialized_script.exclude_globs);
if (serialized_script.js) {
result.js =
convert_serialized_script_sources(std::move(*serialized_script.js));
}
result.matches = std::move(serialized_script.matches);
result.run_at = serialized_script.run_at;
result.world = convert_execution_world(serialized_script.world);
result.world_id = std::move(serialized_script.world_id);
return result;
}
} // namespace
ExtensionFunction::ResponseAction UserScriptsRegisterFunction::Run() {
std::optional<api::user_scripts::Register::Params> params(
api::user_scripts::Register::Params::Create(args()));
EXTENSION_FUNCTION_VALIDATE(params);
EXTENSION_FUNCTION_VALIDATE(extension());
std::vector<api::user_scripts::RegisteredUserScript>& scripts =
params->scripts;
ExtensionUserScriptLoader* loader =
ExtensionSystem::Get(browser_context())
->user_script_manager()
->GetUserScriptLoaderForExtension(extension()->id());
// Create script ids for dynamic user scripts.
std::string error;
std::set<std::string> existing_script_ids =
loader->GetDynamicScriptIDs(UserScript::Source::kDynamicUserScript);
std::set<std::string> new_script_ids = scripting::CreateDynamicScriptIds(
scripts, UserScript::Source::kDynamicUserScript, existing_script_ids,
&error);
if (!error.empty()) {
CHECK(new_script_ids.empty());
return RespondNow(Error(std::move(error)));
}
// Parse user scripts.
UserScriptList parsed_scripts;
parsed_scripts.reserve(scripts.size());
std::u16string parse_error;
bool allowed_in_incognito = scripting::ScriptsShouldBeAllowedInIncognito(
extension()->id(), browser_context());
for (auto& script : scripts) {
std::unique_ptr<UserScript> user_script = ParseUserScript(
*extension(), std::move(script), allowed_in_incognito, &parse_error);
if (!user_script) {
return RespondNow(Error(base::UTF16ToASCII(parse_error)));
}
parsed_scripts.push_back(std::move(user_script));
}
scripts.clear(); // The contents of `scripts` have been std::move()d.
// Add new script IDs now in case another call with the same script IDs is
// made immediately following this one.
loader->AddPendingDynamicScriptIDs(std::move(new_script_ids));
GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&scripting::ValidateParsedScriptsOnFileThread,
script_parsing::GetSymlinkPolicy(extension()),
std::move(parsed_scripts)),
base::BindOnce(&UserScriptsRegisterFunction::OnUserScriptFilesValidated,
this));
// Balanced in `OnUserScriptFilesValidated()` or `OnUserScriptsRegistered()`.
AddRef();
return RespondLater();
}
void UserScriptsRegisterFunction::OnUserScriptFilesValidated(
scripting::ValidateScriptsResult result) {
// We cannot proceed if the `browser_context` is not valid as the
// `ExtensionSystem` will not exist.
if (!browser_context()) {
Release(); // Matches the `AddRef()` in `Run()`.
return;
}
// We cannot proceed if the extension is uninstalled or unloaded in the middle
// of validating its script files.
ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context());
if (!extension() ||
!registry->enabled_extensions().Contains(extension_id())) {
// Note: a Respond() is not needed if the system is shutting down or if the
// extension is no longer enabled.
Release(); // Matches the `AddRef()` in `Run()`.
return;
}
auto error = std::move(result.second);
auto scripts = std::move(result.first);
std::set<std::string> script_ids;
for (const auto& script : scripts) {
script_ids.insert(script->id());
}
ExtensionUserScriptLoader* loader =
ExtensionSystem::Get(browser_context())
->user_script_manager()
->GetUserScriptLoaderForExtension(extension()->id());
if (error.has_value()) {
loader->RemovePendingDynamicScriptIDs(std::move(script_ids));
Respond(Error(std::move(*error)));
Release(); // Matches the `AddRef()` in `Run()`.
return;
}
// User scripts are always persisted across sessions.
loader->AddDynamicScripts(
std::move(scripts), /*persistent_script_ids=*/std::move(script_ids),
base::BindOnce(&UserScriptsRegisterFunction::OnUserScriptsRegistered,
this));
}
void UserScriptsRegisterFunction::OnUserScriptsRegistered(
const std::optional<std::string>& error) {
if (error.has_value()) {
Respond(Error(std::move(*error)));
} else {
Respond(NoArguments());
}
Release(); // Matches the `AddRef()` in `Run()`.
}
ExtensionFunction::ResponseAction UserScriptsGetScriptsFunction::Run() {
std::optional<api::user_scripts::GetScripts::Params> params =
api::user_scripts::GetScripts::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
std::optional<api::user_scripts::UserScriptFilter>& filter = params->filter;
std::set<std::string> id_filter;
if (filter && filter->ids) {
id_filter.insert(std::make_move_iterator(filter->ids->begin()),
std::make_move_iterator(filter->ids->end()));
}
ExtensionUserScriptLoader* loader =
ExtensionSystem::Get(browser_context())
->user_script_manager()
->GetUserScriptLoaderForExtension(extension()->id());
const UserScriptList& dynamic_scripts = loader->GetLoadedDynamicScripts();
std::vector<api::user_scripts::RegisteredUserScript> registered_user_scripts;
for (const std::unique_ptr<UserScript>& script : dynamic_scripts) {
if (script->GetSource() != UserScript::Source::kDynamicUserScript) {
continue;
}
std::string id_without_prefix = script->GetIDWithoutPrefix();
if (filter && filter->ids &&
!base::Contains(id_filter, id_without_prefix)) {
continue;
}
auto user_script = CreateRegisteredUserScriptInfo(*script);
// Remove the internally used prefix from the `script`'s ID before
// returning.
user_script.id = id_without_prefix;
registered_user_scripts.push_back(std::move(user_script));
}
return RespondNow(ArgumentList(
api::user_scripts::GetScripts::Results::Create(registered_user_scripts)));
}
ExtensionFunction::ResponseAction UserScriptsUnregisterFunction::Run() {
std::optional<api::user_scripts::Unregister::Params> params(
api::user_scripts::Unregister::Params::Create(args()));
EXTENSION_FUNCTION_VALIDATE(params);
EXTENSION_FUNCTION_VALIDATE(extension());
std::optional<api::user_scripts::UserScriptFilter>& filter = params->filter;
std::optional<std::vector<std::string>> ids = std::nullopt;
if (filter && filter->ids) {
ids = filter->ids;
}
std::string error;
bool removal_triggered = scripting::RemoveScripts(
ids, UserScript::Source::kDynamicUserScript, browser_context(),
extension()->id(),
base::BindOnce(&UserScriptsUnregisterFunction::OnUserScriptsUnregistered,
this),
&error);
if (!removal_triggered) {
CHECK(!error.empty());
return RespondNow(Error(std::move(error)));
}
return RespondLater();
}
void UserScriptsUnregisterFunction::OnUserScriptsUnregistered(
const std::optional<std::string>& error) {
if (error.has_value()) {
Respond(Error(std::move(*error)));
} else {
Respond(NoArguments());
}
}
ExtensionFunction::ResponseAction UserScriptsUpdateFunction::Run() {
std::optional<api::user_scripts::Update::Params> params(
api::user_scripts::Update::Params::Create(args()));
EXTENSION_FUNCTION_VALIDATE(params);
EXTENSION_FUNCTION_VALIDATE(extension());
std::vector<api::user_scripts::RegisteredUserScript>& scripts_to_update =
params->scripts;
std::string error;
// Add the prefix for dynamic user scripts onto the IDs of all `scripts`
// before continuing.
std::set<std::string> ids_to_update = scripting::CreateDynamicScriptIds(
scripts_to_update, UserScript::Source::kDynamicUserScript,
/*existing_script_ids=*/std::set<std::string>(), &error);
if (!error.empty()) {
CHECK(ids_to_update.empty());
return RespondNow(Error(std::move(error)));
}
ExtensionUserScriptLoader* loader =
ExtensionSystem::Get(browser_context())
->user_script_manager()
->GetUserScriptLoaderForExtension(extension()->id());
UserScriptList parsed_scripts = scripting::UpdateScripts(
scripts_to_update, UserScript::Source::kDynamicUserScript, *loader,
base::BindRepeating(&CreateRegisteredUserScriptInfo),
base::BindRepeating(&UserScriptsUpdateFunction::ApplyUpdate, this),
&error);
if (!error.empty()) {
CHECK(parsed_scripts.empty());
return RespondNow(Error(std::move(error)));
}
// Add new script IDs now in case another call with the same script IDs is
// made immediately following this one.
loader->AddPendingDynamicScriptIDs(std::move(ids_to_update));
GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&scripting::ValidateParsedScriptsOnFileThread,
script_parsing::GetSymlinkPolicy(extension()),
std::move(parsed_scripts)),
base::BindOnce(&UserScriptsUpdateFunction::OnUserScriptFilesValidated,
this));
// Balanced in `OnUserScriptFilesValidated()`.
AddRef();
return RespondLater();
}
std::unique_ptr<UserScript> UserScriptsUpdateFunction::ApplyUpdate(
api::user_scripts::RegisteredUserScript& new_script,
api::user_scripts::RegisteredUserScript& original_script,
std::u16string* parse_error) {
if (new_script.run_at != api::extension_types::RunAt::kNone) {
original_script.run_at = new_script.run_at;
}
if (new_script.all_frames) {
original_script.all_frames = *new_script.all_frames;
}
if (new_script.matches) {
original_script.matches = std::move(new_script.matches);
}
if (new_script.exclude_matches) {
original_script.exclude_matches = std::move(new_script.exclude_matches);
}
if (new_script.js) {
original_script.js = std::move(new_script.js);
}
if (new_script.world != api::user_scripts::ExecutionWorld::kNone) {
original_script.world = new_script.world;
}
if (new_script.world_id) {
original_script.world_id = std::move(new_script.world_id);
}
// Note: for the update application, we disregard allowed_in_incognito.
// We'll set it on the resulting scripts.
constexpr bool kAllowedInIncognito = false;
std::unique_ptr<UserScript> parsed_script =
ParseUserScript(*extension(), std::move(original_script),
kAllowedInIncognito, parse_error);
return parsed_script;
}
void UserScriptsUpdateFunction::OnUserScriptFilesValidated(
scripting::ValidateScriptsResult result) {
// We cannot proceed if the `browser_context` is not valid as the
// `ExtensionSystem` will not exist.
if (!browser_context()) {
Release(); // Matches the `AddRef()` in `Run()`.
return;
}
// We cannot proceed if the extension is uninstalled or unloaded in the middle
// of validating its script files.
ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context());
if (!extension() ||
!registry->enabled_extensions().Contains(extension_id())) {
// Note: a Respond() is not needed if the system is shutting down or if the
// extension is no longer enabled.
Release(); // Matches the `AddRef()` in `Run()`.
return;
}
auto error = std::move(result.second);
auto scripts = std::move(result.first);
ExtensionUserScriptLoader* loader =
ExtensionSystem::Get(browser_context())
->user_script_manager()
->GetUserScriptLoaderForExtension(extension()->id());
bool allowed_in_incognito = scripting::ScriptsShouldBeAllowedInIncognito(
extension()->id(), browser_context());
std::set<std::string> script_ids;
for (const auto& script : scripts) {
script_ids.insert(script->id());
script->set_incognito_enabled(allowed_in_incognito);
}
if (error.has_value()) {
loader->RemovePendingDynamicScriptIDs(script_ids);
Respond(Error(std::move(*error)));
Release(); // Matches the `AddRef()` in `Run()`.
return;
}
// User scripts are always persisted across sessions.
std::set<std::string> persistent_script_ids = script_ids;
loader->UpdateDynamicScripts(
std::move(scripts), std::move(script_ids),
std::move(persistent_script_ids),
base::BindOnce(&UserScriptsUpdateFunction::OnUserScriptsUpdated, this));
}
void UserScriptsUpdateFunction::OnUserScriptsUpdated(
const std::optional<std::string>& error) {
if (error.has_value()) {
Respond(Error(std::move(*error)));
} else {
Respond(NoArguments());
}
Release(); // Matches the `AddRef()` in `Run()`.
}
ExtensionFunction::ResponseAction UserScriptsConfigureWorldFunction::Run() {
std::optional<api::user_scripts::ConfigureWorld::Params> params(
api::user_scripts::ConfigureWorld::Params::Create(args()));
EXTENSION_FUNCTION_VALIDATE(params);
EXTENSION_FUNCTION_VALIDATE(extension());
std::optional<std::string> world_id;
if (base::FeatureList::IsEnabled(
extensions_features::kApiUserScriptsMultipleWorlds)) {
world_id = std::move(params->properties.world_id);
}
std::string error;
if (!IsValidWorldId(api::user_scripts::ExecutionWorld::kUserScript, world_id,
&error)) {
return RespondNow(Error(std::move(error)));
}
UserScriptWorldConfigurationManager* config_manager =
UserScriptWorldConfigurationManager::Get(browser_context());
static constexpr size_t kMaxNumberOfRegisteredWorlds = 100;
if (config_manager->GetAllUserScriptWorlds(extension()->id()).size() >=
kMaxNumberOfRegisteredWorlds) {
return RespondNow(
Error(base::StringPrintf("You may only configure up to %" PRIuS
" individual user script worlds.",
kMaxNumberOfRegisteredWorlds)));
}
mojom::UserScriptWorldInfoPtr world_info =
config_manager->GetUserScriptWorldInfo(extension()->id(), world_id);
bool changed = false;
std::optional<std::string> csp = std::move(params->properties.csp);
if (csp && csp != world_info->csp) {
changed = true;
world_info->csp = std::move(csp);
}
std::optional<bool> enable_messaging = params->properties.messaging;
if (enable_messaging && *enable_messaging != world_info->enable_messaging) {
changed = true;
world_info->enable_messaging = *enable_messaging;
}
if (changed) {
config_manager->SetUserScriptWorldInfo(*extension(), std::move(world_info));
}
return RespondNow(NoArguments());
}
ExtensionFunction::ResponseAction UserScriptsExecuteFunction::Run() {
std::optional<api::user_scripts::Execute::Params> params(
api::user_scripts::Execute::Params::Create(args()));
EXTENSION_FUNCTION_VALIDATE(params);
EXTENSION_FUNCTION_VALIDATE(extension());
injection_ = std::move(params->injection);
// Validate injection sources.
if (injection_.js.empty()) {
return RespondNow(Error(kEmptySourceErrorWithoutIdError));
}
for (const api::user_scripts::ScriptSource& source : injection_.js) {
if ((source.code && source.file) || (!source.code && !source.file)) {
return RespondNow(Error(kInvalidSourceWithoutIdError));
}
}
// Validate injection world id.
std::string error;
if (!IsValidWorldId(injection_.world, injection_.world_id, &error)) {
return RespondNow(Error(std::move(error)));
}
// Validate injection target.
scripting::InjectionTarget internal_injection_target =
ConvertToInternalInjectionTarget(std::move(injection_.target));
ScriptExecutor* script_executor = nullptr;
ScriptExecutor::FrameScope frame_scope = ScriptExecutor::SPECIFIED_FRAMES;
std::set<int> frame_ids;
if (!scripting::CanAccessTarget(
*extension()->permissions_data(), internal_injection_target,
browser_context(), include_incognito_information(), &script_executor,
&frame_scope, &frame_ids, &error)) {
return RespondNow(Error(std::move(error)));
}
// Retrieve injection sources in order. Since file sources need to be loaded,
// we use a nullopt for placeholder in the source list. After files are
// successfully loaded, they should be converted to js source too.
std::vector<std::string> file_sources;
std::vector<std::optional<mojom::JSSourcePtr>> sources;
for (api::user_scripts::ScriptSource& source : injection_.js) {
if (source.file) {
file_sources.push_back(std::move(*source.file));
sources.push_back(std::nullopt);
} else {
CHECK(source.code);
sources.push_back(mojom::JSSource::New(std::move(*source.code), GURL()));
}
}
if (!file_sources.empty()) {
// JS files don't require localization.
constexpr bool kRequiresLocalization = false;
scripting::CheckAndLoadFiles(
std::move(file_sources), script_parsing::ContentScriptType::kJs,
*extension(), kRequiresLocalization,
base::BindOnce(&UserScriptsExecuteFunction::DidLoadResources, this,
script_executor, frame_scope, std::move(frame_ids),
std::move(sources)),
&error);
} else {
Execute(std::move(sources), script_executor, frame_scope, frame_ids,
&error);
}
return RespondLater();
}
void UserScriptsExecuteFunction::DidLoadResources(
ScriptExecutor* script_executor,
ScriptExecutor::FrameScope frame_scope,
std::set<int> frame_ids,
std::vector<std::optional<mojom::JSSourcePtr>> sources,
std::vector<scripting::InjectedFileSource> file_sources,
std::optional<std::string> load_error) {
if (load_error) {
Respond(Error(std::move(*load_error)));
return;
}
DCHECK(!file_sources.empty());
// Convert the file sources to js and add them to sources in their
// corresponding execution order.
int file_index = 0;
for (std::optional<mojom::JSSourcePtr>& source : sources) {
// Only file sources have a nullopt placeholder value.
if (!source.has_value()) {
CHECK_LT(file_index, static_cast<int>(file_sources.size()));
source = mojom::JSSource::New(
std::move(*file_sources[file_index].data),
extension()->GetResourceURL(
base::EscapePath(file_sources[file_index].file_name)));
file_index++;
}
}
// Verify all the file sources where properly added to the sources.
CHECK_EQ(file_index, static_cast<int>(file_sources.size()));
std::string error;
Execute(std::move(sources), script_executor, frame_scope, frame_ids, &error);
}
void UserScriptsExecuteFunction::Execute(
std::vector<std::optional<mojom::JSSourcePtr>> sources,
ScriptExecutor* script_executor,
ScriptExecutor::FrameScope frame_scope,
std::set<int> frame_ids,
std::string* error) {
mojom::ExecutionWorld execution_world =
ConvertExecutionWorld(injection_.world);
std::optional<std::string> execution_world_id = injection_.world_id;
bool inject_immediately = injection_.inject_immediately.value_or(false);
std::vector<mojom::JSSourcePtr> js_sources;
js_sources.reserve(sources.size());
for (std::optional<mojom::JSSourcePtr>& source : sources) {
CHECK(source.has_value());
js_sources.push_back(std::move(*source));
}
scripting::ExecuteScript(
extension()->id(), std::move(js_sources), execution_world,
execution_world_id, script_executor, frame_scope, frame_ids,
inject_immediately, user_gesture(),
base::BindOnce(&UserScriptsExecuteFunction::OnScriptExecuted, this));
}
void UserScriptsExecuteFunction::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::user_scripts::InjectionResult> injection_results;
for (auto& result : frame_results) {
if (!result.error.empty()) {
continue;
}
api::user_scripts::InjectionResult injection_result;
injection_result.result = std::move(result.value);
injection_result.frame_id = result.frame_id;
if (result.document_id) {
injection_result.document_id = result.document_id.ToString();
}
// 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::user_scripts::Execute::Results::Create(injection_results)));
}
ExtensionFunction::ResponseAction
UserScriptsGetWorldConfigurationsFunction::Run() {
EXTENSION_FUNCTION_VALIDATE(extension());
std::vector<mojom::UserScriptWorldInfoPtr> world_configurations =
UserScriptWorldConfigurationManager::Get(browser_context())
->GetAllUserScriptWorlds(extension()->id());
std::vector<api::user_scripts::WorldProperties> result;
result.reserve(world_configurations.size());
for (const auto& world : world_configurations) {
api::user_scripts::WorldProperties converted;
converted.messaging = world->enable_messaging;
converted.csp = world->csp;
converted.world_id = world->world_id;
result.push_back(std::move(converted));
}
return RespondNow(ArgumentList(
api::user_scripts::GetWorldConfigurations::Results::Create(result)));
}
ExtensionFunction::ResponseAction
UserScriptsResetWorldConfigurationFunction::Run() {
std::optional<api::user_scripts::ResetWorldConfiguration::Params> params(
api::user_scripts::ResetWorldConfiguration::Params::Create(args()));
EXTENSION_FUNCTION_VALIDATE(params);
EXTENSION_FUNCTION_VALIDATE(extension());
// In theory, it'd be safe to just pass in `world_id` without validating it
// because we should never have an invalid world ID in the preferences. But
// that's a fragile guarantee and may change if e.g. we start using reserved
// world IDs. Validate to be on the safe side.
std::string error;
if (!IsValidWorldId(api::user_scripts::ExecutionWorld::kUserScript,
params->world_id, &error)) {
return RespondNow(Error(std::move(error)));
}
UserScriptWorldConfigurationManager::Get(browser_context())
->ClearUserScriptWorldInfo(*extension(), params->world_id);
return RespondNow(NoArguments());
}
} // namespace extensions