Browse Source

feat: support `chrome.scripting` extension APIs (#39395)

feat: support chrome.scripting extension  APIs
Shelley Vohr 1 year ago
parent
commit
f0ad357af2

+ 40 - 14
docs/api/extensions.md

@@ -61,14 +61,20 @@ See [Manifest file format](https://developer.chrome.com/docs/extensions/mv3/mani
 
 All features of this API are supported.
 
+See [official documentation](https://developer.chrome.com/docs/extensions/reference/devtools_inspectedWindow) for more information.
+
 ### `chrome.devtools.network`
 
 All features of this API are supported.
 
+See [official documentation](https://developer.chrome.com/docs/extensions/reference/devtools_network) for more information.
+
 ### `chrome.devtools.panels`
 
 All features of this API are supported.
 
+See [official documentation](https://developer.chrome.com/docs/extensions/reference/devtools_panels) for more information.
+
 ### `chrome.extension`
 
 The following properties of `chrome.extension` are supported:
@@ -80,6 +86,25 @@ The following methods of `chrome.extension` are supported:
 - `chrome.extension.getURL`
 - `chrome.extension.getBackgroundPage`
 
+See [official documentation](https://developer.chrome.com/docs/extensions/reference/extension) for more information.
+
+### `chrome.management`
+
+The following methods of `chrome.management` are supported:
+
+- `chrome.management.getAll`
+- `chrome.management.get`
+- `chrome.management.getSelf`
+- `chrome.management.getPermissionWarningsById`
+- `chrome.management.getPermissionWarningsByManifest`
+
+The following events of `chrome.management` are supported:
+
+- `chrome.management.onEnabled`
+- `chrome.management.onDisabled`
+
+See [official documentation](https://developer.chrome.com/docs/extensions/reference/management) for more information.
+
 ### `chrome.runtime`
 
 The following properties of `chrome.runtime` are supported:
@@ -106,12 +131,24 @@ The following events of `chrome.runtime` are supported:
 - `chrome.runtime.onConnect`
 - `chrome.runtime.onMessage`
 
+See [official documentation](https://developer.chrome.com/docs/extensions/reference/runtime) for more information.
+
+### `chrome.scripting`
+
+All features of this API are supported.
+
+See [official documentation](https://developer.chrome.com/docs/extensions/reference/scripting) for more information.
+
 ### `chrome.storage`
 
 The following methods of `chrome.storage` are supported:
 
 - `chrome.storage.local`
 
+`chrome.storage.sync` and `chrome.storage.managed` are **not** supported.
+
+See [official documentation](https://developer.chrome.com/docs/extensions/reference/storage) for more information.
+
 ### `chrome.tabs`
 
 The following methods of `chrome.tabs` are supported:
@@ -128,23 +165,12 @@ The following methods of `chrome.tabs` are supported:
 > tab". Since Electron has no such concept, passing `-1` as a tab ID is not
 > supported and will raise an error.
 
-### `chrome.management`
-
-The following methods of `chrome.management` are supported:
-
-- `chrome.management.getAll`
-- `chrome.management.get`
-- `chrome.management.getSelf`
-- `chrome.management.getPermissionWarningsById`
-- `chrome.management.getPermissionWarningsByManifest`
-
-The following events of `chrome.management` are supported:
-
-- `chrome.management.onEnabled`
-- `chrome.management.onDisabled`
+See [official documentation](https://developer.chrome.com/docs/extensions/reference/tabs) for more information.
 
 ### `chrome.webRequest`
 
 All features of this API are supported.
 
 > **NOTE:** Electron's [`webRequest`](web-request.md) module takes precedence over `chrome.webRequest` if there are conflicting handlers.
+
+See [official documentation](https://developer.chrome.com/docs/extensions/reference/webRequest) for more information.

+ 2 - 0
filenames.gni

@@ -714,6 +714,8 @@ 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",

+ 1 - 0
shell/browser/extensions/api/BUILD.gn

@@ -13,6 +13,7 @@ function_registration("api_registration") {
   sources = [
     "//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",
   ]
 

+ 1390 - 0
shell/browser/extensions/api/scripting/scripting_api.cc

@@ -0,0 +1,1390 @@
+// 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_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/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/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 kFilesExceededSizeLimitError[] =
+    "Scripts could not be loaded because '*' exceeds the maximum script size "
+    "or the extension's maximum total script size.";
+
+// 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;
+
+// Returns if the extension provided `script_id` (without an internal UserScript
+// prefix) is valid and populates `error` if invalid.
+bool IsScriptIDValid(const std::string& script_id, std::string* error) {
+  if (script_id.empty()) {
+    *error = "Script's ID must not be empty";
+    return false;
+  }
+
+  if (script_id[0] == UserScript::kReservedScriptIDPrefix) {
+    *error = base::StringPrintf("Script's ID '%s' must not start with '%c'",
+                                script_id.c_str(),
+                                UserScript::kReservedScriptIDPrefix);
+    return false;
+  }
+
+  return true;
+}
+
+// Iterates over `scripts` and adds a prefix to each script's id to indicate
+// that the script is a dynamic content script. Returns false and populates
+// `error` if any extension provided id in `scripts` is invalid. While this
+// might result in only some of the ids in `scripts` being modified, this should
+// have no effect on API calls as the API method handler will return with
+// `error`.
+bool AddDynamicScriptPrefixToScriptIDs(
+    std::vector<api::scripting::RegisteredContentScript>& scripts,
+    std::string* error) {
+  CHECK(error);
+  for (auto& script : scripts) {
+    if (!IsScriptIDValid(script.id, error)) {
+      return false;
+    }
+
+    script.id = scripting::CreateDynamicScriptID(script.id);
+  }
+
+  return true;
+}
+
+// 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::STYLE_ORIGIN_NONE:
+    case api::scripting::STYLE_ORIGIN_AUTHOR:
+      css_origin = mojom::CSSOrigin::kAuthor;
+      break;
+    case api::scripting::STYLE_ORIGIN_USER:
+      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::EXECUTION_WORLD_NONE:
+    case api::scripting::EXECUTION_WORLD_ISOLATED:
+      break;  // Default to mojom::ExecutionWorld::kIsolated.
+    case api::scripting::EXECUTION_WORLD_MAIN:
+      execution_world = mojom::ExecutionWorld::kMain;
+  }
+
+  return execution_world;
+}
+
+api::scripting::ExecutionWorld ConvertExecutionWorldForAPI(
+    mojom::ExecutionWorld world) {
+  switch (world) {
+    case mojom::ExecutionWorld::kIsolated:
+      return api::scripting::EXECUTION_WORLD_ISOLATED;
+    case mojom::ExecutionWorld::kMain:
+      return api::scripting::EXECUTION_WORLD_MAIN;
+    case mojom::ExecutionWorld::kUserScript:
+      NOTREACHED() << "UserScript worlds are not supported in this API.";
+  }
+
+  NOTREACHED();
+  return api::scripting::EXECUTION_WORLD_ISOLATED;
+}
+
+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>,
+                            absl::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,
+                          absl::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), absl::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;
+}
+
+std::unique_ptr<UserScript> ParseUserScript(
+    content::BrowserContext* browser_context,
+    const Extension& extension,
+    const api::scripting::RegisteredContentScript& content_script,
+    int definition_index,
+    int valid_schemes,
+    std::u16string* error) {
+  auto result = std::make_unique<UserScript>();
+  result->set_id(content_script.id);
+  result->set_host_id(
+      mojom::HostID(mojom::HostID::HostType::kExtensions, extension.id()));
+
+  if (content_script.run_at != api::extension_types::RunAt::kNone) {
+    result->set_run_location(ConvertRunLocation(content_script.run_at));
+  }
+
+  if (content_script.all_frames)
+    result->set_match_all_frames(*content_script.all_frames);
+
+  DCHECK(content_script.matches);
+  if (!script_parsing::ParseMatchPatterns(
+          *content_script.matches,
+          base::OptionalToPtr(content_script.exclude_matches), definition_index,
+          extension.creation_flags(), scripting::kScriptsCanExecuteEverywhere,
+          valid_schemes, scripting::kAllUrlsIncludesChromeUrls, result.get(),
+          error,
+          /*wants_file_access=*/nullptr)) {
+    return nullptr;
+  }
+
+  if (!script_parsing::ParseFileSources(
+          &extension, base::OptionalToPtr(content_script.js),
+          base::OptionalToPtr(content_script.css), definition_index,
+          result.get(), error)) {
+    return nullptr;
+  }
+
+  result->set_incognito_enabled(
+      util::IsIncognitoEnabled(extension.id(), browser_context));
+  result->set_execution_world(ConvertExecutionWorld(content_script.world));
+  return result;
+}
+
+ValidateContentScriptsResult ValidateParsedScriptsOnFileThread(
+    ExtensionResource::SymlinkPolicy symlink_policy,
+    std::unique_ptr<UserScriptList> scripts) {
+  DCHECK(GetExtensionFileTaskRunner()->RunsTasksInCurrentSequence());
+
+  // Validate that claimed script resources actually exist, and are UTF-8
+  // encoded.
+  std::string error;
+  std::vector<InstallWarning> warnings;
+  bool are_script_files_valid = script_parsing::ValidateFileSources(
+      *scripts, symlink_policy, &error, &warnings);
+
+  // Script files over the per script/extension limit are recorded as warnings.
+  // However, for the scripting API we should treat "install warnings" as
+  // errors by turning this call into a no-op and returning an error.
+  if (!warnings.empty() && error.empty()) {
+    error = ErrorUtils::FormatErrorMessage(kFilesExceededSizeLimitError,
+                                           warnings[0].specific);
+    are_script_files_valid = false;
+  }
+
+  return std::make_pair(std::move(scripts), are_script_files_valid
+                                                ? absl::nullopt
+                                                : absl::make_optional(error));
+}
+
+// Converts a UserScript object to a api::scripting::RegisteredContentScript
+// object, used for getRegisteredContentScripts.
+api::scripting::RegisteredContentScript CreateRegisteredContentScriptInfo(
+    const UserScript& script) {
+  api::scripting::RegisteredContentScript script_info;
+  script_info.id = script.id();
+
+  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 registerContentScripts.
+  if (!script.js_scripts().empty()) {
+    script_info.js.emplace();
+    script_info.js->reserve(script.js_scripts().size());
+    for (const auto& js_script : script.js_scripts())
+      script_info.js->push_back(js_script->relative_path().AsUTF8Unsafe());
+  }
+
+  if (!script.css_scripts().empty()) {
+    script_info.css.emplace();
+    script_info.css->reserve(script.css_scripts().size());
+    for (const auto& css_script : script.css_scripts())
+      script_info.css->push_back(css_script->relative_path().AsUTF8Unsafe());
+  }
+
+  script_info.all_frames = script.match_all_frames();
+  script_info.match_origin_as_fallback = script.match_origin_as_fallback() ==
+                                         MatchOriginAsFallbackBehavior::kAlways;
+  script_info.run_at = ConvertRunLocationForAPI(script.run_location());
+  script_info.world = ConvertExecutionWorldForAPI(script.execution_world());
+
+  return script_info;
+}
+
+}  // 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() {
+  absl::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) {
+      std::string json;
+      if (!base::JSONWriter::Write(arg, &json))
+        return RespondNow(Error("Unserializable argument passed."));
+      string_args.push_back(std::move(json));
+    }
+    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,
+    absl::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,
+          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() {
+  absl::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,
+    absl::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() {
+  absl::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
+ScriptingRegisterContentScriptsFunction::Run() {
+  absl::optional<api::scripting::RegisterContentScripts::Params> params =
+      api::scripting::RegisterContentScripts::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.
+  if (!AddDynamicScriptPrefixToScriptIDs(scripts, &error)) {
+    return RespondNow(Error(std::move(error)));
+  }
+
+  ExtensionUserScriptLoader* loader =
+      ExtensionSystem::Get(browser_context())
+          ->user_script_manager()
+          ->GetUserScriptLoaderForExtension(extension()->id());
+  std::set<std::string> existing_script_ids = loader->GetDynamicScriptIDs();
+  std::set<std::string> new_script_ids;
+
+  for (const auto& script : scripts) {
+    if (base::Contains(existing_script_ids, script.id) ||
+        base::Contains(new_script_ids, script.id)) {
+      std::string error_script_id =
+          UserScript::TrimPrefixFromScriptID(script.id);
+      return RespondNow(Error(base::StringPrintf("Duplicate script ID '%s'",
+                                                 error_script_id.c_str())));
+    }
+
+    new_script_ids.insert(script.id);
+  }
+
+  std::u16string parse_error;
+  auto parsed_scripts = std::make_unique<UserScriptList>();
+  std::set<std::string> persistent_script_ids;
+  const int valid_schemes = UserScript::ValidUserScriptSchemes(
+      scripting::kScriptsCanExecuteEverywhere);
+
+  parsed_scripts->reserve(scripts.size());
+  for (size_t i = 0; i < scripts.size(); ++i) {
+    if (!scripts[i].matches) {
+      std::string error_script_id =
+          UserScript::TrimPrefixFromScriptID(scripts[i].id);
+      return RespondNow(
+          Error(base::StringPrintf("Script with ID '%s' must specify 'matches'",
+                                   error_script_id.c_str())));
+    }
+
+    // Parse/Create user script.
+    std::unique_ptr<UserScript> user_script =
+        ParseUserScript(browser_context(), *extension(), scripts[i], i,
+                        valid_schemes, &parse_error);
+    if (!user_script)
+      return RespondNow(Error(base::UTF16ToASCII(parse_error)));
+
+    // Scripts will persist across sessions by default.
+    if (!scripts[i].persist_across_sessions ||
+        *scripts[i].persist_across_sessions) {
+      persistent_script_ids.insert(user_script->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(new_script_ids));
+
+  GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult(
+      FROM_HERE,
+      base::BindOnce(&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 ScriptingRegisterContentScriptsFunction::OnContentScriptFilesValidated(
+    std::set<std::string> persistent_script_ids,
+    ValidateContentScriptsResult result) {
+  // We cannot proceed if the `browser_context` is not valid as the
+  // `ExtensionSystem` will not exist.
+  if (!browser_context()) {
+    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 absl::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() {
+  absl::optional<api::scripting::GetRegisteredContentScripts::Params> params =
+      api::scripting::GetRegisteredContentScripts::Params::Create(args());
+  EXTENSION_FUNCTION_VALIDATE(params);
+
+  const absl::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::CreateDynamicScriptID(id));
+    }
+  }
+
+  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 (id_filter.empty() || base::Contains(id_filter, script->id())) {
+      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);
+
+  absl::optional<api::scripting::ContentScriptFilter>& filter = params->filter;
+  std::set<std::string> ids_to_remove;
+
+  ExtensionUserScriptLoader* loader =
+      ExtensionSystem::Get(browser_context())
+          ->user_script_manager()
+          ->GetUserScriptLoaderForExtension(extension()->id());
+  std::set<std::string> existing_script_ids = loader->GetDynamicScriptIDs();
+  if (filter && filter->ids) {
+    for (const auto& provided_id : *filter->ids) {
+      std::string error;
+      if (!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::CreateDynamicScriptID(provided_id);
+      if (!base::Contains(existing_script_ids, id_with_prefix)) {
+        return RespondNow(Error(base::StringPrintf("Nonexistent script ID '%s'",
+                                                   provided_id.c_str())));
+      }
+
+      ids_to_remove.insert(id_with_prefix);
+    }
+  }
+
+  // TODO(crbug.com/1300657): Only clear all scripts if `filter` did not specify
+  // the list of scripts ids to remove.
+  if (ids_to_remove.empty()) {
+    loader->ClearDynamicScripts(
+        base::BindOnce(&ScriptingUnregisterContentScriptsFunction::
+                           OnContentScriptsUnregistered,
+                       this));
+  } else {
+    loader->RemoveDynamicScripts(
+        std::move(ids_to_remove),
+        base::BindOnce(&ScriptingUnregisterContentScriptsFunction::
+                           OnContentScriptsUnregistered,
+                       this));
+  }
+
+  return RespondLater();
+}
+
+void ScriptingUnregisterContentScriptsFunction::OnContentScriptsUnregistered(
+    const absl::optional<std::string>& error) {
+  if (error.has_value())
+    Respond(Error(std::move(*error)));
+  else
+    Respond(NoArguments());
+}
+
+ScriptingUpdateContentScriptsFunction::ScriptingUpdateContentScriptsFunction() =
+    default;
+ScriptingUpdateContentScriptsFunction::
+    ~ScriptingUpdateContentScriptsFunction() = default;
+
+ExtensionFunction::ResponseAction ScriptingUpdateContentScriptsFunction::Run() {
+  absl::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.
+  if (!AddDynamicScriptPrefixToScriptIDs(scripts, &error)) {
+    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) {
+    loaded_scripts_metadata.emplace(script->id(),
+                                    CreateRegisteredContentScriptInfo(*script));
+  }
+
+  std::set<std::string> ids_to_update;
+  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())));
+    }
+
+    if (base::Contains(ids_to_update, script.id)) {
+      return RespondNow(Error(base::StringPrintf("Duplicate script ID '%s'",
+                                                 error_script_id.c_str())));
+    }
+
+    ids_to_update.insert(script.id);
+  }
+
+  std::u16string parse_error;
+  auto parsed_scripts = std::make_unique<UserScriptList>();
+  const int valid_schemes = UserScript::ValidUserScriptSchemes(
+      scripting::kScriptsCanExecuteEverywhere);
+
+  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(), updated_script, i,
+                        valid_schemes, &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(&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 ScriptingUpdateContentScriptsFunction::OnContentScriptFilesValidated(
+    std::set<std::string> persistent_script_ids,
+    ValidateContentScriptsResult result) {
+  // We cannot proceed if the `browser_context` is not valid as the
+  // `ExtensionSystem` will not exist.
+  if (!browser_context()) {
+    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 absl::optional<std::string>& error) {
+  if (error.has_value())
+    Respond(Error(std::move(*error)));
+  else
+    Respond(NoArguments());
+  Release();  // Matches the `AddRef()` in `Run()`.
+}
+
+}  // namespace extensions

+ 207 - 0
shell/browser/extensions/api/scripting/scripting_api.h

@@ -0,0 +1,207 @@
+// 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 <string>
+#include <utility>
+#include <vector>
+
+#include "chrome/common/extensions/api/scripting.h"
+#include "extensions/browser/extension_function.h"
+#include "extensions/browser/script_executor.h"
+#include "extensions/common/mojom/code_injection.mojom.h"
+#include "extensions/common/user_script.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+namespace extensions {
+
+// A simple helper struct to represent a read file (either CSS or JS) to be
+// injected.
+struct InjectedFileSource {
+  InjectedFileSource(std::string file_name, std::unique_ptr<std::string> data);
+  InjectedFileSource(InjectedFileSource&&);
+  ~InjectedFileSource();
+
+  std::string file_name;
+  std::unique_ptr<std::string> data;
+};
+
+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<InjectedFileSource> file_sources,
+                        absl::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<InjectedFileSource> file_sources,
+                        absl::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);
+};
+
+using ValidateContentScriptsResult =
+    std::pair<std::unique_ptr<UserScriptList>, absl::optional<std::string>>;
+
+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,
+      ValidateContentScriptsResult result);
+
+  // Called when content scripts have been registered.
+  void OnContentScriptsRegistered(const absl::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 absl::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;
+
+  // Called when script files have been checked.
+  void OnContentScriptFilesValidated(
+      std::set<std::string> persistent_script_ids,
+      ValidateContentScriptsResult result);
+
+  // Called when content scripts have been updated.
+  void OnContentScriptsUpdated(const absl::optional<std::string>& error);
+};
+
+}  // namespace extensions
+
+#endif  // ELECTRON_SHELL_BROWSER_EXTENSIONS_API_SCRIPTING_SCRIPTING_API_H_

+ 1 - 0
shell/browser/extensions/electron_extensions_browser_api_provider.cc

@@ -7,6 +7,7 @@
 #include "extensions/browser/api/i18n/i18n_api.h"
 #include "extensions/browser/extension_function_registry.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 {

+ 2 - 0
shell/common/extensions/api/BUILD.gn

@@ -39,6 +39,7 @@ generated_json_strings("generated_api_json_strings") {
   sources = [
     "extension.json",
     "resources_private.idl",
+    "scripting.idl",
     "tabs.json",
   ]
 
@@ -59,6 +60,7 @@ generated_json_strings("generated_api_json_strings") {
 generated_types("generated_api_types") {
   sources = [
     "resources_private.idl",
+    "scripting.idl",
     "tabs.json",
   ]
 

+ 5 - 1
shell/common/extensions/api/_api_features.json

@@ -51,5 +51,9 @@
     "matches": [
       "chrome://print/*"
     ]
-  }]
+  }],
+  "scripting": {
+    "dependencies": ["permission:scripting"],
+    "contexts": ["blessed_extension"]
+  }
 }

+ 7 - 0
shell/common/extensions/api/_permission_features.json

@@ -26,5 +26,12 @@
     "extension_types": [
       "extension"
     ]
+  },
+  "scripting": {
+    "channel": "stable",
+    "extension_types": [
+      "extension"
+    ],
+    "min_manifest_version": 3
   }
 }

+ 262 - 0
shell/common/extensions/api/scripting.idl

@@ -0,0 +1,262 @@
+// 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> and <code>func</code> must be
+    // specified.
+    [serializableFunction]InjectedFunction? func;
+
+    // The arguments to curry into a 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> and <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="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="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;
+    // TODO(devlin): Add documentation once the implementation is complete. See
+    // crbug.com/55084.
+    [nodoc]
+    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/1054624): Enable this once implementation is complete.
+    [nodoc, nocompile] static long globalParams();
+  };
+
+  interface Functions {
+    // Injects a script into a target context. The script will be run at
+    // <code>document_idle</code>. 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.
+    [supportsPromises] 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.
+    [supportsPromises] 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.
+    [supportsPromises] 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.
+    [supportsPromises] 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.
+    [supportsPromises] 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.
+    [supportsPromises] 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.
+    [supportsPromises] static void updateContentScripts(
+        RegisteredContentScript[] scripts,
+        optional RegisterContentScriptsCallback callback);
+  };
+};

