Bug 1850713: remove duplicated setting of early hint preloader id in `ScriptLoader...
[gecko.git] / dom / browser-element / BrowserElementParent.jsm
blobec342dace2ad17eb42a8f262a0bbda3a47152164
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 /* BrowserElementParent injects script to listen for certain events in the
8  * child.  We then listen to messages from the child script and take
9  * appropriate action here in the parent.
10  */
12 const { BrowserElementPromptService } = ChromeUtils.import(
13   "resource://gre/modules/BrowserElementPromptService.jsm"
16 function debug(msg) {
17   // dump("BrowserElementParent - " + msg + "\n");
20 function handleWindowEvent(e) {
21   if (this._browserElementParents) {
22     let beps = ChromeUtils.nondeterministicGetWeakMapKeys(
23       this._browserElementParents
24     );
25     beps.forEach(bep => bep._handleOwnerEvent(e));
26   }
29 function BrowserElementParent() {
30   debug("Creating new BrowserElementParent object");
33 BrowserElementParent.prototype = {
34   classDescription: "BrowserElementAPI implementation",
35   classID: Components.ID("{9f171ac4-0939-4ef8-b360-3408aedc3060}"),
36   contractID: "@mozilla.org/dom/browser-element-api;1",
37   QueryInterface: ChromeUtils.generateQI([
38     "nsIBrowserElementAPI",
39     "nsISupportsWeakReference",
40   ]),
42   setFrameLoader(frameLoader) {
43     debug("Setting frameLoader");
44     this._frameLoader = frameLoader;
45     this._frameElement = frameLoader.ownerElement;
46     if (!this._frameElement) {
47       debug("No frame element?");
48       return;
49     }
50     // Listen to visibilitychange on the iframe's owner window, and forward
51     // changes down to the child.  We want to do this while registering as few
52     // visibilitychange listeners on _window as possible, because such a listener
53     // may live longer than this BrowserElementParent object.
54     //
55     // To accomplish this, we register just one listener on the window, and have
56     // it reference a WeakMap whose keys are all the BrowserElementParent objects
57     // on the window.  Then when the listener fires, we iterate over the
58     // WeakMap's keys (which we can do, because we're chrome) to notify the
59     // BrowserElementParents.
60     if (!this._window._browserElementParents) {
61       this._window._browserElementParents = new WeakMap();
62       let handler = handleWindowEvent.bind(this._window);
63       let windowEvents = ["visibilitychange"];
64       for (let event of windowEvents) {
65         Services.els.addSystemEventListener(
66           this._window,
67           event,
68           handler,
69           /* useCapture = */ true
70         );
71       }
72     }
74     this._window._browserElementParents.set(this, null);
76     // Insert ourself into the prompt service.
77     BrowserElementPromptService.mapFrameToBrowserElementParent(
78       this._frameElement,
79       this
80     );
81     this._setupMessageListener();
82   },
84   destroyFrameScripts() {
85     debug("Destroying frame scripts");
86     this._mm.sendAsyncMessage("browser-element-api:destroy");
87   },
89   _setupMessageListener() {
90     this._mm = this._frameLoader.messageManager;
91     this._mm.addMessageListener("browser-element-api:call", this);
92   },
94   receiveMessage(aMsg) {
95     if (!this._isAlive()) {
96       return undefined;
97     }
99     // Messages we receive are handed to functions which take a (data) argument,
100     // where |data| is the message manager's data object.
101     // We use a single message and dispatch to various function based
102     // on data.msg_name
103     let mmCalls = {
104       hello: this._recvHello,
105     };
107     let mmSecuritySensitiveCalls = {
108       showmodalprompt: this._handleShowModalPrompt,
109     };
111     if (aMsg.data.msg_name in mmCalls) {
112       return mmCalls[aMsg.data.msg_name].apply(this, arguments);
113     } else if (aMsg.data.msg_name in mmSecuritySensitiveCalls) {
114       return mmSecuritySensitiveCalls[aMsg.data.msg_name].apply(
115         this,
116         arguments
117       );
118     }
119     return undefined;
120   },
122   _removeMessageListener() {
123     this._mm.removeMessageListener("browser-element-api:call", this);
124   },
126   /**
127    * You shouldn't touch this._frameElement or this._window if _isAlive is
128    * false.  (You'll likely get an exception if you do.)
129    */
130   _isAlive() {
131     return (
132       !Cu.isDeadWrapper(this._frameElement) &&
133       !Cu.isDeadWrapper(this._frameElement.ownerDocument) &&
134       !Cu.isDeadWrapper(this._frameElement.ownerGlobal)
135     );
136   },
138   get _window() {
139     return this._frameElement.ownerGlobal;
140   },
142   _sendAsyncMsg(msg, data) {
143     try {
144       if (!data) {
145         data = {};
146       }
148       data.msg_name = msg;
149       this._mm.sendAsyncMessage("browser-element-api:call", data);
150     } catch (e) {
151       return false;
152     }
153     return true;
154   },
156   _recvHello() {
157     debug("recvHello");
159     // Inform our child if our owner element's document is invisible.  Note
160     // that we must do so here, rather than in the BrowserElementParent
161     // constructor, because the BrowserElementChild may not be initialized when
162     // we run our constructor.
163     if (this._window.document.hidden) {
164       this._ownerVisibilityChange();
165     }
166   },
168   /**
169    * Fire either a vanilla or a custom event, depending on the contents of
170    * |data|.
171    */
172   _fireEventFromMsg(data) {
173     let detail = data.json;
174     let name = detail.msg_name;
176     // For events that send a "_payload_" property, we just want to transmit
177     // this in the event.
178     if ("_payload_" in detail) {
179       detail = detail._payload_;
180     }
182     debug("fireEventFromMsg: " + name + ", " + JSON.stringify(detail));
183     let evt = this._createEvent(name, detail, /* cancelable = */ false);
184     this._frameElement.dispatchEvent(evt);
185   },
187   _handleShowModalPrompt(data) {
188     // Fire a showmodalprmopt event on the iframe.  When this method is called,
189     // the child is spinning in a nested event loop waiting for an
190     // unblock-modal-prompt message.
191     //
192     // If the embedder calls preventDefault() on the showmodalprompt event,
193     // we'll block the child until event.detail.unblock() is called.
194     //
195     // Otherwise, if preventDefault() is not called, we'll send the
196     // unblock-modal-prompt message to the child as soon as the event is done
197     // dispatching.
199     let detail = data.json;
200     debug("handleShowPrompt " + JSON.stringify(detail));
202     // Strip off the windowID property from the object we send along in the
203     // event.
204     let windowID = detail.windowID;
205     delete detail.windowID;
206     debug("Event will have detail: " + JSON.stringify(detail));
207     let evt = this._createEvent(
208       "showmodalprompt",
209       detail,
210       /* cancelable = */ true
211     );
213     let self = this;
214     let unblockMsgSent = false;
215     function sendUnblockMsg() {
216       if (unblockMsgSent) {
217         return;
218       }
219       unblockMsgSent = true;
221       // We don't need to sanitize evt.detail.returnValue (e.g. converting the
222       // return value of confirm() to a boolean); Gecko does that for us.
224       let data = { windowID, returnValue: evt.detail.returnValue };
225       self._sendAsyncMsg("unblock-modal-prompt", data);
226     }
228     Cu.exportFunction(sendUnblockMsg, evt.detail, { defineAs: "unblock" });
230     this._frameElement.dispatchEvent(evt);
232     if (!evt.defaultPrevented) {
233       // Unblock the inner frame immediately.  Otherwise we'll unblock upon
234       // evt.detail.unblock().
235       sendUnblockMsg();
236     }
237   },
239   _createEvent(evtName, detail, cancelable) {
240     // This will have to change if we ever want to send a CustomEvent with null
241     // detail.  For now, it's OK.
242     if (detail !== undefined && detail !== null) {
243       detail = Cu.cloneInto(detail, this._window);
244       return new this._window.CustomEvent("mozbrowser" + evtName, {
245         bubbles: true,
246         cancelable,
247         detail,
248       });
249     }
251     return new this._window.Event("mozbrowser" + evtName, {
252       bubbles: true,
253       cancelable,
254     });
255   },
257   _handleOwnerEvent(evt) {
258     switch (evt.type) {
259       case "visibilitychange":
260         this._ownerVisibilityChange();
261         break;
262     }
263   },
265   /**
266    * Called when the visibility of the window which owns this iframe changes.
267    */
268   _ownerVisibilityChange() {
269     let bc = this._frameLoader?.browsingContext;
270     if (bc) {
271       bc.isActive = !this._window.document.hidden;
272     }
273   },
276 var EXPORTED_SYMBOLS = ["BrowserElementParent"];