Bug 1608150 [wpt PR 21112] - Add missing space in `./wpt lint` command line docs...
[gecko.git] / toolkit / modules / WebChannel.jsm
blob725883165481000fd68da47e186326ae066ece3a
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 /**
6  * WebChannel is an abstraction that uses the Message Manager and Custom Events
7  * to create a two-way communication channel between chrome and content code.
8  */
10 var EXPORTED_SYMBOLS = ["WebChannel", "WebChannelBroker"];
12 const ERRNO_UNKNOWN_ERROR = 999;
13 const ERROR_UNKNOWN = "UNKNOWN_ERROR";
15 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
17 /**
18  * WebChannelBroker is a global object that helps manage WebChannel objects.
19  * This object handles channel registration, origin validation and message multiplexing.
20  */
22 var WebChannelBroker = Object.create({
23   /**
24    * Register a new channel that callbacks messages
25    * based on proper origin and channel name
26    *
27    * @param channel {WebChannel}
28    */
29   registerChannel(channel) {
30     if (!this._channelMap.has(channel)) {
31       this._channelMap.set(channel);
32     } else {
33       Cu.reportError("Failed to register the channel. Channel already exists.");
34     }
35   },
37   /**
38    * Unregister a channel
39    *
40    * @param channelToRemove {WebChannel}
41    *        WebChannel to remove from the channel map
42    *
43    * Removes the specified channel from the channel map
44    */
45   unregisterChannel(channelToRemove) {
46     if (!this._channelMap.delete(channelToRemove)) {
47       Cu.reportError("Failed to unregister the channel. Channel not found.");
48     }
49   },
51   /**
52    * Object to store pairs of message origins and callback functions
53    */
54   _channelMap: new Map(),
56   /**
57    * Deliver a message to a registered channel.
58    *
59    * @returns bool whether we managed to find a registered channel.
60    */
61   tryToDeliver(data, sendingContext) {
62     let validChannelFound = false;
63     data.message = data.message || {};
65     for (var channel of this._channelMap.keys()) {
66       if (
67         channel.id === data.id &&
68         channel._originCheckCallback(sendingContext.principal)
69       ) {
70         validChannelFound = true;
71         channel.deliver(data, sendingContext);
72       }
73     }
74     return validChannelFound;
75   },
76 });
78 /**
79  * Creates a new WebChannel that listens and sends messages over some channel id
80  *
81  * @param id {String}
82  *        WebChannel id
83  * @param originOrPermission {nsIURI/string}
84  *        If an nsIURI, incoming events will be accepted from any origin matching
85  *        that URI's origin.
86  *        If a string, it names a permission, and incoming events will be accepted
87  *        from any https:// origin that has been granted that permission by the
88  *        permission manager.
89  * @constructor
90  */
91 var WebChannel = function(id, originOrPermission) {
92   if (!id || !originOrPermission) {
93     throw new Error("WebChannel id and originOrPermission are required.");
94   }
96   this.id = id;
97   // originOrPermission can be either an nsIURI or a string representing a
98   // permission name.
99   if (typeof originOrPermission == "string") {
100     this._originCheckCallback = requestPrincipal => {
101       // Accept events from any secure origin having the named permission.
102       // The permission manager operates on domain names rather than true
103       // origins (bug 1066517).  To mitigate that, we explicitly check that
104       // the scheme is https://.
105       let uri = Services.io.newURI(requestPrincipal.originNoSuffix);
106       if (uri.scheme != "https") {
107         return false;
108       }
109       // OK - we have https - now we can check the permission.
110       let perm = Services.perms.testExactPermissionFromPrincipal(
111         requestPrincipal,
112         originOrPermission
113       );
114       return perm == Ci.nsIPermissionManager.ALLOW_ACTION;
115     };
116   } else {
117     // Accept events from any origin matching the given URI.
118     // We deliberately use `originNoSuffix` here because we only want to
119     // restrict based on the site's origin, not on other origin attributes
120     // such as containers or private browsing.
121     this._originCheckCallback = requestPrincipal => {
122       return originOrPermission.prePath === requestPrincipal.originNoSuffix;
123     };
124   }
125   this._originOrPermission = originOrPermission;
128 this.WebChannel.prototype = {
129   /**
130    * WebChannel id
131    */
132   id: null,
134   /**
135    * The originOrPermission value passed to the constructor, mainly for
136    * debugging and tests.
137    */
138   _originOrPermission: null,
140   /**
141    * Callback that will be called with the principal of an incoming message
142    * to check if the request should be dispatched to the listeners.
143    */
144   _originCheckCallback: null,
146   /**
147    * WebChannelBroker that manages WebChannels
148    */
149   _broker: WebChannelBroker,
151   /**
152    * Callback that will be called with the contents of an incoming message
153    */
154   _deliverCallback: null,
156   /**
157    * Registers the callback for messages on this channel
158    * Registers the channel itself with the WebChannelBroker
159    *
160    * @param callback {Function}
161    *        Callback that will be called when there is a message
162    *        @param {String} id
163    *        The WebChannel id that was used for this message
164    *        @param {Object} message
165    *        The message itself
166    *        @param sendingContext {Object}
167    *        The sending context of the source of the message. Can be passed to
168    *        `send` to respond to a message.
169    *               @param sendingContext.browser {browser}
170    *                      The <browser> object that captured the
171    *                      WebChannelMessageToChrome.
172    *               @param sendingContext.eventTarget {EventTarget}
173    *                      The <EventTarget> where the message was sent.
174    *               @param sendingContext.principal {Principal}
175    *                      The <Principal> of the EventTarget where the
176    *                      message was sent.
177    */
178   listen(callback) {
179     if (this._deliverCallback) {
180       throw new Error("Failed to listen. Listener already attached.");
181     } else if (!callback) {
182       throw new Error("Failed to listen. Callback argument missing.");
183     } else {
184       this._deliverCallback = callback;
185       this._broker.registerChannel(this);
186     }
187   },
189   /**
190    * Resets the callback for messages on this channel
191    * Removes the channel from the WebChannelBroker
192    */
193   stopListening() {
194     this._broker.unregisterChannel(this);
195     this._deliverCallback = null;
196   },
198   /**
199    * Sends messages over the WebChannel id using the "WebChannelMessageToContent" event
200    *
201    * @param message {Object}
202    *        The message object that will be sent
203    * @param target {Object}
204    *        A <target> with the information of where to send the message.
205    *        @param target.browsingContext {BrowsingContext}
206    *               The browsingContext we should send the message to.
207    *        @param target.principal {Principal}
208    *               Principal of the target. Prevents messages from
209    *               being dispatched to unexpected origins. The system principal
210    *               can be specified to send to any target.
211    *        @param [target.eventTarget] {EventTarget}
212    *               Optional eventTarget within the browser, use to send to a
213    *               specific element. Can be null; if not null, should be
214    *               a ContentDOMReference.
215    */
216   send(message, target) {
217     let { browsingContext, principal, eventTarget } = target;
219     if (message && browsingContext && principal) {
220       let { currentWindowGlobal } = browsingContext;
221       if (!currentWindowGlobal) {
222         Cu.reportError(
223           "Failed to send a WebChannel message. No currentWindowGlobal."
224         );
225         return;
226       }
227       currentWindowGlobal
228         .getActor("WebChannel")
229         .sendAsyncMessage("WebChannelMessageToContent", {
230           id: this.id,
231           message,
232           eventTarget,
233           principal,
234         });
235     } else if (!message) {
236       Cu.reportError("Failed to send a WebChannel message. Message not set.");
237     } else {
238       Cu.reportError("Failed to send a WebChannel message. Target invalid.");
239     }
240   },
242   /**
243    * Deliver WebChannel messages to the set "_channelCallback"
244    *
245    * @param data {Object}
246    *        Message data
247    * @param sendingContext {Object}
248    *        Message sending context.
249    *        @param sendingContext.browsingContext {BrowsingContext}
250    *               The browsingcontext from which the
251    *               WebChannelMessageToChrome was sent.
252    *        @param sendingContext.eventTarget {EventTarget}
253    *               The <EventTarget> where the message was sent.
254    *               Can be null; if not null, should be a ContentDOMReference.
255    *        @param sendingContext.principal {Principal}
256    *               The <Principal> of the EventTarget where the message was sent.
257    *
258    */
259   deliver(data, sendingContext) {
260     if (this._deliverCallback) {
261       try {
262         this._deliverCallback(data.id, data.message, sendingContext);
263       } catch (ex) {
264         this.send(
265           {
266             errno: ERRNO_UNKNOWN_ERROR,
267             error: ex.message ? ex.message : ERROR_UNKNOWN,
268           },
269           sendingContext
270         );
271         Cu.reportError("Failed to execute WebChannel callback:");
272         Cu.reportError(ex);
273       }
274     } else {
275       Cu.reportError("No callback set for this channel.");
276     }
277   },