summaryrefslogtreecommitdiffstats
path: root/toolkit/components/remotepagemanager/RemotePageManagerParent.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/remotepagemanager/RemotePageManagerParent.jsm')
-rw-r--r--toolkit/components/remotepagemanager/RemotePageManagerParent.jsm351
1 files changed, 351 insertions, 0 deletions
diff --git a/toolkit/components/remotepagemanager/RemotePageManagerParent.jsm b/toolkit/components/remotepagemanager/RemotePageManagerParent.jsm
new file mode 100644
index 0000000000..4d19483c7e
--- /dev/null
+++ b/toolkit/components/remotepagemanager/RemotePageManagerParent.jsm
@@ -0,0 +1,351 @@
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["RemotePages", "RemotePageManager"];
+
+/*
+ * 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.
+ */
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { MessageListener, MessagePort } = ChromeUtils.import(
+ "resource://gre/modules/remotepagemanager/MessagePort.jsm"
+);
+
+/**
+ * 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.
+ */
+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) {
+ Cu.reportError(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) {
+ Cu.reportError("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
+var RemotePageManager = {
+ addRemotePageListener: RemotePageManagerInternal.addRemotePageListener.bind(
+ RemotePageManagerInternal
+ ),
+ removeRemotePageListener: RemotePageManagerInternal.removeRemotePageListener.bind(
+ RemotePageManagerInternal
+ ),
+};