blob: cad05745d044200530636174aea39446272e464c [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 "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/user_script_manager.h"
#include "extensions/common/api/extension_types.h"
#include "extensions/common/api/user_scripts.h"
#include "extensions/common/user_script.h"
#include "extensions/common/utils/content_script_utils.h"
#include "extensions/common/utils/extension_types_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'.";
std::unique_ptr<UserScript> ParseUserScript(
const Extension& extension,
const api::user_scripts::RegisteredUserScript& user_script,
std::u16string* error) {
auto result = std::make_unique<UserScript>();
result->set_id(user_script.id);
result->set_host_id(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension.id()));
if (user_script.run_at != api::extension_types::RunAt::kNone) {
result->set_run_location(ConvertRunLocation(user_script.run_at));
}
if (user_script.all_frames) {
result->set_match_all_frames(*user_script.all_frames);
}
if (!user_script.matches) {
*error = ErrorUtils::FormatErrorMessageUTF16(
kMatchesMissingError,
UserScript::TrimPrefixFromScriptID(user_script.id));
return nullptr;
}
const int valid_schemes = UserScript::ValidUserScriptSchemes(
scripting::kScriptsCanExecuteEverywhere);
if (!script_parsing::ParseMatchPatterns(
*user_script.matches,
base::OptionalToPtr(user_script.exclude_matches),
extension.creation_flags(), scripting::kScriptsCanExecuteEverywhere,
valid_schemes, scripting::kAllUrlsIncludesChromeUrls, result.get(),
error, /*wants_file_access=*/nullptr,
/*definition_index=*/absl::nullopt)) {
return nullptr;
}
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;
}
if (source.code) {
// TODO(crbug.com/1385165): Register user scripts when code is given.
} else {
DCHECK(source.file);
GURL url = extension.GetResourceURL(*source.file);
ExtensionResource resource = extension.GetResource(*source.file);
result->js_scripts().push_back(UserScript::Content::CreateFile(
resource.extension_root(), resource.relative_path(), url));
}
}
return result;
}
// Converts a UserScript object to a api::user_scripts::RegisteredUserScript
// object, used for getScripts.
api::user_scripts::RegisteredUserScript CreateRegisteredUserScriptInfo(
const UserScript& script) {
api::user_scripts::RegisteredUserScript script_info;
CHECK_EQ(UserScript::Source::kDynamicUserScript, script.GetSource());
script_info.id = script.id();
script_info.all_frames = script.match_all_frames();
script_info.run_at = ConvertRunLocationForAPI(script.run_location());
script_info.matches.emplace();
script_info.matches->reserve(script.url_patterns().size());
for (const URLPattern& pattern : script.url_patterns()) {
script_info.matches->push_back(pattern.GetAsString());
}
if (!script.exclude_url_patterns().is_empty()) {
script_info.exclude_matches.emplace();
script_info.exclude_matches->reserve(script.exclude_url_patterns().size());
for (const URLPattern& pattern : script.exclude_url_patterns()) {
script_info.exclude_matches->push_back(pattern.GetAsString());
}
}
// File paths may be normalized in the returned object and can differ slightly
// compared to what was originally passed into userScripts.register.
if (!script.js_scripts().empty()) {
script_info.js.reserve(script.js_scripts().size());
for (const auto& file : script.js_scripts()) {
api::user_scripts::ScriptSource source;
source.file = file->relative_path().AsUTF8Unsafe();
script_info.js.push_back(std::move(source));
}
}
return script_info;
}
} // namespace
ExtensionFunction::ResponseAction UserScriptsRegisterFunction::Run() {
absl::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.
auto parsed_scripts = std::make_unique<UserScriptList>();
parsed_scripts->reserve(scripts.size());
std::u16string parse_error;
for (const auto& script : scripts) {
std::unique_ptr<UserScript> user_script =
ParseUserScript(*extension(), script, &parse_error);
if (!user_script) {
return RespondNow(Error(base::UTF16ToASCII(parse_error)));
}
parsed_scripts->push_back(std::move(user_script));
}
// 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;
}
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 absl::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() {
absl::optional<api::user_scripts::GetScripts::Params> params =
api::user_scripts::GetScripts::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
absl::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() {
absl::optional<api::user_scripts::Unregister::Params> params(
api::user_scripts::Unregister::Params::Create(args()));
EXTENSION_FUNCTION_VALIDATE(params);
EXTENSION_FUNCTION_VALIDATE(extension());
absl::optional<api::user_scripts::UserScriptFilter>& filter = params->filter;
absl::optional<std::vector<std::string>> ids = absl::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 absl::optional<std::string>& error) {
if (error.has_value()) {
Respond(Error(std::move(*error)));
} else {
Respond(NoArguments());
}
}
} // namespace extensions