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/. */
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.
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");
18 * WebChannelBroker is a global object that helps manage WebChannel objects.
19 * This object handles channel registration, origin validation and message multiplexing.
22 var WebChannelBroker = Object.create({
24 * Register a new channel that callbacks messages
25 * based on proper origin and channel name
27 * @param channel {WebChannel}
29 registerChannel(channel) {
30 if (!this._channelMap.has(channel)) {
31 this._channelMap.set(channel);
33 Cu.reportError("Failed to register the channel. Channel already exists.");
38 * Unregister a channel
40 * @param channelToRemove {WebChannel}
41 * WebChannel to remove from the channel map
43 * Removes the specified channel from the channel map
45 unregisterChannel(channelToRemove) {
46 if (!this._channelMap.delete(channelToRemove)) {
47 Cu.reportError("Failed to unregister the channel. Channel not found.");
52 * Object to store pairs of message origins and callback functions
54 _channelMap: new Map(),
57 * Deliver a message to a registered channel.
59 * @returns bool whether we managed to find a registered channel.
61 tryToDeliver(data, sendingContext) {
62 let validChannelFound = false;
63 data.message = data.message || {};
65 for (var channel of this._channelMap.keys()) {
67 channel.id === data.id &&
68 channel._originCheckCallback(sendingContext.principal)
70 validChannelFound = true;
71 channel.deliver(data, sendingContext);
74 return validChannelFound;
79 * Creates a new WebChannel that listens and sends messages over some channel id
83 * @param originOrPermission {nsIURI/string}
84 * If an nsIURI, incoming events will be accepted from any origin matching
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
91 var WebChannel = function(id, originOrPermission) {
92 if (!id || !originOrPermission) {
93 throw new Error("WebChannel id and originOrPermission are required.");
97 // originOrPermission can be either an nsIURI or a string representing a
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") {
109 // OK - we have https - now we can check the permission.
110 let perm = Services.perms.testExactPermissionFromPrincipal(
114 return perm == Ci.nsIPermissionManager.ALLOW_ACTION;
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;
125 this._originOrPermission = originOrPermission;
128 this.WebChannel.prototype = {
135 * The originOrPermission value passed to the constructor, mainly for
136 * debugging and tests.
138 _originOrPermission: null,
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.
144 _originCheckCallback: null,
147 * WebChannelBroker that manages WebChannels
149 _broker: WebChannelBroker,
152 * Callback that will be called with the contents of an incoming message
154 _deliverCallback: null,
157 * Registers the callback for messages on this channel
158 * Registers the channel itself with the WebChannelBroker
160 * @param callback {Function}
161 * Callback that will be called when there is a message
163 * The WebChannel id that was used for this message
164 * @param {Object} message
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
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.");
184 this._deliverCallback = callback;
185 this._broker.registerChannel(this);
190 * Resets the callback for messages on this channel
191 * Removes the channel from the WebChannelBroker
194 this._broker.unregisterChannel(this);
195 this._deliverCallback = null;
199 * Sends messages over the WebChannel id using the "WebChannelMessageToContent" event
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.
216 send(message, target) {
217 let { browsingContext, principal, eventTarget } = target;
219 if (message && browsingContext && principal) {
220 let { currentWindowGlobal } = browsingContext;
221 if (!currentWindowGlobal) {
223 "Failed to send a WebChannel message. No currentWindowGlobal."
228 .getActor("WebChannel")
229 .sendAsyncMessage("WebChannelMessageToContent", {
235 } else if (!message) {
236 Cu.reportError("Failed to send a WebChannel message. Message not set.");
238 Cu.reportError("Failed to send a WebChannel message. Target invalid.");
243 * Deliver WebChannel messages to the set "_channelCallback"
245 * @param data {Object}
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.
259 deliver(data, sendingContext) {
260 if (this._deliverCallback) {
262 this._deliverCallback(data.id, data.message, sendingContext);
266 errno: ERRNO_UNKNOWN_ERROR,
267 error: ex.message ? ex.message : ERROR_UNKNOWN,
271 Cu.reportError("Failed to execute WebChannel callback:");
275 Cu.reportError("No callback set for this channel.");