+ 2 - 0
shell/common/extensions/electron_extensions_api_provider.cc

@@ -41,6 +41,8 @@ constexpr APIPermissionInfo::InitInfo permissions_to_register[] = {
     {mojom::APIPermissionID::kManagement, "management"},
     {mojom::APIPermissionID::kTab, "tabs",
      APIPermissionInfo::kFlagRequiresManagementUIWarning},
+    {mojom::APIPermissionID::kScripting, "scripting",
+     APIPermissionInfo::kFlagRequiresManagementUIWarning},
 };
 base::span<const APIPermissionInfo::InitInfo> GetPermissionInfos() {
   return base::make_span(permissions_to_register);

+ 70 - 0
spec/extensions-spec.ts

@@ -1129,5 +1129,75 @@ describe('chrome extensions', () => {
         });
       });
     });
+
+    describe('chrome.scripting', () => {
+      let customSession: Session;
+      let w = null as unknown as BrowserWindow;
+
+      before(async () => {
+        customSession = session.fromPartition(`persist:${uuid.v4()}`);
+        await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-scripting'));
+      });
+
+      beforeEach(() => {
+        w = new BrowserWindow({
+          show: false,
+          webPreferences: {
+            session: customSession,
+            nodeIntegration: true
+          }
+        });
+      });
+
+      afterEach(closeAllWindows);
+
+      it('executeScript', async () => {
+        await w.loadURL(url);
+
+        const message = { method: 'executeScript' };
+        w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
+
+        const updated = await once(w.webContents, 'page-title-updated');
+        expect(updated[1]).to.equal('HEY HEY HEY');
+      });
+
+      it('registerContentScripts', async () => {
+        await w.loadURL(url);
+
+        const message = { method: 'registerContentScripts' };
+        w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
+
+        const [,, responseString] = await once(w.webContents, 'console-message');
+        const response = JSON.parse(responseString);
+        expect(response).to.be.an('array').with.lengthOf(1);
+        expect(response[0]).to.deep.equal({
+          allFrames: false,
+          id: 'session-script',
+          js: ['content.js'],
+          matchOriginAsFallback: false,
+          matches: ['<all_urls>'],
+          persistAcrossSessions: false,
+          runAt: 'document_start',
+          world: 'ISOLATED'
+        });
+      });
+
+      it('insertCSS', async () => {
+        await w.loadURL(url);
+
+        const bgBefore = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).backgroundColor');
+        expect(bgBefore).to.equal('rgba(0, 0, 0, 0)');
+
+        const message = { method: 'insertCSS' };
+        w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
+
+        const [,, responseString] = await once(w.webContents, 'console-message');
+        const response = JSON.parse(responseString);
+        expect(response.success).to.be.true();
+
+        const bgAfter = await w.webContents.executeJavaScript('window.getComputedStyle(document.body).backgroundColor');
+        expect(bgAfter).to.equal('rgb(255, 0, 0)');
+      });
+    });
   });
 });

