summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/WebChannel.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/modules/WebChannel.sys.mjs274
1 files changed, 274 insertions, 0 deletions
diff --git a/toolkit/modules/WebChannel.sys.mjs b/toolkit/modules/WebChannel.sys.mjs
new file mode 100644
index 0000000000..1b5d725cbd
--- /dev/null
+++ b/toolkit/modules/WebChannel.sys.mjs
@@ -0,0 +1,274 @@
+/* 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/. */
+
+/**
+ * WebChannel is an abstraction that uses the Message Manager and Custom Events
+ * to create a two-way communication channel between chrome and content code.
+ */
+
+const ERRNO_UNKNOWN_ERROR = 999;
+const ERROR_UNKNOWN = "UNKNOWN_ERROR";
+
+/**
+ * WebChannelBroker is a global object that helps manage WebChannel objects.
+ * This object handles channel registration, origin validation and message multiplexing.
+ */
+
+export var WebChannelBroker = Object.create({
+ /**
+ * Register a new channel that callbacks messages
+ * based on proper origin and channel name
+ *
+ * @param channel {WebChannel}
+ */
+ registerChannel(channel) {
+ if (!this._channelMap.has(channel)) {
+ this._channelMap.set(channel);
+ } else {
+ console.error("Failed to register the channel. Channel already exists.");
+ }
+ },
+
+ /**
+ * Unregister a channel
+ *
+ * @param channelToRemove {WebChannel}
+ * WebChannel to remove from the channel map
+ *
+ * Removes the specified channel from the channel map
+ */
+ unregisterChannel(channelToRemove) {
+ if (!this._channelMap.delete(channelToRemove)) {
+ console.error("Failed to unregister the channel. Channel not found.");
+ }
+ },
+
+ /**
+ * Object to store pairs of message origins and callback functions
+ */
+ _channelMap: new Map(),
+
+ /**
+ * Deliver a message to a registered channel.
+ *
+ * @returns bool whether we managed to find a registered channel.
+ */
+ tryToDeliver(data, sendingContext) {
+ let validChannelFound = false;
+ data.message = data.message || {};
+
+ for (var channel of this._channelMap.keys()) {
+ if (
+ channel.id === data.id &&
+ channel._originCheckCallback(sendingContext.principal)
+ ) {
+ validChannelFound = true;
+ channel.deliver(data, sendingContext);
+ }
+ }
+ return validChannelFound;
+ },
+});
+
+/**
+ * Creates a new WebChannel that listens and sends messages over some channel id
+ *
+ * @param id {String}
+ * WebChannel id
+ * @param originOrPermission {nsIURI/string}
+ * If an nsIURI, incoming events will be accepted from any origin matching
+ * that URI's origin.
+ * If a string, it names a permission, and incoming events will be accepted
+ * from any https:// origin that has been granted that permission by the
+ * permission manager.
+ * @constructor
+ */
+export var WebChannel = function (id, originOrPermission) {
+ if (!id || !originOrPermission) {
+ throw new Error("WebChannel id and originOrPermission are required.");
+ }
+
+ this.id = id;
+ // originOrPermission can be either an nsIURI or a string representing a
+ // permission name.
+ if (typeof originOrPermission == "string") {
+ this._originCheckCallback = requestPrincipal => {
+ // Accept events from any secure origin having the named permission.
+ // The permission manager operates on domain names rather than true
+ // origins (bug 1066517). To mitigate that, we explicitly check that
+ // the scheme is https://.
+ let uri = Services.io.newURI(requestPrincipal.originNoSuffix);
+ if (uri.scheme != "https") {
+ return false;
+ }
+ // OK - we have https - now we can check the permission.
+ let perm = Services.perms.testExactPermissionFromPrincipal(
+ requestPrincipal,
+ originOrPermission
+ );
+ return perm == Ci.nsIPermissionManager.ALLOW_ACTION;
+ };
+ } else {
+ // Accept events from any origin matching the given URI.
+ // We deliberately use `originNoSuffix` here because we only want to
+ // restrict based on the site's origin, not on other origin attributes
+ // such as containers or private browsing.
+ this._originCheckCallback = requestPrincipal => {
+ return originOrPermission.prePath === requestPrincipal.originNoSuffix;
+ };
+ }
+ this._originOrPermission = originOrPermission;
+};
+
+WebChannel.prototype = {
+ /**
+ * WebChannel id
+ */
+ id: null,
+
+ /**
+ * The originOrPermission value passed to the constructor, mainly for
+ * debugging and tests.
+ */
+ _originOrPermission: null,
+
+ /**
+ * Callback that will be called with the principal of an incoming message
+ * to check if the request should be dispatched to the listeners.
+ */
+ _originCheckCallback: null,
+
+ /**
+ * WebChannelBroker that manages WebChannels
+ */
+ _broker: WebChannelBroker,
+
+ /**
+ * Callback that will be called with the contents of an incoming message
+ */
+ _deliverCallback: null,
+
+ /**
+ * Registers the callback for messages on this channel
+ * Registers the channel itself with the WebChannelBroker
+ *
+ * @param callback {Function}
+ * Callback that will be called when there is a message
+ * @param {String} id
+ * The WebChannel id that was used for this message
+ * @param {Object} message
+ * The message itself
+ * @param sendingContext {Object}
+ * The sending context of the source of the message. Can be passed to
+ * `send` to respond to a message.
+ * @param sendingContext.browser {browser}
+ * The <browser> object that captured the
+ * WebChannelMessageToChrome.
+ * @param sendingContext.eventTarget {EventTarget}
+ * The <EventTarget> where the message was sent.
+ * @param sendingContext.principal {Principal}
+ * The <Principal> of the EventTarget where the
+ * message was sent.
+ */
+ listen(callback) {
+ if (this._deliverCallback) {
+ throw new Error("Failed to listen. Listener already attached.");
+ } else if (!callback) {
+ throw new Error("Failed to listen. Callback argument missing.");
+ } else {
+ this._deliverCallback = callback;
+ this._broker.registerChannel(this);
+ }
+ },
+
+ /**
+ * Resets the callback for messages on this channel
+ * Removes the channel from the WebChannelBroker
+ */
+ stopListening() {
+ this._broker.unregisterChannel(this);
+ this._deliverCallback = null;
+ },
+
+ /**
+ * Sends messages over the WebChannel id using the "WebChannelMessageToContent" event
+ *
+ * @param message {Object}
+ * The message object that will be sent
+ * @param target {Object}
+ * A <target> with the information of where to send the message.
+ * @param target.browsingContext {BrowsingContext}
+ * The browsingContext we should send the message to.
+ * @param target.principal {Principal}
+ * Principal of the target. Prevents messages from
+ * being dispatched to unexpected origins. The system principal
+ * can be specified to send to any target.
+ * @param [target.eventTarget] {EventTarget}
+ * Optional eventTarget within the browser, use to send to a
+ * specific element. Can be null; if not null, should be
+ * a ContentDOMReference.
+ */
+ send(message, target) {
+ let { browsingContext, principal, eventTarget } = target;
+
+ if (message && browsingContext && principal) {
+ let { currentWindowGlobal } = browsingContext;
+ if (!currentWindowGlobal) {
+ console.error(
+ "Failed to send a WebChannel message. No currentWindowGlobal."
+ );
+ return;
+ }
+ currentWindowGlobal
+ .getActor("WebChannel")
+ .sendAsyncMessage("WebChannelMessageToContent", {
+ id: this.id,
+ message,
+ eventTarget,
+ principal,
+ });
+ } else if (!message) {
+ console.error("Failed to send a WebChannel message. Message not set.");
+ } else {
+ console.error("Failed to send a WebChannel message. Target invalid.");
+ }
+ },
+
+ /**
+ * Deliver WebChannel messages to the set "_channelCallback"
+ *
+ * @param data {Object}
+ * Message data
+ * @param sendingContext {Object}
+ * Message sending context.
+ * @param sendingContext.browsingContext {BrowsingContext}
+ * The browsingcontext from which the
+ * WebChannelMessageToChrome was sent.
+ * @param sendingContext.eventTarget {EventTarget}
+ * The <EventTarget> where the message was sent.
+ * Can be null; if not null, should be a ContentDOMReference.
+ * @param sendingContext.principal {Principal}
+ * The <Principal> of the EventTarget where the message was sent.
+ *
+ */
+ deliver(data, sendingContext) {
+ if (this._deliverCallback) {
+ try {
+ this._deliverCallback(data.id, data.message, sendingContext);
+ } catch (ex) {
+ this.send(
+ {
+ errno: ERRNO_UNKNOWN_ERROR,
+ error: ex.message ? ex.message : ERROR_UNKNOWN,
+ },
+ sendingContext
+ );
+ console.error("Failed to execute WebChannel callback:");
+ console.error(ex);
+ }
+ } else {
+ console.error("No callback set for this channel.");
+ }
+ },
+};