mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
Bug 1587541 - Make tab.executeScript, insertCSS, removeCSS Fission compatible r=rpl
Differential Revision: https://phabricator.services.mozilla.com/D81772
This commit is contained in:
parent
f7a0736a34
commit
17939666b8
@ -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]}`
|
||||
);
|
||||
}),
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
@ -51,6 +51,11 @@ let JSPROCESSACTORS = {
|
||||
moduleURI: "resource://gre/modules/ContentPrefServiceChild.jsm",
|
||||
},
|
||||
},
|
||||
ExtensionContent: {
|
||||
child: {
|
||||
moduleURI: "resource://gre/modules/ExtensionContent.jsm",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -740,6 +740,8 @@ module.exports = {
|
||||
WebrtcGlobalInformation: false,
|
||||
WheelEvent: false,
|
||||
Window: false,
|
||||
WindowGlobalChild: false,
|
||||
WindowGlobalParent: false,
|
||||
WindowRoot: false,
|
||||
Worker: false,
|
||||
Worklet: false,
|
||||
|
Loading…
Reference in New Issue
Block a user