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