| // 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 |