mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-09 11:25:00 +00:00
Backed out changeset 980bbb2d3e06 (bug 1814210) for causing multiple dt/bc failures in modules/remotepagemanager/RemotePageManagerParent.sys.mjs CLOSED TREE
This commit is contained in:
parent
7390510f3f
commit
657465b8bd
@ -14,6 +14,7 @@ export class ASRouterChild extends JSWindowActorChild {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.observers = new Set();
|
this.observers = new Set();
|
||||||
|
this.rpmInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
didDestroy() {
|
didDestroy() {
|
||||||
@ -24,6 +25,7 @@ export class ASRouterChild extends JSWindowActorChild {
|
|||||||
// NOTE: DOMDocElementInserted may be called multiple times per
|
// NOTE: DOMDocElementInserted may be called multiple times per
|
||||||
// PWindowGlobal due to the initial about:blank document's window global
|
// PWindowGlobal due to the initial about:blank document's window global
|
||||||
// being re-used.
|
// being re-used.
|
||||||
|
this.initializeRPM();
|
||||||
const window = this.contentWindow;
|
const window = this.contentWindow;
|
||||||
Cu.exportFunction(this.asRouterMessage.bind(this), window, {
|
Cu.exportFunction(this.asRouterMessage.bind(this), window, {
|
||||||
defineAs: "ASRouterMessage",
|
defineAs: "ASRouterMessage",
|
||||||
@ -108,4 +110,30 @@ export class ASRouterChild extends JSWindowActorChild {
|
|||||||
}
|
}
|
||||||
throw new Error(`Unexpected type "${type}"`);
|
throw new Error(`Unexpected type "${type}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Creates a new PageListener for this process. This will listen for page loads
|
||||||
|
// and for those that match URLs provided by the parent process will set up
|
||||||
|
// a dedicated message port and notify the parent process.
|
||||||
|
// Note: this is part of the old message-manager based RPM and is only still
|
||||||
|
// used by newtab/home/welcome.
|
||||||
|
initializeRPM() {
|
||||||
|
if (this.rpmInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip the hash from the URL, because it's not part of the origin.
|
||||||
|
let url = this.document.documentURI.replace(/[\#?].*$/, "");
|
||||||
|
|
||||||
|
let registeredURLs = Services.cpmm.sharedData.get("RemotePageManager:urls");
|
||||||
|
|
||||||
|
if (registeredURLs && registeredURLs.has(url)) {
|
||||||
|
let { ChildMessagePort } = ChromeUtils.importESModule(
|
||||||
|
"resource://gre/modules/remotepagemanager/RemotePageManagerChild.sys.mjs"
|
||||||
|
);
|
||||||
|
// Set up the child side of the message port
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new ChildMessagePort(this.contentWindow);
|
||||||
|
this.rpmInitialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,7 @@ DIRS += [
|
|||||||
"prompts",
|
"prompts",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
"remotebrowserutils",
|
"remotebrowserutils",
|
||||||
|
"remotepagemanager",
|
||||||
"reflect",
|
"reflect",
|
||||||
"reputationservice",
|
"reputationservice",
|
||||||
"resistfingerprinting",
|
"resistfingerprinting",
|
||||||
|
276
toolkit/components/remotepagemanager/MessagePort.sys.mjs
Normal file
276
toolkit/components/remotepagemanager/MessagePort.sys.mjs
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
const lazy = {};
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
|
PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
export class MessageListener {
|
||||||
|
constructor() {
|
||||||
|
this.listeners = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
return this.listeners.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
has(name) {
|
||||||
|
return this.listeners.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
callListeners(message) {
|
||||||
|
let listeners = this.listeners.get(message.name);
|
||||||
|
if (!listeners) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let listener of listeners.values()) {
|
||||||
|
try {
|
||||||
|
listener(message);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessageListener(name, callback) {
|
||||||
|
if (!this.listeners.has(name)) {
|
||||||
|
this.listeners.set(name, new Set([callback]));
|
||||||
|
} else {
|
||||||
|
this.listeners.get(name).add(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMessageListener(name, callback) {
|
||||||
|
if (!this.listeners.has(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.get(name).delete(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A message port sits on each side of the process boundary for every remote
|
||||||
|
* page. Each has a port ID that is unique to the message manager it talks
|
||||||
|
* through.
|
||||||
|
*
|
||||||
|
* We roughly implement the same contract as nsIMessageSender and
|
||||||
|
* nsIMessageListenerManager
|
||||||
|
*/
|
||||||
|
export class MessagePort {
|
||||||
|
constructor(messageManagerOrActor, portID) {
|
||||||
|
this.messageManager = messageManagerOrActor;
|
||||||
|
this.portID = portID;
|
||||||
|
this.destroyed = false;
|
||||||
|
this.listener = new MessageListener();
|
||||||
|
|
||||||
|
// This is a sparse array of pending requests. The id of each request is
|
||||||
|
// simply its index in the array. The next id is the current length of the
|
||||||
|
// array (which includes the count of missing indexes).
|
||||||
|
this.requests = [];
|
||||||
|
|
||||||
|
this.message = this.message.bind(this);
|
||||||
|
this.receiveRequest = this.receiveRequest.bind(this);
|
||||||
|
this.receiveResponse = this.receiveResponse.bind(this);
|
||||||
|
this.addMessageListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessageListeners() {
|
||||||
|
if (!(this.messageManager instanceof Ci.nsIMessageSender)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messageManager.addMessageListener("RemotePage:Message", this.message);
|
||||||
|
this.messageManager.addMessageListener(
|
||||||
|
"RemotePage:Request",
|
||||||
|
this.receiveRequest
|
||||||
|
);
|
||||||
|
this.messageManager.addMessageListener(
|
||||||
|
"RemotePage:Response",
|
||||||
|
this.receiveResponse
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMessageListeners() {
|
||||||
|
if (!(this.messageManager instanceof Ci.nsIMessageSender)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messageManager.removeMessageListener(
|
||||||
|
"RemotePage:Message",
|
||||||
|
this.message
|
||||||
|
);
|
||||||
|
this.messageManager.removeMessageListener(
|
||||||
|
"RemotePage:Request",
|
||||||
|
this.receiveRequest
|
||||||
|
);
|
||||||
|
this.messageManager.removeMessageListener(
|
||||||
|
"RemotePage:Response",
|
||||||
|
this.receiveResponse
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the message manager used to connect to the other process has
|
||||||
|
// changed, i.e. when a tab is detached.
|
||||||
|
swapMessageManager(messageManager) {
|
||||||
|
this.removeMessageListeners();
|
||||||
|
this.messageManager = messageManager;
|
||||||
|
this.addMessageListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends a request to the other process and returns a promise that completes
|
||||||
|
// once the other process has responded to the request or some error occurs.
|
||||||
|
sendRequest(name, data = null) {
|
||||||
|
if (this.destroyed) {
|
||||||
|
return this.window.Promise.reject(
|
||||||
|
new Error("Message port has been destroyed")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let deferred = lazy.PromiseUtils.defer();
|
||||||
|
this.requests.push(deferred);
|
||||||
|
|
||||||
|
this.messageManager.sendAsyncMessage("RemotePage:Request", {
|
||||||
|
portID: this.portID,
|
||||||
|
requestID: this.requests.length - 1,
|
||||||
|
name,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.wrapPromise(deferred.promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles an IPC message to perform a request of some kind.
|
||||||
|
async receiveRequest({ data: messagedata }) {
|
||||||
|
if (this.destroyed || messagedata.portID != this.portID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
portID: this.portID,
|
||||||
|
requestID: messagedata.requestID,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
data.resolve = await this.handleRequest(
|
||||||
|
messagedata.name,
|
||||||
|
messagedata.data
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
data.reject = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messageManager.sendAsyncMessage("RemotePage:Response", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles an IPC message with the response of a request.
|
||||||
|
receiveResponse({ data: messagedata }) {
|
||||||
|
if (this.destroyed || messagedata.portID != this.portID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deferred = this.requests[messagedata.requestID];
|
||||||
|
if (!deferred) {
|
||||||
|
console.error("Received a response to an unknown request.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.requests[messagedata.requestID];
|
||||||
|
|
||||||
|
if ("resolve" in messagedata) {
|
||||||
|
deferred.resolve(messagedata.resolve);
|
||||||
|
} else if ("reject" in messagedata) {
|
||||||
|
deferred.reject(messagedata.reject);
|
||||||
|
} else {
|
||||||
|
deferred.reject(new Error("Internal RPM error."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles an IPC message containing any message.
|
||||||
|
message({ data: messagedata }) {
|
||||||
|
if (this.destroyed || messagedata.portID != this.portID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleMessage(messagedata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adds a listener for messages. Many callbacks can be registered for the
|
||||||
|
* same message if necessary. An attempt to register the same callback for the
|
||||||
|
* same message twice will be ignored. When called the callback is passed an
|
||||||
|
* object with these properties:
|
||||||
|
* target: This message port
|
||||||
|
* name: The message name
|
||||||
|
* data: Any data sent with the message
|
||||||
|
*/
|
||||||
|
addMessageListener(name, callback) {
|
||||||
|
if (this.destroyed) {
|
||||||
|
throw new Error("Message port has been destroyed");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listener.addMessageListener(name, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Removes a listener for messages.
|
||||||
|
*/
|
||||||
|
removeMessageListener(name, callback) {
|
||||||
|
if (this.destroyed) {
|
||||||
|
throw new Error("Message port has been destroyed");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listener.removeMessageListener(name, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends a message asynchronously to the other process
|
||||||
|
sendAsyncMessage(name, data = null) {
|
||||||
|
if (this.destroyed) {
|
||||||
|
throw new Error("Message port has been destroyed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let id;
|
||||||
|
if (this.window) {
|
||||||
|
id = this.window.docShell.browsingContext.id;
|
||||||
|
}
|
||||||
|
if (this.messageManager instanceof Ci.nsIMessageSender) {
|
||||||
|
this.messageManager.sendAsyncMessage("RemotePage:Message", {
|
||||||
|
portID: this.portID,
|
||||||
|
browsingContextID: id,
|
||||||
|
name,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.messageManager.sendAsyncMessage(name, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called to destroy this port
|
||||||
|
destroy() {
|
||||||
|
try {
|
||||||
|
// This can fail in the child process if the tab has already been closed
|
||||||
|
this.removeMessageListeners();
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
for (let deferred of this.requests) {
|
||||||
|
if (deferred) {
|
||||||
|
deferred.reject(new Error("Message port has been destroyed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messageManager = null;
|
||||||
|
this.destroyed = true;
|
||||||
|
this.portID = null;
|
||||||
|
this.listener = null;
|
||||||
|
this.requests = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapPromise(promise) {
|
||||||
|
return new this.window.Promise((resolve, reject) =>
|
||||||
|
promise.then(resolve, reject)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { MessagePort } from "resource://gre/modules/remotepagemanager/MessagePort.sys.mjs";
|
||||||
|
|
||||||
|
// The content side of a message port
|
||||||
|
export class ChildMessagePort extends MessagePort {
|
||||||
|
constructor(window) {
|
||||||
|
let portID =
|
||||||
|
Services.appinfo.processID + ":" + ChildMessagePort.nextPortID++;
|
||||||
|
super(window.docShell.messageManager, portID);
|
||||||
|
|
||||||
|
this.window = window;
|
||||||
|
|
||||||
|
// Add functionality to the content page
|
||||||
|
Cu.exportFunction(this.sendAsyncMessage.bind(this), window, {
|
||||||
|
defineAs: "RPMSendAsyncMessage",
|
||||||
|
});
|
||||||
|
Cu.exportFunction(this.addMessageListener.bind(this), window, {
|
||||||
|
defineAs: "RPMAddMessageListener",
|
||||||
|
allowCallbacks: true,
|
||||||
|
});
|
||||||
|
Cu.exportFunction(this.removeMessageListener.bind(this), window, {
|
||||||
|
defineAs: "RPMRemoveMessageListener",
|
||||||
|
allowCallbacks: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// The actor form only needs the functions set up above. The actor
|
||||||
|
// will send and receive messages directly.
|
||||||
|
if (!(this.messageManager instanceof Ci.nsIMessageSender)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a message for load events
|
||||||
|
let loadListener = () => {
|
||||||
|
this.sendAsyncMessage("RemotePage:Load");
|
||||||
|
window.removeEventListener("load", loadListener);
|
||||||
|
};
|
||||||
|
window.addEventListener("load", loadListener);
|
||||||
|
|
||||||
|
// Destroy the port when the window is unloaded
|
||||||
|
window.addEventListener("unload", () => {
|
||||||
|
try {
|
||||||
|
this.sendAsyncMessage("RemotePage:Unload");
|
||||||
|
} catch (e) {
|
||||||
|
// If the tab has been closed the frame message manager has already been
|
||||||
|
// destroyed
|
||||||
|
}
|
||||||
|
this.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tell the main process to set up its side of the message pipe.
|
||||||
|
this.messageManager.sendAsyncMessage("RemotePage:InitPort", {
|
||||||
|
portID,
|
||||||
|
url: window.document.documentURI.replace(/[\#|\?].*$/, ""),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the content process is requesting some data.
|
||||||
|
async handleRequest(name, data) {
|
||||||
|
throw new Error(`Unknown request ${name}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when a message is received from the message manager or actor.
|
||||||
|
handleMessage(messagedata) {
|
||||||
|
let message = {
|
||||||
|
name: messagedata.name,
|
||||||
|
data: messagedata.data,
|
||||||
|
};
|
||||||
|
this.listener.callListeners(Cu.cloneInto(message, this.window));
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.window = null;
|
||||||
|
super.destroy.call(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChildMessagePort.nextPortID = 0;
|
@ -0,0 +1,347 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Using the RemotePageManager:
|
||||||
|
* * Create a new page listener by calling 'new RemotePages(URI)' which
|
||||||
|
* then injects functions like RPMGetBoolPref() into the registered page.
|
||||||
|
* One can then use those exported functions to communicate between
|
||||||
|
* child and parent.
|
||||||
|
*
|
||||||
|
* * When adding a new consumer of RPM that relies on other functionality
|
||||||
|
* then simple message passing provided by the RPM, then one has to
|
||||||
|
* whitelist permissions for the new URI within the RPMAccessManager
|
||||||
|
* from MessagePort.jsm.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
MessageListener,
|
||||||
|
MessagePort,
|
||||||
|
} from "resource://gre/modules/remotepagemanager/MessagePort.sys.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a RemotePages object which listens for new remote pages of some
|
||||||
|
* particular URLs. A "RemotePage:Init" message will be dispatched to this
|
||||||
|
* object for every page loaded. Message listeners added to this object receive
|
||||||
|
* messages from all loaded pages from the requested urls.
|
||||||
|
*/
|
||||||
|
export class RemotePages {
|
||||||
|
constructor(urls) {
|
||||||
|
this.urls = Array.isArray(urls) ? urls : [urls];
|
||||||
|
this.messagePorts = new Set();
|
||||||
|
this.listener = new MessageListener();
|
||||||
|
this.destroyed = false;
|
||||||
|
|
||||||
|
this.portCreated = this.portCreated.bind(this);
|
||||||
|
this.portMessageReceived = this.portMessageReceived.bind(this);
|
||||||
|
|
||||||
|
for (const url of this.urls) {
|
||||||
|
RemotePageManager.addRemotePageListener(url, this.portCreated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
for (const url of this.urls) {
|
||||||
|
RemotePageManager.removeRemotePageListener(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let port of this.messagePorts.values()) {
|
||||||
|
this.removeMessagePort(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messagePorts = null;
|
||||||
|
this.listener = null;
|
||||||
|
this.destroyed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when a page matching one of the urls has loaded in a frame.
|
||||||
|
portCreated(port) {
|
||||||
|
this.messagePorts.add(port);
|
||||||
|
|
||||||
|
port.loaded = false;
|
||||||
|
port.addMessageListener("RemotePage:Load", this.portMessageReceived);
|
||||||
|
port.addMessageListener("RemotePage:Unload", this.portMessageReceived);
|
||||||
|
|
||||||
|
for (let name of this.listener.keys()) {
|
||||||
|
this.registerPortListener(port, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listener.callListeners({ target: port, name: "RemotePage:Init" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// A message has been received from one of the pages
|
||||||
|
portMessageReceived(message) {
|
||||||
|
switch (message.name) {
|
||||||
|
case "RemotePage:Load":
|
||||||
|
message.target.loaded = true;
|
||||||
|
break;
|
||||||
|
case "RemotePage:Unload":
|
||||||
|
message.target.loaded = false;
|
||||||
|
this.removeMessagePort(message.target);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listener.callListeners(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A page has closed
|
||||||
|
removeMessagePort(port) {
|
||||||
|
for (let name of this.listener.keys()) {
|
||||||
|
port.removeMessageListener(name, this.portMessageReceived);
|
||||||
|
}
|
||||||
|
|
||||||
|
port.removeMessageListener("RemotePage:Load", this.portMessageReceived);
|
||||||
|
port.removeMessageListener("RemotePage:Unload", this.portMessageReceived);
|
||||||
|
this.messagePorts.delete(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPortListener(port, name) {
|
||||||
|
port.addMessageListener(name, this.portMessageReceived);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends a message to all known pages
|
||||||
|
sendAsyncMessage(name, data = null) {
|
||||||
|
for (let port of this.messagePorts.values()) {
|
||||||
|
try {
|
||||||
|
port.sendAsyncMessage(name, data);
|
||||||
|
} catch (e) {
|
||||||
|
// Unless the port is in the process of unloading, something strange
|
||||||
|
// happened but allow other ports to receive the message
|
||||||
|
if (e.result !== Cr.NS_ERROR_NOT_INITIALIZED) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessageListener(name, callback) {
|
||||||
|
if (this.destroyed) {
|
||||||
|
throw new Error("RemotePages has been destroyed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.listener.has(name)) {
|
||||||
|
for (let port of this.messagePorts.values()) {
|
||||||
|
this.registerPortListener(port, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listener.addMessageListener(name, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMessageListener(name, callback) {
|
||||||
|
if (this.destroyed) {
|
||||||
|
throw new Error("RemotePages has been destroyed");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listener.removeMessageListener(name, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
portsForBrowser(browser) {
|
||||||
|
return [...this.messagePorts].filter(port => port.browser == browser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only exposes the public properties of the MessagePort
|
||||||
|
function publicMessagePort(port) {
|
||||||
|
let properties = [
|
||||||
|
"addMessageListener",
|
||||||
|
"removeMessageListener",
|
||||||
|
"sendAsyncMessage",
|
||||||
|
"destroy",
|
||||||
|
];
|
||||||
|
|
||||||
|
let clean = {};
|
||||||
|
for (let property of properties) {
|
||||||
|
clean[property] = port[property].bind(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(clean, "portID", {
|
||||||
|
enumerable: true,
|
||||||
|
get() {
|
||||||
|
return port.portID;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (port instanceof ChromeMessagePort) {
|
||||||
|
Object.defineProperty(clean, "browser", {
|
||||||
|
enumerable: true,
|
||||||
|
get() {
|
||||||
|
return port.browser;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(clean, "url", {
|
||||||
|
enumerable: true,
|
||||||
|
get() {
|
||||||
|
return port.url;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The chome side of a message port
|
||||||
|
class ChromeMessagePort extends MessagePort {
|
||||||
|
constructor(browser, portID, url) {
|
||||||
|
super(browser.messageManager, portID);
|
||||||
|
|
||||||
|
this._browser = browser;
|
||||||
|
this._permanentKey = browser.permanentKey;
|
||||||
|
this._url = url;
|
||||||
|
|
||||||
|
Services.obs.addObserver(this, "message-manager-disconnect");
|
||||||
|
this.publicPort = publicMessagePort(this);
|
||||||
|
|
||||||
|
this.swapBrowsers = this.swapBrowsers.bind(this);
|
||||||
|
this._browser.addEventListener("SwapDocShells", this.swapBrowsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
get browser() {
|
||||||
|
return this._browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
get url() {
|
||||||
|
return this._url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the docshell is being swapped with another browser. We have to
|
||||||
|
// update to use the new browser's message manager
|
||||||
|
swapBrowsers({ detail: newBrowser }) {
|
||||||
|
// We can see this event for the new browser before the swap completes so
|
||||||
|
// check that the browser we're tracking has our permanentKey.
|
||||||
|
if (this._browser.permanentKey != this._permanentKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._browser.removeEventListener("SwapDocShells", this.swapBrowsers);
|
||||||
|
|
||||||
|
this._browser = newBrowser;
|
||||||
|
this.swapMessageManager(newBrowser.messageManager);
|
||||||
|
|
||||||
|
this._browser.addEventListener("SwapDocShells", this.swapBrowsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when a message manager has been disconnected indicating that the
|
||||||
|
// tab has closed or crashed
|
||||||
|
observe(messageManager) {
|
||||||
|
if (messageManager != this.messageManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listener.callListeners({
|
||||||
|
target: this.publicPort,
|
||||||
|
name: "RemotePage:Unload",
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the content process is requesting some data.
|
||||||
|
async handleRequest(name, data) {
|
||||||
|
throw new Error(`Unknown request ${name}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when a message is received from the message manager.
|
||||||
|
handleMessage(messagedata) {
|
||||||
|
let message = {
|
||||||
|
target: this.publicPort,
|
||||||
|
name: messagedata.name,
|
||||||
|
data: messagedata.data,
|
||||||
|
browsingContextID: messagedata.browsingContextID,
|
||||||
|
};
|
||||||
|
this.listener.callListeners(message);
|
||||||
|
|
||||||
|
if (messagedata.name == "RemotePage:Unload") {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
try {
|
||||||
|
this._browser.removeEventListener("SwapDocShells", this.swapBrowsers);
|
||||||
|
} catch (e) {
|
||||||
|
// It's possible the browser instance is already dead so we can just ignore
|
||||||
|
// this error.
|
||||||
|
}
|
||||||
|
|
||||||
|
this._browser = null;
|
||||||
|
Services.obs.removeObserver(this, "message-manager-disconnect");
|
||||||
|
super.destroy.call(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allows callers to register to connect to specific content pages. Registration
|
||||||
|
// is done through the addRemotePageListener method
|
||||||
|
var RemotePageManagerInternal = {
|
||||||
|
// The currently registered remote pages
|
||||||
|
pages: new Map(),
|
||||||
|
|
||||||
|
// Initialises all the needed listeners
|
||||||
|
init() {
|
||||||
|
Services.mm.addMessageListener(
|
||||||
|
"RemotePage:InitPort",
|
||||||
|
this.initPort.bind(this)
|
||||||
|
);
|
||||||
|
this.updateProcessUrls();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateProcessUrls() {
|
||||||
|
Services.ppmm.sharedData.set(
|
||||||
|
"RemotePageManager:urls",
|
||||||
|
new Set(this.pages.keys())
|
||||||
|
);
|
||||||
|
Services.ppmm.sharedData.flush();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Registers interest in a remote page. A callback is called with a port for
|
||||||
|
// the new page when loading begins (i.e. the page hasn't actually loaded yet).
|
||||||
|
// Only one callback can be registered per URL.
|
||||||
|
addRemotePageListener(url, callback) {
|
||||||
|
if (this.pages.has(url)) {
|
||||||
|
throw new Error("Remote page already registered: " + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pages.set(url, callback);
|
||||||
|
this.updateProcessUrls();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Removes any interest in a remote page.
|
||||||
|
removeRemotePageListener(url) {
|
||||||
|
if (!this.pages.has(url)) {
|
||||||
|
throw new Error("Remote page is not registered: " + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pages.delete(url);
|
||||||
|
this.updateProcessUrls();
|
||||||
|
},
|
||||||
|
|
||||||
|
// A remote page has been created and a port is ready in the content side
|
||||||
|
initPort({ target: browser, data: { url, portID } }) {
|
||||||
|
let callback = this.pages.get(url);
|
||||||
|
if (!callback) {
|
||||||
|
console.error("Unexpected remote page load: ", url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let port = new ChromeMessagePort(browser, portID, url);
|
||||||
|
callback(port.publicPort);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
|
||||||
|
throw new Error("RemotePageManager can only be used in the main process.");
|
||||||
|
}
|
||||||
|
|
||||||
|
RemotePageManagerInternal.init();
|
||||||
|
|
||||||
|
// The public API for the above object
|
||||||
|
export var RemotePageManager = {
|
||||||
|
addRemotePageListener: RemotePageManagerInternal.addRemotePageListener.bind(
|
||||||
|
RemotePageManagerInternal
|
||||||
|
),
|
||||||
|
removeRemotePageListener: RemotePageManagerInternal.removeRemotePageListener.bind(
|
||||||
|
RemotePageManagerInternal
|
||||||
|
),
|
||||||
|
};
|
16
toolkit/components/remotepagemanager/moz.build
Normal file
16
toolkit/components/remotepagemanager/moz.build
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||||
|
# vim: set filetype=python:
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
with Files("**"):
|
||||||
|
BUG_COMPONENT = ("Toolkit", "General")
|
||||||
|
|
||||||
|
EXTRA_JS_MODULES.remotepagemanager = [
|
||||||
|
"MessagePort.sys.mjs",
|
||||||
|
"RemotePageManagerChild.sys.mjs",
|
||||||
|
"RemotePageManagerParent.sys.mjs",
|
||||||
|
]
|
||||||
|
|
||||||
|
BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"]
|
@ -0,0 +1,29 @@
|
|||||||
|
/* vim: set ts=2 sw=2 sts=2 et tw=80: */
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
export class LegacyRPMChild extends JSWindowActorChild {
|
||||||
|
#rpmInitialized = false;
|
||||||
|
actorCreated() {
|
||||||
|
if (this.#rpmInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Strip the hash from the URL, because it's not part of the origin.
|
||||||
|
let url = this.document.documentURI.replace(/[\#?].*$/, "");
|
||||||
|
|
||||||
|
let registeredURLs = Services.cpmm.sharedData.get("RemotePageManager:urls");
|
||||||
|
|
||||||
|
if (registeredURLs && registeredURLs.has(url)) {
|
||||||
|
let { ChildMessagePort } = ChromeUtils.importESModule(
|
||||||
|
"resource://gre/modules/remotepagemanager/RemotePageManagerChild.sys.mjs"
|
||||||
|
);
|
||||||
|
// Set up the child side of the message port
|
||||||
|
new ChildMessagePort(this.contentWindow);
|
||||||
|
this.#rpmInitialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOMDocElementInserted is only used to create the actor.
|
||||||
|
handleEvent() {}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
support-files =
|
||||||
|
testremotepagemanager.html
|
||||||
|
testremotepagemanager2.html
|
||||||
|
LegacyRPMChild.sys.mjs
|
||||||
|
|
||||||
|
[browser_RemotePageManager.js]
|
||||||
|
https_first_disabled = true
|
@ -0,0 +1,585 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||||
|
*/
|
||||||
|
|
||||||
|
const TEST_URL =
|
||||||
|
"http://www.example.com/browser/toolkit/components/remotepagemanager/tests/browser/testremotepagemanager.html";
|
||||||
|
|
||||||
|
var { RemotePages, RemotePageManager } = ChromeUtils.importESModule(
|
||||||
|
"resource://gre/modules/remotepagemanager/RemotePageManagerParent.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
function failOnMessage(message) {
|
||||||
|
ok(false, "Should not have seen message " + message.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForMessage(port, message, expectedPort = port) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
function listener(message) {
|
||||||
|
is(
|
||||||
|
message.target,
|
||||||
|
expectedPort,
|
||||||
|
"Message should be from the right port."
|
||||||
|
);
|
||||||
|
|
||||||
|
port.removeMessageListener(listener);
|
||||||
|
resolve(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
port.addMessageListener(message, listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForPort(url, createTab = true) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
RemotePageManager.addRemotePageListener(url, port => {
|
||||||
|
RemotePageManager.removeRemotePageListener(url);
|
||||||
|
|
||||||
|
waitForMessage(port, "RemotePage:Load").then(() => resolve(port));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (createTab) {
|
||||||
|
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForPage(pages, url = TEST_URL) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
function listener({ target }) {
|
||||||
|
pages.removeMessageListener("RemotePage:Init", listener);
|
||||||
|
|
||||||
|
waitForMessage(target, "RemotePage:Load").then(() => resolve(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.addMessageListener("RemotePage:Init", listener);
|
||||||
|
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function swapDocShells(browser1, browser2) {
|
||||||
|
// Swap frameLoaders.
|
||||||
|
browser1.swapDocShells(browser2);
|
||||||
|
|
||||||
|
// Swap permanentKeys.
|
||||||
|
let tmp = browser1.permanentKey;
|
||||||
|
browser1.permanentKey = browser2.permanentKey;
|
||||||
|
browser2.permanentKey = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_setup(async function() {
|
||||||
|
ChromeUtils.registerWindowActor("LegacyRPM", {
|
||||||
|
child: {
|
||||||
|
esModuleURI: getRootDirectory(gTestPath) + "LegacyRPMChild.sys.mjs",
|
||||||
|
events: {
|
||||||
|
DOMDocElementInserted: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
messageManagerGroups: ["browsers"],
|
||||||
|
});
|
||||||
|
|
||||||
|
registerCleanupFunction(() => ChromeUtils.unregisterWindowActor("LegacyRPM"));
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function sharedData_aka_initialProcessData() {
|
||||||
|
const includesTest = () =>
|
||||||
|
Services.cpmm.sharedData.get("RemotePageManager:urls").has(TEST_URL);
|
||||||
|
is(
|
||||||
|
includesTest(),
|
||||||
|
false,
|
||||||
|
"Shouldn't have test url in initial process data yet"
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadedPort = waitForPort(TEST_URL);
|
||||||
|
is(includesTest(), true, "Should have test url when waiting for it to load");
|
||||||
|
|
||||||
|
await loadedPort;
|
||||||
|
is(includesTest(), false, "Should have test url removed when done listening");
|
||||||
|
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that opening a page creates a port, sends the load event and then
|
||||||
|
// navigating to a new page sends the unload event. Going back should create a
|
||||||
|
// new port
|
||||||
|
add_task(async function init_navigate() {
|
||||||
|
let port = await waitForPort(TEST_URL);
|
||||||
|
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
|
||||||
|
|
||||||
|
let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
||||||
|
BrowserTestUtils.loadURIString(gBrowser, "about:blank");
|
||||||
|
|
||||||
|
await waitForMessage(port, "RemotePage:Unload");
|
||||||
|
|
||||||
|
// Port should be destroyed now
|
||||||
|
try {
|
||||||
|
port.addMessageListener("Foo", failOnMessage);
|
||||||
|
ok(false, "Should have seen exception");
|
||||||
|
} catch (e) {
|
||||||
|
ok(true, "Should have seen exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
port.sendAsyncMessage("Foo");
|
||||||
|
ok(false, "Should have seen exception");
|
||||||
|
} catch (e) {
|
||||||
|
ok(true, "Should have seen exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
await loaded;
|
||||||
|
|
||||||
|
gBrowser.goBack();
|
||||||
|
port = await waitForPort(TEST_URL, false);
|
||||||
|
|
||||||
|
port.sendAsyncMessage("Ping2");
|
||||||
|
await waitForMessage(port, "Pong2");
|
||||||
|
port.destroy();
|
||||||
|
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that opening a page creates a port, sends the load event and then
|
||||||
|
// closing the tab sends the unload event
|
||||||
|
add_task(async function init_close() {
|
||||||
|
let port = await waitForPort(TEST_URL);
|
||||||
|
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
|
||||||
|
|
||||||
|
let unloadPromise = waitForMessage(port, "RemotePage:Unload");
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
await unloadPromise;
|
||||||
|
|
||||||
|
// Port should be destroyed now
|
||||||
|
try {
|
||||||
|
port.addMessageListener("Foo", failOnMessage);
|
||||||
|
ok(false, "Should have seen exception");
|
||||||
|
} catch (e) {
|
||||||
|
ok(true, "Should have seen exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
port.sendAsyncMessage("Foo");
|
||||||
|
ok(false, "Should have seen exception");
|
||||||
|
} catch (e) {
|
||||||
|
ok(true, "Should have seen exception");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tests that we can send messages to individual pages even when more than one
|
||||||
|
// is open
|
||||||
|
add_task(async function multiple_ports() {
|
||||||
|
let port1 = await waitForPort(TEST_URL);
|
||||||
|
is(
|
||||||
|
port1.browser,
|
||||||
|
gBrowser.selectedBrowser,
|
||||||
|
"Port is for the correct browser"
|
||||||
|
);
|
||||||
|
|
||||||
|
let port2 = await waitForPort(TEST_URL);
|
||||||
|
is(
|
||||||
|
port2.browser,
|
||||||
|
gBrowser.selectedBrowser,
|
||||||
|
"Port is for the correct browser"
|
||||||
|
);
|
||||||
|
|
||||||
|
port2.addMessageListener("Pong", failOnMessage);
|
||||||
|
port1.sendAsyncMessage("Ping", { str: "foobar", counter: 0 });
|
||||||
|
let message = await waitForMessage(port1, "Pong");
|
||||||
|
port2.removeMessageListener("Pong", failOnMessage);
|
||||||
|
is(message.data.str, "foobar", "String should pass through");
|
||||||
|
is(message.data.counter, 1, "Counter should be incremented");
|
||||||
|
|
||||||
|
port1.addMessageListener("Pong", failOnMessage);
|
||||||
|
port2.sendAsyncMessage("Ping", { str: "foobaz", counter: 5 });
|
||||||
|
message = await waitForMessage(port2, "Pong");
|
||||||
|
port1.removeMessageListener("Pong", failOnMessage);
|
||||||
|
is(message.data.str, "foobaz", "String should pass through");
|
||||||
|
is(message.data.counter, 6, "Counter should be incremented");
|
||||||
|
|
||||||
|
let unloadPromise = waitForMessage(port2, "RemotePage:Unload");
|
||||||
|
gBrowser.removeTab(gBrowser.getTabForBrowser(port2.browser));
|
||||||
|
await unloadPromise;
|
||||||
|
|
||||||
|
try {
|
||||||
|
port2.addMessageListener("Pong", failOnMessage);
|
||||||
|
ok(
|
||||||
|
false,
|
||||||
|
"Should not have been able to add a new message listener to a destroyed port."
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
ok(
|
||||||
|
true,
|
||||||
|
"Should not have been able to add a new message listener to a destroyed port."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
port1.sendAsyncMessage("Ping", { str: "foobar", counter: 0 });
|
||||||
|
message = await waitForMessage(port1, "Pong");
|
||||||
|
is(message.data.str, "foobar", "String should pass through");
|
||||||
|
is(message.data.counter, 1, "Counter should be incremented");
|
||||||
|
|
||||||
|
unloadPromise = waitForMessage(port1, "RemotePage:Unload");
|
||||||
|
gBrowser.removeTab(gBrowser.getTabForBrowser(port1.browser));
|
||||||
|
await unloadPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tests that swapping browser docshells doesn't break the ports
|
||||||
|
add_task(async function browser_switch() {
|
||||||
|
let port1 = await waitForPort(TEST_URL);
|
||||||
|
is(
|
||||||
|
port1.browser,
|
||||||
|
gBrowser.selectedBrowser,
|
||||||
|
"Port is for the correct browser"
|
||||||
|
);
|
||||||
|
let browser1 = gBrowser.selectedBrowser;
|
||||||
|
port1.sendAsyncMessage("SetCookie", { value: "om nom" });
|
||||||
|
|
||||||
|
let port2 = await waitForPort(TEST_URL);
|
||||||
|
is(
|
||||||
|
port2.browser,
|
||||||
|
gBrowser.selectedBrowser,
|
||||||
|
"Port is for the correct browser"
|
||||||
|
);
|
||||||
|
let browser2 = gBrowser.selectedBrowser;
|
||||||
|
port2.sendAsyncMessage("SetCookie", { value: "om nom nom" });
|
||||||
|
|
||||||
|
port2.addMessageListener("Cookie", failOnMessage);
|
||||||
|
port1.sendAsyncMessage("GetCookie");
|
||||||
|
let message = await waitForMessage(port1, "Cookie");
|
||||||
|
port2.removeMessageListener("Cookie", failOnMessage);
|
||||||
|
is(message.data.value, "om nom", "Should have the right cookie");
|
||||||
|
|
||||||
|
port1.addMessageListener("Cookie", failOnMessage);
|
||||||
|
port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
|
||||||
|
message = await waitForMessage(port2, "Cookie");
|
||||||
|
port1.removeMessageListener("Cookie", failOnMessage);
|
||||||
|
is(message.data.value, "om nom nom", "Should have the right cookie");
|
||||||
|
|
||||||
|
swapDocShells(browser1, browser2);
|
||||||
|
is(port1.browser, browser2, "Should have noticed the swap");
|
||||||
|
is(port2.browser, browser1, "Should have noticed the swap");
|
||||||
|
|
||||||
|
// Cookies should have stayed the same
|
||||||
|
port2.addMessageListener("Cookie", failOnMessage);
|
||||||
|
port1.sendAsyncMessage("GetCookie");
|
||||||
|
message = await waitForMessage(port1, "Cookie");
|
||||||
|
port2.removeMessageListener("Cookie", failOnMessage);
|
||||||
|
is(message.data.value, "om nom", "Should have the right cookie");
|
||||||
|
|
||||||
|
port1.addMessageListener("Cookie", failOnMessage);
|
||||||
|
port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
|
||||||
|
message = await waitForMessage(port2, "Cookie");
|
||||||
|
port1.removeMessageListener("Cookie", failOnMessage);
|
||||||
|
is(message.data.value, "om nom nom", "Should have the right cookie");
|
||||||
|
|
||||||
|
swapDocShells(browser1, browser2);
|
||||||
|
is(port1.browser, browser1, "Should have noticed the swap");
|
||||||
|
is(port2.browser, browser2, "Should have noticed the swap");
|
||||||
|
|
||||||
|
// Cookies should have stayed the same
|
||||||
|
port2.addMessageListener("Cookie", failOnMessage);
|
||||||
|
port1.sendAsyncMessage("GetCookie");
|
||||||
|
message = await waitForMessage(port1, "Cookie");
|
||||||
|
port2.removeMessageListener("Cookie", failOnMessage);
|
||||||
|
is(message.data.value, "om nom", "Should have the right cookie");
|
||||||
|
|
||||||
|
port1.addMessageListener("Cookie", failOnMessage);
|
||||||
|
port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
|
||||||
|
message = await waitForMessage(port2, "Cookie");
|
||||||
|
port1.removeMessageListener("Cookie", failOnMessage);
|
||||||
|
is(message.data.value, "om nom nom", "Should have the right cookie");
|
||||||
|
|
||||||
|
let unloadPromise = waitForMessage(port2, "RemotePage:Unload");
|
||||||
|
gBrowser.removeTab(gBrowser.getTabForBrowser(browser2));
|
||||||
|
await unloadPromise;
|
||||||
|
|
||||||
|
unloadPromise = waitForMessage(port1, "RemotePage:Unload");
|
||||||
|
gBrowser.removeTab(gBrowser.getTabForBrowser(browser1));
|
||||||
|
await unloadPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tests that removeMessageListener in chrome works
|
||||||
|
add_task(async function remove_chrome_listener() {
|
||||||
|
let port = await waitForPort(TEST_URL);
|
||||||
|
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
|
||||||
|
|
||||||
|
// This relies on messages sent arriving in the same order. Pong will be
|
||||||
|
// sent back before Pong2 so if removeMessageListener fails the test will fail
|
||||||
|
port.addMessageListener("Pong", failOnMessage);
|
||||||
|
port.removeMessageListener("Pong", failOnMessage);
|
||||||
|
port.sendAsyncMessage("Ping", { str: "remove_listener", counter: 27 });
|
||||||
|
port.sendAsyncMessage("Ping2");
|
||||||
|
await waitForMessage(port, "Pong2");
|
||||||
|
|
||||||
|
let unloadPromise = waitForMessage(port, "RemotePage:Unload");
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
await unloadPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tests that removeMessageListener in content works
|
||||||
|
add_task(async function remove_content_listener() {
|
||||||
|
let port = await waitForPort(TEST_URL);
|
||||||
|
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
|
||||||
|
|
||||||
|
// This relies on messages sent arriving in the same order. Pong3 would be
|
||||||
|
// sent back before Pong2 so if removeMessageListener fails the test will fail
|
||||||
|
port.addMessageListener("Pong3", failOnMessage);
|
||||||
|
port.sendAsyncMessage("Ping3");
|
||||||
|
port.sendAsyncMessage("Ping2");
|
||||||
|
await waitForMessage(port, "Pong2");
|
||||||
|
|
||||||
|
let unloadPromise = waitForMessage(port, "RemotePage:Unload");
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
await unloadPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test RemotePages works
|
||||||
|
add_task(async function remote_pages_basic() {
|
||||||
|
let pages = new RemotePages(TEST_URL);
|
||||||
|
let port = await waitForPage(pages);
|
||||||
|
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
|
||||||
|
|
||||||
|
// Listening to global messages should work
|
||||||
|
let unloadPromise = waitForMessage(pages, "RemotePage:Unload", port);
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
await unloadPromise;
|
||||||
|
|
||||||
|
pages.destroy();
|
||||||
|
|
||||||
|
// RemotePages should be destroyed now
|
||||||
|
try {
|
||||||
|
pages.addMessageListener("Foo", failOnMessage);
|
||||||
|
ok(false, "Should have seen exception");
|
||||||
|
} catch (e) {
|
||||||
|
ok(true, "Should have seen exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
pages.sendAsyncMessage("Foo");
|
||||||
|
ok(false, "Should have seen exception");
|
||||||
|
} catch (e) {
|
||||||
|
ok(true, "Should have seen exception");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that properties exist on the target port provided to listeners
|
||||||
|
add_task(async function check_port_properties() {
|
||||||
|
let pages = new RemotePages(TEST_URL);
|
||||||
|
|
||||||
|
const expectedProperties = [
|
||||||
|
"addMessageListener",
|
||||||
|
"browser",
|
||||||
|
"destroy",
|
||||||
|
"loaded",
|
||||||
|
"portID",
|
||||||
|
"removeMessageListener",
|
||||||
|
"sendAsyncMessage",
|
||||||
|
"url",
|
||||||
|
];
|
||||||
|
function checkProperties(port, description) {
|
||||||
|
const expected = [];
|
||||||
|
const unexpected = [];
|
||||||
|
for (const key in port) {
|
||||||
|
(expectedProperties.includes(key) ? expected : unexpected).push(key);
|
||||||
|
}
|
||||||
|
is(
|
||||||
|
`${expected.sort()}`,
|
||||||
|
`${expectedProperties}`,
|
||||||
|
`${description} has expected keys`
|
||||||
|
);
|
||||||
|
is(
|
||||||
|
`${unexpected.sort()}`,
|
||||||
|
"",
|
||||||
|
`${description} should not have unexpected keys`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function portFrom(message, extraFn = () => {}) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
function onMessage({ target }) {
|
||||||
|
pages.removeMessageListener(message, onMessage);
|
||||||
|
resolve(target);
|
||||||
|
}
|
||||||
|
pages.addMessageListener(message, onMessage);
|
||||||
|
extraFn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let portFromInit = await portFrom(
|
||||||
|
"RemotePage:Init",
|
||||||
|
() => (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, TEST_URL))
|
||||||
|
);
|
||||||
|
checkProperties(portFromInit, "inited port");
|
||||||
|
ok(
|
||||||
|
["about:blank", TEST_URL].includes(portFromInit.browser.currentURI.spec),
|
||||||
|
`inited port browser is either still blank or already at the target url - got ${portFromInit.browser.currentURI.spec}`
|
||||||
|
);
|
||||||
|
is(portFromInit.loaded, false, "inited port has not been loaded yet");
|
||||||
|
is(portFromInit.url, TEST_URL, "got expected url");
|
||||||
|
|
||||||
|
let portFromLoad = await portFrom("RemotePage:Load");
|
||||||
|
is(portFromLoad, portFromInit, "got the same port from init and load");
|
||||||
|
checkProperties(portFromLoad, "loaded port");
|
||||||
|
is(
|
||||||
|
portFromInit.browser.currentURI.spec,
|
||||||
|
TEST_URL,
|
||||||
|
"loaded port has browser with actual url"
|
||||||
|
);
|
||||||
|
is(portFromInit.loaded, true, "loaded port is now loaded");
|
||||||
|
is(portFromInit.url, TEST_URL, "still got expected url");
|
||||||
|
|
||||||
|
let portFromUnload = await portFrom("RemotePage:Unload", () =>
|
||||||
|
BrowserTestUtils.removeTab(gBrowser.selectedTab)
|
||||||
|
);
|
||||||
|
is(portFromUnload, portFromInit, "got the same port from init and unload");
|
||||||
|
checkProperties(portFromUnload, "unloaded port");
|
||||||
|
is(portFromInit.browser, null, "unloaded port has no browser");
|
||||||
|
is(portFromInit.loaded, false, "unloaded port is now not loaded");
|
||||||
|
is(portFromInit.url, TEST_URL, "still got expected url");
|
||||||
|
|
||||||
|
pages.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test sending messages to all remote pages works
|
||||||
|
add_task(async function remote_pages_multiple_pages() {
|
||||||
|
let pages = new RemotePages(TEST_URL);
|
||||||
|
let port1 = await waitForPage(pages);
|
||||||
|
let port2 = await waitForPage(pages);
|
||||||
|
|
||||||
|
let pongPorts = [];
|
||||||
|
await new Promise(resolve => {
|
||||||
|
function listener({ name, target, data }) {
|
||||||
|
is(name, "Pong", "Should have seen the right response.");
|
||||||
|
is(data.str, "remote_pages", "String should pass through");
|
||||||
|
is(data.counter, 43, "Counter should be incremented");
|
||||||
|
pongPorts.push(target);
|
||||||
|
if (pongPorts.length == 2) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.addMessageListener("Pong", listener);
|
||||||
|
pages.sendAsyncMessage("Ping", { str: "remote_pages", counter: 42 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// We don't make any guarantees about which order messages are sent to known
|
||||||
|
// pages so the pongs could have come back in any order.
|
||||||
|
isnot(
|
||||||
|
pongPorts[0],
|
||||||
|
pongPorts[1],
|
||||||
|
"Should have received pongs from different ports"
|
||||||
|
);
|
||||||
|
ok(pongPorts.includes(port1), "Should have seen a pong from port1");
|
||||||
|
ok(pongPorts.includes(port2), "Should have seen a pong from port2");
|
||||||
|
|
||||||
|
// After destroy we should see no messages
|
||||||
|
pages.addMessageListener("RemotePage:Unload", failOnMessage);
|
||||||
|
pages.destroy();
|
||||||
|
|
||||||
|
gBrowser.removeTab(gBrowser.getTabForBrowser(port1.browser));
|
||||||
|
gBrowser.removeTab(gBrowser.getTabForBrowser(port2.browser));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that RemotePages with multiple urls works
|
||||||
|
add_task(async function remote_pages_multiple_urls() {
|
||||||
|
const TEST_URLS = [TEST_URL, TEST_URL.replace(".html", "2.html")];
|
||||||
|
const pages = new RemotePages(TEST_URLS);
|
||||||
|
|
||||||
|
const ports = [];
|
||||||
|
// Load two pages for each url
|
||||||
|
for (const [i, url] of TEST_URLS.entries()) {
|
||||||
|
const port = await waitForPage(pages, url);
|
||||||
|
is(
|
||||||
|
port.browser,
|
||||||
|
gBrowser.selectedBrowser,
|
||||||
|
`port${i} is for the correct browser`
|
||||||
|
);
|
||||||
|
ports.push(port);
|
||||||
|
ports.push(await waitForPage(pages, url));
|
||||||
|
}
|
||||||
|
|
||||||
|
let unloadPromise = waitForMessage(pages, "RemotePage:Unload", ports.pop());
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
await unloadPromise;
|
||||||
|
|
||||||
|
const pongPorts = new Set();
|
||||||
|
await new Promise(resolve => {
|
||||||
|
function listener({ name, target, data }) {
|
||||||
|
is(name, "Pong", "Should have seen the right response.");
|
||||||
|
is(data.str, "FAKE_DATA", "String should pass through");
|
||||||
|
is(data.counter, 1235, "Counter should be incremented");
|
||||||
|
pongPorts.add(target);
|
||||||
|
if (pongPorts.size === ports.length) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.addMessageListener("Pong", listener);
|
||||||
|
pages.sendAsyncMessage("Ping", { str: "FAKE_DATA", counter: 1234 });
|
||||||
|
});
|
||||||
|
|
||||||
|
ports.forEach(port => ok(pongPorts.has(port)));
|
||||||
|
|
||||||
|
pages.destroy();
|
||||||
|
ports.forEach(port =>
|
||||||
|
gBrowser.removeTab(gBrowser.getTabForBrowser(port.browser))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test sending various types of data across the boundary
|
||||||
|
add_task(async function send_data() {
|
||||||
|
let port = await waitForPort(TEST_URL);
|
||||||
|
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
integer: 45,
|
||||||
|
real: 45.78,
|
||||||
|
str: "foobar",
|
||||||
|
array: [1, 2, 3, 5, 27],
|
||||||
|
};
|
||||||
|
|
||||||
|
port.sendAsyncMessage("SendData", data);
|
||||||
|
let message = await waitForMessage(port, "ReceivedData");
|
||||||
|
|
||||||
|
ok(message.data.result, message.data.status);
|
||||||
|
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test sending an object of data across the boundary
|
||||||
|
add_task(async function send_data2() {
|
||||||
|
let port = await waitForPort(TEST_URL);
|
||||||
|
is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
integer: 45,
|
||||||
|
real: 45.78,
|
||||||
|
str: "foobar",
|
||||||
|
array: [1, 2, 3, 5, 27],
|
||||||
|
};
|
||||||
|
|
||||||
|
port.sendAsyncMessage("SendData2", { data });
|
||||||
|
let message = await waitForMessage(port, "ReceivedData2");
|
||||||
|
|
||||||
|
ok(message.data.result, message.data.status);
|
||||||
|
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function get_ports_for_browser() {
|
||||||
|
let pages = new RemotePages(TEST_URL);
|
||||||
|
let port = await waitForPage(pages);
|
||||||
|
// waitForPage creates a new tab and selects it by default, so
|
||||||
|
// the selected tab should be the one hosting this port.
|
||||||
|
let browser = gBrowser.selectedBrowser;
|
||||||
|
let foundPorts = pages.portsForBrowser(browser);
|
||||||
|
is(
|
||||||
|
foundPorts.length,
|
||||||
|
1,
|
||||||
|
"There should only be one port for this simple page"
|
||||||
|
);
|
||||||
|
is(foundPorts[0], port, "Should find the port");
|
||||||
|
|
||||||
|
pages.destroy();
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
});
|
@ -0,0 +1,68 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript">
|
||||||
|
/* global RPMAddMessageListener, RPMSendAsyncMessage, RPMRemoveMessageListener */
|
||||||
|
|
||||||
|
RPMAddMessageListener("Ping", function(message) {
|
||||||
|
RPMSendAsyncMessage("Pong", {
|
||||||
|
str: message.data.str,
|
||||||
|
counter: message.data.counter + 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPMAddMessageListener("Ping2", function(message) {
|
||||||
|
RPMSendAsyncMessage("Pong2", message.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
function neverCalled() {
|
||||||
|
RPMSendAsyncMessage("Pong3");
|
||||||
|
}
|
||||||
|
RPMAddMessageListener("Pong3", neverCalled);
|
||||||
|
RPMRemoveMessageListener("Pong3", neverCalled);
|
||||||
|
|
||||||
|
function testData(data) {
|
||||||
|
var response = {
|
||||||
|
result: true,
|
||||||
|
status: "All data correctly received",
|
||||||
|
};
|
||||||
|
|
||||||
|
function compare(prop, expected) {
|
||||||
|
if (JSON.stringify(data[prop]) == JSON.stringify(expected))
|
||||||
|
return;
|
||||||
|
if (response.result)
|
||||||
|
response.status = "";
|
||||||
|
response.result = false;
|
||||||
|
response.status += "Property " + prop + " should have been " + expected + " but was " + data[prop] + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
compare("integer", 45);
|
||||||
|
compare("real", 45.78);
|
||||||
|
compare("str", "foobar");
|
||||||
|
compare("array", [1, 2, 3, 5, 27]);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
RPMAddMessageListener("SendData", function(message) {
|
||||||
|
RPMSendAsyncMessage("ReceivedData", testData(message.data));
|
||||||
|
});
|
||||||
|
|
||||||
|
RPMAddMessageListener("SendData2", function(message) {
|
||||||
|
RPMSendAsyncMessage("ReceivedData2", testData(message.data.data));
|
||||||
|
});
|
||||||
|
|
||||||
|
var cookie = "nom";
|
||||||
|
RPMAddMessageListener("SetCookie", function(message) {
|
||||||
|
cookie = message.data.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
RPMAddMessageListener("GetCookie", function(message) {
|
||||||
|
RPMSendAsyncMessage("Cookie", { value: cookie });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<!-- A second page to test that RemotePages works with multiple urls -->
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript">
|
||||||
|
/* global RPMAddMessageListener, RPMSendAsyncMessage */
|
||||||
|
|
||||||
|
RPMAddMessageListener("Ping", function(message) {
|
||||||
|
RPMSendAsyncMessage("Pong", {
|
||||||
|
str: message.data.str,
|
||||||
|
counter: message.data.counter + 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -233,6 +233,7 @@ avoid-blacklist-and-whitelist:
|
|||||||
- toolkit/actors/RemotePageChild.sys.mjs
|
- toolkit/actors/RemotePageChild.sys.mjs
|
||||||
- toolkit/actors/WebChannelChild.sys.mjs
|
- toolkit/actors/WebChannelChild.sys.mjs
|
||||||
- toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js
|
- toolkit/components/antitracking/test/browser/browser_socialtracking_save_image.js
|
||||||
|
- toolkit/components/remotepagemanager/RemotePageManagerParent.sys.mjs
|
||||||
- toolkit/components/reputationservice/ApplicationReputation.cpp
|
- toolkit/components/reputationservice/ApplicationReputation.cpp
|
||||||
- toolkit/components/reputationservice/chromium/chrome/common/safe_browsing/csd.pb.h
|
- toolkit/components/reputationservice/chromium/chrome/common/safe_browsing/csd.pb.h
|
||||||
- toolkit/components/reputationservice/LoginReputation.cpp
|
- toolkit/components/reputationservice/LoginReputation.cpp
|
||||||
|
Loading…
Reference in New Issue
Block a user