mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 22:32:46 +00:00
Bug 1911836 - Implement onUserScriptConnect r=zombie
Differential Revision: https://phabricator.services.mozilla.com/D229387
This commit is contained in:
parent
d99a5fd35f
commit
e5573410c8
@ -330,6 +330,14 @@ export class Messenger {
|
||||
this.onMessageEx = new MessageEvent(context, "runtime.onMessageExternal");
|
||||
}
|
||||
|
||||
get onUserScriptConnect() {
|
||||
return redefineGetter(
|
||||
this,
|
||||
"onUserScriptConnect",
|
||||
new SimpleEventAPI(this.context, "runtime.onUserScriptConnect")
|
||||
);
|
||||
}
|
||||
|
||||
get onUserScriptMessage() {
|
||||
return redefineGetter(
|
||||
this,
|
||||
@ -367,17 +375,25 @@ export class Messenger {
|
||||
return context.wrapPromise(response, callback);
|
||||
}
|
||||
|
||||
connect({ name, native, ...args }) {
|
||||
connect({ context, name, native, ...args }) {
|
||||
// this.context is usually used, except with user scripts, where we pass a
|
||||
// custom context to ensure that the return value is cloned into the right
|
||||
// USER_SCRIPT world.
|
||||
context ??= this.context;
|
||||
let portId = getUniqueId();
|
||||
let port = new Port(this.context, portId, name, !!native);
|
||||
let port = new Port(context, portId, name, !!native);
|
||||
this.conduit
|
||||
.queryPortConnect({ portId, name, native, ...args })
|
||||
.catch(error => port.recvPortDisconnect({ error }));
|
||||
return port.api;
|
||||
}
|
||||
|
||||
recvPortConnect({ extensionId, portId, name, sender }) {
|
||||
recvPortConnect({ extensionId, portId, name, sender, userScriptWorldId }) {
|
||||
let event = sender.id === extensionId ? this.onConnect : this.onConnectEx;
|
||||
if (typeof userScriptWorldId == "string") {
|
||||
sender = { ...sender, userScriptWorldId };
|
||||
event = this.onUserScriptConnect;
|
||||
}
|
||||
if (this.context.active && event.fires.size) {
|
||||
let port = new Port(this.context, portId, name, false, sender);
|
||||
return event.emit(port.api).length;
|
||||
|
@ -62,7 +62,7 @@ class WorldConfigHolder {
|
||||
* instance to support APIs exposed to USER_SCRIPT worlds. Such contexts are
|
||||
* usually heavier due to the need to track the document lifetime, but because
|
||||
* all user script worlds and a content script for a document (and extension)
|
||||
* shares the same lifetime, we delegate to the only ContentScriptContextChild
|
||||
* share the same lifetime, we delegate to the only ContentScriptContextChild
|
||||
* that exists for the document+extension.
|
||||
*/
|
||||
class UserScriptContext extends BaseContext {
|
||||
@ -107,6 +107,10 @@ class UserScriptContext extends BaseContext {
|
||||
this.sandbox = null;
|
||||
}
|
||||
|
||||
async logActivity(type, name, data) {
|
||||
return this.contentContext.logActivity(type, name, data);
|
||||
}
|
||||
|
||||
get cloneScope() {
|
||||
return this.sandbox;
|
||||
}
|
||||
@ -138,12 +142,27 @@ class UserScriptContext extends BaseContext {
|
||||
|
||||
if (this.enableMessaging) {
|
||||
browser.runtime = {};
|
||||
browser.runtime.connect = wrapF(this.runtimeConnect);
|
||||
browser.runtime.sendMessage = wrapF(this.runtimeSendMessage);
|
||||
}
|
||||
const value = Cu.cloneInto(browser, this.sandbox, { cloneFunctions: true });
|
||||
return redefineGetter(this, "browserObj", value);
|
||||
}
|
||||
|
||||
runtimeConnect(...args) {
|
||||
args = this.#schemaCheckParameters("runtime", "connect", args);
|
||||
let [extensionId, options] = args;
|
||||
if (extensionId !== null) {
|
||||
throw new ExtensionError("extensionId is not supported");
|
||||
}
|
||||
let name = options?.name ?? "";
|
||||
return this.contentContext.messenger.connect({
|
||||
context: this,
|
||||
userScriptWorldId: this.worldId,
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
runtimeSendMessage(...args) {
|
||||
// Simplified version of parseBonkersArgs in child/ext-runtime.js
|
||||
let callback = typeof args[args.length - 1] === "function" && args.pop();
|
||||
@ -171,6 +190,12 @@ class UserScriptContext extends BaseContext {
|
||||
callback,
|
||||
});
|
||||
}
|
||||
|
||||
#schemaCheckParameters(namespace, method, args) {
|
||||
let ns = this.contentContext.childManager.schema.getNamespace(namespace);
|
||||
let schemaContext = lazy.Schemas.paramsValidationContexts.get(this);
|
||||
return ns.get(method).checkParameters(args, schemaContext);
|
||||
}
|
||||
}
|
||||
|
||||
class WorldCollection {
|
||||
|
@ -63,6 +63,13 @@ this.runtime = class extends ExtensionAPI {
|
||||
onConnectExternal: context.messenger.onConnectEx.api(),
|
||||
onMessageExternal: context.messenger.onMessageEx.api(),
|
||||
|
||||
get onUserScriptConnect() {
|
||||
return ExtensionCommon.redefineGetter(
|
||||
this,
|
||||
"onUserScriptConnect",
|
||||
context.messenger.onUserScriptConnect.api()
|
||||
);
|
||||
},
|
||||
get onUserScriptMessage() {
|
||||
return ExtensionCommon.redefineGetter(
|
||||
this,
|
||||
|
@ -36,6 +36,7 @@ this.runtime = class extends ExtensionAPIPersistent {
|
||||
// - runtime.onMessage
|
||||
// - runtime.onMessageExternal
|
||||
// - runtime.onUserScriptMessage
|
||||
// - runtime.onUserScriptConnect
|
||||
// For details, see bug 1852317 and test_ext_eventpage_messaging_wakeup.js.
|
||||
|
||||
onInstalled({ fire }) {
|
||||
|
@ -174,7 +174,7 @@
|
||||
"userScriptWorldId": {
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "The worldId of the USER_SCRIPT world that sent the message. Only present on onUserScriptMessage events."
|
||||
"description": "The worldId of the USER_SCRIPT world that sent the message. Only present on onUserScriptMessage and onUserScriptConnect (in port.sender) events."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -750,6 +750,13 @@
|
||||
"description": "Fired when a connection is made from either an extension process or a content script.",
|
||||
"parameters": [{ "$ref": "Port", "name": "port" }]
|
||||
},
|
||||
{
|
||||
"name": "onUserScriptConnect",
|
||||
"type": "function",
|
||||
"description": "Fired when a connection is made from a USER_SCRIPT world registered through the userScripts API.",
|
||||
"permissions": ["userScripts"],
|
||||
"parameters": [{ "$ref": "Port", "name": "port" }]
|
||||
},
|
||||
{
|
||||
"name": "onConnectExternal",
|
||||
"type": "function",
|
||||
|
@ -71,6 +71,7 @@ add_task(async function test_runtime_messaging_errors() {
|
||||
}
|
||||
|
||||
try {
|
||||
// runtime.sendMessage tests:
|
||||
assertThrows(
|
||||
() => browser.runtime.sendMessage(),
|
||||
"runtime.sendMessage's message argument is missing",
|
||||
@ -96,6 +97,28 @@ add_task(async function test_runtime_messaging_errors() {
|
||||
"Could not establish connection. Receiving end does not exist.",
|
||||
"Expected error when there is no onUserScriptMessage handler"
|
||||
);
|
||||
|
||||
// runtime.connect tests:
|
||||
assertThrows(
|
||||
() => browser.runtime.connect("extensionId", {}),
|
||||
"extensionId is not supported",
|
||||
"connect with unsupported extensionId parameter"
|
||||
);
|
||||
assertThrows(
|
||||
() => browser.runtime.connect("extensionId"),
|
||||
"extensionId is not supported",
|
||||
"connect with unsupported extensionId parameter and no options"
|
||||
);
|
||||
assertThrows(
|
||||
() => browser.runtime.connect({ unknownProp: true }),
|
||||
`Type error for parameter connectInfo (Unexpected property "unknownProp") for runtime.connect.`,
|
||||
"connect with unrecognized property"
|
||||
);
|
||||
assertThrows(
|
||||
() => browser.runtime.connect({}, {}),
|
||||
"Incorrect argument types for runtime.connect.",
|
||||
"connect with too many parameters"
|
||||
);
|
||||
} catch (e) {
|
||||
contentscriptTest.fail(`Unexpected error in userscript.js: ${e}`);
|
||||
}
|
||||
@ -357,3 +380,203 @@ add_task(async function test_configureWorld_messaging_existing_world() {
|
||||
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
// This test tests that runtime.connect() works when called from a user script.
|
||||
add_task(async function test_onUserScriptConnect() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
useAddonManager: "permanent",
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
permissions: ["userScripts"],
|
||||
host_permissions: ["*://example.com/*"],
|
||||
},
|
||||
files: {
|
||||
"userscript.js": () => {
|
||||
let port = browser.runtime.connect({ name: "first_port" });
|
||||
port.onMessage.addListener(msg => {
|
||||
port.postMessage({ messageBack: msg });
|
||||
});
|
||||
port.onDisconnect.addListener(() => {
|
||||
let port2 = browser.runtime.connect({ name: "second_port" });
|
||||
port2.postMessage({ errorFromFirstPort: port.error });
|
||||
port2.disconnect();
|
||||
});
|
||||
},
|
||||
},
|
||||
background() {
|
||||
browser.runtime.onUserScriptConnect.addListener(port => {
|
||||
browser.test.assertEq(
|
||||
"connect_world",
|
||||
port.sender.userScriptWorldId,
|
||||
`Expected userScriptWorldId in onUserScriptConnect: ${port.name}`
|
||||
);
|
||||
if (port.name === "first_port") {
|
||||
port.onMessage.addListener(msg => {
|
||||
browser.test.assertDeepEq(
|
||||
{ messageBack: { hi: 1 } },
|
||||
msg,
|
||||
"port.onMessage triggered from user script"
|
||||
);
|
||||
port.disconnect();
|
||||
// ^ should trigger port.onDisconnect in the user script, which
|
||||
// will signal back the status by connecting again to second_port.
|
||||
});
|
||||
port.onDisconnect.addListener(() => {
|
||||
// We should not expect a disconnect, because we call
|
||||
// port.disconnect() from this end.
|
||||
browser.test.fail(`Unexpected port.onDisconnect: ${port.error}`);
|
||||
});
|
||||
port.postMessage({ hi: 1 });
|
||||
return;
|
||||
}
|
||||
if (port.name === "second_port") {
|
||||
port.onMessage.addListener(msg => {
|
||||
browser.test.assertDeepEq(
|
||||
{ errorFromFirstPort: null },
|
||||
msg,
|
||||
"When we disconnect first_port, other port.error should be null"
|
||||
);
|
||||
browser.test.sendMessage("port.onMessage:done");
|
||||
});
|
||||
port.onDisconnect.addListener(() => {
|
||||
browser.test.assertDeepEq(
|
||||
null,
|
||||
port.error,
|
||||
"Our port.error when other side (user script) disconnects"
|
||||
);
|
||||
browser.test.sendMessage("port.onDisconnect:done");
|
||||
});
|
||||
return;
|
||||
}
|
||||
browser.test.fail(`Unexpected port: ${port.name}`);
|
||||
});
|
||||
browser.runtime.onInstalled.addListener(async () => {
|
||||
await browser.userScripts.configureWorld({ messaging: true });
|
||||
await browser.userScripts.register([
|
||||
{
|
||||
id: "messaging checker",
|
||||
matches: ["*://example.com/dummy"],
|
||||
js: [{ file: "userscript.js" }],
|
||||
worldId: "connect_world",
|
||||
},
|
||||
]);
|
||||
browser.test.sendMessage("registered");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("registered");
|
||||
|
||||
async function testRuntimeConnect() {
|
||||
let contentPage = await ExtensionTestUtils.loadContentPage(
|
||||
"http://example.com/dummy"
|
||||
);
|
||||
await Promise.all([
|
||||
extension.awaitMessage("port.onMessage:done"),
|
||||
extension.awaitMessage("port.onDisconnect:done"),
|
||||
]);
|
||||
await contentPage.close();
|
||||
}
|
||||
info("Loading page that should trigger runtime.connect");
|
||||
await testRuntimeConnect();
|
||||
|
||||
await AddonTestUtils.promiseShutdownManager();
|
||||
ExtensionUserScripts._getStoreForTesting()._uninitForTesting();
|
||||
await AddonTestUtils.promiseStartupManager();
|
||||
|
||||
// Because the background has a persistent listener (runtime.onInstalled), it
|
||||
// stays suspended after a restart.
|
||||
await extension.awaitStartup();
|
||||
ExtensionTestCommon.testAssertions.assertBackgroundStatusStopped(extension);
|
||||
|
||||
// We expect that the load of the page that calls runtime.connect from a
|
||||
// user script will wake it to fire runtime.onUserScriptConnect.
|
||||
info("Loading page that should load user script and wake event page");
|
||||
await testRuntimeConnect();
|
||||
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
// This tests:
|
||||
// - That port.onDisconnect is fired when the document navigates away.
|
||||
// - That runtime.connect() can be called without parameters.
|
||||
add_task(
|
||||
{
|
||||
// We want to disable the bfcache and use the unload listener to force
|
||||
// that below. But on Android bfcache.allow_unload_listeners is true by
|
||||
// default, so we force the pref to false to make sure that the bfcache is
|
||||
// indeed disabled for the test page.
|
||||
pref_set: [["docshell.shistory.bfcache.allow_unload_listeners", false]],
|
||||
},
|
||||
async function test_onUserScriptConnect_port_disconnect_on_navigate() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
useAddonManager: "permanent",
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
permissions: ["userScripts"],
|
||||
host_permissions: ["*://example.com/*"],
|
||||
},
|
||||
files: {
|
||||
"userscript.js": () => {
|
||||
let port = browser.runtime.connect();
|
||||
|
||||
// Prevent bfcache from keeping doc + port alive.
|
||||
// eslint-disable-next-line mozilla/balanced-listeners
|
||||
window.addEventListener("unload", () => {});
|
||||
|
||||
// Global var to avoid garbage collection:
|
||||
globalThis.portReference = port;
|
||||
|
||||
port.onMessage.addListener(msg => {
|
||||
dump(`Will navigate elsewhere after bye message: ${msg}\n`);
|
||||
// Change URL. Note: not matched by matches[] above.
|
||||
location.search = "?something_else";
|
||||
// ^ Note: we expect the context to unload. If the test times out
|
||||
// after this point, it may be due to the page unexpectedly not
|
||||
// being unloaded, e.g. due to it being stored in the bfcache.
|
||||
// We disable the bfcache with the unload listener above.
|
||||
});
|
||||
},
|
||||
},
|
||||
async background() {
|
||||
browser.runtime.onUserScriptConnect.addListener(port => {
|
||||
browser.test.assertEq("", port.name, "Got default port.name");
|
||||
browser.test.assertEq(
|
||||
"",
|
||||
port.sender.userScriptWorldId,
|
||||
"Got default userScriptWorldId"
|
||||
);
|
||||
port.onDisconnect.addListener(() => {
|
||||
browser.test.assertDeepEq(
|
||||
null,
|
||||
port.error,
|
||||
"Closing port due to a navigation is not an error"
|
||||
);
|
||||
browser.test.sendMessage("done");
|
||||
});
|
||||
port.postMessage("bye");
|
||||
});
|
||||
await browser.userScripts.configureWorld({ messaging: true });
|
||||
await browser.userScripts.register([
|
||||
{
|
||||
id: "messaging checker",
|
||||
matches: ["*://example.com/dummy"],
|
||||
js: [{ file: "userscript.js" }],
|
||||
},
|
||||
]);
|
||||
browser.test.sendMessage("registered");
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("registered");
|
||||
|
||||
let contentPage = await ExtensionTestUtils.loadContentPage(
|
||||
"http://example.com/dummy"
|
||||
);
|
||||
await extension.awaitMessage("done");
|
||||
await contentPage.close();
|
||||
await extension.unload();
|
||||
}
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user