[Extensions] Add main world injections for dynamic content scripts
This CL adds a new field "world" for dynamic content scripts which
allows the extension to specify if the script will run in the isolated
or main world. By default, scripts which do not specify this field will
run in the isolated world.
Bug: 1207006
Change-Id: Ie28bf1cb7c6d7c90cf74d7ead4554ee04810c652
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3573464
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Commit-Queue: Kelvin Jiang <kelvinjiang@chromium.org>
Cr-Commit-Position: refs/heads/main@{#992318}
diff --git a/chrome/browser/extensions/api/scripting/scripting_api.cc b/chrome/browser/extensions/api/scripting/scripting_api.cc
index 7a1c26403..08ba987 100644
--- a/chrome/browser/extensions/api/scripting/scripting_api.cc
+++ b/chrome/browser/extensions/api/scripting/scripting_api.cc
@@ -74,6 +74,33 @@
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;
+ }
+
+ 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(),
@@ -463,6 +490,7 @@
result->set_incognito_enabled(
util::IsIncognitoEnabled(extension.id(), browser_context));
+ result->set_execution_world(ConvertExecutionWorld(content_script.world));
return result;
}
@@ -522,6 +550,7 @@
std::make_unique<bool>(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;
}
@@ -638,14 +667,8 @@
return false;
}
- mojom::ExecutionWorld execution_world = mojom::ExecutionWorld::kIsolated;
- switch (injection_.world) {
- case api::scripting::EXECUTION_WORLD_NONE:
- case api::scripting::EXECUTION_WORLD_ISOLATED:
- break; // mojom::ExecutionWorld::kIsolated is correct.
- case api::scripting::EXECUTION_WORLD_MAIN:
- execution_world = mojom::ExecutionWorld::kMain;
- }
+ 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
diff --git a/chrome/browser/extensions/api/scripting/scripting_apitest.cc b/chrome/browser/extensions/api/scripting/scripting_apitest.cc
index dd875f6..04b80d4a 100644
--- a/chrome/browser/extensions/api/scripting/scripting_apitest.cc
+++ b/chrome/browser/extensions/api/scripting/scripting_apitest.cc
@@ -144,6 +144,11 @@
<< message_;
}
+IN_PROC_BROWSER_TEST_F(ScriptingAPITest, DynamicContentScriptsMainWorld) {
+ ASSERT_TRUE(RunExtensionTest("scripting/dynamic_scripts_main_world"))
+ << message_;
+}
+
// Test that if an extension with persistent scripts is quickly unloaded while
// these scripts are being fetched, requests that wait on that extension's
// script load will be unblocked. Regression for crbug.com/1250575
diff --git a/chrome/common/extensions/api/scripting.idl b/chrome/common/extensions/api/scripting.idl
index cba7551..d87a872 100644
--- a/chrome/common/extensions/api/scripting.idl
+++ b/chrome/common/extensions/api/scripting.idl
@@ -150,6 +150,9 @@
// 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
diff --git a/chrome/test/data/extensions/api_test/scripting/dynamic_scripts/worker.js b/chrome/test/data/extensions/api_test/scripting/dynamic_scripts/worker.js
index c2a2683..9ab3806 100644
--- a/chrome/test/data/extensions/api_test/scripting/dynamic_scripts/worker.js
+++ b/chrome/test/data/extensions/api_test/scripting/dynamic_scripts/worker.js
@@ -31,7 +31,8 @@
matches: ['*://asdfasdf.com/*'],
js: ['/dynamic_1.js'],
runAt: 'document_end',
- persistAcrossSessions: false
+ persistAcrossSessions: false,
+ world: chrome.scripting.ExecutionWorld.MAIN
}
];
@@ -46,7 +47,8 @@
allFrames: true,
runAt: 'document_idle',
matchOriginAsFallback: false,
- persistAcrossSessions: true
+ persistAcrossSessions: true,
+ world: chrome.scripting.ExecutionWorld.ISOLATED
},
{
id: 'GRS_2',
@@ -55,7 +57,8 @@
allFrames: false,
runAt: 'document_end',
matchOriginAsFallback: false,
- persistAcrossSessions: false
+ persistAcrossSessions: false,
+ world: chrome.scripting.ExecutionWorld.MAIN
}
];
@@ -482,7 +485,8 @@
runAt: 'document_end',
allFrames: false,
matchOriginAsFallback: false,
- persistAcrossSessions: false
+ persistAcrossSessions: false,
+ world: chrome.scripting.ExecutionWorld.ISOLATED
}];
scripts = await chrome.scripting.getRegisteredContentScripts();
@@ -525,7 +529,8 @@
runAt: 'document_end',
allFrames: false,
matchOriginAsFallback: false,
- persistAcrossSessions: true
+ persistAcrossSessions: true,
+ world: chrome.scripting.ExecutionWorld.ISOLATED
}];
scripts = await chrome.scripting.getRegisteredContentScripts();
@@ -575,7 +580,8 @@
runAt: 'document_end',
allFrames: false,
matchOriginAsFallback: false,
- persistAcrossSessions: true
+ persistAcrossSessions: true,
+ world: chrome.scripting.ExecutionWorld.ISOLATED
}];
scripts = await chrome.scripting.getRegisteredContentScripts();
@@ -615,7 +621,8 @@
runAt: 'document_end',
allFrames: false,
matchOriginAsFallback: false,
- persistAcrossSessions: true
+ persistAcrossSessions: true,
+ world: chrome.scripting.ExecutionWorld.ISOLATED
}];
scripts = await chrome.scripting.getRegisteredContentScripts();
diff --git a/chrome/test/data/extensions/api_test/scripting/dynamic_scripts_main_world/change_title.js b/chrome/test/data/extensions/api_test/scripting/dynamic_scripts_main_world/change_title.js
new file mode 100644
index 0000000..1ffcdd3
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/scripting/dynamic_scripts_main_world/change_title.js
@@ -0,0 +1,8 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Changes the document's title based on the existence/value of
+// window.mainWorldFlag, which is set by a script that's part of a web page.
+document.title = window.mainWorldFlag === 'from main world' ? 'MAIN_WORLD' :
+ 'ISOLATED_WORLD';
diff --git a/chrome/test/data/extensions/api_test/scripting/dynamic_scripts_main_world/manifest.json b/chrome/test/data/extensions/api_test/scripting/dynamic_scripts_main_world/manifest.json
new file mode 100644
index 0000000..83a866f
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/scripting/dynamic_scripts_main_world/manifest.json
@@ -0,0 +1,12 @@
+{
+ "manifest_version": 3,
+ "name": "Main world dynamic content script test extension",
+ "version": "0.1",
+ "description": "Tests that dynamic scripts can be injected into the main world",
+ "background": {
+ "service_worker": "worker.js",
+ "type": "module"
+ },
+ "permissions": ["scripting", "tabs"],
+ "host_permissions": ["*://hostperms.com/*"]
+}
diff --git a/chrome/test/data/extensions/api_test/scripting/dynamic_scripts_main_world/worker.js b/chrome/test/data/extensions/api_test/scripting/dynamic_scripts_main_world/worker.js
new file mode 100644
index 0000000..1109d14b
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/scripting/dynamic_scripts_main_world/worker.js
@@ -0,0 +1,45 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {openTab} from '/_test_resources/test_util/tabs_util.js';
+
+// Inject a script which changes the page's title based on the execution world
+// it's running on, then call executeScript which checks the title.
+async function runTest(world, expectedTitle) {
+ await chrome.scripting.unregisterContentScripts();
+ var scripts = [{
+ id: 'script1',
+ matches: ['*://hostperms.com/*'],
+ js: ['change_title.js'],
+ world,
+ runAt: 'document_end',
+ }];
+
+ await chrome.scripting.registerContentScripts(scripts);
+ const config = await chrome.test.getConfig();
+
+ // After the scripts has been registered, navigate to a url where they will be
+ // injected.
+ const url = `http://hostperms.com:${
+ config.testServer.port}/extensions/main_world_script_flag.html`;
+ let tab = await openTab(url);
+ let results = await chrome.scripting.executeScript({
+ target: {tabId: tab.id},
+ func: () => document.title,
+ });
+
+ chrome.test.assertEq(1, results.length);
+ chrome.test.assertEq(expectedTitle, results[0].result);
+ chrome.test.succeed();
+}
+
+chrome.test.runTests([
+ async function mainWorld() {
+ runTest(chrome.scripting.ExecutionWorld.MAIN, 'MAIN_WORLD');
+ },
+
+ async function isolatedWorld() {
+ runTest(chrome.scripting.ExecutionWorld.ISOLATED, 'ISOLATED_WORLD');
+ },
+]);
diff --git a/chrome/test/data/extensions/api_test/scripting/persistent_dynamic_scripts/worker.js b/chrome/test/data/extensions/api_test/scripting/persistent_dynamic_scripts/worker.js
index 6b08127e..63877226 100644
--- a/chrome/test/data/extensions/api_test/scripting/persistent_dynamic_scripts/worker.js
+++ b/chrome/test/data/extensions/api_test/scripting/persistent_dynamic_scripts/worker.js
@@ -18,7 +18,8 @@
id: 'inject_element',
matches: ['*://*/*'],
js: ['inject_element.js'],
- runAt: 'document_end'
+ runAt: 'document_end',
+ world: chrome.scripting.ExecutionWorld.MAIN
},
{
id: 'inject_element_2',
@@ -57,7 +58,8 @@
allFrames: false,
runAt: 'document_end',
matchOriginAsFallback: false,
- persistAcrossSessions: true
+ persistAcrossSessions: true,
+ world: chrome.scripting.ExecutionWorld.MAIN
}];
chrome.test.assertEq(expectedScripts, scripts);
@@ -109,7 +111,8 @@
allFrames: false,
runAt: 'document_end',
matchOriginAsFallback: false,
- persistAcrossSessions: true
+ persistAcrossSessions: true,
+ world: chrome.scripting.ExecutionWorld.ISOLATED
}];
chrome.test.assertEq(expectedScripts, scripts);
diff --git a/extensions/browser/api/extension_types_utils.cc b/extensions/browser/api/extension_types_utils.cc
index c96a04aa..d5795f90 100644
--- a/extensions/browser/api/extension_types_utils.cc
+++ b/extensions/browser/api/extension_types_utils.cc
@@ -42,4 +42,31 @@
return api::extension_types::RUN_AT_DOCUMENT_IDLE;
}
+mojom::ExecutionWorld ConvertExecutionWorld(
+ api::extension_types::ExecutionWorld world) {
+ mojom::ExecutionWorld execution_world = mojom::ExecutionWorld::kIsolated;
+ switch (world) {
+ case api::extension_types::EXECUTION_WORLD_NONE:
+ case api::extension_types::EXECUTION_WORLD_ISOLATED:
+ break; // Default to mojom::ExecutionWorld::kIsolated.
+ case api::extension_types::EXECUTION_WORLD_MAIN:
+ execution_world = mojom::ExecutionWorld::kMain;
+ }
+
+ return execution_world;
+}
+
+api::extension_types::ExecutionWorld ConvertExecutionWorldForAPI(
+ mojom::ExecutionWorld world) {
+ switch (world) {
+ case mojom::ExecutionWorld::kIsolated:
+ return api::extension_types::EXECUTION_WORLD_ISOLATED;
+ case mojom::ExecutionWorld::kMain:
+ return api::extension_types::EXECUTION_WORLD_MAIN;
+ }
+
+ NOTREACHED();
+ return api::extension_types::EXECUTION_WORLD_ISOLATED;
+}
+
} // namespace extensions
diff --git a/extensions/browser/api/extension_types_utils.h b/extensions/browser/api/extension_types_utils.h
index 0cd1177..c4fb739 100644
--- a/extensions/browser/api/extension_types_utils.h
+++ b/extensions/browser/api/extension_types_utils.h
@@ -6,6 +6,7 @@
#define EXTENSIONS_BROWSER_API_EXTENSION_TYPES_UTILS_H_
#include "extensions/common/api/extension_types.h"
+#include "extensions/common/mojom/execution_world.mojom-shared.h"
#include "extensions/common/mojom/run_location.mojom-shared.h"
// Contains helper methods for converting from extension_types
@@ -18,6 +19,12 @@
// Converts mojom::RunLocation to api::extension_types::RunAt.
api::extension_types::RunAt ConvertRunLocationForAPI(mojom::RunLocation run_at);
+mojom::ExecutionWorld ConvertExecutionWorld(
+ api::extension_types::ExecutionWorld world);
+
+api::extension_types::ExecutionWorld ConvertExecutionWorldForAPI(
+ mojom::ExecutionWorld world);
+
} // namespace extensions
#endif // EXTENSIONS_BROWSER_API_EXTENSION_TYPES_UTILS_H_
diff --git a/extensions/browser/extension_user_script_loader.cc b/extensions/browser/extension_user_script_loader.cc
index 1742cb4..9a53ace 100644
--- a/extensions/browser/extension_user_script_loader.cc
+++ b/extensions/browser/extension_user_script_loader.cc
@@ -30,6 +30,7 @@
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/render_process_host.h"
+#include "extensions/browser/api/extension_types_utils.h"
#include "extensions/browser/api/scripting/scripting_constants.h"
#include "extensions/browser/api/scripting/scripting_utils.h"
#include "extensions/browser/component_extension_resource_manager.h"
@@ -264,6 +265,7 @@
script->set_match_all_frames(*content_script->all_frames);
script->set_run_location(
script_parsing::ConvertManifestRunLocation(content_script->run_at));
+ script->set_execution_world(ConvertExecutionWorld(content_script->world));
if (!script_parsing::ParseMatchPatterns(
content_script->matches, content_script->exclude_matches.get(),
@@ -329,6 +331,7 @@
content_script.run_at =
script_parsing::ConvertRunLocationToManifestType(script.run_location());
+ content_script.world = ConvertExecutionWorldForAPI(script.execution_world());
return content_script;
}
diff --git a/extensions/common/api/content_scripts.idl b/extensions/common/api/content_scripts.idl
index ebc4ba35..73633686 100644
--- a/extensions/common/api/content_scripts.idl
+++ b/extensions/common/api/content_scripts.idl
@@ -75,6 +75,11 @@
// Specifies when JavaScript files are injected into the web page. The
// preferred and default value is <code>document_idle</code>.
RunAt? run_at;
+ // Describes the JavaScript world that this script will execute in.
+ // Currently manifest scripts will always run in the isolated world and this
+ // field should not be specified. Eventually, main world support may be
+ // added.
+ [nodoc] extensionTypes.ExecutionWorld? world;
};
dictionary ManifestKeys {
diff --git a/extensions/common/api/extension_types.json b/extensions/common/api/extension_types.json
index ceaff4a..0a297c4 100644
--- a/extensions/common/api/extension_types.json
+++ b/extensions/common/api/extension_types.json
@@ -114,6 +114,13 @@
"nodoc": true,
"enum": ["prerender", "active", "cached", "pending_deletion"],
"description": "The document lifecycle of the frame."
+ },
+ {
+ "id": "ExecutionWorld",
+ "type": "string",
+ "nodoc": true,
+ "enum": ["ISOLATED", "MAIN"],
+ "description": "The JavaScript world for a script to execute within. Can either be an isolated world, unique to this extension, or the main world of the DOM which is shared with the page's JavaScript."
}
]
}
diff --git a/extensions/common/user_script.cc b/extensions/common/user_script.cc
index 0c2287a..a5b5b4f 100644
--- a/extensions/common/user_script.cc
+++ b/extensions/common/user_script.cc
@@ -134,6 +134,7 @@
script->match_all_frames_ = other.match_all_frames_;
script->match_origin_as_fallback_ = other.match_origin_as_fallback_;
script->incognito_enabled_ = other.incognito_enabled_;
+ script->execution_world_ = other.execution_world_;
return script;
}
@@ -200,6 +201,7 @@
pickle->WriteBool(match_all_frames());
pickle->WriteInt(static_cast<int>(match_origin_as_fallback()));
pickle->WriteBool(is_incognito_enabled());
+ pickle->WriteInt(static_cast<int>(execution_world()));
PickleHostID(pickle, host_id_);
pickle->WriteInt(consumer_instance_type());
@@ -260,6 +262,13 @@
static_cast<MatchOriginAsFallbackBehavior>(match_origin_as_fallback_int);
CHECK(iter->ReadBool(&incognito_enabled_));
+ // Read the execution world.
+ int execution_world = 0;
+ CHECK(iter->ReadInt(&execution_world));
+ CHECK(execution_world >= static_cast<int>(mojom::ExecutionWorld::kIsolated) &&
+ execution_world <= static_cast<int>(mojom::ExecutionWorld::kMaxValue));
+ execution_world_ = static_cast<mojom::ExecutionWorld>(execution_world);
+
UnpickleHostID(pickle, iter, &host_id_);
int consumer_instance_type = 0;
diff --git a/extensions/common/user_script.h b/extensions/common/user_script.h
index 9028cfb..288d3948 100644
--- a/extensions/common/user_script.h
+++ b/extensions/common/user_script.h
@@ -11,6 +11,7 @@
#include "base/files/file_path.h"
#include "base/strings/string_piece.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/script_constants.h"
@@ -212,6 +213,11 @@
bool is_incognito_enabled() const { return incognito_enabled_; }
void set_incognito_enabled(bool enabled) { incognito_enabled_ = enabled; }
+ mojom::ExecutionWorld execution_world() const { return execution_world_; }
+ void set_execution_world(mojom::ExecutionWorld world) {
+ execution_world_ = world;
+ }
+
// Returns true if the script should be applied to the specified URL, false
// otherwise.
bool MatchesURL(const GURL& url) const;
@@ -318,6 +324,8 @@
// True if the script should be injected into an incognito tab.
bool incognito_enabled_ = false;
+
+ mojom::ExecutionWorld execution_world_ = mojom::ExecutionWorld::kIsolated;
};
using UserScriptList = std::vector<std::unique_ptr<UserScript>>;
diff --git a/extensions/renderer/user_script_injector.cc b/extensions/renderer/user_script_injector.cc
index d81f1e8..c689193a 100644
--- a/extensions/renderer/user_script_injector.cc
+++ b/extensions/renderer/user_script_injector.cc
@@ -144,7 +144,7 @@
}
mojom::ExecutionWorld UserScriptInjector::GetExecutionWorld() const {
- return mojom::ExecutionWorld::kIsolated;
+ return script_->execution_world();
}
bool UserScriptInjector::ExpectsResults() const {
diff --git a/third_party/closure_compiler/externs/extension_types.js b/third_party/closure_compiler/externs/extension_types.js
index ce8acf23..f4ce2e6 100644
--- a/third_party/closure_compiler/externs/extension_types.js
+++ b/third_party/closure_compiler/externs/extension_types.js
@@ -1,4 +1,4 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
+// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@@ -7,7 +7,8 @@
// NOTE: The format of types has changed. 'FooType' is now
// 'chrome.extensionTypes.FooType'.
// Please run the closure compiler before committing changes.
-// See https://chromium.googlesource.com/chromium/src/+/master/docs/closure_compilation.md
+// See
+// https://chromium.googlesource.com/chromium/src/+/main/docs/closure_compilation.md
/** @fileoverview Externs generated from namespace: extensionTypes */
@@ -66,3 +67,48 @@
* @see https://developer.chrome.com/extensions/extensionTypes#type-InjectDetails
*/
chrome.extensionTypes.InjectDetails;
+
+/**
+ * Details of the CSS to remove. Either the code or the file property must be
+ * set, but both may not be set at the same time.
+ * @typedef {{
+ * code: (string|undefined),
+ * file: (string|undefined),
+ * allFrames: (boolean|undefined),
+ * frameId: (number|undefined),
+ * matchAboutBlank: (boolean|undefined),
+ * cssOrigin: (!chrome.extensionTypes.CSSOrigin|undefined)
+ * }}
+ * @see https://developer.chrome.com/extensions/extensionTypes#type-DeleteInjectionDetails
+ */
+chrome.extensionTypes.DeleteInjectionDetails;
+
+/**
+ * @enum {string}
+ * @see https://developer.chrome.com/extensions/extensionTypes#type-FrameType
+ */
+chrome.extensionTypes.FrameType = {
+ OUTERMOST_FRAME: 'outermost_frame',
+ FENCED_FRAME: 'fenced_frame',
+ SUB_FRAME: 'sub_frame',
+};
+
+/**
+ * @enum {string}
+ * @see https://developer.chrome.com/extensions/extensionTypes#type-DocumentLifecycle
+ */
+chrome.extensionTypes.DocumentLifecycle = {
+ PRERENDER: 'prerender',
+ ACTIVE: 'active',
+ CACHED: 'cached',
+ PENDING_DELETION: 'pending_deletion',
+};
+
+/**
+ * @enum {string}
+ * @see https://developer.chrome.com/extensions/extensionTypes#type-ExecutionWorld
+ */
+chrome.extensionTypes.ExecutionWorld = {
+ ISOLATED: 'ISOLATED',
+ MAIN: 'MAIN',
+};