Bug 1587541 - Make tab.executeScript, insertCSS, removeCSS Fission compatible r=rpl

Differential Revision: https://phabricator.services.mozilla.com/D81772
This commit is contained in:
Tomislav Jovanovic 2020-07-12 17:11:58 +00:00
parent f7a0736a34
commit 17939666b8
7 changed files with 146 additions and 80 deletions

View File

@ -35,6 +35,9 @@ add_task(async function testExecuteScript() {
currentWindow: true,
});
let frames = await browser.webNavigation.getAllFrames({ tabId: tab.id });
browser.test.assertEq(3, frames.length, "Expect exactly three frames");
browser.test.assertEq(0, frames[0].frameId, "Main frame has frameId:0");
browser.test.assertTrue(frames[1].frameId > 0, "Subframe has a valid id");
browser.test.log(
`FRAMES: ${frames[1].frameId} ${JSON.stringify(frames)}\n`
@ -351,7 +354,7 @@ add_task(async function testExecuteScript() {
browser.test.assertEq(1, result.length, "Expected one result");
browser.test.assertTrue(
/\/file_iframe_document\.html$/.test(result[0]),
`Result for frameId[0] is correct: ${result[0]}`
`Result for main frame (frameId:0) is correct: ${result[0]}`
);
}),

View File

@ -5,7 +5,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["ExtensionContent"];
var EXPORTED_SYMBOLS = ["ExtensionContent", "ExtensionContentChild"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
@ -1171,59 +1171,42 @@ var ExtensionContent = {
},
// Used to executeScript, insertCSS and removeCSS.
async handleExtensionExecute(global, target, options, script) {
let executeInWin = window => {
if (script.matchesWindow(window)) {
return script.injectInto(window);
async handleActorExecute({ options, windows }) {
let policy = WebExtensionPolicy.getByID(options.extensionId);
let matcher = new WebExtensionContentScript(policy, options);
Object.assign(matcher, {
wantReturnValue: options.wantReturnValue,
removeCSS: options.removeCSS,
cssOrigin: options.cssOrigin,
jsCode: options.jsCode,
});
let script = contentScripts.get(matcher);
// Add the cssCode to the script, so that it can be converted into a cached URL.
await script.addCSSCode(options.cssCode);
delete options.cssCode;
const executeInWin = innerId => {
let wg = WindowGlobalChild.getByInnerWindowId(innerId);
let bc = wg && !wg.isClosed && wg.isCurrentGlobal && wg.browsingContext;
if (bc && script.matchesWindow(bc.window)) {
return script.injectInto(bc.window);
}
return null;
};
let promises;
try {
promises = Array.from(
this.enumerateWindows(global.docShell),
executeInWin
).filter(promise => promise);
} catch (e) {
Cu.reportError(e);
return Promise.reject({ message: "An unexpected error occurred" });
}
if (!promises.length) {
if (options.frameID) {
return Promise.reject({
message: `Frame not found, or missing host permission`,
});
}
let frames = options.allFrames ? ", and any iframes" : "";
return Promise.reject({
message: `Missing host permission for the tab${frames}`,
});
}
if (!options.allFrames && promises.length > 1) {
return Promise.reject({
message: `Internal error: Script matched multiple windows`,
});
}
let result = await Promise.all(promises);
let all = Promise.all(windows.map(executeInWin).filter(p => p));
let result = await all.catch(e => Promise.reject({ message: e.message }));
try {
// Make sure we can structured-clone the result value before
// we try to send it back over the message manager.
Cu.cloneInto(result, target);
// Check if the result can be structured-cloned before sending back.
return Cu.cloneInto(result, this);
} catch (e) {
const { jsPaths } = options;
const fileName = jsPaths.length
? jsPaths[jsPaths.length - 1]
: "<anonymous code>";
const message = `Script '${fileName}' result is non-structured-clonable data`;
return Promise.reject({ message, fileName });
let path = options.jsPaths.slice(-1)[0] ?? "<anonymous code>";
let message = `Script '${path}' result is non-structured-clonable data`;
return Promise.reject({ message, fileName: path });
}
return result;
},
handleWebNavigationGetFrame(global, { frameId }) {
@ -1245,30 +1228,6 @@ var ExtensionContent = {
);
case "Extension:DetectLanguage":
return this.handleDetectLanguage(global, target);
case "Extension:Execute":
let policy = WebExtensionPolicy.getByID(recipient.extensionId);
let matcher = new WebExtensionContentScript(policy, data.options);
Object.assign(matcher, {
wantReturnValue: data.options.wantReturnValue,
removeCSS: data.options.removeCSS,
cssOrigin: data.options.cssOrigin,
jsCode: data.options.jsCode,
});
let script = contentScripts.get(matcher);
// Add the cssCode to the script, so that it can be converted into a cached URL.
await script.addCSSCode(data.options.cssCode);
delete data.options.cssCode;
return this.handleExtensionExecute(
global,
target,
data.options,
script
);
case "WebNavigation:GetFrame":
return this.handleWebNavigationGetFrame(global, data.options);
case "WebNavigation:GetAllFrames":
@ -1295,3 +1254,18 @@ var ExtensionContent = {
}
},
};
/**
* Child side of the ExtensionContent process actor, handles some tabs.* APIs.
*/
class ExtensionContentChild extends JSProcessActorChild {
receiveMessage({ name, data }) {
if (!isContentScriptProcess) {
return;
}
switch (name) {
case "Execute":
return ExtensionContent.handleActorExecute(data);
}
}
}

View File

@ -71,7 +71,6 @@ class ExtensionGlobal {
MessageChannel.addListener(global, "Extension:Capture", this);
MessageChannel.addListener(global, "Extension:DetectLanguage", this);
MessageChannel.addListener(global, "Extension:Execute", this);
MessageChannel.addListener(global, "WebNavigation:GetFrame", this);
MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this);
}
@ -104,11 +103,6 @@ class ExtensionGlobal {
}
return;
}
// Prevent script compilation in the parent process when we would never
// use them.
if (!isContentScriptProcess && messageName === "Extension:Execute") {
return;
}
// SetFrameData does not have a recipient extension, or it would be
// an extension process. Anything following this point must have

View File

@ -308,6 +308,15 @@ class TabBase {
throw new Error("Not implemented");
}
/**
* @property {BrowsingContext} browsingContext
* Returns the BrowsingContext for the given tab.
* @readonly
*/
get browsingContext() {
return this.browser?.browsingContext;
}
/**
* @property {FrameLoader} frameLoader
* Returns the frameloader for the given tab.
@ -677,6 +686,62 @@ class TabBase {
return result;
}
/**
* Query each content process hosting subframes of the tab, return results.
* @param {string} message
* @param {object} options
* @param {number} options.frameID
* @param {boolean} options.allFrames
* @returns {Promise[]}
*/
async queryContent(message, options) {
let { allFrames, frameID } = options;
/** @type {Map<nsIDOMProcessParent, innerWindowId[]>} */
let byProcess = new DefaultMap(() => []);
// Recursively walk the tab's BC tree, find all frames, group by process.
function visit(bc) {
let win = bc.currentWindowGlobal;
if (win?.domProcess && (!frameID || frameID === bc.id)) {
byProcess.get(win.domProcess).push(win.innerWindowId);
}
if (allFrames || (frameID && !byProcess.size)) {
bc.children.forEach(visit);
}
}
visit(this.browsingContext);
let promises = Array.from(byProcess.entries(), ([proc, windows]) =>
proc.getActor("ExtensionContent").sendQuery(message, { windows, options })
);
let results = await Promise.all(promises).catch(err => {
if (err.name === "DataCloneError") {
let fileName = options.jsPaths.slice(-1)[0] ?? "<anonymous code>";
let message = `Script '${fileName}' result is non-structured-clonable data`;
return Promise.reject({ message, fileName });
}
throw err;
});
results = results.flat();
if (!results.length) {
if (frameID) {
throw new ExtensionError("Frame not found, or missing host permission");
}
let frames = allFrames ? ", and any iframes" : "";
throw new ExtensionError(`Missing host permission for the tab${frames}`);
}
if (!allFrames && results.length > 1) {
throw new ExtensionError("Internal error: multiple windows matched");
}
return results;
}
/**
* Inserts a script or stylesheet in the given tab, and returns a promise
* which resolves when the operation has completed.
@ -701,6 +766,7 @@ class TabBase {
jsPaths: [],
cssPaths: [],
removeCSS: method == "removeCSS",
extensionId: context.extension.id,
};
// We require a `code` or a `file` property, but we can't accept both.
@ -754,8 +820,7 @@ class TabBase {
}
options.wantReturnValue = true;
return this.sendMessage(context, "Extension:Execute", { options });
return this.queryContent("Execute", options);
}
/**

View File

@ -18,6 +18,7 @@ add_task(async function test_content_script_cross_origin_frame() {
all_frames: true,
js: ["cs.js"],
}],
permissions: ["http://example.net/"],
},
background() {
@ -28,6 +29,26 @@ add_task(async function test_content_script_cross_origin_frame() {
browser.test.assertTrue(frameId > 0, "sender frameId is ok");
browser.test.assertTrue(url.endsWith("file_sample.html"), "url is ok");
let shared = await browser.tabs.executeScript(tab.id, {
allFrames: true,
code: `window.sharedVal`,
});
browser.test.assertEq(shared[0], 357, "CS runs in a shared Sandbox");
let code = "does.not.exist";
await browser.test.assertRejects(
browser.tabs.executeScript(tab.id, { allFrames: true, code }),
/does is not defined/,
"Got the expected rejection from tabs.executeScript"
);
code = "() => {}";
await browser.test.assertRejects(
browser.tabs.executeScript(tab.id, { allFrames: true, code }),
/Script .* result is non-structured-clonable data/,
"Got the expected rejection from tabs.executeScript"
);
let result = await browser.tabs.sendMessage(tab.id, num);
port.postMessage(result);
port.disconnect();
@ -50,6 +71,8 @@ add_task(async function test_content_script_cross_origin_frame() {
})
let response;
window.sharedVal = 357;
let port = browser.runtime.connect();
port.onMessage.addListener(num => {
response = num;

View File

@ -51,6 +51,11 @@ let JSPROCESSACTORS = {
moduleURI: "resource://gre/modules/ContentPrefServiceChild.jsm",
},
},
ExtensionContent: {
child: {
moduleURI: "resource://gre/modules/ExtensionContent.jsm",
},
},
};
/**

View File

@ -740,6 +740,8 @@ module.exports = {
WebrtcGlobalInformation: false,
WheelEvent: false,
Window: false,
WindowGlobalChild: false,
WindowGlobalParent: false,
WindowRoot: false,
Worker: false,
Worklet: false,