+ 51 - 0
spec/fixtures/extensions/chrome-scripting/background.js

@@ -0,0 +1,51 @@
+/* global chrome */
+
+const handleRequest = async (request, sender, sendResponse) => {
+  const { method } = request;
+  const tabId = sender.tab.id;
+
+  switch (method) {
+    case 'executeScript': {
+      chrome.scripting.executeScript({
+        target: { tabId },
+        function: () => {
+          document.title = 'HEY HEY HEY';
+          return document.title;
+        }
+      }).then(() => {
+        console.log('success');
+      }).catch((err) => {
+        console.log('error', err);
+      });
+      break;
+    }
+
+    case 'registerContentScripts': {
+      await chrome.scripting.registerContentScripts([{
+        id: 'session-script',
+        js: ['content.js'],
+        persistAcrossSessions: false,
+        matches: ['<all_urls>'],
+        runAt: 'document_start'
+      }]);
+
+      chrome.scripting.getRegisteredContentScripts().then(sendResponse);
+      break;
+    }
+
+    case 'insertCSS': {
+      chrome.scripting.insertCSS({
+        target: { tabId },
+        css: 'body { background-color: red; }'
+      }).then(() => {
+        sendResponse({ success: true });
+      });
+      break;
+    }
+  }
+};
+
+chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+  handleRequest(request, sender, sendResponse);
+  return true;
+});

