refactor: use upstream's chrome.scripting impl (#51376)

Remove our implementation of the scripting api and use upstream's
version. It was recently moved to `extensions/` by
https://chromium-review.googlesource.com/c/chromium/src/+/7784831,
so we link it directly.

Update `ElectronExtensionsBrowserClient` to overrides `IsValidTabId()`
and `GetScriptExecutorForTab()` to provide tab validation and
script-executor hooks.

Remove now-redundant local copy of `scripting.idl`.
Upstream now provides everything we used this for.

Updated breaking-changes.md to document a CSS matching difference.

Co-authored-by: GitHub Copilot <github-copilot[bot]@users.noreply.github.com>
This commit is contained in:
Charles Kerr
2026-04-29 17:15:04 -05:00
committed by GitHub
parent d58c5a5562
commit 19cff61d04
13 changed files with 41 additions and 1722 deletions

View File

@@ -14,6 +14,17 @@ This document uses the following convention to categorize breaking changes:
## Planned Breaking API Changes (43.0)
### Behavior Changed: `chrome.scripting` CSS injection matches more fallback frames
Extensions using `chrome.scripting.insertCSS()` or `chrome.scripting.removeCSS()`
now follow Chrome's behavior when Electron cannot match a frame's URL directly,
such as with `about:blank` or `data:` frames. If the extension has access to the
page that created the frame, CSS may now be inserted into or removed from those
fallback frames as well.
Apps or extensions that relied on Electron skipping those frames should narrow their
injection target, frame IDs, or match patterns.
### Behavior Changed: Dialog methods default to Downloads directory
The `defaultPath` option for the following methods now defaults to the user's Downloads folder (or their home directory if Downloads doesn't exist) when not explicitly provided:

View File

