| // 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 <string> |
| #include <vector> |
| |
| #include "base/functional/bind.h" |
| #include "base/notreached.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/types/optional_util.h" |
| #include "extensions/browser/api/scripting/scripting_constants.h" |
| #include "extensions/browser/api/scripting/scripting_utils.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/extension_util.h" |
| #include "extensions/browser/user_script_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/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 kInvalidSourceError[] = |
| "User script with ID '*' must specify exactly one of 'code' or 'file' as a " |
| "js source."; |
| constexpr char kMatchesMissingError[] = |
| "User script with ID '*' must specify 'matches'."; |
| |
| 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); |
| |
| 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 not be empty. |
| if (user_script.js.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; |
| } |
| } |
| |
| // 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_NORETURN() |
| << "Execution world should always be present in serialization."; |
| case api::extension_types::ExecutionWorld::kIsolated: |
| NOTREACHED_NORETURN() |
| << "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); |
| |
| 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.empty()) { |
| original_script.js = std::move(new_script.js); |
| } |
| |
| if (new_script.world != api::user_scripts::ExecutionWorld::kNone) { |
| original_script.world = new_script.world; |
| } |
| |
| // 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> csp = params->properties.csp; |
| bool enable_messaging = params->properties.messaging.value_or(false); |
| |
| util::SetUserScriptWorldInfo(*extension(), browser_context(), csp, |
| enable_messaging); |
| |
| return RespondNow(NoArguments()); |
| } |
| |
| } // namespace extensions |