/* 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"; /* BrowserElementParent injects script to listen for certain events in the * child. We then listen to messages from the child script and take * appropriate action here in the parent. */ const { BrowserElementPromptService } = ChromeUtils.import( "resource://gre/modules/BrowserElementPromptService.jsm" ); function debug(msg) { // dump("BrowserElementParent - " + msg + "\n"); } function handleWindowEvent(e) { if (this._browserElementParents) { let beps = ChromeUtils.nondeterministicGetWeakMapKeys( this._browserElementParents ); beps.forEach(bep => bep._handleOwnerEvent(e)); } } function BrowserElementParent() { debug("Creating new BrowserElementParent object"); } BrowserElementParent.prototype = { classDescription: "BrowserElementAPI implementation", classID: Components.ID("{9f171ac4-0939-4ef8-b360-3408aedc3060}"), contractID: "@mozilla.org/dom/browser-element-api;1", QueryInterface: ChromeUtils.generateQI([ "nsIBrowserElementAPI", "nsISupportsWeakReference", ]), setFrameLoader(frameLoader) { debug("Setting frameLoader"); this._frameLoader = frameLoader; this._frameElement = frameLoader.ownerElement; if (!this._frameElement) { debug("No frame element?"); return; } // Listen to visibilitychange on the iframe's owner window, and forward // changes down to the child. We want to do this while registering as few // visibilitychange listeners on _window as possible, because such a listener // may live longer than this BrowserElementParent object. // // To accomplish this, we register just one listener on the window, and have // it reference a WeakMap whose keys are all the BrowserElementParent objects // on the window. Then when the listener fires, we iterate over the // WeakMap's keys (which we can do, because we're chrome) to notify the // BrowserElementParents. if (!this._window._browserElementParents) { this._window._browserElementParents = new WeakMap(); let handler = handleWindowEvent.bind(this._window); let windowEvents = ["visibilitychange"]; for (let event of windowEvents) { Services.els.addSystemEventListener( this._window, event, handler, /* useCapture = */ true ); } } this._window._browserElementParents.set(this, null); // Insert ourself into the prompt service. BrowserElementPromptService.mapFrameToBrowserElementParent( this._frameElement, this ); this._setupMessageListener(); }, destroyFrameScripts() { debug("Destroying frame scripts"); this._mm.sendAsyncMessage("browser-element-api:destroy"); }, _setupMessageListener() { this._mm = this._frameLoader.messageManager; this._mm.addMessageListener("browser-element-api:call", this); }, receiveMessage(aMsg) { if (!this._isAlive()) { return undefined; } // Messages we receive are handed to functions which take a (data) argument, // where |data| is the message manager's data object. // We use a single message and dispatch to various function based // on data.msg_name let mmCalls = { hello: this._recvHello, }; let mmSecuritySensitiveCalls = { showmodalprompt: this._handleShowModalPrompt, }; if (aMsg.data.msg_name in mmCalls) { return mmCalls[aMsg.data.msg_name].apply(this, arguments); } else if (aMsg.data.msg_name in mmSecuritySensitiveCalls) { return mmSecuritySensitiveCalls[aMsg.data.msg_name].apply( this, arguments ); } return undefined; }, _removeMessageListener() { this._mm.removeMessageListener("browser-element-api:call", this); }, /** * You shouldn't touch this._frameElement or this._window if _isAlive is * false. (You'll likely get an exception if you do.) */ _isAlive() { return ( !Cu.isDeadWrapper(this._frameElement) && !Cu.isDeadWrapper(this._frameElement.ownerDocument) && !Cu.isDeadWrapper(this._frameElement.ownerGlobal) ); }, get _window() { return this._frameElement.ownerGlobal; }, _sendAsyncMsg(msg, data) { try { if (!data) { data = {}; } data.msg_name = msg; this._mm.sendAsyncMessage("browser-element-api:call", data); } catch (e) { return false; } return true; }, _recvHello() { debug("recvHello"); // Inform our child if our owner element's document is invisible. Note // that we must do so here, rather than in the BrowserElementParent // constructor, because the BrowserElementChild may not be initialized when // we run our constructor. if (this._window.document.hidden) { this._ownerVisibilityChange(); } }, /** * Fire either a vanilla or a custom event, depending on the contents of * |data|. */ _fireEventFromMsg(data) { let detail = data.json; let name = detail.msg_name; // For events that send a "_payload_" property, we just want to transmit // this in the event. if ("_payload_" in detail) { detail = detail._payload_; } debug("fireEventFromMsg: " + name + ", " + JSON.stringify(detail)); let evt = this._createEvent(name, detail, /* cancelable = */ false); this._frameElement.dispatchEvent(evt); }, _handleShowModalPrompt(data) { // Fire a showmodalprmopt event on the iframe. When this method is called, // the child is spinning in a nested event loop waiting for an // unblock-modal-prompt message. // // If the embedder calls preventDefault() on the showmodalprompt event, // we'll block the child until event.detail.unblock() is called. // // Otherwise, if preventDefault() is not called, we'll send the // unblock-modal-prompt message to the child as soon as the event is done // dispatching. let detail = data.json; debug("handleShowPrompt " + JSON.stringify(detail)); // Strip off the windowID property from the object we send along in the // event. let windowID = detail.windowID; delete detail.windowID; debug("Event will have detail: " + JSON.stringify(detail)); let evt = this._createEvent( "showmodalprompt", detail, /* cancelable = */ true ); let self = this; let unblockMsgSent = false; function sendUnblockMsg() { if (unblockMsgSent) { return; } unblockMsgSent = true; // We don't need to sanitize evt.detail.returnValue (e.g. converting the // return value of confirm() to a boolean); Gecko does that for us. let data = { windowID, returnValue: evt.detail.returnValue }; self._sendAsyncMsg("unblock-modal-prompt", data); } Cu.exportFunction(sendUnblockMsg, evt.detail, { defineAs: "unblock" }); this._frameElement.dispatchEvent(evt); if (!evt.defaultPrevented) { // Unblock the inner frame immediately. Otherwise we'll unblock upon // evt.detail.unblock(). sendUnblockMsg(); } }, _createEvent(evtName, detail, cancelable) { // This will have to change if we ever want to send a CustomEvent with null // detail. For now, it's OK. if (detail !== undefined && detail !== null) { detail = Cu.cloneInto(detail, this._window); return new this._window.CustomEvent("mozbrowser" + evtName, { bubbles: true, cancelable, detail, }); } return new this._window.Event("mozbrowser" + evtName, { bubbles: true, cancelable, }); }, _handleOwnerEvent(evt) { switch (evt.type) { case "visibilitychange": this._ownerVisibilityChange(); break; } }, /** * Called when the visibility of the window which owns this iframe changes. */ _ownerVisibilityChange() { let bc = this._frameLoader?.browsingContext; if (bc) { bc.isActive = !this._window.document.hidden; } }, }; var EXPORTED_SYMBOLS = ["BrowserElementParent"];