/* 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. */ ChromeUtils.import("resource://gre/modules/Services.jsm"); 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. */ var RemotePages = function(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); } }; RemotePages.prototype = { urls: null, messagePorts: null, listener: null, destroyed: null, 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 function ChromeMessagePort(browser, portID, url) { MessagePort.call(this, 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); } ChromeMessagePort.prototype = Object.create(MessagePort.prototype); Object.defineProperty(ChromeMessagePort.prototype, "browser", { get() { return this._browser; }, }); Object.defineProperty(ChromeMessagePort.prototype, "url", { get() { 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 ChromeMessagePort.prototype.swapBrowsers = function({ 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 ChromeMessagePort.prototype.observe = function(messageManager) { if (messageManager != this.messageManager) return; this.listener.callListeners({ target: this.publicPort, name: "RemotePage:Unload", data: null, }); this.destroy(); }; // Called when a message is received from the message manager. This could // have come from any port in the message manager so verify the port ID. ChromeMessagePort.prototype.message = function({ data: messagedata }) { if (this.destroyed || (messagedata.portID != this.portID)) { return; } let message = { target: this.publicPort, name: messagedata.name, data: messagedata.data, }; this.listener.callListeners(message); if (messagedata.name == "RemotePage:Unload") this.destroy(); }; ChromeMessagePort.prototype.destroy = function() { 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"); MessagePort.prototype.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), };