electron/shell/browser/extensions/api/scripting/scripting_api.cc
electron-roller[bot] c670e38b4b
chore: bump chromium to 125.0.6412.0 (main) (#41610)
* chore: bump chromium in DEPS to 124.0.6361.0

* chore: bump chromium in DEPS to 124.0.6363.0

* chore: update patches

Manually apply printing.patch w/no code changes due to upstream shear.
Xref: https://chromium-review.googlesource.com/c/chromium/src/+/5349263

* chore: bump chromium in DEPS to 124.0.6365.0

* chore: bump chromium in DEPS to 124.0.6367.0

* update patches

* 5371735: Rename SystemGeolocationSourceMac to SystemGeolocationSourceApple

https://chromium-review.googlesource.com/c/chromium/src/+/5371735

* missed a MAS bit

* chore: update windows toolchain

5350823: New toolchain for Windows 11 10.0.22621.2428 SDK | https://chromium-review.googlesource.com/c/chromium/src/+/5350823

* chore: bump chromium in DEPS to 125.0.6368.0

* fix patches

* chore: update patches

* 5232401: [PDF] Move generic utils from //chrome to //components/pdf (1/2)

https://chromium-review.googlesource.com/c/chromium/src/+/5232401

* revert https://chromium-review.googlesource.com/c/chromium/src/+/5380898

* chore: bump chromium in DEPS to 125.0.6370.0

* build: use updated windows toolchain

* fix patches

* chore: update patches

* more pdf_util to components

* 5372414: [Extensions] Remove DispatcherDelegate

https://chromium-review.googlesource.com/c/chromium/src/+/5372414

* fix accessibility_ui patch

* chore: bump chromium in DEPS to 125.0.6372.0

* chore: bump chromium in DEPS to 125.0.6374.0

* chore: bump chromium in DEPS to 125.0.6376.0

* chore: bump chromium in DEPS to 125.0.6378.0

* chore: bump chromium in DEPS to 125.0.6379.3

* chore: update patches (+ MAS patch changes)

* chore: update patches

* 5381159: Cleanup media::KeySystemSupportObserver
https://chromium-review.googlesource.com/c/chromium/src/+/5381159

* 5382233: Reland "Web `Speech to Text` with SODA backend"
https://chromium-review.googlesource.com/c/chromium/src/+/5382233

* chore: update `exclusive_access` patch

- 5367497: Add a metric for the website state when Fullscreen API is requested
  - https://chromium-review.googlesource.com/c/chromium/src/+/5367497

* chore: add build dependency

5367497: Add a metric for the website state when Fullscreen API is requested
https://chromium-review.googlesource.com/c/chromium/src/+/5367497

* chore: bump chromium in DEPS to 125.0.6382.0

* chore: update libcxx filenames

* chore: update patches

* chore: bump chromium in DEPS to 125.0.6384.0

* chore: remove old patch

* 5394039: [Extensions] Change "blessed" -> "privileged" in extension feature files
https://chromium-review.googlesource.com/c/chromium/src/+/5394039

* fix: remove deprecated errno constants in node/libuv

* 5362194: Return expected from ProcessMetrics CPU methods
https://chromium-review.googlesource.com/c/chromium/src/+/5362194

* 5383927: Add new Pickle factory functions with explicit ownership
https://chromium-review.googlesource.com/c/chromium/src/+/5383927

* 5373340: Simplify app-region/Draggable Region implementation
https://chromium-review.googlesource.com/c/chromium/src/+/5373340

* 5386875: Cleanup printing preferences files
https://chromium-review.googlesource.com/c/chromium/src/+/5386875

* chore: update libc++ filenames

* fix: add enterprise buildflags dep

* chore: bump chromium in DEPS to 125.0.6386.0

* chore: add build dep

* chore: update patches

* chore: bump chromium in DEPS to 125.0.6388.0

* chore: bump chromium in DEPS to 125.0.6390.0

* chore: update patches

* 4918014: preloading: Add NewTabPagePageLoadMetricsObserver
https://chromium-review.googlesource.com/c/chromium/src/+/4918014

* 5401234: [PDF] Remove `PDFDocumentHelperClient::FindPdfChildFrame` API
https://chromium-review.googlesource.com/c/chromium/src/+/5401234

* 5116175: Relocate Windows XPS printing feature helper methods
https://chromium-review.googlesource.com/c/chromium/src/+/5116175

* fixup! 5373340: Simplify app-region/Draggable Region implementation https://chromium-review.googlesource.com/c/chromium/src/+/5373340

* fixup! chore: add build dep

* chore: remove dead code & dead patch

Was dealing with https://chromium-review.googlesource.com/c/chromium/src/+/5402805 when I realized this code is no longer possible to call. It seems like this code became dead in the previous roll (#41514).

The patch exposed a `DxdiagDx12VulkanRequested` method on Chromium's `GpuDataManagerImpl`, which we consumed only in our own `GPUInfoManager::NeedsCompleteGpuInfoCollection`. There are no other references to this method, so it and the patch can both be deleted. Yay!

* chore: bump chromium in DEPS to 125.0.6392.0

* chore: bump chromium in DEPS to 125.0.6393.0

* chore: update patches

* chore: bump chromium in DEPS to 125.0.6394.0

* chore: bump chromium in DEPS to 125.0.6396.0

* chore: bump chromium in DEPS to 125.0.6397.0

* chore: update printing.patch

Xref: https://chromium-review.googlesource.com/c/chromium/src/+/5100842

No code changes, but had to apply patch manually due to upstream code shear

* chore: update add_maximized_parameter_to_linuxui_getwindowframeprovider.patch

No manual changes; patch applied with fuzz 1

* chore: update feat_allow_code_cache_in_custom_schemes.patch

No manual changes; patch applied with fuzz 2

* chore: silence "space before tab in indent" git rebase-apply warning

* chore: e patches all

* build: update all.gn to avoid FTBFS when disabling raw_ptr

Xref: https://chromium-review.googlesource.com/c/chromium/src/+/5371737

* Rename PdfService Mojo interface to PdfHost

Xref: https://chromium-review.googlesource.com/c/chromium/src/+/5411957

* chore: bump chromium in DEPS to 125.0.6398.0

* chore: update patches

* chore: bump chromium in DEPS to 125.0.6400.0

* chore: update patches

* [media] Remove unused `GetSupportedKeySystems` from MediaClient

Xref: https://chromium-review.googlesource.com/c/chromium/src/+/5420247

* chore: update JSInjection::New call to match upstream change

Xref: https://chromium-review.googlesource.com/c/chromium/src/+/5403967

[Extensions] Wire up the renderer for multiple user script worlds

* 5362362: Derive display ID from monitor adapter ID instead of szDevice.

https://chromium-review.googlesource.com/c/chromium/src/+/5362362

* 5116175: Relocate Windows XPS printing feature helper methods

https://chromium-review.googlesource.com/c/chromium/src/+/5116175

* chore: add v8-sandbox.h to electron-node

* chore: update patches

* chore: update patches

* fixup! 5394039: [Extensions] Change blessed -> privileged in extension feature files

* chore: bump chromium in DEPS to 125.0.6412.0

* chore: update patches

* chore: node script/gen-libc++-filenames.js

* [FPF] Create Fingerprinting Protection ruleset service.

Refs https://chromium-review.googlesource.com/c/chromium/src/+/5420158

* Add ExclusiveAccessPermissionManager

Refs https://chromium-review.googlesource.com/c/chromium/src/+/5273787

* Preserve the PNG colorspace when decoding into a SkBitmap.

Refs https://chromium-review.googlesource.com/c/chromium/src/+/5421254

* chore: iwyu

* fix: abstract-socket compilation

* ci: bump container for node 20 support

* fixup! abstract-socket compilation

* fix: compiling nan specs

* chore: revert winreg version bump

accidental bump to 1.2.5 revealed failing app.setasdefaultprotocolclient
test suite. Should be revisited separately.

* ci: set node 20 for darwin x64 tests

* fix: broken patch export

* chore: cleanup mas_avoid_private_macos_api_usage.patch.patch

Removed code that was inadvertently put back after https://chromium-review.googlesource.com/c/chromium/src/+/5348565 removed it

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: Jeremy Rose <jeremya@chromium.org>
Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
Co-authored-by: clavin <clavin@electronjs.org>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
Co-authored-by: deepak1556 <hop2deep@gmail.com>
2024-04-15 18:10:32 -04:00

1376 lines
52 KiB
C++

// Copyright 2023 Microsoft, GmbH
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/extensions/api/scripting/scripting_api.h"
#include <algorithm>
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/json/json_writer.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/types/optional_util.h"
#include "chrome/common/extensions/api/scripting.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "extensions/browser/api/scripting/scripting_constants.h"
#include "extensions/browser/api/scripting/scripting_utils.h"
#include "extensions/browser/extension_api_frame_id_map.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extension_user_script_loader.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/load_and_localize_file.h"
#include "extensions/browser/script_executor.h"
#include "extensions/browser/user_script_manager.h"
#include "extensions/common/api/extension_types.h"
#include "extensions/common/api/scripts_internal.h"
#include "extensions/common/api/scripts_internal/script_serialization.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/mojom/css_origin.mojom-shared.h"
#include "extensions/common/mojom/execution_world.mojom-shared.h"
#include "extensions/common/mojom/host_id.mojom.h"
#include "extensions/common/mojom/run_location.mojom-shared.h"
#include "extensions/common/permissions/api_permission.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/common/script_constants.h"
#include "extensions/common/utils/content_script_utils.h"
#include "extensions/common/utils/extension_types_utils.h"
#include "shell/browser/api/electron_api_web_contents.h"
namespace extensions {
namespace {
constexpr char kCouldNotLoadFileError[] = "Could not load file: '*'.";
constexpr char kDuplicateFileSpecifiedError[] =
"Duplicate file specified: '*'.";
constexpr char kExactlyOneOfCssAndFilesError[] =
"Exactly one of 'css' and 'files' must be specified.";
constexpr char kNonExistentScriptIdError[] = "Nonexistent script ID '*'";
// Note: CSS always injects as soon as possible, so we default to
// document_start. Because of tab loading, there's no guarantee this will
// *actually* inject before page load, but it will at least inject "soon".
constexpr mojom::RunLocation kCSSRunLocation =
mojom::RunLocation::kDocumentStart;
// Converts the given `style_origin` to a CSSOrigin.
mojom::CSSOrigin ConvertStyleOriginToCSSOrigin(
api::scripting::StyleOrigin style_origin) {
mojom::CSSOrigin css_origin = mojom::CSSOrigin::kAuthor;
switch (style_origin) {
case api::scripting::StyleOrigin::kNone:
case api::scripting::StyleOrigin::kAuthor:
css_origin = mojom::CSSOrigin::kAuthor;
break;
case api::scripting::StyleOrigin::kUser:
css_origin = mojom::CSSOrigin::kUser;
break;
}
return css_origin;
}
mojom::ExecutionWorld ConvertExecutionWorld(
api::scripting::ExecutionWorld world) {
mojom::ExecutionWorld execution_world = mojom::ExecutionWorld::kIsolated;
switch (world) {
case api::scripting::ExecutionWorld::kNone:
case api::scripting::ExecutionWorld::kIsolated:
break; // Default to mojom::ExecutionWorld::kIsolated.
case api::scripting::ExecutionWorld::kMain:
execution_world = mojom::ExecutionWorld::kMain;
}
return execution_world;
}
std::string InjectionKeyForCode(const mojom::HostID& host_id,
const std::string& code) {
return ScriptExecutor::GenerateInjectionKey(host_id, /*script_url=*/GURL(),
code);
}
std::string InjectionKeyForFile(const mojom::HostID& host_id,
const GURL& resource_url) {
return ScriptExecutor::GenerateInjectionKey(host_id, resource_url,
/*code=*/std::string());
}
// Constructs an array of file sources from the read file `data`.
std::vector<InjectedFileSource> ConstructFileSources(
std::vector<std::unique_ptr<std::string>> data,
std::vector<std::string> file_names) {
// Note: CHECK (and not DCHECK) because if it fails, we have an out-of-bounds
// access.
CHECK_EQ(data.size(), file_names.size());
const size_t num_sources = data.size();
std::vector<InjectedFileSource> sources;
sources.reserve(num_sources);
for (size_t i = 0; i < num_sources; ++i)
sources.emplace_back(std::move(file_names[i]), std::move(data[i]));
return sources;
}
std::vector<mojom::JSSourcePtr> FileSourcesToJSSources(
const Extension& extension,
std::vector<InjectedFileSource> file_sources) {
std::vector<mojom::JSSourcePtr> js_sources;
js_sources.reserve(file_sources.size());
for (auto& file_source : file_sources) {
js_sources.push_back(
mojom::JSSource::New(std::move(*file_source.data),
extension.GetResourceURL(file_source.file_name)));
}
return js_sources;
}
std::vector<mojom::CSSSourcePtr> FileSourcesToCSSSources(
const Extension& extension,
std::vector<InjectedFileSource> file_sources) {
mojom::HostID host_id(mojom::HostID::HostType::kExtensions, extension.id());
std::vector<mojom::CSSSourcePtr> css_sources;
css_sources.reserve(file_sources.size());
for (auto& file_source : file_sources) {
css_sources.push_back(mojom::CSSSource::New(
std::move(*file_source.data),
InjectionKeyForFile(host_id,
extension.GetResourceURL(file_source.file_name))));
}
return css_sources;
}
// Checks `files` and populates `resources_out` with the appropriate extension
// resource. Returns true on success; on failure, populates `error_out`.
bool GetFileResources(const std::vector<std::string>& files,
const Extension& extension,
std::vector<ExtensionResource>* resources_out,
std::string* error_out) {
if (files.empty()) {
static constexpr char kAtLeastOneFileError[] =
"At least one file must be specified.";
*error_out = kAtLeastOneFileError;
return false;
}
std::vector<ExtensionResource> resources;
for (const auto& file : files) {
ExtensionResource resource = extension.GetResource(file);
if (resource.extension_root().empty() || resource.relative_path().empty()) {
*error_out = ErrorUtils::FormatErrorMessage(kCouldNotLoadFileError, file);
return false;
}
// ExtensionResource doesn't implement an operator==.
if (base::Contains(resources, resource.relative_path(),
&ExtensionResource::relative_path)) {
// Disallow duplicates. Note that we could allow this, if we wanted (and
// there *might* be reason to with JS injection, to perform an operation
// twice?). However, this matches content script behavior, and injecting
// twice can be done by chaining calls to executeScript() / insertCSS().
// This isn't a robust check, and could probably be circumvented by
// passing two paths that look different but are the same - but in that
// case, we just try to load and inject the script twice, which is
// inefficient, but safe.
*error_out =
ErrorUtils::FormatErrorMessage(kDuplicateFileSpecifiedError, file);
return false;
}
resources.push_back(std::move(resource));
}
resources_out->swap(resources);
return true;
}
using ResourcesLoadedCallback =
base::OnceCallback<void(std::vector<InjectedFileSource>,
std::optional<std::string>)>;
// Checks the loaded content of extension resources. Invokes `callback` with
// the constructed file sources on success or with an error on failure.
void CheckLoadedResources(std::vector<std::string> file_names,
ResourcesLoadedCallback callback,
std::vector<std::unique_ptr<std::string>> file_data,
std::optional<std::string> load_error) {
if (load_error) {
std::move(callback).Run({}, std::move(load_error));
return;
}
std::vector<InjectedFileSource> file_sources =
ConstructFileSources(std::move(file_data), std::move(file_names));
for (const auto& source : file_sources) {
DCHECK(source.data);
// TODO(devlin): What necessitates this encoding requirement? Is it needed
// for blink injection?
if (!base::IsStringUTF8(*source.data)) {
static constexpr char kBadFileEncodingError[] =
"Could not load file '*'. It isn't UTF-8 encoded.";
std::string error = ErrorUtils::FormatErrorMessage(kBadFileEncodingError,
source.file_name);
std::move(callback).Run({}, std::move(error));
return;
}
}
std::move(callback).Run(std::move(file_sources), std::nullopt);
}
// Checks the specified `files` for validity, and attempts to load and localize
// them, invoking `callback` with the result. Returns true on success; on
// failure, populates `error`.
bool CheckAndLoadFiles(std::vector<std::string> files,
const Extension& extension,
bool requires_localization,
ResourcesLoadedCallback callback,
std::string* error) {
std::vector<ExtensionResource> resources;
if (!GetFileResources(files, extension, &resources, error))
return false;
LoadAndLocalizeResources(
extension, resources, requires_localization,
script_parsing::GetMaxScriptLength(),
base::BindOnce(&CheckLoadedResources, std::move(files),
std::move(callback)));
return true;
}
// Returns an error message string for when an extension cannot access a page it
// is attempting to.
std::string GetCannotAccessPageErrorMessage(const PermissionsData& permissions,
const GURL& url) {
if (permissions.HasAPIPermission(mojom::APIPermissionID::kTab)) {
return ErrorUtils::FormatErrorMessage(
manifest_errors::kCannotAccessPageWithUrl, url.spec());
}
return manifest_errors::kCannotAccessPage;
}
// Returns true if the `permissions` allow for injection into the given `frame`.
// If false, populates `error`.
bool HasPermissionToInjectIntoFrame(const PermissionsData& permissions,
int tab_id,
content::RenderFrameHost* frame,
std::string* error) {
GURL committed_url = frame->GetLastCommittedURL();
if (committed_url.is_empty()) {
if (!frame->IsInPrimaryMainFrame()) {
// We can't check the pending URL for subframes from the //chrome layer.
// Assume the injection is allowed; the renderer has additional checks
// later on.
return true;
}
// Unknown URL, e.g. because no load was committed yet. In this case we look
// for any pending entry on the NavigationController associated with the
// WebContents for the frame.
content::WebContents* web_contents =
content::WebContents::FromRenderFrameHost(frame);
content::NavigationEntry* pending_entry =
web_contents->GetController().GetPendingEntry();
if (!pending_entry) {
*error = manifest_errors::kCannotAccessPage;
return false;
}
GURL pending_url = pending_entry->GetURL();
if (pending_url.SchemeIsHTTPOrHTTPS() &&
!permissions.CanAccessPage(pending_url, tab_id, error)) {
// This catches the majority of cases where an extension tried to inject
// on a newly-created navigating tab, saving us a potentially-costly IPC
// and, maybe, slightly reducing (but not by any stretch eliminating) an
// attack surface.
*error = GetCannotAccessPageErrorMessage(permissions, pending_url);
return false;
}
// Otherwise allow for now. The renderer has additional checks and will
// fail the injection if needed.
return true;
}
// TODO(devlin): Add more schemes here, in line with
// https://crbug.com/55084.
if (committed_url.SchemeIs(url::kAboutScheme) ||
committed_url.SchemeIs(url::kDataScheme)) {
url::Origin origin = frame->GetLastCommittedOrigin();
const url::SchemeHostPort& tuple_or_precursor_tuple =
origin.GetTupleOrPrecursorTupleIfOpaque();
if (!tuple_or_precursor_tuple.IsValid()) {
*error = GetCannotAccessPageErrorMessage(permissions, committed_url);
return false;
}
committed_url = tuple_or_precursor_tuple.GetURL();
}
return permissions.CanAccessPage(committed_url, tab_id, error);
}
// Collects the frames for injection. Method will return false if an error is
// encountered.
bool CollectFramesForInjection(const api::scripting::InjectionTarget& target,
content::WebContents* tab,
std::set<int>& frame_ids,
std::set<content::RenderFrameHost*>& frames,
std::string* error_out) {
if (target.document_ids) {
for (const auto& id : *target.document_ids) {
ExtensionApiFrameIdMap::DocumentId document_id =
ExtensionApiFrameIdMap::DocumentIdFromString(id);
if (!document_id) {
*error_out = base::StringPrintf("Invalid document id %s", id.c_str());
return false;
}
content::RenderFrameHost* frame =
ExtensionApiFrameIdMap::Get()->GetRenderFrameHostByDocumentId(
document_id);
// If the frame was not found or it matched another tab reject this
// request.
if (!frame || content::WebContents::FromRenderFrameHost(frame) != tab) {
*error_out =
base::StringPrintf("No document with id %s in tab with id %d",
id.c_str(), target.tab_id);
return false;
}
// Convert the documentId into a frameId since the content will be
// injected synchronously.
frame_ids.insert(ExtensionApiFrameIdMap::GetFrameId(frame));
frames.insert(frame);
}
} else {
if (target.frame_ids) {
frame_ids.insert(target.frame_ids->begin(), target.frame_ids->end());
} else {
frame_ids.insert(ExtensionApiFrameIdMap::kTopFrameId);
}
for (int frame_id : frame_ids) {
content::RenderFrameHost* frame =
ExtensionApiFrameIdMap::GetRenderFrameHostById(tab, frame_id);
if (!frame) {
*error_out = base::StringPrintf("No frame with id %d in tab with id %d",
frame_id, target.tab_id);
return false;
}
frames.insert(frame);
}
}
return true;
}
// Returns true if the `target` can be accessed with the given `permissions`.
// If the target can be accessed, populates `script_executor_out`,
// `frame_scope_out`, and `frame_ids_out` with the appropriate values;
// if the target cannot be accessed, populates `error_out`.
bool CanAccessTarget(const PermissionsData& permissions,
const api::scripting::InjectionTarget& target,
content::BrowserContext* browser_context,
bool include_incognito_information,
ScriptExecutor** script_executor_out,
ScriptExecutor::FrameScope* frame_scope_out,
std::set<int>* frame_ids_out,
std::string* error_out) {
auto* contents = electron::api::WebContents::FromID(target.tab_id);
if (!contents) {
*error_out = base::StringPrintf("No tab with id: %d", target.tab_id);
return false;
}
content::WebContents* tab = contents->web_contents();
if ((target.all_frames && *target.all_frames == true) &&
(target.frame_ids || target.document_ids)) {
*error_out =
"Cannot specify 'allFrames' if either 'frameIds' or 'documentIds' is "
"specified.";
return false;
}
if (target.frame_ids && target.document_ids) {
*error_out = "Cannot specify both 'frameIds' and 'documentIds'.";
return false;
}
ScriptExecutor* script_executor = contents->script_executor();
DCHECK(script_executor);
ScriptExecutor::FrameScope frame_scope =
target.all_frames && *target.all_frames == true
? ScriptExecutor::INCLUDE_SUB_FRAMES
: ScriptExecutor::SPECIFIED_FRAMES;
std::set<int> frame_ids;
std::set<content::RenderFrameHost*> frames;
if (!CollectFramesForInjection(target, tab, frame_ids, frames, error_out))
return false;
// TODO(devlin): If `allFrames` is true, we error out if the extension
// doesn't have access to the top frame (even if it may inject in child
// frames). This is inconsistent with content scripts (which can execute on
// child frames), but consistent with the old tabs.executeScript() API.
for (content::RenderFrameHost* frame : frames) {
DCHECK_EQ(content::WebContents::FromRenderFrameHost(frame), tab);
if (!HasPermissionToInjectIntoFrame(permissions, target.tab_id, frame,
error_out)) {
return false;
}
}
*frame_ids_out = std::move(frame_ids);
*frame_scope_out = frame_scope;
*script_executor_out = script_executor;
return true;
}
api::scripts_internal::SerializedUserScript
ConvertRegisteredContentScriptToSerializedUserScript(
api::scripting::RegisteredContentScript content_script) {
auto convert_execution_world = [](api::scripting::ExecutionWorld world) {
switch (world) {
case api::scripting::ExecutionWorld::kNone:
case api::scripting::ExecutionWorld::kIsolated:
return api::extension_types::ExecutionWorld::kIsolated;
case api::scripting::ExecutionWorld::kMain:
return api::extension_types::ExecutionWorld::kMain;
}
};
api::scripts_internal::SerializedUserScript serialized_script;
serialized_script.source =
api::scripts_internal::Source::kDynamicContentScript;
// Note: IDs have already been prefixed appropriately.
serialized_script.id = std::move(content_script.id);
// Note: `matches` are guaranteed to be non-null.
serialized_script.matches = std::move(*content_script.matches);
serialized_script.exclude_matches = std::move(content_script.exclude_matches);
if (content_script.css) {
serialized_script.css = script_serialization::GetSourcesFromFileNames(
std::move(*content_script.css));
}
if (content_script.js) {
serialized_script.js = script_serialization::GetSourcesFromFileNames(
std::move(*content_script.js));
}
serialized_script.all_frames = content_script.all_frames;
serialized_script.match_origin_as_fallback =
content_script.match_origin_as_fallback;
serialized_script.run_at = content_script.run_at;
serialized_script.world = convert_execution_world(content_script.world);
return serialized_script;
}
std::unique_ptr<UserScript> ParseUserScript(
content::BrowserContext* browser_context,
const Extension& extension,
api::scripting::RegisteredContentScript content_script,
std::u16string* error) {
api::scripts_internal::SerializedUserScript serialized_script =
ConvertRegisteredContentScriptToSerializedUserScript(
std::move(content_script));
std::unique_ptr<UserScript> user_script =
script_serialization::ParseSerializedUserScript(serialized_script,
extension, error);
if (!user_script) {
return nullptr; // Parsing failed.
}
// Post conversion validation and values.
// TODO(https://crbug.com/1494155): See which of these can be moved into
// script_serialization::ParseSerializedUserScript().
if (!script_parsing::ValidateMatchOriginAsFallback(
user_script->match_origin_as_fallback(), user_script->url_patterns(),
error)) {
return nullptr;
}
user_script->set_incognito_enabled(
util::IsIncognitoEnabled(extension.id(), browser_context));
return user_script;
}
// Converts a UserScript object to a api::scripting::RegisteredContentScript
// object, used for getRegisteredContentScripts.
api::scripting::RegisteredContentScript CreateRegisteredContentScriptInfo(
const UserScript& script) {
CHECK_EQ(UserScript::Source::kDynamicContentScript, 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<std::string> converted;
converted.reserve(sources.size());
for (auto& source : sources) {
CHECK(source.file)
<< "Content scripts don't allow arbtirary code strings";
converted.push_back(std::move(*source.file));
}
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:
return api::scripting::ExecutionWorld::kIsolated;
case api::extension_types::ExecutionWorld::kUserScript:
NOTREACHED_NORETURN()
<< "ISOLATED worlds are not supported in this API.";
case api::extension_types::ExecutionWorld::kMain:
return api::scripting::ExecutionWorld::kMain;
}
};
api::scripting::RegisteredContentScript content_script;
content_script.id = std::move(serialized_script.id);
content_script.matches = std::move(serialized_script.matches);
content_script.exclude_matches = std::move(serialized_script.exclude_matches);
if (serialized_script.css) {
content_script.css =
convert_serialized_script_sources(std::move(*serialized_script.css));
}
if (serialized_script.js) {
content_script.js =
convert_serialized_script_sources(std::move(*serialized_script.js));
}
content_script.all_frames = serialized_script.all_frames;
content_script.match_origin_as_fallback =
serialized_script.match_origin_as_fallback;
content_script.run_at = serialized_script.run_at;
content_script.world = convert_execution_world(serialized_script.world);
return content_script;
}
} // namespace
InjectedFileSource::InjectedFileSource(std::string file_name,
std::unique_ptr<std::string> data)
: file_name(std::move(file_name)), data(std::move(data)) {}
InjectedFileSource::InjectedFileSource(InjectedFileSource&&) = default;
InjectedFileSource::~InjectedFileSource() = default;
ScriptingExecuteScriptFunction::ScriptingExecuteScriptFunction() = default;
ScriptingExecuteScriptFunction::~ScriptingExecuteScriptFunction() = default;
ExtensionFunction::ResponseAction ScriptingExecuteScriptFunction::Run() {
std::optional<api::scripting::ExecuteScript::Params> params =
api::scripting::ExecuteScript::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
injection_ = std::move(params->injection);
// Silently alias `function` to `func` for backwards compatibility.
// TODO(devlin): Remove this in M95.
if (injection_.function) {
if (injection_.func) {
return RespondNow(
Error("Both 'func' and 'function' were specified. "
"Only 'func' should be used."));
}
injection_.func = std::move(injection_.function);
}
if ((injection_.files && injection_.func) ||
(!injection_.files && !injection_.func)) {
return RespondNow(
Error("Exactly one of 'func' and 'files' must be specified"));
}
if (injection_.files) {
if (injection_.args)
return RespondNow(Error("'args' may not be used with file injections."));
// JS files don't require localization.
constexpr bool kRequiresLocalization = false;
std::string error;
if (!CheckAndLoadFiles(
std::move(*injection_.files), *extension(), kRequiresLocalization,
base::BindOnce(&ScriptingExecuteScriptFunction::DidLoadResources,
this),
&error)) {
return RespondNow(Error(std::move(error)));
}
return RespondLater();
}
DCHECK(injection_.func);
// TODO(devlin): This (wrapping a function to create an IIFE) is pretty hacky,
// and along with the JSON-serialization of the arguments to curry in.
// Add support to the ScriptExecutor to better support this case.
std::string args_expression;
if (injection_.args) {
std::vector<std::string> string_args;
string_args.reserve(injection_.args->size());
for (const auto& arg : *injection_.args) {
if (auto json = base::WriteJson(arg)) {
string_args.push_back(std::move(*json));
} else {
return RespondNow(Error("Unserializable argument passed."));
}
}
args_expression = base::JoinString(string_args, ",");
}
std::string code_to_execute = base::StringPrintf(
"(%s)(%s)", injection_.func->c_str(), args_expression.c_str());
std::vector<mojom::JSSourcePtr> sources;
sources.push_back(mojom::JSSource::New(std::move(code_to_execute), GURL()));
std::string error;
if (!Execute(std::move(sources), &error))
return RespondNow(Error(std::move(error)));
return RespondLater();
}
void ScriptingExecuteScriptFunction::DidLoadResources(
std::vector<InjectedFileSource> file_sources,
std::optional<std::string> load_error) {
if (load_error) {
Respond(Error(std::move(*load_error)));
return;
}
DCHECK(!file_sources.empty());
std::string error;
if (!Execute(FileSourcesToJSSources(*extension(), std::move(file_sources)),
&error)) {
Respond(Error(std::move(error)));
}
}
bool ScriptingExecuteScriptFunction::Execute(
std::vector<mojom::JSSourcePtr> sources,
std::string* error) {
ScriptExecutor* script_executor = nullptr;
ScriptExecutor::FrameScope frame_scope = ScriptExecutor::SPECIFIED_FRAMES;
std::set<int> frame_ids;
if (!CanAccessTarget(*extension()->permissions_data(), injection_.target,
browser_context(), include_incognito_information(),
&script_executor, &frame_scope, &frame_ids, error)) {
return false;
}
mojom::ExecutionWorld execution_world =
ConvertExecutionWorld(injection_.world);
// Extensions can specify that the script should be injected "immediately".
// In this case, we specify kDocumentStart as the injection time. Due to
// inherent raciness between tab creation and load and this function
// execution, there is no guarantee that it will actually happen at
// document start, but the renderer will appropriately inject it
// immediately if document start has already passed.
mojom::RunLocation run_location =
injection_.inject_immediately && *injection_.inject_immediately
? mojom::RunLocation::kDocumentStart
: mojom::RunLocation::kDocumentIdle;
script_executor->ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension()->id()),
mojom::CodeInjection::NewJs(mojom::JSInjection::New(
std::move(sources), execution_world, /*world_id=*/std::nullopt,
blink::mojom::WantResultOption::kWantResult,
user_gesture() ? blink::mojom::UserActivationOption::kActivate
: blink::mojom::UserActivationOption::kDoNotActivate,
blink::mojom::PromiseResultOption::kAwait)),
frame_scope, frame_ids, ScriptExecutor::MATCH_ABOUT_BLANK, run_location,
ScriptExecutor::DEFAULT_PROCESS,
/* webview_src */ GURL(),
base::BindOnce(&ScriptingExecuteScriptFunction::OnScriptExecuted, this));
return true;
}
void ScriptingExecuteScriptFunction::OnScriptExecuted(
std::vector<ScriptExecutor::FrameResult> frame_results) {
// If only a single frame was included and the injection failed, respond with
// an error.
if (frame_results.size() == 1 && !frame_results[0].error.empty()) {
Respond(Error(std::move(frame_results[0].error)));
return;
}
// Otherwise, respond successfully. We currently just skip over individual
// frames that failed. In the future, we can bubble up these error messages
// to the extension.
std::vector<api::scripting::InjectionResult> injection_results;
for (auto& result : frame_results) {
if (!result.error.empty())
continue;
api::scripting::InjectionResult injection_result;
injection_result.result = 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::scripting::ExecuteScript::Results::Create(injection_results)));
}
ScriptingInsertCSSFunction::ScriptingInsertCSSFunction() = default;
ScriptingInsertCSSFunction::~ScriptingInsertCSSFunction() = default;
ExtensionFunction::ResponseAction ScriptingInsertCSSFunction::Run() {
std::optional<api::scripting::InsertCSS::Params> params =
api::scripting::InsertCSS::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
injection_ = std::move(params->injection);
if ((injection_.files && injection_.css) ||
(!injection_.files && !injection_.css)) {
return RespondNow(Error(kExactlyOneOfCssAndFilesError));
}
if (injection_.files) {
// CSS files require localization.
constexpr bool kRequiresLocalization = true;
std::string error;
if (!CheckAndLoadFiles(
std::move(*injection_.files), *extension(), kRequiresLocalization,
base::BindOnce(&ScriptingInsertCSSFunction::DidLoadResources, this),
&error)) {
return RespondNow(Error(std::move(error)));
}
return RespondLater();
}
DCHECK(injection_.css);
mojom::HostID host_id(mojom::HostID::HostType::kExtensions,
extension()->id());
std::vector<mojom::CSSSourcePtr> sources;
sources.push_back(
mojom::CSSSource::New(std::move(*injection_.css),
InjectionKeyForCode(host_id, *injection_.css)));
std::string error;
if (!Execute(std::move(sources), &error)) {
return RespondNow(Error(std::move(error)));
}
return RespondLater();
}
void ScriptingInsertCSSFunction::DidLoadResources(
std::vector<InjectedFileSource> file_sources,
std::optional<std::string> load_error) {
if (load_error) {
Respond(Error(std::move(*load_error)));
return;
}
DCHECK(!file_sources.empty());
std::vector<mojom::CSSSourcePtr> sources =
FileSourcesToCSSSources(*extension(), std::move(file_sources));
std::string error;
if (!Execute(std::move(sources), &error))
Respond(Error(std::move(error)));
}
bool ScriptingInsertCSSFunction::Execute(
std::vector<mojom::CSSSourcePtr> sources,
std::string* error) {
ScriptExecutor* script_executor = nullptr;
ScriptExecutor::FrameScope frame_scope = ScriptExecutor::SPECIFIED_FRAMES;
std::set<int> frame_ids;
if (!CanAccessTarget(*extension()->permissions_data(), injection_.target,
browser_context(), include_incognito_information(),
&script_executor, &frame_scope, &frame_ids, error)) {
return false;
}
DCHECK(script_executor);
script_executor->ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension()->id()),
mojom::CodeInjection::NewCss(mojom::CSSInjection::New(
std::move(sources), ConvertStyleOriginToCSSOrigin(injection_.origin),
mojom::CSSInjection::Operation::kAdd)),
frame_scope, frame_ids, ScriptExecutor::MATCH_ABOUT_BLANK,
kCSSRunLocation, ScriptExecutor::DEFAULT_PROCESS,
/* webview_src */ GURL(),
base::BindOnce(&ScriptingInsertCSSFunction::OnCSSInserted, this));
return true;
}
void ScriptingInsertCSSFunction::OnCSSInserted(
std::vector<ScriptExecutor::FrameResult> results) {
// If only a single frame was included and the injection failed, respond with
// an error.
if (results.size() == 1 && !results[0].error.empty()) {
Respond(Error(std::move(results[0].error)));
return;
}
Respond(NoArguments());
}
ScriptingRemoveCSSFunction::ScriptingRemoveCSSFunction() = default;
ScriptingRemoveCSSFunction::~ScriptingRemoveCSSFunction() = default;
ExtensionFunction::ResponseAction ScriptingRemoveCSSFunction::Run() {
std::optional<api::scripting::RemoveCSS::Params> params =
api::scripting::RemoveCSS::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
api::scripting::CSSInjection& injection = params->injection;
if ((injection.files && injection.css) ||
(!injection.files && !injection.css)) {
return RespondNow(Error(kExactlyOneOfCssAndFilesError));
}
ScriptExecutor* script_executor = nullptr;
ScriptExecutor::FrameScope frame_scope = ScriptExecutor::SPECIFIED_FRAMES;
std::set<int> frame_ids;
std::string error;
if (!CanAccessTarget(*extension()->permissions_data(), injection.target,
browser_context(), include_incognito_information(),
&script_executor, &frame_scope, &frame_ids, &error)) {
return RespondNow(Error(std::move(error)));
}
DCHECK(script_executor);
mojom::HostID host_id(mojom::HostID::HostType::kExtensions,
extension()->id());
std::vector<mojom::CSSSourcePtr> sources;
if (injection.files) {
std::vector<ExtensionResource> resources;
if (!GetFileResources(*injection.files, *extension(), &resources, &error))
return RespondNow(Error(std::move(error)));
// Note: Since we're just removing the CSS, we don't actually need to load
// the file here. It's okay for `code` to be empty in this case.
const std::string empty_code;
sources.reserve(injection.files->size());
for (const auto& file : *injection.files) {
sources.push_back(mojom::CSSSource::New(
empty_code,
InjectionKeyForFile(host_id, extension()->GetResourceURL(file))));
}
} else {
DCHECK(injection.css);
sources.push_back(
mojom::CSSSource::New(std::move(*injection.css),
InjectionKeyForCode(host_id, *injection.css)));
}
script_executor->ExecuteScript(
std::move(host_id),
mojom::CodeInjection::NewCss(mojom::CSSInjection::New(
std::move(sources), ConvertStyleOriginToCSSOrigin(injection.origin),
mojom::CSSInjection::Operation::kRemove)),
frame_scope, frame_ids, ScriptExecutor::MATCH_ABOUT_BLANK,
kCSSRunLocation, ScriptExecutor::DEFAULT_PROCESS,
/* webview_src */ GURL(),
base::BindOnce(&ScriptingRemoveCSSFunction::OnCSSRemoved, this));
return RespondLater();
}
void ScriptingRemoveCSSFunction::OnCSSRemoved(
std::vector<ScriptExecutor::FrameResult> results) {
// If only a single frame was included and the injection failed, respond with
// an error.
if (results.size() == 1 && !results[0].error.empty()) {
Respond(Error(std::move(results[0].error)));
return;
}
Respond(NoArguments());
}
ScriptingRegisterContentScriptsFunction::
ScriptingRegisterContentScriptsFunction() = default;
ScriptingRegisterContentScriptsFunction::
~ScriptingRegisterContentScriptsFunction() = default;
ExtensionFunction::ResponseAction ScriptingUpdateContentScriptsFunction::Run() {
std::optional<api::scripting::UpdateContentScripts::Params> params =
api::scripting::UpdateContentScripts::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
std::vector<api::scripting::RegisteredContentScript>& scripts =
params->scripts;
std::string error;
// Add the prefix for dynamic content scripts onto the IDs of all scripts in
// `scripts` before continuing.
std::set<std::string> ids_to_update = scripting::CreateDynamicScriptIds(
scripts, UserScript::Source::kDynamicContentScript,
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());
std::map<std::string, api::scripting::RegisteredContentScript>
loaded_scripts_metadata;
const UserScriptList& dynamic_scripts = loader->GetLoadedDynamicScripts();
for (const std::unique_ptr<UserScript>& script : dynamic_scripts) {
if (script->GetSource() == UserScript::Source::kDynamicContentScript) {
loaded_scripts_metadata.emplace(
script->id(), CreateRegisteredContentScriptInfo(*script));
}
}
for (const auto& script : scripts) {
std::string error_script_id = UserScript::TrimPrefixFromScriptID(script.id);
if (loaded_scripts_metadata.find(script.id) ==
loaded_scripts_metadata.end()) {
return RespondNow(
Error(base::StringPrintf("Script with ID '%s' does not exist "
"or is not fully registered",
error_script_id.c_str())));
}
}
std::u16string parse_error;
UserScriptList parsed_scripts;
std::set<std::string> updated_script_ids_to_persist;
std::set<std::string> persistent_script_ids =
loader->GetPersistentDynamicScriptIDs();
parsed_scripts.reserve(scripts.size());
for (size_t i = 0; i < scripts.size(); ++i) {
api::scripting::RegisteredContentScript& update_delta = scripts[i];
DCHECK(base::Contains(loaded_scripts_metadata, update_delta.id));
api::scripting::RegisteredContentScript& updated_script =
loaded_scripts_metadata[update_delta.id];
if (update_delta.matches)
updated_script.matches = std::move(update_delta.matches);
if (update_delta.exclude_matches)
updated_script.exclude_matches = std::move(update_delta.exclude_matches);
if (update_delta.js)
updated_script.js = std::move(update_delta.js);
if (update_delta.css)
updated_script.css = std::move(update_delta.css);
if (update_delta.all_frames)
*updated_script.all_frames = *update_delta.all_frames;
if (update_delta.match_origin_as_fallback) {
*updated_script.match_origin_as_fallback =
*update_delta.match_origin_as_fallback;
}
if (update_delta.run_at != api::extension_types::RunAt::kNone) {
updated_script.run_at = update_delta.run_at;
}
// Parse/Create user script.
std::unique_ptr<UserScript> user_script =
ParseUserScript(browser_context(), *extension(),
std::move(updated_script), &parse_error);
if (!user_script)
return RespondNow(Error(base::UTF16ToASCII(parse_error)));
// Persist the updated script if the flag is specified as true, or if the
// original script is persisted and the flag is not specified.
if ((update_delta.persist_across_sessions &&
*update_delta.persist_across_sessions) ||
(!update_delta.persist_across_sessions &&
base::Contains(persistent_script_ids, update_delta.id))) {
updated_script_ids_to_persist.insert(update_delta.id);
}
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(ids_to_update));
GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&scripting::ValidateParsedScriptsOnFileThread,
script_parsing::GetSymlinkPolicy(extension()),
std::move(parsed_scripts)),
base::BindOnce(
&ScriptingUpdateContentScriptsFunction::OnContentScriptFilesValidated,
this, std::move(updated_script_ids_to_persist)));
// Balanced in `OnContentScriptFilesValidated()` or
// `OnContentScriptsRegistered()`.
AddRef();
return RespondLater();
}
void ScriptingRegisterContentScriptsFunction::OnContentScriptFilesValidated(
std::set<std::string> persistent_script_ids,
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());
if (error.has_value()) {
std::set<std::string> ids_to_remove;
for (const auto& script : scripts) {
ids_to_remove.insert(script->id());
}
loader->RemovePendingDynamicScriptIDs(std::move(ids_to_remove));
Respond(Error(std::move(*error)));
Release(); // Matches the `AddRef()` in `Run()`.
return;
}
loader->AddDynamicScripts(
std::move(scripts), std::move(persistent_script_ids),
base::BindOnce(
&ScriptingRegisterContentScriptsFunction::OnContentScriptsRegistered,
this));
}
void ScriptingRegisterContentScriptsFunction::OnContentScriptsRegistered(
const std::optional<std::string>& error) {
if (error.has_value())
Respond(Error(std::move(*error)));
else
Respond(NoArguments());
Release(); // Matches the `AddRef()` in `Run()`.
}
ScriptingGetRegisteredContentScriptsFunction::
ScriptingGetRegisteredContentScriptsFunction() = default;
ScriptingGetRegisteredContentScriptsFunction::
~ScriptingGetRegisteredContentScriptsFunction() = default;
ExtensionFunction::ResponseAction
ScriptingGetRegisteredContentScriptsFunction::Run() {
std::optional<api::scripting::GetRegisteredContentScripts::Params> params =
api::scripting::GetRegisteredContentScripts::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
const std::optional<api::scripting::ContentScriptFilter>& filter =
params->filter;
std::set<std::string> id_filter;
if (filter && filter->ids) {
for (const std::string& id : *(filter->ids)) {
id_filter.insert(scripting::AddPrefixToDynamicScriptId(
id, UserScript::Source::kDynamicContentScript));
}
}
ExtensionUserScriptLoader* loader =
ExtensionSystem::Get(browser_context())
->user_script_manager()
->GetUserScriptLoaderForExtension(extension()->id());
const UserScriptList& dynamic_scripts = loader->GetLoadedDynamicScripts();
std::vector<api::scripting::RegisteredContentScript> script_infos;
std::set<std::string> persistent_script_ids =
loader->GetPersistentDynamicScriptIDs();
for (const std::unique_ptr<UserScript>& script : dynamic_scripts) {
if (script->GetSource() != UserScript::Source::kDynamicContentScript) {
continue;
}
if (!id_filter.empty() && !base::Contains(id_filter, script->id())) {
continue;
}
auto registered_script = CreateRegisteredContentScriptInfo(*script);
registered_script.persist_across_sessions =
base::Contains(persistent_script_ids, script->id());
// Remove the internally used prefix from the `script`'s ID before
// returning.
registered_script.id = script->GetIDWithoutPrefix();
script_infos.push_back(std::move(registered_script));
}
return RespondNow(
ArgumentList(api::scripting::GetRegisteredContentScripts::Results::Create(
script_infos)));
}
ScriptingUnregisterContentScriptsFunction::
ScriptingUnregisterContentScriptsFunction() = default;
ScriptingUnregisterContentScriptsFunction::
~ScriptingUnregisterContentScriptsFunction() = default;
ExtensionFunction::ResponseAction
ScriptingUnregisterContentScriptsFunction::Run() {
auto params =
api::scripting::UnregisterContentScripts::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
std::optional<api::scripting::ContentScriptFilter>& filter = params->filter;
ExtensionUserScriptLoader* loader =
ExtensionSystem::Get(browser_context())
->user_script_manager()
->GetUserScriptLoaderForExtension(extension()->id());
// TODO(crbug.com/1300657): Only clear all scripts if `filter` did not specify
// the list of scripts ids to remove.
if (!filter || !filter->ids || filter->ids->empty()) {
loader->ClearDynamicScripts(
UserScript::Source::kDynamicContentScript,
base::BindOnce(&ScriptingUnregisterContentScriptsFunction::
OnContentScriptsUnregistered,
this));
return RespondLater();
}
std::set<std::string> ids_to_remove;
std::set<std::string> existing_script_ids =
loader->GetDynamicScriptIDs(UserScript::Source::kDynamicContentScript);
std::string error;
for (const auto& provided_id : *filter->ids) {
if (!scripting::IsScriptIdValid(provided_id, &error)) {
return RespondNow(Error(std::move(error)));
}
// Add the dynamic content script prefix to `provided_id` before checking
// against `existing_script_ids`.
std::string id_with_prefix = scripting::AddPrefixToDynamicScriptId(
provided_id, UserScript::Source::kDynamicContentScript);
if (!base::Contains(existing_script_ids, id_with_prefix)) {
return RespondNow(Error(ErrorUtils::FormatErrorMessage(
kNonExistentScriptIdError, provided_id.c_str())));
}
ids_to_remove.insert(id_with_prefix);
}
loader->RemoveDynamicScripts(
std::move(ids_to_remove),
base::BindOnce(&ScriptingUnregisterContentScriptsFunction::
OnContentScriptsUnregistered,
this));
return RespondLater();
}
void ScriptingUnregisterContentScriptsFunction::OnContentScriptsUnregistered(
const std::optional<std::string>& error) {
if (error.has_value())
Respond(Error(std::move(*error)));
else
Respond(NoArguments());
}
ScriptingUpdateContentScriptsFunction::ScriptingUpdateContentScriptsFunction() =
default;
ScriptingUpdateContentScriptsFunction::
~ScriptingUpdateContentScriptsFunction() = default;
ExtensionFunction::ResponseAction
ScriptingRegisterContentScriptsFunction::Run() {
std::optional<api::scripting::RegisterContentScripts::Params> params =
api::scripting::RegisterContentScripts::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
std::vector<api::scripting::RegisteredContentScript>& scripts =
params->scripts;
ExtensionUserScriptLoader* loader =
ExtensionSystem::Get(browser_context())
->user_script_manager()
->GetUserScriptLoaderForExtension(extension()->id());
// Create script ids for dynamic content scripts.
std::string error;
std::set<std::string> existing_script_ids =
loader->GetDynamicScriptIDs(UserScript::Source::kDynamicContentScript);
std::set<std::string> new_script_ids = scripting::CreateDynamicScriptIds(
scripts, UserScript::Source::kDynamicContentScript, existing_script_ids,
&error);
if (!error.empty()) {
CHECK(new_script_ids.empty());
return RespondNow(Error(std::move(error)));
}
// Parse content scripts.
std::u16string parse_error;
UserScriptList parsed_scripts;
std::set<std::string> persistent_script_ids;
parsed_scripts.reserve(scripts.size());
for (auto& script : scripts) {
if (!script.matches) {
std::string error_script_id =
UserScript::TrimPrefixFromScriptID(script.id);
return RespondNow(
Error(base::StringPrintf("Script with ID '%s' must specify 'matches'",
error_script_id.c_str())));
}
// Scripts will persist across sessions by default.
bool persist_across_sessions =
script.persist_across_sessions.value_or(true);
std::unique_ptr<UserScript> user_script = ParseUserScript(
browser_context(), *extension(), std::move(script), &parse_error);
if (!user_script)
return RespondNow(Error(base::UTF16ToASCII(parse_error)));
// Scripts will persist across sessions by default.
if (persist_across_sessions) {
persistent_script_ids.insert(user_script->id());
}
parsed_scripts.push_back(std::move(user_script));
}
// The contents of `scripts` have all been std::move()'d.
scripts.clear();
// 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(&ScriptingRegisterContentScriptsFunction::
OnContentScriptFilesValidated,
this, std::move(persistent_script_ids)));
// Balanced in `OnContentScriptFilesValidated()` or
// `OnContentScriptsRegistered()`.
AddRef();
return RespondLater();
}
void ScriptingUpdateContentScriptsFunction::OnContentScriptFilesValidated(
std::set<std::string> persistent_script_ids,
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());
std::set<std::string> script_ids;
for (const auto& script : scripts)
script_ids.insert(script->id());
if (error.has_value()) {
loader->RemovePendingDynamicScriptIDs(script_ids);
Respond(Error(std::move(*error)));
Release(); // Matches the `AddRef()` in `Run()`.
return;
}
// To guarantee that scripts are updated, they need to be removed then added
// again. It should be guaranteed that the new scripts are added after the old
// ones are removed.
loader->RemoveDynamicScripts(script_ids, /*callback=*/base::DoNothing());
// Since RemoveDynamicScripts will remove pending script IDs, but
// AddDynamicScripts will only add scripts that are marked as pending, we must
// mark `script_ids` as pending again here.
loader->AddPendingDynamicScriptIDs(std::move(script_ids));
loader->AddDynamicScripts(
std::move(scripts), std::move(persistent_script_ids),
base::BindOnce(
&ScriptingUpdateContentScriptsFunction::OnContentScriptsUpdated,
this));
}
void ScriptingUpdateContentScriptsFunction::OnContentScriptsUpdated(
const std::optional<std::string>& error) {
if (error.has_value())
Respond(Error(std::move(*error)));
else
Respond(NoArguments());
Release(); // Matches the `AddRef()` in `Run()`.
}
} // namespace extensions