@@ -772,8 +772,6 @@ filenames = {
"shell/browser/extensions/api/resources_private/resources_private_api.h",
"shell/browser/extensions/api/runtime/electron_runtime_api_delegate.cc",
"shell/browser/extensions/api/runtime/electron_runtime_api_delegate.h",
"shell/browser/extensions/api/scripting/scripting_api.cc",
"shell/browser/extensions/api/scripting/scripting_api.h",
"shell/browser/extensions/api/streams_private/streams_private_api.cc",
"shell/browser/extensions/api/streams_private/streams_private_api.h",
"shell/browser/extensions/api/tabs/tabs_api.cc",

View File

@@ -152,4 +152,3 @@ chore_register_node_as_a_dynamic_trace_category_prefix.patch
fix_make_macos_text_replacement_work_on_contenteditable.patch
fix_allow_reentrancy_on_downloadmanagerimpl_observer_list.patch
build_gn_arg_to_support_linker_wrapper_script_on_windows.patch
chore_exclude_upstream_scripting_api_when_building_electron.patch

View File

@@ -1,34 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Electron Scripts <scripts@electron>
Date: Sat, 25 Apr 2026 21:10:03 -0500
Subject: chore: exclude upstream scripting API when building Electron
Electron provides its own implementation of the Scripting API at
shell/browser/extensions/api/scripting/ that includes Electron-specific
modifications. CL 7784831 moved the upstream implementation from
//chrome to //extensions, which caused it to be transitively linked
into Electron, resulting in duplicate symbols.
Exclude the upstream sources when is_electron_build is set.
diff --git a/extensions/browser/api/scripting/BUILD.gn b/extensions/browser/api/scripting/BUILD.gn
index 14d18bfbc36673f3eb9ffe0777d34ddd2163af0d..cff84c1a9e713e2845b2331913dde9be730305b1 100644
--- a/extensions/browser/api/scripting/BUILD.gn
+++ b/extensions/browser/api/scripting/BUILD.gn
@@ -12,6 +12,16 @@ source_set("scripting") {
"scripting_api.cc",
"scripting_api.h",
]
+
+ # Electron provides its own scripting API implementation in
+ # shell/browser/extensions/api/scripting/.
+ if (is_electron_build) {
+ sources -= [
+ "scripting_api.cc",
+ "scripting_api.h",
+ ]
+ }
+
public_deps = [
"//extensions/browser:browser_sources",
"//extensions/common/api",

View File

@@ -14,7 +14,6 @@ function_registration("api_registration") {
"//electron/shell/common/extensions/api/action.json",
"//electron/shell/common/extensions/api/extension.json",
"//electron/shell/common/extensions/api/resources_private.idl",
"//electron/shell/common/extensions/api/scripting.idl",
"//electron/shell/common/extensions/api/tabs.json",
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,203 +0,0 @@
// Copyright 2023 Microsoft, GmbH
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_EXTENSIONS_API_SCRIPTING_SCRIPTING_API_H_
#define ELECTRON_SHELL_BROWSER_EXTENSIONS_API_SCRIPTING_SCRIPTING_API_H_
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include "extensions/browser/extension_function.h"
#include "extensions/browser/script_executor.h"
#include "extensions/browser/scripting_utils.h"
#include "extensions/common/api/scripting.h"
#include "extensions/common/mojom/code_injection.mojom.h"
#include "extensions/common/user_script.h"
namespace extensions {
class ScriptingExecuteScriptFunction : public ExtensionFunction {
public:
DECLARE_EXTENSION_FUNCTION("scripting.executeScript", SCRIPTING_EXECUTESCRIPT)
ScriptingExecuteScriptFunction();
ScriptingExecuteScriptFunction(const ScriptingExecuteScriptFunction&) =
delete;
ScriptingExecuteScriptFunction& operator=(
const ScriptingExecuteScriptFunction&) = delete;
// ExtensionFunction:
ResponseAction Run() override;
private:
~ScriptingExecuteScriptFunction() override;
// Called when the resource files to be injected has been loaded.
void DidLoadResources(std::vector<scripting::InjectedFileSource> file_sources,
std::optional<std::string> load_error);
// Triggers the execution of `sources` in the appropriate context.
// Returns true on success; on failure, populates `error`.
bool Execute(std::vector<mojom::JSSourcePtr> sources, std::string* error);
// Invoked when script execution is complete.
void OnScriptExecuted(std::vector<ScriptExecutor::FrameResult> frame_results);
api::scripting::ScriptInjection injection_;
};
class ScriptingInsertCSSFunction : public ExtensionFunction {
public:
DECLARE_EXTENSION_FUNCTION("scripting.insertCSS", SCRIPTING_INSERTCSS)
ScriptingInsertCSSFunction();
ScriptingInsertCSSFunction(const ScriptingInsertCSSFunction&) = delete;
ScriptingInsertCSSFunction& operator=(const ScriptingInsertCSSFunction&) =
delete;
// ExtensionFunction:
ResponseAction Run() override;
private:
~ScriptingInsertCSSFunction() override;
// Called when the resource files to be injected has been loaded.
void DidLoadResources(std::vector<scripting::InjectedFileSource> file_sources,
std::optional<std::string> load_error);
// Triggers the execution of `sources` in the appropriate context.
// Returns true on success; on failure, populates `error`.
bool Execute(std::vector<mojom::CSSSourcePtr> sources, std::string* error);
// Called when the CSS insertion is complete.
void OnCSSInserted(std::vector<ScriptExecutor::FrameResult> results);
api::scripting::CSSInjection injection_;
};
class ScriptingRemoveCSSFunction : public ExtensionFunction {
public:
DECLARE_EXTENSION_FUNCTION("scripting.removeCSS", SCRIPTING_REMOVECSS)
ScriptingRemoveCSSFunction();
ScriptingRemoveCSSFunction(const ScriptingRemoveCSSFunction&) = delete;
ScriptingRemoveCSSFunction& operator=(const ScriptingRemoveCSSFunction&) =
delete;
// ExtensionFunction:
ResponseAction Run() override;
private:
~ScriptingRemoveCSSFunction() override;
// Called when the CSS removal is complete.
void OnCSSRemoved(std::vector<ScriptExecutor::FrameResult> results);
};
class ScriptingRegisterContentScriptsFunction : public ExtensionFunction {
public:
DECLARE_EXTENSION_FUNCTION("scripting.registerContentScripts",
SCRIPTING_REGISTERCONTENTSCRIPTS)
ScriptingRegisterContentScriptsFunction();
ScriptingRegisterContentScriptsFunction(
const ScriptingRegisterContentScriptsFunction&) = delete;
ScriptingRegisterContentScriptsFunction& operator=(
const ScriptingRegisterContentScriptsFunction&) = delete;
// ExtensionFunction:
ResponseAction Run() override;
private:
~ScriptingRegisterContentScriptsFunction() override;
// Called when script files have been checked.
void OnContentScriptFilesValidated(
std::set<std::string> persistent_script_ids,
scripting::ValidateScriptsResult result);
// Called when content scripts have been registered.
void OnContentScriptsRegistered(const std::optional<std::string>& error);
};
class ScriptingGetRegisteredContentScriptsFunction : public ExtensionFunction {
public:
DECLARE_EXTENSION_FUNCTION("scripting.getRegisteredContentScripts",
SCRIPTING_GETREGISTEREDCONTENTSCRIPTS)
ScriptingGetRegisteredContentScriptsFunction();
ScriptingGetRegisteredContentScriptsFunction(
const ScriptingGetRegisteredContentScriptsFunction&) = delete;
ScriptingGetRegisteredContentScriptsFunction& operator=(
const ScriptingGetRegisteredContentScriptsFunction&) = delete;
// ExtensionFunction:
ResponseAction Run() override;
private:
~ScriptingGetRegisteredContentScriptsFunction() override;
};
class ScriptingUnregisterContentScriptsFunction : public ExtensionFunction {
public:
DECLARE_EXTENSION_FUNCTION("scripting.unregisterContentScripts",
SCRIPTING_UNREGISTERCONTENTSCRIPTS)
ScriptingUnregisterContentScriptsFunction();
ScriptingUnregisterContentScriptsFunction(
const ScriptingUnregisterContentScriptsFunction&) = delete;
ScriptingUnregisterContentScriptsFunction& operator=(
const ScriptingUnregisterContentScriptsFunction&) = delete;
// ExtensionFunction:
ResponseAction Run() override;
private:
~ScriptingUnregisterContentScriptsFunction() override;
// Called when content scripts have been unregistered.
void OnContentScriptsUnregistered(const std::optional<std::string>& error);
};
class ScriptingUpdateContentScriptsFunction : public ExtensionFunction {
public:
DECLARE_EXTENSION_FUNCTION("scripting.updateContentScripts",
SCRIPTING_UPDATECONTENTSCRIPTS)
ScriptingUpdateContentScriptsFunction();
ScriptingUpdateContentScriptsFunction(
const ScriptingUpdateContentScriptsFunction&) = delete;
ScriptingUpdateContentScriptsFunction& operator=(
const ScriptingUpdateContentScriptsFunction&) = delete;
// ExtensionFunction:
ResponseAction Run() override;
private:
~ScriptingUpdateContentScriptsFunction() override;
// Returns a UserScript object by updating the `original_script` with the
// `new_script` given delta. If the updated script cannot be parsed, populates
// `parse_error` and returns nullptr.
std::unique_ptr<UserScript> ApplyUpdate(
std::set<std::string>* script_ids_to_persist,
api::scripting::RegisteredContentScript& new_script,
api::scripting::RegisteredContentScript& original_script,
std::u16string* parse_error);
// Called when script files have been checked.
void OnContentScriptFilesValidated(
std::set<std::string> persistent_script_ids,
scripting::ValidateScriptsResult result);
// Called when content scripts have been updated.
void OnContentScriptsUpdated(const std::optional<std::string>& error);
};
} // namespace extensions
#endif // ELECTRON_SHELL_BROWSER_EXTENSIONS_API_SCRIPTING_SCRIPTING_API_H_

View File

@@ -8,7 +8,6 @@
#include "extensions/browser/extension_function_registry.h"
#include "shell/browser/extensions/api/extension_action/extension_action_api.h"
#include "shell/browser/extensions/api/generated_api_registration.h"
#include "shell/browser/extensions/api/scripting/scripting_api.h"
#include "shell/browser/extensions/api/tabs/tabs_api.h"
namespace extensions {

View File

@@ -35,6 +35,7 @@
#include "extensions/common/manifest_handlers/devtools_page_handler.h"
#include "extensions/common/manifest_url_handlers.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "shell/browser/api/electron_api_web_contents.h"
#include "shell/browser/browser.h"
#include "shell/browser/electron_browser_client.h"
#include "shell/browser/electron_browser_context.h"
@@ -42,6 +43,7 @@
#include "shell/browser/extensions/electron_component_extension_resource_manager.h"
#include "shell/browser/extensions/electron_extension_host_delegate.h"
#include "shell/browser/extensions/electron_extension_system_factory.h"
#include "shell/browser/extensions/electron_extension_tab_util.h"
#include "shell/browser/extensions/electron_extension_web_contents_observer.h"
#include "shell/browser/extensions/electron_extensions_api_client.h"
#include "shell/browser/extensions/electron_extensions_browser_api_provider.h"
@@ -381,6 +383,28 @@ ElectronExtensionsBrowserClient::GetExtensionWebContentsObserver(
web_contents);
}
bool ElectronExtensionsBrowserClient::IsValidTabId(
content::BrowserContext* browser_context,
const int tab_id,
const bool include_incognito,
content::WebContents** web_contents) const {
auto* contents = extensions::GetElectronTabById(tab_id, browser_context);
if (!contents)
return false;
if (web_contents)
*web_contents = contents->web_contents();
return true;
}
extensions::ScriptExecutor*
ElectronExtensionsBrowserClient::GetScriptExecutorForTab(
content::WebContents& web_contents) {
auto* contents = electron::api::WebContents::From(&web_contents);
return contents ? contents->script_executor() : nullptr;
}
extensions::KioskDelegate* ElectronExtensionsBrowserClient::GetKioskDelegate() {
if (!kiosk_delegate_)
kiosk_delegate_ = std::make_unique<ElectronKioskDelegate>();

View File

@@ -139,6 +139,12 @@ class ElectronExtensionsBrowserClient
content::WebContents* web_contents) override;
extensions::ExtensionWebContentsObserver* GetExtensionWebContentsObserver(
content::WebContents* web_contents) override;
bool IsValidTabId(content::BrowserContext* browser_context,
int tab_id,
bool include_incognito,
content::WebContents** web_contents) const override;
extensions::ScriptExecutor* GetScriptExecutorForTab(
content::WebContents& web_contents) override;
extensions::KioskDelegate* GetKioskDelegate() override;
std::string GetApplicationLocale() override;
void RegisterBrowserInterfaceBindersForFrame(

View File

@@ -41,7 +41,6 @@ generated_json_strings("generated_api_json_strings") {
"browser_action.json",
"extension.json",
"resources_private.idl",
"scripting.idl",
"tabs.json",
]
@@ -62,7 +61,6 @@ generated_json_strings("generated_api_json_strings") {
generated_types("generated_api_types") {
sources = [
"resources_private.idl",
"scripting.idl",
"tabs.json",
]

View File

@@ -65,15 +65,6 @@
"chrome://print/*"
]
}],
"scripting": {
"dependencies": ["permission:scripting"],
"contexts": ["privileged_extension"]
},
"scripting.globalParams": {
"channel": "trunk",
"dependencies": ["permission:scripting"],
"contexts": ["content_script"]
},
"tabs": {
"channel": "stable",
"extension_types": ["extension"],

View File

@@ -1,268 +0,0 @@
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Use the <code>chrome.scripting</code> API to execute script in different
// contexts.
namespace scripting {
callback InjectedFunction = void();
// The origin for a style change.
// See <a href="https://developer.mozilla.org/en-US/docs/Glossary/Style_origin">style origins</a>
// for more info.
enum StyleOrigin {
AUTHOR,
USER
};
// The JavaScript world for a script to execute within.
enum ExecutionWorld {
// Specifies the isolated world, which is the execution environment unique
// to this extension.
ISOLATED,
// Specifies the main world of the DOM, which is the execution environment
// shared with the host page's JavaScript.
MAIN
};
dictionary InjectionTarget {
// The ID of the tab into which to inject.
long tabId;
// The <a href="https://developer.chrome.com/extensions/webNavigation#frame_ids">IDs</a>
// of specific frames to inject into.
long[]? frameIds;
// The <a href="https://developer.chrome.com/extensions/webNavigation#document_ids">IDs</a>
// of specific documentIds to inject into. This must not be set if
// <code>frameIds</code> is set.
DOMString[]? documentIds;
// Whether the script should inject into all frames within the tab. Defaults
// to false.
// This must not be true if <code>frameIds</code> is specified.
boolean? allFrames;
};
dictionary ScriptInjection {
// A JavaScript function to inject. This function will be serialized, and
// then deserialized for injection. This means that any bound parameters
// and execution context will be lost.
// Exactly one of <code>files</code> or <code>func</code> must be
// specified.
[serializableFunction]InjectedFunction? func;
// The arguments to pass to the provided function. This is only valid if
// the <code>func</code> parameter is specified. These arguments must be
// JSON-serializable.
any[]? args;
// We used to call the injected function `function`, but this is
// incompatible with JavaScript's object declaration shorthand (see
// https://crbug.com/1166438). We leave this silently in for backwards
// compatibility.
// TODO(devlin): Remove this in M95.
[nodoc, serializableFunction]InjectedFunction? function;
// The path of the JS or CSS files to inject, relative to the extension's
// root directory.
// Exactly one of <code>files</code> or <code>func</code> must be
// specified.
DOMString[]? files;
// Details specifying the target into which to inject the script.
InjectionTarget target;
// The JavaScript "world" to run the script in. Defaults to
// <code>ISOLATED</code>.
ExecutionWorld? world;
// Whether the injection should be triggered in the target as soon as
// possible. Note that this is not a guarantee that injection will occur
// prior to page load, as the page may have already loaded by the time the
// script reaches the target.
boolean? injectImmediately;
};
dictionary CSSInjection {
// Details specifying the target into which to insert the CSS.
InjectionTarget target;
// A string containing the CSS to inject.
// Exactly one of <code>files</code> and <code>css</code> must be
// specified.
DOMString? css;
// The path of the CSS files to inject, relative to the extension's root
// directory.
// Exactly one of <code>files</code> and <code>css</code> must be
// specified.
DOMString[]? files;
// The style origin for the injection. Defaults to <code>'AUTHOR'</code>.
StyleOrigin? origin;
};
dictionary InjectionResult {
// The result of the script execution.
any? result;
// The frame associated with the injection.
long frameId;
// The document associated with the injection.
DOMString documentId;
};
// Describes a content script to be injected into a web page registered
// through this API.
dictionary RegisteredContentScript {
// The id of the content script, specified in the API call. Must not start
// with a '_' as it's reserved as a prefix for generated script IDs.
DOMString id;
// Specifies which pages this content script will be injected into. See
// <a href="develop/concepts/match-patterns">Match Patterns</a> for more
// details on the syntax of these strings. Must be specified for
// $(ref:registerContentScripts).
DOMString[]? matches;
// Excludes pages that this content script would otherwise be injected into.
// See <a href="develop/concepts/match-patterns">Match Patterns</a> for
// more details on the syntax of these strings.
DOMString[]? excludeMatches;
// The list of CSS files to be injected into matching pages. These are
// injected in the order they appear in this array, before any DOM is
// constructed or displayed for the page.
DOMString[]? css;
// The list of JavaScript files to be injected into matching pages. These
// are injected in the order they appear in this array.
DOMString[]? js;
// If specified true, it will inject into all frames, even if the frame is
// not the top-most frame in the tab. Each frame is checked independently
// for URL requirements; it will not inject into child frames if the URL
// requirements are not met. Defaults to false, meaning that only the top
// frame is matched.
boolean? allFrames;
// Indicates whether the script can be injected into frames where the URL
// contains an unsupported scheme; specifically: about:, data:, blob:, or
// filesystem:. In these cases, the URL's origin is checked to determine if
// the script should be injected. If the origin is `null` (as is the case
// for data: URLs) then the used origin is either the frame that created
// the current frame or the frame that initiated the navigation to this
// frame. Note that this may not be the parent frame.
boolean? matchOriginAsFallback;
// Specifies when JavaScript files are injected into the web page. The
// preferred and default value is <code>document_idle</code>.
extensionTypes.RunAt? runAt;
// Specifies if this content script will persist into future sessions. The
// default is true.
boolean? persistAcrossSessions;
// The JavaScript "world" to run the script in. Defaults to
// <code>ISOLATED</code>.
ExecutionWorld? world;
};
// An object used to filter content scripts for
// ${ref:getRegisteredContentScripts}.
dictionary ContentScriptFilter {
// If specified, $(ref:getRegisteredContentScripts) will only return scripts
// with an id specified in this list.
DOMString[]? ids;
};
callback ScriptInjectionCallback = void(InjectionResult[] results);
callback CSSInjectionCallback = void();
callback RegisterContentScriptsCallback = void();
callback GetRegisteredContentScriptsCallback = void(
RegisteredContentScript[] scripts);
callback UnregisterContentScriptsCallback = void();
callback UpdateContentScriptsCallback = void();
interface Properties {
// An object available for content scripts running in isolated worlds to use
// and modify as a JS object. One instance exists per frame and is shared
// between all content scripts for a given extension. This object is
// initialized when the frame is created, before document_start.
// TODO(crbug.com/40119604): Enable this once implementation is complete.
[nodoc, nocompile] static long globalParams();
};
interface Functions {
// Injects a script into a target context. By default, the script will be run
// at <code>document_idle</code>, or immediately if the page has already
// loaded. If the <code>injectImmediately</code> property is set, the script
// will inject without waiting, even if the page has not finished loading. If
// the script evaluates to a promise, the browser will wait for the promise to
// settle and return the resulting value.
// |injection|: The details of the script which to inject.
// |callback|: Invoked upon completion of the injection. The resulting
// array contains the result of execution for each frame where the
// injection succeeded.
static void executeScript(
ScriptInjection injection,
optional ScriptInjectionCallback callback);
// Inserts a CSS stylesheet into a target context.
// If multiple frames are specified, unsuccessful injections are ignored.
// |injection|: The details of the styles to insert.
// |callback|: Invoked upon completion of the insertion.
static void insertCSS(
CSSInjection injection,
optional CSSInjectionCallback callback);
// Removes a CSS stylesheet that was previously inserted by this extension
// from a target context.
// |injection|: The details of the styles to remove. Note that the
// <code>css</code>, <code>files</code>, and <code>origin</code> properties
// must exactly match the stylesheet inserted through $(ref:insertCSS).
// Attempting to remove a non-existent stylesheet is a no-op.
// |callback|: A callback to be invoked upon the completion of the removal.
static void removeCSS(
CSSInjection injection,
optional CSSInjectionCallback callback);
// Registers one or more content scripts for this extension.
// |scripts|: Contains a list of scripts to be registered. If there are
// errors during script parsing/file validation, or if the IDs specified
// already exist, then no scripts are registered.
// |callback|: A callback to be invoked once scripts have been fully
// registered or if an error has occurred.
static void registerContentScripts(
RegisteredContentScript[] scripts,
optional RegisterContentScriptsCallback callback);
// Returns all dynamically registered content scripts for this extension
// that match the given filter.
// |filter|: An object to filter the extension's dynamically registered
// scripts.
static void getRegisteredContentScripts(
optional ContentScriptFilter filter,
GetRegisteredContentScriptsCallback callback);
// Unregisters content scripts for this extension.
// |filter|: If specified, only unregisters dynamic content scripts which
// match the filter. Otherwise, all of the extension's dynamic content
// scripts are unregistered.
// |callback|: A callback to be invoked once scripts have been unregistered
// or if an error has occurred.
static void unregisterContentScripts(
optional ContentScriptFilter filter,
optional UnregisterContentScriptsCallback callback);
// Updates one or more content scripts for this extension.
// |scripts|: Contains a list of scripts to be updated. A property is only
// updated for the existing script if it is specified in this object. If
// there are errors during script parsing/file validation, or if the IDs
// specified do not correspond to a fully registered script, then no scripts
// are updated.
// |callback|: A callback to be invoked once scripts have been updated or
// if an error has occurred.
static void updateContentScripts(
RegisteredContentScript[] scripts,
optional RegisterContentScriptsCallback callback);
};
};