Skip to content

Commit

Permalink
[Extensions] Add main world injections for dynamic content scripts
Browse files Browse the repository at this point in the history
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}
  • Loading branch information
Celsius273 authored and Chromium LUCI CQ committed Apr 14, 2022
1 parent 3a08137 commit e5ad345
Show file tree
Hide file tree
Showing 17 changed files with 239 additions and 21 deletions.
39 changes: 31 additions & 8 deletions chrome/browser/extensions/api/scripting/scripting_api.cc
Expand Up @@ -74,6 +74,33 @@ mojom::CSSOrigin ConvertStyleOriginToCSSOrigin(
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(),
Expand Down Expand Up @@ -463,6 +490,7 @@ std::unique_ptr<UserScript> ParseUserScript(

result->set_incognito_enabled(
util::IsIncognitoEnabled(extension.id(), browser_context));
result->set_execution_world(ConvertExecutionWorld(content_script.world));
return result;
}

Expand Down Expand Up @@ -522,6 +550,7 @@ api::scripting::RegisteredContentScript CreateRegisteredContentScriptInfo(
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;
}
Expand Down Expand Up @@ -638,14 +667,8 @@ bool ScriptingExecuteScriptFunction::Execute(
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
Expand Down
5 changes: 5 additions & 0 deletions chrome/browser/extensions/api/scripting/scripting_apitest.cc
Expand Up @@ -144,6 +144,11 @@ IN_PROC_BROWSER_TEST_F(ScriptingAPITest, DynamicContentScriptParameters) {
<< 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
Expand Down
3 changes: 3 additions & 0 deletions chrome/common/extensions/api/scripting.idl
Expand Up @@ -150,6 +150,9 @@ namespace scripting {
// 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
Expand Down
Expand Up @@ -31,7 +31,8 @@ chrome.test.runTests([
matches: ['*://asdfasdf.com/*'],
js: ['/dynamic_1.js'],
runAt: 'document_end',
persistAcrossSessions: false
persistAcrossSessions: false,
world: chrome.scripting.ExecutionWorld.MAIN
}
];

Expand All @@ -46,7 +47,8 @@ chrome.test.runTests([
allFrames: true,
runAt: 'document_idle',
matchOriginAsFallback: false,
persistAcrossSessions: true
persistAcrossSessions: true,
world: chrome.scripting.ExecutionWorld.ISOLATED
},
{
id: 'GRS_2',
Expand All @@ -55,7 +57,8 @@ chrome.test.runTests([
allFrames: false,
runAt: 'document_end',
matchOriginAsFallback: false,
persistAcrossSessions: false
persistAcrossSessions: false,
world: chrome.scripting.ExecutionWorld.MAIN
}
];

Expand Down Expand Up @@ -482,7 +485,8 @@ chrome.test.runTests([
runAt: 'document_end',
allFrames: false,
matchOriginAsFallback: false,
persistAcrossSessions: false
persistAcrossSessions: false,
world: chrome.scripting.ExecutionWorld.ISOLATED
}];

scripts = await chrome.scripting.getRegisteredContentScripts();
Expand Down Expand Up @@ -525,7 +529,8 @@ chrome.test.runTests([
runAt: 'document_end',
allFrames: false,
matchOriginAsFallback: false,
persistAcrossSessions: true
persistAcrossSessions: true,
world: chrome.scripting.ExecutionWorld.ISOLATED
}];

scripts = await chrome.scripting.getRegisteredContentScripts();
Expand Down Expand Up @@ -575,7 +580,8 @@ chrome.test.runTests([
runAt: 'document_end',
allFrames: false,
matchOriginAsFallback: false,
persistAcrossSessions: true
persistAcrossSessions: true,
world: chrome.scripting.ExecutionWorld.ISOLATED
}];

scripts = await chrome.scripting.getRegisteredContentScripts();
Expand Down Expand Up @@ -615,7 +621,8 @@ chrome.test.runTests([
runAt: 'document_end',
allFrames: false,
matchOriginAsFallback: false,
persistAcrossSessions: true
persistAcrossSessions: true,
world: chrome.scripting.ExecutionWorld.ISOLATED
}];

scripts = await chrome.scripting.getRegisteredContentScripts();
Expand Down
@@ -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';
@@ -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/*"]
}
@@ -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');
},
]);
Expand Up @@ -18,7 +18,8 @@ async function runFirstSession() {
id: 'inject_element',
matches: ['*://*/*'],
js: ['inject_element.js'],
runAt: 'document_end'
runAt: 'document_end',
world: chrome.scripting.ExecutionWorld.MAIN
},
{
id: 'inject_element_2',
Expand Down Expand Up @@ -57,7 +58,8 @@ async function runSecondSession() {
allFrames: false,
runAt: 'document_end',
matchOriginAsFallback: false,
persistAcrossSessions: true
persistAcrossSessions: true,
world: chrome.scripting.ExecutionWorld.MAIN
}];

chrome.test.assertEq(expectedScripts, scripts);
Expand Down Expand Up @@ -109,7 +111,8 @@ async function runThirdSession() {
allFrames: false,
runAt: 'document_end',
matchOriginAsFallback: false,
persistAcrossSessions: true
persistAcrossSessions: true,
world: chrome.scripting.ExecutionWorld.ISOLATED
}];

chrome.test.assertEq(expectedScripts, scripts);
Expand Down
27 changes: 27 additions & 0 deletions extensions/browser/api/extension_types_utils.cc
Expand Up @@ -42,4 +42,31 @@ api::extension_types::RunAt ConvertRunLocationForAPI(
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
7 changes: 7 additions & 0 deletions extensions/browser/api/extension_types_utils.h
Expand Up @@ -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
Expand All @@ -18,6 +19,12 @@ mojom::RunLocation ConvertRunLocation(api::extension_types::RunAt run_at);
// 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_
3 changes: 3 additions & 0 deletions extensions/browser/extension_user_script_loader.cc
Expand Up @@ -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"
Expand Down Expand Up @@ -264,6 +265,7 @@ UserScriptList ConvertValueToScripts(const Extension& extension,
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(),
Expand Down Expand Up @@ -329,6 +331,7 @@ api::content_scripts::ContentScript CreateContentScriptObject(

content_script.run_at =
script_parsing::ConvertRunLocationToManifestType(script.run_location());
content_script.world = ConvertExecutionWorldForAPI(script.execution_world());
return content_script;
}

Expand Down
5 changes: 5 additions & 0 deletions extensions/common/api/content_scripts.idl
Expand Up @@ -75,6 +75,11 @@ namespace contentScripts {
// 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 {
Expand Down
7 changes: 7 additions & 0 deletions extensions/common/api/extension_types.json
Expand Up @@ -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."
}
]
}
Expand Down
9 changes: 9 additions & 0 deletions extensions/common/user_script.cc
Expand Up @@ -134,6 +134,7 @@ std::unique_ptr<UserScript> UserScript::CopyMetadataFrom(
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;
}
Expand Down Expand Up @@ -200,6 +201,7 @@ void UserScript::Pickle(base::Pickle* pickle) const {
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());
Expand Down Expand Up @@ -260,6 +262,13 @@ void UserScript::Unpickle(const base::Pickle& pickle,
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;
Expand Down

0 comments on commit e5ad345

Please sign in to comment.