Bug 1911835 - Accept USER_SCRIPT as world value in internals r=zombie

This patch adds the USER_SCRIPT value as a supported value in the
internal WebExtensionContentScript constructor.

This patch does not introduce a distinct USER_SCRIPT sandbox yet; that
will be done in the next patch.

Differential Revision: https://phabricator.services.mozilla.com/D228973
This commit is contained in:
Rob Wu 2024-11-20 16:50:14 +00:00
parent 2ce8ff0bb2
commit c6451a49d3
5 changed files with 138 additions and 7 deletions

View File

@ -165,12 +165,21 @@ enum ContentScriptExecutionWorld {
* The name refers to "isolated world", which is a concept from Chromium and
* WebKit, used to enforce isolation of the JavaScript execution environments
* of content scripts and web pages.
*
* Not supported when isUserScript=true.
*/
"ISOLATED",
/**
* The execution environment of the web page.
*/
"MAIN",
/**
* The execution environment of a sandbox running scripts registered through
* the MV3 userScripts API.
*
* Only supported when isUserScript=true.
*/
"USER_SCRIPT",
};
[ChromeOnly, Exposed=Window]

View File

@ -771,6 +771,12 @@ WebExtensionContentScript::WebExtensionContentScript(
if (mExtension->ManifestVersion() >= 3) {
mCheckPermissions = true;
}
// The USER_SCRIPT world is not supported for regular content scripts.
MOZ_ASSERT_IF(!mIsUserScript,
mWorld != ContentScriptExecutionWorld::USER_SCRIPT);
// User scripts should never run in privileged content script worlds.
MOZ_ASSERT_IF(mIsUserScript, mWorld != ContentScriptExecutionWorld::ISOLATED);
}
bool MozDocumentMatcher::Matches(const DocInfo& aDoc,

View File

@ -129,6 +129,11 @@ add_task(async function test_WebExtensionContentScript_isUserScript() {
localizeCallback() {},
});
// WebExtensionContentScript defaults to world "ISOLATED", but for user
// scripts only "MAIN" and "USER_SCRIPT" worlds are permitted. "MAIN" is
// supported by user scripts and non-userScripts, so use that here.
const world = "MAIN";
const matches = new MatchPatternSet(["https://example.com/match/*"]);
const includeGlobs = [new MatchGlob("*/glob/*")];
const exampleMatchesURI = newURI("https://example.com/match/");
@ -137,16 +142,19 @@ add_task(async function test_WebExtensionContentScript_isUserScript() {
const exampleNoPermissionURI = newURI("https://example.net/glob/");
let defaultScript = new WebExtensionContentScript(policy, {
world,
matches,
includeGlobs,
});
let nonUserScript = new WebExtensionContentScript(policy, {
isUserScript: false,
world,
matches,
includeGlobs,
});
let userScript = new WebExtensionContentScript(policy, {
isUserScript: true,
world,
matches,
includeGlobs,
});
@ -193,11 +201,13 @@ add_task(async function test_WebExtensionContentScript_isUserScript() {
// Now verify that empty matches is permitted.
let nonUserScriptEmptyMatches = new WebExtensionContentScript(policy, {
isUserScript: false,
world,
matches: [],
includeGlobs,
});
let userScriptEmptyMatches = new WebExtensionContentScript(policy, {
isUserScript: true,
world,
matches: [],
includeGlobs,
});
@ -223,6 +233,7 @@ add_task(async function test_WebExtensionContentScript_isUserScript() {
// we just do a sanity check for isUserScript=true.
let userScriptNoGlobs = new WebExtensionContentScript(policy, {
isUserScript: true,
world,
matches,
});
ok(

View File

@ -147,3 +147,111 @@ add_task(async function userScript_require_host_permissions() {
await extension.unload();
});
// Tests that user scripts can run in the USER_SCRIPT world, and only for new
// document loads (not existing ones).
add_task(async function userScript_runs_in_USER_SCRIPT_world() {
let extension = ExtensionTestUtils.loadExtension({
useAddonManager: "permanent",
manifest: {
manifest_version: 3,
permissions: ["userScripts"],
host_permissions: ["*://example.com/*"],
},
files: {
"1.file.js": `
var resultCollector_push = msg => {
// window.wrappedJSObject will be undefined if we unexpectedly run
// in the MAIN world instead of the "USER_SCRIPT" world.
window.wrappedJSObject.resultCollector.push(msg);
};
resultCollector_push("1.file");dump("1.file.js ran\\n");
`,
"3.file.js": "resultCollector_push('3.file');dump('3.file.js ran\\n');",
},
async background() {
async function register() {
await browser.userScripts.register([
{
id: "userScripts ID (for register and update)",
matches: ["*://example.com/resultCollector"],
js: [
{ file: "1.file.js" },
// 1.file.js defines the "resultCollector_push" function, and that
// function should be available to the other scripts since they all
// run in the same USER_SCRIPT world.
{ code: "resultCollector_push('2.code');dump('1.code ran\\n');" },
{ file: "3.file.js" },
{ code: "resultCollector_push('4.code');dump('4.code ran\\n');" },
],
runAt: "document_end",
world: "USER_SCRIPT",
},
]);
}
async function update() {
await browser.userScripts.update([
{
id: "userScripts ID (for register and update)",
js: [
{ file: "1.file.js" },
{ code: "resultCollector_push('2.updated');" },
],
},
]);
}
browser.test.onMessage.addListener(async msg => {
browser.test.assertEq("update_userScripts", msg, "Expected msg");
await update();
browser.test.sendMessage("update_userScripts:done");
});
await register();
browser.test.sendMessage("registered");
},
});
let contentPageBeforeExtStarted = await ExtensionTestUtils.loadContentPage(
"http://example.com/resultCollector"
);
await extension.startup();
await extension.awaitMessage("registered");
let contentPageAfterRegister = await ExtensionTestUtils.loadContentPage(
"http://example.com/resultCollector"
);
Assert.deepEqual(
await collectResults(contentPageAfterRegister),
["1.file", "2.code", "3.file", "4.code"],
"All USER_SCRIPT world scripts executed in a new page after registration"
);
Assert.deepEqual(
await collectResults(contentPageBeforeExtStarted),
[],
"User scripts did not execute in content that existed before registration"
);
// Now call userScripts.update() and check whether it injects.
extension.sendMessage("update_userScripts");
await extension.awaitMessage("update_userScripts:done");
// Reload - this is a new load after the userScripts.update() call.
await contentPageAfterRegister.loadURL("http://example.com/resultCollector");
Assert.deepEqual(
await collectResults(contentPageAfterRegister),
["1.file", "2.updated"],
"userScripts.update() applies new scripts to new documents"
);
Assert.deepEqual(
await collectResults(contentPageBeforeExtStarted),
[],
"userScripts.update() does not run code in existing documents"
);
await contentPageAfterRegister.close();
await contentPageBeforeExtStarted.close();
await extension.unload();
});

View File

@ -70,12 +70,6 @@ add_task(async function register_and_restart() {
id: "test1",
matches: ["https://example.com/*"],
js: [{ file: "file1.js" }, { file: "file2.js" }],
// We must set this to MAIN because the USER_SCRIPT world is not yet
// supported (bug 1911835), and using USER_SCRIPT would prevent the
// extension from initializing in the extension process after a restart
// due to the WebExtensionContentScript constructor not recognizing the
// USER_SCRIPT world value. TODO: delete this after fixing bug 1911835.
world: "MAIN",
};
const expectedScriptOut = {
...scriptIn,
@ -84,7 +78,10 @@ add_task(async function register_and_restart() {
includeGlobs: null,
excludeGlobs: null,
runAt: "document_idle",
// world: "USER_SCRIPT", // TODO 1911835: uncomment when supported.
// Notable call-out: All other content script APIs default to "ISOLATED"
// as the default world. In the userScripts API, we want to default to
// "USER_SCRIPT" when the world is not specified.
world: "USER_SCRIPT",
};
browser.runtime.onInstalled.addListener(async ({ reason }) => {
browser.test.assertEq("install", reason, "onInstalled reason");