+ 0 - 0
spec/fixtures/extensions/chrome-scripting/content.js


+ 30 - 0
spec/fixtures/extensions/chrome-scripting/main.js

@@ -0,0 +1,30 @@
+/* global chrome */
+
+chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+  sendResponse(request);
+});
+
+const map = {
+  executeScript () {
+    chrome.runtime.sendMessage({ method: 'executeScript' }, response => {
+      console.log(JSON.stringify(response));
+    });
+  },
+  registerContentScripts () {
+    chrome.runtime.sendMessage({ method: 'registerContentScripts' }, response => {
+      console.log(JSON.stringify(response));
+    });
+  },
+  insertCSS () {
+    chrome.runtime.sendMessage({ method: 'insertCSS' }, response => {
+      console.log(JSON.stringify(response));
+    });
+  }
+};
+
+const dispatchTest = (event) => {
+  const { method, args = [] } = JSON.parse(event.data);
+  map[method](...args);
+};
+
+window.addEventListener('message', dispatchTest, false);

+ 17 - 0
spec/fixtures/extensions/chrome-scripting/manifest.json

@@ -0,0 +1,17 @@
+{
+  "name": "execute-script",
+  "version": "1.0",
+  "permissions": [
+    "scripting"
+  ],
+  "host_permissions": ["<all_urls>"],
+  "content_scripts": [{
+    "matches": [ "<all_urls>"],
+    "js": ["main.js"],
+    "run_at": "document_start"
+  }],
+  "background": {
+    "service_worker": "background.js"
+  },
+  "manifest_version": 3
+}