![electron-roller[bot]](/assets/img/avatar_default.png)
* chore: bump chromium in DEPS to 136.0.7081.1 * chore: bump chromium in DEPS to 136.0.7083.1 * chore: bump chromium in DEPS to 136.0.7085.1 * chore: bump chromium in DEPS to 136.0.7087.1 * chore: bump chromium in DEPS to 136.0.7089.0 * chore: bump chromium in DEPS to 136.0.7091.0 * chore: bump chromium in DEPS to 136.0.7092.0 * chore: bump chromium in DEPS to 136.0.7093.1 * chore: bump chromium in DEPS to 136.0.7095.1 * chore: bump chromium in DEPS to 136.0.7097.1 * chore: bump chromium in DEPS to 136.0.7099.1 * chore: bump chromium in DEPS to 136.0.7101.0 * chore: bump chromium in DEPS to 136.0.7103.0 * chore: bump chromium in DEPS to 136.0.7103.15 * chore: bump chromium in DEPS to 136.0.7103.17 * chore: bump chromium to 136.0.7095.0 (main) (#46118) * chore: bump chromium in DEPS to 136.0.7076.0 * chore: bump chromium in DEPS to 136.0.7077.0 * 6368856: Migrate absl variant.h and utility.h in content (part 2/2) |6368856
* 6356528: Clean up LegacyRenderWidgetHostHWND code |6356528
* chore: export patches * 6339113: [Viewport Segments] Add CDP commands to override Viewport Segments without overriding other device properties. |6339113
* 6352169: [DevTools][MultiInstance] Support new tab in another window on Android |6352169
* 6368856: Migrate absl variant.h and utility.h in content (part 2/2) |6368856
* 6360858:Clickiness: Wire response from URLLoader to DB, add e2e tests|6360858
* chore: bump chromium in DEPS to 136.0.7079.0 * chore: export patches * chore: bump chromium in DEPS to 136.0.7081.0 * chore: export patches * chore: bump chromium in DEPS to 136.0.7083.0 * 6361987: Remove double-declaration with gfx::NativeView and gfx::NativeWindow |6361987
* chore: export patches * chore: bump chromium in DEPS to 136.0.7087.0 * chore: export patches * fix: include node patch for missing AtomicsWaitEvent6385540
* build: add depot_tools python to path * fix: cppgc init and unregistering v8 isolate6333562
CppGc is now initialized earlier so Node can skip reinitializing it. Additionally, gin::IsolateHandle was attempting to destruct an already destructed v8::Isolate upon electron::JavaScriptEnvironment destruction. By removing the call to NodePlatform::UnregisterIsolate, this fixes the crash on app shutdown. * fix: unregister isolate after destruction See code comment. * chore: bump chromium in DEPS to 136.0.7095.0 * chore: sync patches * fix: add script_parsing::ContentScriptType parameter6298395
* fix: migrate content::BrowserAccessibilityState methods6401437
6383275
* feat: enableHappyEyeballs option for host resolver6332599
* fix: add new cookie exclusion reason6343479
* fix: add new url loader method6337340
* fix: add new cppgc header file for electron_node headers6348644
* fix: disable CREL on Linux ARM64 https://chromium-review.googlesource.com/q/I3a62f02f564f07be63173b0773b4ecaffbe939b9 * fixup! fix: add new cppgc header file for electron_node headers6348644
* chore: update corner smoothing patch * fixup! chore: update corner smoothing patch * chore: disable NAN weak tests These two tests are incompatible with a V8 change that disallows running JS code from a weak finalizer callback. Ref:4733273
* test: fix task starvation in node test A V8 change makes these contexts get collected in a task that is posted and run asynchronously. The tests were synchronously GC'ing in an infinite loop, preventing the task loop from running the task that would GC these contexts. This change should be upstreamed in some way. Ref:4733273
--------- Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com> Co-authored-by: alice <alice@makenotion.com> Co-authored-by: Samuel Maddock <smaddock@slack-corp.com> Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org> Co-authored-by: clavin <clavin@electronjs.org> (cherry picked from commit9c019b6147
) * Remove file-wide unsafe buffer suppression from content/ [3 of N]6341711
--------- Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com> Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
1199 lines
44 KiB
C++
1199 lines
44 KiB
C++
// Copyright 2023 Microsoft, GmbH
|
|
// Use of this source code is governed by the MIT license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "shell/browser/extensions/api/scripting/scripting_api.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "base/check.h"
|
|
#include "base/json/json_writer.h"
|
|
#include "base/strings/string_util.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/navigation_controller.h"
|
|
#include "content/public/browser/navigation_entry.h"
|
|
#include "extensions/browser/extension_api_frame_id_map.h"
|
|
#include "extensions/browser/extension_file_task_runner.h"
|
|
#include "extensions/browser/extension_registry.h"
|
|
#include "extensions/browser/extension_system.h"
|
|
#include "extensions/browser/extension_user_script_loader.h"
|
|
#include "extensions/browser/extension_util.h"
|
|
#include "extensions/browser/load_and_localize_file.h"
|
|
#include "extensions/browser/script_executor.h"
|
|
#include "extensions/browser/scripting_constants.h"
|
|
#include "extensions/browser/user_script_manager.h"
|
|
#include "extensions/common/api/extension_types.h"
|
|
#include "extensions/common/api/scripts_internal.h"
|
|
#include "extensions/common/api/scripts_internal/script_serialization.h"
|
|
#include "extensions/common/error_utils.h"
|
|
#include "extensions/common/extension.h"
|
|
#include "extensions/common/manifest_constants.h"
|
|
#include "extensions/common/mojom/css_origin.mojom-shared.h"
|
|
#include "extensions/common/mojom/execution_world.mojom-shared.h"
|
|
#include "extensions/common/mojom/host_id.mojom.h"
|
|
#include "extensions/common/mojom/match_origin_as_fallback.mojom-shared.h"
|
|
#include "extensions/common/mojom/run_location.mojom-shared.h"
|
|
#include "extensions/common/permissions/permissions_data.h"
|
|
#include "extensions/common/user_script.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"
|
|
#include "third_party/abseil-cpp/absl/strings/str_format.h"
|
|
|
|
namespace extensions {
|
|
|
|
namespace {
|
|
|
|
constexpr std::string_view kEmptyMatchesError =
|
|
"Script with ID '*' must specify 'matches'.";
|
|
constexpr char kExactlyOneOfCssAndFilesError[] =
|
|
"Exactly one of 'css' and 'files' must be specified.";
|
|
|
|
// Note: CSS always injects as soon as possible, so we default to
|
|
// document_start. Because of tab loading, there's no guarantee this will
|
|
// *actually* inject before page load, but it will at least inject "soon".
|
|
constexpr mojom::RunLocation kCSSRunLocation =
|
|
mojom::RunLocation::kDocumentStart;
|
|
|
|
// Converts the given `style_origin` to a CSSOrigin.
|
|
mojom::CSSOrigin ConvertStyleOriginToCSSOrigin(
|
|
api::scripting::StyleOrigin style_origin) {
|
|
mojom::CSSOrigin css_origin = mojom::CSSOrigin::kAuthor;
|
|
switch (style_origin) {
|
|
case api::scripting::StyleOrigin::kNone:
|
|
case api::scripting::StyleOrigin::kAuthor:
|
|
css_origin = mojom::CSSOrigin::kAuthor;
|
|
break;
|
|
case api::scripting::StyleOrigin::kUser:
|
|
css_origin = mojom::CSSOrigin::kUser;
|
|
break;
|
|
}
|
|
|
|
return css_origin;
|
|
}
|
|
|
|
mojom::ExecutionWorld ConvertExecutionWorld(
|
|
api::scripting::ExecutionWorld world) {
|
|
mojom::ExecutionWorld execution_world = mojom::ExecutionWorld::kIsolated;
|
|
switch (world) {
|
|
case api::scripting::ExecutionWorld::kNone:
|
|
case api::scripting::ExecutionWorld::kIsolated:
|
|
break; // Default to mojom::ExecutionWorld::kIsolated.
|
|
case api::scripting::ExecutionWorld::kMain:
|
|
execution_world = mojom::ExecutionWorld::kMain;
|
|
}
|
|
|
|
return execution_world;
|
|
}
|
|
|
|
std::string InjectionKeyForCode(const mojom::HostID& host_id,
|
|
const std::string& code) {
|
|
return ScriptExecutor::GenerateInjectionKey(host_id, /*script_url=*/GURL(),
|
|
code);
|
|
}
|
|
|
|
std::string InjectionKeyForFile(const mojom::HostID& host_id,
|
|
const GURL& resource_url) {
|
|
return ScriptExecutor::GenerateInjectionKey(host_id, resource_url,
|
|
/*code=*/std::string());
|
|
}
|
|
|
|
std::vector<mojom::JSSourcePtr> FileSourcesToJSSources(
|
|
const Extension& extension,
|
|
std::vector<scripting::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<scripting::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;
|
|
}
|
|
|
|
// 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 = absl::StrFormat("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 = absl::StrFormat("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 = absl::StrFormat("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 = absl::StrFormat("No tab with id: %d", target.tab_id);
|
|
return false;
|
|
}
|
|
|
|
content::WebContents* tab = contents->web_contents();
|
|
|
|
if ((target.all_frames && *target.all_frames == true) &&
|
|
(target.frame_ids || target.document_ids)) {
|
|
*error_out =
|
|
"Cannot specify 'allFrames' if either 'frameIds' or 'documentIds' is "
|
|
"specified.";
|
|
return false;
|
|
}
|
|
|
|
if (target.frame_ids && target.document_ids) {
|
|
*error_out = "Cannot specify both 'frameIds' and 'documentIds'.";
|
|
return false;
|
|
}
|
|
|
|
ScriptExecutor* script_executor = contents->script_executor();
|
|
DCHECK(script_executor);
|
|
|
|
ScriptExecutor::FrameScope frame_scope =
|
|
target.all_frames && *target.all_frames == true
|
|
? ScriptExecutor::INCLUDE_SUB_FRAMES
|
|
: ScriptExecutor::SPECIFIED_FRAMES;
|
|
|
|
std::set<int> frame_ids;
|
|
std::set<content::RenderFrameHost*> frames;
|
|
if (!CollectFramesForInjection(target, tab, frame_ids, frames, error_out))
|
|
return false;
|
|
|
|
// TODO(devlin): If `allFrames` is true, we error out if the extension
|
|
// doesn't have access to the top frame (even if it may inject in child
|
|
// frames). This is inconsistent with content scripts (which can execute on
|
|
// child frames), but consistent with the old tabs.executeScript() API.
|
|
for (content::RenderFrameHost* frame : frames) {
|
|
DCHECK_EQ(content::WebContents::FromRenderFrameHost(frame), tab);
|
|
if (!HasPermissionToInjectIntoFrame(permissions, target.tab_id, frame,
|
|
error_out)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
*frame_ids_out = std::move(frame_ids);
|
|
*frame_scope_out = frame_scope;
|
|
*script_executor_out = script_executor;
|
|
return true;
|
|
}
|
|
|
|
api::scripts_internal::SerializedUserScript
|
|
ConvertRegisteredContentScriptToSerializedUserScript(
|
|
api::scripting::RegisteredContentScript content_script) {
|
|
auto convert_execution_world = [](api::scripting::ExecutionWorld world) {
|
|
switch (world) {
|
|
case api::scripting::ExecutionWorld::kNone:
|
|
case api::scripting::ExecutionWorld::kIsolated:
|
|
return api::extension_types::ExecutionWorld::kIsolated;
|
|
case api::scripting::ExecutionWorld::kMain:
|
|
return api::extension_types::ExecutionWorld::kMain;
|
|
}
|
|
};
|
|
|
|
api::scripts_internal::SerializedUserScript serialized_script;
|
|
serialized_script.source =
|
|
api::scripts_internal::Source::kDynamicContentScript;
|
|
|
|
// Note: IDs have already been prefixed appropriately.
|
|
serialized_script.id = std::move(content_script.id);
|
|
// Note: `matches` are guaranteed to be non-null.
|
|
serialized_script.matches = std::move(*content_script.matches);
|
|
serialized_script.exclude_matches = std::move(content_script.exclude_matches);
|
|
if (content_script.css) {
|
|
serialized_script.css = script_serialization::GetSourcesFromFileNames(
|
|
std::move(*content_script.css));
|
|
}
|
|
if (content_script.js) {
|
|
serialized_script.js = script_serialization::GetSourcesFromFileNames(
|
|
std::move(*content_script.js));
|
|
}
|
|
serialized_script.all_frames = content_script.all_frames;
|
|
serialized_script.match_origin_as_fallback =
|
|
content_script.match_origin_as_fallback;
|
|
serialized_script.run_at = content_script.run_at;
|
|
serialized_script.world = convert_execution_world(content_script.world);
|
|
|
|
return serialized_script;
|
|
}
|
|
|
|
std::unique_ptr<UserScript> ParseUserScript(
|
|
content::BrowserContext* browser_context,
|
|
const Extension& extension,
|
|
bool allowed_in_incognito,
|
|
api::scripting::RegisteredContentScript content_script,
|
|
std::u16string* error) {
|
|
api::scripts_internal::SerializedUserScript serialized_script =
|
|
ConvertRegisteredContentScriptToSerializedUserScript(
|
|
std::move(content_script));
|
|
|
|
return script_serialization::ParseSerializedUserScript(
|
|
serialized_script, extension, allowed_in_incognito, error);
|
|
}
|
|
|
|
// Converts a UserScript object to a api::scripting::RegisteredContentScript
|
|
// object, used for getRegisteredContentScripts.
|
|
api::scripting::RegisteredContentScript CreateRegisteredContentScriptInfo(
|
|
const UserScript& script) {
|
|
CHECK_EQ(UserScript::Source::kDynamicContentScript, script.GetSource());
|
|
|
|
// To convert a `UserScript`, we first go through our script_internal
|
|
// serialization; this allows us to do simple conversions and avoid any
|
|
// complex logic.
|
|
api::scripts_internal::SerializedUserScript serialized_script =
|
|
script_serialization::SerializeUserScript(script);
|
|
|
|
auto convert_serialized_script_sources =
|
|
[](std::vector<api::scripts_internal::ScriptSource> sources) {
|
|
std::vector<std::string> converted;
|
|
converted.reserve(sources.size());
|
|
for (auto& source : sources) {
|
|
CHECK(source.file)
|
|
<< "Content scripts don't allow arbitrary code strings";
|
|
converted.push_back(std::move(*source.file));
|
|
}
|
|
return converted;
|
|
};
|
|
|
|
auto convert_execution_world =
|
|
[](api::extension_types::ExecutionWorld world) {
|
|
switch (world) {
|
|
case api::extension_types::ExecutionWorld::kNone:
|
|
NOTREACHED()
|
|
<< "Execution world should always be present in serialization.";
|
|
case api::extension_types::ExecutionWorld::kIsolated:
|
|
return api::scripting::ExecutionWorld::kIsolated;
|
|
case api::extension_types::ExecutionWorld::kUserScript:
|
|
NOTREACHED() << "ISOLATED worlds are not supported in this API.";
|
|
case api::extension_types::ExecutionWorld::kMain:
|
|
return api::scripting::ExecutionWorld::kMain;
|
|
}
|
|
};
|
|
|
|
api::scripting::RegisteredContentScript content_script;
|
|
content_script.id = std::move(serialized_script.id);
|
|
content_script.matches = std::move(serialized_script.matches);
|
|
content_script.exclude_matches = std::move(serialized_script.exclude_matches);
|
|
if (serialized_script.css) {
|
|
content_script.css =
|
|
convert_serialized_script_sources(std::move(*serialized_script.css));
|
|
}
|
|
if (serialized_script.js) {
|
|
content_script.js =
|
|
convert_serialized_script_sources(std::move(*serialized_script.js));
|
|
}
|
|
content_script.all_frames = serialized_script.all_frames;
|
|
content_script.match_origin_as_fallback =
|
|
serialized_script.match_origin_as_fallback;
|
|
content_script.run_at = serialized_script.run_at;
|
|
content_script.world = convert_execution_world(serialized_script.world);
|
|
|
|
return content_script;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ScriptingExecuteScriptFunction::ScriptingExecuteScriptFunction() = default;
|
|
ScriptingExecuteScriptFunction::~ScriptingExecuteScriptFunction() = default;
|
|
|
|
ExtensionFunction::ResponseAction ScriptingExecuteScriptFunction::Run() {
|
|
std::optional<api::scripting::ExecuteScript::Params> params =
|
|
api::scripting::ExecuteScript::Params::Create(args());
|
|
EXTENSION_FUNCTION_VALIDATE(params);
|
|
injection_ = std::move(params->injection);
|
|
|
|
// Silently alias `function` to `func` for backwards compatibility.
|
|
// TODO(devlin): Remove this in M95.
|
|
if (injection_.function) {
|
|
if (injection_.func) {
|
|
return RespondNow(
|
|
Error("Both 'func' and 'function' were specified. "
|
|
"Only 'func' should be used."));
|
|
}
|
|
injection_.func = std::move(injection_.function);
|
|
}
|
|
|
|
if ((injection_.files && injection_.func) ||
|
|
(!injection_.files && !injection_.func)) {
|
|
return RespondNow(
|
|
Error("Exactly one of 'func' and 'files' must be specified"));
|
|
}
|
|
|
|
if (injection_.files) {
|
|
if (injection_.args)
|
|
return RespondNow(Error("'args' may not be used with file injections."));
|
|
|
|
// JS files don't require localization.
|
|
constexpr bool kRequiresLocalization = false;
|
|
std::string error;
|
|
if (!CheckAndLoadFiles(
|
|
std::move(*injection_.files),
|
|
script_parsing::ContentScriptType::kJs, *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 = absl::StrFormat(
|
|
"(%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<scripting::InjectedFileSource> file_sources,
|
|
std::optional<std::string> load_error) {
|
|
if (load_error) {
|
|
Respond(Error(std::move(*load_error)));
|
|
return;
|
|
}
|
|
|
|
DCHECK(!file_sources.empty());
|
|
|
|
std::string error;
|
|
if (!Execute(FileSourcesToJSSources(*extension(), std::move(file_sources)),
|
|
&error)) {
|
|
Respond(Error(std::move(error)));
|
|
}
|
|
}
|
|
|
|
bool ScriptingExecuteScriptFunction::Execute(
|
|
std::vector<mojom::JSSourcePtr> sources,
|
|
std::string* error) {
|
|
ScriptExecutor* script_executor = nullptr;
|
|
ScriptExecutor::FrameScope frame_scope = ScriptExecutor::SPECIFIED_FRAMES;
|
|
std::set<int> frame_ids;
|
|
if (!CanAccessTarget(*extension()->permissions_data(), injection_.target,
|
|
browser_context(), include_incognito_information(),
|
|
&script_executor, &frame_scope, &frame_ids, error)) {
|
|
return false;
|
|
}
|
|
|
|
mojom::ExecutionWorld execution_world =
|
|
ConvertExecutionWorld(injection_.world);
|
|
// scripting.executeScript() doesn't support selecting execution world id.
|
|
std::optional<std::string> execution_world_id = std::nullopt;
|
|
bool inject_immediately = injection_.inject_immediately.value_or(false);
|
|
|
|
scripting::ExecuteScript(
|
|
extension()->id(), std::move(sources), execution_world,
|
|
execution_world_id, script_executor, frame_scope, frame_ids,
|
|
inject_immediately, user_gesture(),
|
|
base::BindOnce(&ScriptingExecuteScriptFunction::OnScriptExecuted, this));
|
|
|
|
return true;
|
|
}
|
|
|
|
void ScriptingExecuteScriptFunction::OnScriptExecuted(
|
|
std::vector<ScriptExecutor::FrameResult> frame_results) {
|
|
// If only a single frame was included and the injection failed, respond with
|
|
// an error.
|
|
if (frame_results.size() == 1 && !frame_results[0].error.empty()) {
|
|
Respond(Error(std::move(frame_results[0].error)));
|
|
return;
|
|
}
|
|
|
|
// Otherwise, respond successfully. We currently just skip over individual
|
|
// frames that failed. In the future, we can bubble up these error messages
|
|
// to the extension.
|
|
std::vector<api::scripting::InjectionResult> injection_results;
|
|
for (auto& result : frame_results) {
|
|
if (!result.error.empty())
|
|
continue;
|
|
api::scripting::InjectionResult injection_result;
|
|
injection_result.result = std::move(result.value);
|
|
injection_result.frame_id = result.frame_id;
|
|
if (result.document_id)
|
|
injection_result.document_id = result.document_id.ToString();
|
|
|
|
// Put the top frame first; otherwise, any order.
|
|
if (result.frame_id == ExtensionApiFrameIdMap::kTopFrameId) {
|
|
injection_results.insert(injection_results.begin(),
|
|
std::move(injection_result));
|
|
} else {
|
|
injection_results.push_back(std::move(injection_result));
|
|
}
|
|
}
|
|
|
|
Respond(ArgumentList(
|
|
api::scripting::ExecuteScript::Results::Create(injection_results)));
|
|
}
|
|
|
|
ScriptingInsertCSSFunction::ScriptingInsertCSSFunction() = default;
|
|
ScriptingInsertCSSFunction::~ScriptingInsertCSSFunction() = default;
|
|
|
|
ExtensionFunction::ResponseAction ScriptingInsertCSSFunction::Run() {
|
|
std::optional<api::scripting::InsertCSS::Params> params =
|
|
api::scripting::InsertCSS::Params::Create(args());
|
|
EXTENSION_FUNCTION_VALIDATE(params);
|
|
|
|
injection_ = std::move(params->injection);
|
|
|
|
if ((injection_.files && injection_.css) ||
|
|
(!injection_.files && !injection_.css)) {
|
|
return RespondNow(Error(kExactlyOneOfCssAndFilesError));
|
|
}
|
|
|
|
if (injection_.files) {
|
|
// CSS files require localization.
|
|
constexpr bool kRequiresLocalization = true;
|
|
std::string error;
|
|
if (!CheckAndLoadFiles(
|
|
std::move(*injection_.files),
|
|
script_parsing::ContentScriptType::kCss, *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<scripting::InjectedFileSource> file_sources,
|
|
std::optional<std::string> load_error) {
|
|
if (load_error) {
|
|
Respond(Error(std::move(*load_error)));
|
|
return;
|
|
}
|
|
|
|
DCHECK(!file_sources.empty());
|
|
std::vector<mojom::CSSSourcePtr> sources =
|
|
FileSourcesToCSSSources(*extension(), std::move(file_sources));
|
|
|
|
std::string error;
|
|
if (!Execute(std::move(sources), &error))
|
|
Respond(Error(std::move(error)));
|
|
}
|
|
|
|
bool ScriptingInsertCSSFunction::Execute(
|
|
std::vector<mojom::CSSSourcePtr> sources,
|
|
std::string* error) {
|
|
ScriptExecutor* script_executor = nullptr;
|
|
ScriptExecutor::FrameScope frame_scope = ScriptExecutor::SPECIFIED_FRAMES;
|
|
std::set<int> frame_ids;
|
|
if (!CanAccessTarget(*extension()->permissions_data(), injection_.target,
|
|
browser_context(), include_incognito_information(),
|
|
&script_executor, &frame_scope, &frame_ids, error)) {
|
|
return false;
|
|
}
|
|
DCHECK(script_executor);
|
|
|
|
script_executor->ExecuteScript(
|
|
mojom::HostID(mojom::HostID::HostType::kExtensions, extension()->id()),
|
|
mojom::CodeInjection::NewCss(mojom::CSSInjection::New(
|
|
std::move(sources), ConvertStyleOriginToCSSOrigin(injection_.origin),
|
|
mojom::CSSInjection::Operation::kAdd)),
|
|
frame_scope, frame_ids,
|
|
mojom::MatchOriginAsFallbackBehavior::kMatchForAboutSchemeAndClimbTree,
|
|
kCSSRunLocation, ScriptExecutor::DEFAULT_PROCESS,
|
|
/* webview_src */ GURL(),
|
|
base::BindOnce(&ScriptingInsertCSSFunction::OnCSSInserted, this));
|
|
|
|
return true;
|
|
}
|
|
|
|
void ScriptingInsertCSSFunction::OnCSSInserted(
|
|
std::vector<ScriptExecutor::FrameResult> results) {
|
|
// If only a single frame was included and the injection failed, respond with
|
|
// an error.
|
|
if (results.size() == 1 && !results[0].error.empty()) {
|
|
Respond(Error(std::move(results[0].error)));
|
|
return;
|
|
}
|
|
|
|
Respond(NoArguments());
|
|
}
|
|
|
|
ScriptingRemoveCSSFunction::ScriptingRemoveCSSFunction() = default;
|
|
ScriptingRemoveCSSFunction::~ScriptingRemoveCSSFunction() = default;
|
|
|
|
ExtensionFunction::ResponseAction ScriptingRemoveCSSFunction::Run() {
|
|
std::optional<api::scripting::RemoveCSS::Params> params =
|
|
api::scripting::RemoveCSS::Params::Create(args());
|
|
EXTENSION_FUNCTION_VALIDATE(params);
|
|
|
|
api::scripting::CSSInjection& injection = params->injection;
|
|
|
|
if ((injection.files && injection.css) ||
|
|
(!injection.files && !injection.css)) {
|
|
return RespondNow(Error(kExactlyOneOfCssAndFilesError));
|
|
}
|
|
|
|
ScriptExecutor* script_executor = nullptr;
|
|
ScriptExecutor::FrameScope frame_scope = ScriptExecutor::SPECIFIED_FRAMES;
|
|
std::set<int> frame_ids;
|
|
std::string error;
|
|
if (!CanAccessTarget(*extension()->permissions_data(), injection.target,
|
|
browser_context(), include_incognito_information(),
|
|
&script_executor, &frame_scope, &frame_ids, &error)) {
|
|
return RespondNow(Error(std::move(error)));
|
|
}
|
|
DCHECK(script_executor);
|
|
|
|
mojom::HostID host_id(mojom::HostID::HostType::kExtensions,
|
|
extension()->id());
|
|
std::vector<mojom::CSSSourcePtr> sources;
|
|
|
|
if (injection.files) {
|
|
std::vector<ExtensionResource> resources;
|
|
if (!scripting::GetFileResources(*injection.files,
|
|
script_parsing::ContentScriptType::kCss,
|
|
*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,
|
|
mojom::MatchOriginAsFallbackBehavior::kMatchForAboutSchemeAndClimbTree,
|
|
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() {
|
|
std::optional<api::scripting::RegisterContentScripts::Params> params =
|
|
api::scripting::RegisterContentScripts::Params::Create(args());
|
|
EXTENSION_FUNCTION_VALIDATE(params);
|
|
|
|
std::vector<api::scripting::RegisteredContentScript>& scripts =
|
|
params->scripts;
|
|
ExtensionUserScriptLoader* loader =
|
|
ExtensionSystem::Get(browser_context())
|
|
->user_script_manager()
|
|
->GetUserScriptLoaderForExtension(extension()->id());
|
|
|
|
// Create script ids for dynamic content scripts.
|
|
std::string error;
|
|
std::set<std::string> existing_script_ids =
|
|
loader->GetDynamicScriptIDs(UserScript::Source::kDynamicContentScript);
|
|
std::set<std::string> new_script_ids = scripting::CreateDynamicScriptIds(
|
|
scripts, UserScript::Source::kDynamicContentScript, existing_script_ids,
|
|
&error);
|
|
|
|
if (!error.empty()) {
|
|
CHECK(new_script_ids.empty());
|
|
return RespondNow(Error(std::move(error)));
|
|
}
|
|
|
|
// Parse content scripts.
|
|
std::u16string parse_error;
|
|
UserScriptList parsed_scripts;
|
|
std::set<std::string> persistent_script_ids;
|
|
|
|
bool allowed_in_incognito = scripting::ScriptsShouldBeAllowedInIncognito(
|
|
extension()->id(), browser_context());
|
|
|
|
parsed_scripts.reserve(scripts.size());
|
|
for (auto& script : scripts) {
|
|
if (!script.matches) {
|
|
return RespondNow(Error(ErrorUtils::FormatErrorMessage(
|
|
kEmptyMatchesError, UserScript::TrimPrefixFromScriptID(script.id))));
|
|
}
|
|
|
|
// Scripts will persist across sessions by default.
|
|
bool persist_across_sessions =
|
|
script.persist_across_sessions.value_or(true);
|
|
|
|
std::unique_ptr<UserScript> user_script =
|
|
ParseUserScript(browser_context(), *extension(), allowed_in_incognito,
|
|
std::move(script), &parse_error);
|
|
if (!user_script) {
|
|
return RespondNow(Error(base::UTF16ToASCII(parse_error)));
|
|
}
|
|
|
|
if (persist_across_sessions) {
|
|
persistent_script_ids.insert(user_script->id());
|
|
}
|
|
parsed_scripts.push_back(std::move(user_script));
|
|
}
|
|
// The contents of `scripts` have all been std::move()'d.
|
|
scripts.clear();
|
|
|
|
// Add new script IDs now in case another call with the same script IDs is
|
|
// made immediately following this one.
|
|
loader->AddPendingDynamicScriptIDs(std::move(new_script_ids));
|
|
|
|
GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult(
|
|
FROM_HERE,
|
|
base::BindOnce(&scripting::ValidateParsedScriptsOnFileThread,
|
|
script_parsing::GetSymlinkPolicy(extension()),
|
|
std::move(parsed_scripts)),
|
|
base::BindOnce(&ScriptingRegisterContentScriptsFunction::
|
|
OnContentScriptFilesValidated,
|
|
this, std::move(persistent_script_ids)));
|
|
|
|
// Balanced in `OnContentScriptFilesValidated()` or
|
|
// `OnContentScriptsRegistered()`.
|
|
AddRef();
|
|
return RespondLater();
|
|
}
|
|
|
|
void ScriptingRegisterContentScriptsFunction::OnContentScriptFilesValidated(
|
|
std::set<std::string> persistent_script_ids,
|
|
scripting::ValidateScriptsResult result) {
|
|
// We cannot proceed if the `browser_context` is not valid as the
|
|
// `ExtensionSystem` will not exist.
|
|
if (!browser_context()) {
|
|
Release(); // Matches the `AddRef()` in `Run()`.
|
|
return;
|
|
}
|
|
|
|
// We cannot proceed if the extension is uninstalled or unloaded in the middle
|
|
// of validating its script files.
|
|
ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context());
|
|
if (!extension() ||
|
|
!registry->enabled_extensions().Contains(extension_id())) {
|
|
// Note: a Respond() is not needed if the system is shutting down or if the
|
|
// extension is no longer enabled.
|
|
Release(); // Matches the `AddRef()` in `Run()`.
|
|
return;
|
|
}
|
|
|
|
auto error = std::move(result.second);
|
|
auto scripts = std::move(result.first);
|
|
ExtensionUserScriptLoader* loader =
|
|
ExtensionSystem::Get(browser_context())
|
|
->user_script_manager()
|
|
->GetUserScriptLoaderForExtension(extension()->id());
|
|
|
|
if (error.has_value()) {
|
|
std::set<std::string> ids_to_remove;
|
|
for (const auto& script : scripts) {
|
|
ids_to_remove.insert(script->id());
|
|
}
|
|
|
|
loader->RemovePendingDynamicScriptIDs(std::move(ids_to_remove));
|
|
Respond(Error(std::move(*error)));
|
|
Release(); // Matches the `AddRef()` in `Run()`.
|
|
return;
|
|
}
|
|
|
|
loader->AddDynamicScripts(
|
|
std::move(scripts), std::move(persistent_script_ids),
|
|
base::BindOnce(
|
|
&ScriptingRegisterContentScriptsFunction::OnContentScriptsRegistered,
|
|
this));
|
|
}
|
|
|
|
void ScriptingRegisterContentScriptsFunction::OnContentScriptsRegistered(
|
|
const std::optional<std::string>& error) {
|
|
if (error.has_value())
|
|
Respond(Error(std::move(*error)));
|
|
else
|
|
Respond(NoArguments());
|
|
Release(); // Matches the `AddRef()` in `Run()`.
|
|
}
|
|
|
|
ScriptingGetRegisteredContentScriptsFunction::
|
|
ScriptingGetRegisteredContentScriptsFunction() = default;
|
|
ScriptingGetRegisteredContentScriptsFunction::
|
|
~ScriptingGetRegisteredContentScriptsFunction() = default;
|
|
|
|
ExtensionFunction::ResponseAction
|
|
ScriptingGetRegisteredContentScriptsFunction::Run() {
|
|
std::optional<api::scripting::GetRegisteredContentScripts::Params> params =
|
|
api::scripting::GetRegisteredContentScripts::Params::Create(args());
|
|
EXTENSION_FUNCTION_VALIDATE(params);
|
|
|
|
const std::optional<api::scripting::ContentScriptFilter>& filter =
|
|
params->filter;
|
|
std::set<std::string> id_filter;
|
|
if (filter && filter->ids) {
|
|
for (const std::string& id : *(filter->ids)) {
|
|
id_filter.insert(scripting::AddPrefixToDynamicScriptId(
|
|
id, UserScript::Source::kDynamicContentScript));
|
|
}
|
|
}
|
|
|
|
ExtensionUserScriptLoader* loader =
|
|
ExtensionSystem::Get(browser_context())
|
|
->user_script_manager()
|
|
->GetUserScriptLoaderForExtension(extension()->id());
|
|
const UserScriptList& dynamic_scripts = loader->GetLoadedDynamicScripts();
|
|
|
|
std::vector<api::scripting::RegisteredContentScript> script_infos;
|
|
std::set<std::string> persistent_script_ids =
|
|
loader->GetPersistentDynamicScriptIDs();
|
|
for (const std::unique_ptr<UserScript>& script : dynamic_scripts) {
|
|
if (script->GetSource() != UserScript::Source::kDynamicContentScript) {
|
|
continue;
|
|
}
|
|
|
|
if (!id_filter.empty() && !id_filter.contains(script->id())) {
|
|
continue;
|
|
}
|
|
|
|
auto registered_script = CreateRegisteredContentScriptInfo(*script);
|
|
registered_script.persist_across_sessions =
|
|
persistent_script_ids.contains(script->id());
|
|
|
|
// Remove the internally used prefix from the `script`'s ID before
|
|
// returning.
|
|
registered_script.id = script->GetIDWithoutPrefix();
|
|
script_infos.push_back(std::move(registered_script));
|
|
}
|
|
|
|
return RespondNow(
|
|
ArgumentList(api::scripting::GetRegisteredContentScripts::Results::Create(
|
|
script_infos)));
|
|
}
|
|
|
|
ScriptingUnregisterContentScriptsFunction::
|
|
ScriptingUnregisterContentScriptsFunction() = default;
|
|
ScriptingUnregisterContentScriptsFunction::
|
|
~ScriptingUnregisterContentScriptsFunction() = default;
|
|
|
|
ExtensionFunction::ResponseAction
|
|
ScriptingUnregisterContentScriptsFunction::Run() {
|
|
auto params =
|
|
api::scripting::UnregisterContentScripts::Params::Create(args());
|
|
EXTENSION_FUNCTION_VALIDATE(params);
|
|
|
|
std::optional<api::scripting::ContentScriptFilter>& filter = params->filter;
|
|
std::optional<std::vector<std::string>> ids = std::nullopt;
|
|
// TODO(crbug.com/40216362): `ids` should have an empty list when filter ids
|
|
// is empty, instead of a nullopt. Otherwise, we are incorrectly removing all
|
|
// content scripts when ids is empty.
|
|
if (filter && filter->ids && !filter->ids->empty()) {
|
|
ids = std::move(filter->ids);
|
|
}
|
|
|
|
std::string error;
|
|
bool removal_triggered = scripting::RemoveScripts(
|
|
ids, UserScript::Source::kDynamicContentScript, browser_context(),
|
|
extension()->id(),
|
|
base::BindOnce(&ScriptingUnregisterContentScriptsFunction::
|
|
OnContentScriptsUnregistered,
|
|
this),
|
|
&error);
|
|
|
|
if (!removal_triggered) {
|
|
CHECK(!error.empty());
|
|
return RespondNow(Error(std::move(error)));
|
|
}
|
|
|
|
return RespondLater();
|
|
}
|
|
|
|
void ScriptingUnregisterContentScriptsFunction::OnContentScriptsUnregistered(
|
|
const std::optional<std::string>& error) {
|
|
if (error.has_value())
|
|
Respond(Error(std::move(*error)));
|
|
else
|
|
Respond(NoArguments());
|
|
}
|
|
|
|
ScriptingUpdateContentScriptsFunction::ScriptingUpdateContentScriptsFunction() =
|
|
default;
|
|
ScriptingUpdateContentScriptsFunction::
|
|
~ScriptingUpdateContentScriptsFunction() = default;
|
|
|
|
ExtensionFunction::ResponseAction ScriptingUpdateContentScriptsFunction::Run() {
|
|
std::optional<api::scripting::UpdateContentScripts::Params> params =
|
|
api::scripting::UpdateContentScripts::Params::Create(args());
|
|
EXTENSION_FUNCTION_VALIDATE(params);
|
|
|
|
std::vector<api::scripting::RegisteredContentScript>& scripts_to_update =
|
|
params->scripts;
|
|
std::string error;
|
|
|
|
// Add the prefix for dynamic content scripts onto the IDs of all
|
|
// `scripts_to_update` before continuing.
|
|
std::set<std::string> ids_to_update = scripting::CreateDynamicScriptIds(
|
|
scripts_to_update, UserScript::Source::kDynamicContentScript,
|
|
std::set<std::string>(), &error);
|
|
|
|
if (!error.empty()) {
|
|
CHECK(ids_to_update.empty());
|
|
return RespondNow(Error(std::move(error)));
|
|
}
|
|
|
|
ExtensionUserScriptLoader* loader =
|
|
ExtensionSystem::Get(browser_context())
|
|
->user_script_manager()
|
|
->GetUserScriptLoaderForExtension(extension()->id());
|
|
|
|
std::set<std::string> updated_script_ids_to_persist;
|
|
UserScriptList parsed_scripts = scripting::UpdateScripts(
|
|
scripts_to_update, UserScript::Source::kDynamicContentScript, *loader,
|
|
base::BindRepeating(&CreateRegisteredContentScriptInfo),
|
|
base::BindRepeating(&ScriptingUpdateContentScriptsFunction::ApplyUpdate,
|
|
this, &updated_script_ids_to_persist),
|
|
&error);
|
|
|
|
if (!error.empty()) {
|
|
CHECK(parsed_scripts.empty());
|
|
return RespondNow(Error(std::move(error)));
|
|
}
|
|
|
|
// Add new script IDs now in case another call with the same script IDs is
|
|
// made immediately following this one.
|
|
loader->AddPendingDynamicScriptIDs(std::move(ids_to_update));
|
|
|
|
GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult(
|
|
FROM_HERE,
|
|
base::BindOnce(&scripting::ValidateParsedScriptsOnFileThread,
|
|
script_parsing::GetSymlinkPolicy(extension()),
|
|
std::move(parsed_scripts)),
|
|
base::BindOnce(
|
|
&ScriptingUpdateContentScriptsFunction::OnContentScriptFilesValidated,
|
|
this, std::move(updated_script_ids_to_persist)));
|
|
|
|
// Balanced in `OnContentScriptFilesValidated()` or
|
|
// `OnContentScriptsRegistered()`.
|
|
AddRef();
|
|
return RespondLater();
|
|
}
|
|
|
|
std::unique_ptr<UserScript> ScriptingUpdateContentScriptsFunction::ApplyUpdate(
|
|
std::set<std::string>* script_ids_to_persist,
|
|
api::scripting::RegisteredContentScript& new_script,
|
|
api::scripting::RegisteredContentScript& original_script,
|
|
std::u16string* parse_error) {
|
|
if (new_script.matches) {
|
|
original_script.matches = std::move(new_script.matches);
|
|
}
|
|
|
|
if (new_script.exclude_matches) {
|
|
original_script.exclude_matches = std::move(new_script.exclude_matches);
|
|
}
|
|
|
|
if (new_script.js) {
|
|
original_script.js = std::move(new_script.js);
|
|
}
|
|
|
|
if (new_script.css) {
|
|
original_script.css = std::move(new_script.css);
|
|
}
|
|
|
|
if (new_script.all_frames) {
|
|
*original_script.all_frames = *new_script.all_frames;
|
|
}
|
|
|
|
if (new_script.match_origin_as_fallback) {
|
|
*original_script.match_origin_as_fallback =
|
|
*new_script.match_origin_as_fallback;
|
|
}
|
|
|
|
if (new_script.run_at != api::extension_types::RunAt::kNone) {
|
|
original_script.run_at = new_script.run_at;
|
|
}
|
|
|
|
// Note: for the update application, we disregard allowed_in_incognito.
|
|
// We'll set it on the resulting scripts.
|
|
constexpr bool kAllowedInIncognito = false;
|
|
|
|
// Parse content script.
|
|
std::unique_ptr<UserScript> parsed_script =
|
|
ParseUserScript(browser_context(), *extension(), kAllowedInIncognito,
|
|
std::move(original_script), parse_error);
|
|
if (!parsed_script) {
|
|
return nullptr;
|
|
}
|
|
|
|
// 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 (new_script.persist_across_sessions.value_or(false) ||
|
|
(!new_script.persist_across_sessions &&
|
|
script_ids_to_persist->contains(new_script.id))) {
|
|
script_ids_to_persist->insert(new_script.id);
|
|
}
|
|
|
|
return parsed_script;
|
|
}
|
|
|
|
void ScriptingUpdateContentScriptsFunction::OnContentScriptFilesValidated(
|
|
std::set<std::string> persistent_script_ids,
|
|
scripting::ValidateScriptsResult result) {
|
|
// We cannot proceed if the `browser_context` is not valid as the
|
|
// `ExtensionSystem` will not exist.
|
|
if (!browser_context()) {
|
|
Release(); // Matches the `AddRef()` in `Run()`.
|
|
return;
|
|
}
|
|
|
|
// We cannot proceed if the extension is uninstalled or unloaded in the middle
|
|
// of validating its script files.
|
|
ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context());
|
|
if (!extension() ||
|
|
!registry->enabled_extensions().Contains(extension_id())) {
|
|
// Note: a Respond() is not needed if the system is shutting down or if the
|
|
// extension is no longer enabled.
|
|
Release(); // Matches the `AddRef()` in `Run()`.
|
|
return;
|
|
}
|
|
|
|
auto error = std::move(result.second);
|
|
auto scripts = std::move(result.first);
|
|
ExtensionUserScriptLoader* loader =
|
|
ExtensionSystem::Get(browser_context())
|
|
->user_script_manager()
|
|
->GetUserScriptLoaderForExtension(extension()->id());
|
|
|
|
bool allowed_in_incognito = scripting::ScriptsShouldBeAllowedInIncognito(
|
|
extension()->id(), browser_context());
|
|
|
|
std::set<std::string> script_ids;
|
|
for (const auto& script : scripts) {
|
|
script_ids.insert(script->id());
|
|
|
|
script->set_incognito_enabled(allowed_in_incognito);
|
|
}
|
|
|
|
if (error.has_value()) {
|
|
loader->RemovePendingDynamicScriptIDs(script_ids);
|
|
Respond(Error(std::move(*error)));
|
|
Release(); // Matches the `AddRef()` in `Run()`.
|
|
return;
|
|
}
|
|
|
|
loader->UpdateDynamicScripts(
|
|
std::move(scripts), std::move(script_ids),
|
|
std::move(persistent_script_ids),
|
|
base::BindOnce(
|
|
&ScriptingUpdateContentScriptsFunction::OnContentScriptsUpdated,
|
|
this));
|
|
}
|
|
|
|
void ScriptingUpdateContentScriptsFunction::OnContentScriptsUpdated(
|
|
const std::optional<std::string>& error) {
|
|
if (error.has_value())
|
|
Respond(Error(std::move(*error)));
|
|
else
|
|
Respond(NoArguments());
|
|
Release(); // Matches the `AddRef()` in `Run()`.
|
|
}
|
|
|
|
} // namespace extensions
|