summaryrefslogtreecommitdiffstats
path: root/dom/browser-element/BrowserElementParent.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'dom/browser-element/BrowserElementParent.jsm')
-rw-r--r--dom/browser-element/BrowserElementParent.jsm277
1 files changed, 277 insertions, 0 deletions
diff --git a/dom/browser-element/BrowserElementParent.jsm b/dom/browser-element/BrowserElementParent.jsm
new file mode 100644
index 0000000000..73d528456e
--- /dev/null
+++ b/dom/browser-element/BrowserElementParent.jsm
@@ -0,0 +1,277 @@
+/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+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"];