summaryrefslogtreecommitdiffstats
path: root/toolkit/components/webextensions/MessageChannel.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/webextensions/MessageChannel.jsm')
-rw-r--r--toolkit/components/webextensions/MessageChannel.jsm797
1 files changed, 0 insertions, 797 deletions
diff --git a/toolkit/components/webextensions/MessageChannel.jsm b/toolkit/components/webextensions/MessageChannel.jsm
deleted file mode 100644
index c5b326405..000000000
--- a/toolkit/components/webextensions/MessageChannel.jsm
+++ /dev/null
@@ -1,797 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-/**
- * This module provides wrappers around standard message managers to
- * simplify bidirectional communication. It currently allows a caller to
- * send a message to a single listener, and receive a reply. If there
- * are no matching listeners, or the message manager disconnects before
- * a reply is received, the caller is returned an error.
- *
- * The listener end may specify filters for the messages it wishes to
- * receive, and the sender end likewise may specify recipient tags to
- * match the filters.
- *
- * The message handler on the listener side may return its response
- * value directly, or may return a promise, the resolution or rejection
- * of which will be returned instead. The sender end likewise receives a
- * promise which resolves or rejects to the listener's response.
- *
- *
- * A basic setup works something like this:
- *
- * A content script adds a message listener to its global
- * nsIContentFrameMessageManager, with an appropriate set of filters:
- *
- * {
- * init(messageManager, window, extensionID) {
- * this.window = window;
- *
- * MessageChannel.addListener(
- * messageManager, "ContentScript:TouchContent",
- * this);
- *
- * this.messageFilterStrict = {
- * innerWindowID: getInnerWindowID(window),
- * extensionID: extensionID,
- * };
- *
- * this.messageFilterPermissive = {
- * outerWindowID: getOuterWindowID(window),
- * };
- * },
- *
- * receiveMessage({ target, messageName, sender, recipient, data }) {
- * if (messageName == "ContentScript:TouchContent") {
- * return new Promise(resolve => {
- * this.touchWindow(data.touchWith, result => {
- * resolve({ touchResult: result });
- * });
- * });
- * }
- * },
- * };
- *
- * A script in the parent process sends a message to the content process
- * via a tab message manager, including recipient tags to match its
- * filter, and an optional sender tag to identify itself:
- *
- * let data = { touchWith: "pencil" };
- * let sender = { extensionID, contextID };
- * let recipient = { innerWindowID: tab.linkedBrowser.innerWindowID, extensionID };
- *
- * MessageChannel.sendMessage(
- * tab.linkedBrowser.messageManager, "ContentScript:TouchContent",
- * data, {recipient, sender}
- * ).then(result => {
- * alert(result.touchResult);
- * });
- *
- * Since the lifetimes of message senders and receivers may not always
- * match, either side of the message channel may cancel pending
- * responses which match its sender or recipient tags.
- *
- * For the above client, this might be done from an
- * inner-window-destroyed observer, when its target scope is destroyed:
- *
- * observe(subject, topic, data) {
- * if (topic == "inner-window-destroyed") {
- * let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
- *
- * MessageChannel.abortResponses({ innerWindowID });
- * }
- * },
- *
- * From the parent, it may be done when its context is being destroyed:
- *
- * onDestroy() {
- * MessageChannel.abortResponses({
- * extensionID: this.extensionID,
- * contextID: this.contextID,
- * });
- * },
- *
- */
-
-this.EXPORTED_SYMBOLS = ["MessageChannel"];
-
-/* globals MessageChannel */
-
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-const Cr = Components.results;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
- "resource://gre/modules/ExtensionUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
- "resource://gre/modules/PromiseUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Task",
- "resource://gre/modules/Task.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "MessageManagerProxy",
- () => ExtensionUtils.MessageManagerProxy);
-
-/**
- * Handles the mapping and dispatching of messages to their registered
- * handlers. There is one broker per message manager and class of
- * messages. Each class of messages is mapped to one native message
- * name, e.g., "MessageChannel:Message", and is dispatched to handlers
- * based on an internal message name, e.g., "Extension:ExecuteScript".
- */
-class FilteringMessageManager {
- /**
- * @param {string} messageName
- * The name of the native message this broker listens for.
- * @param {function} callback
- * A function which is called for each message after it has been
- * mapped to its handler. The function receives two arguments:
- *
- * result:
- * An object containing either a `handler` or an `error` property.
- * If no error occurs, `handler` will be a matching handler that
- * was registered by `addHandler`. Otherwise, the `error` property
- * will contain an object describing the error.
- *
- * data:
- * An object describing the message, as defined in
- * `MessageChannel.addListener`.
- * @param {nsIMessageListenerManager} messageManager
- */
- constructor(messageName, callback, messageManager) {
- this.messageName = messageName;
- this.callback = callback;
- this.messageManager = messageManager;
-
- this.messageManager.addMessageListener(this.messageName, this, true);
-
- this.handlers = new Map();
- }
-
- /**
- * Receives a message from our message manager, maps it to a handler, and
- * passes the result to our message callback.
- */
- receiveMessage({data, target}) {
- let handlers = Array.from(this.getHandlers(data.messageName, data.sender, data.recipient));
-
- data.target = target;
- this.callback(handlers, data);
- }
-
- /**
- * Iterates over all handlers for the given message name. If `recipient`
- * is provided, only iterates over handlers whose filters match it.
- *
- * @param {string|number} messageName
- * The message for which to return handlers.
- * @param {object} sender
- * The sender data on which to filter handlers.
- * @param {object} recipient
- * The recipient data on which to filter handlers.
- */
- * getHandlers(messageName, sender, recipient) {
- let handlers = this.handlers.get(messageName) || new Set();
- for (let handler of handlers) {
- if (MessageChannel.matchesFilter(handler.messageFilterStrict || {}, recipient) &&
- MessageChannel.matchesFilter(handler.messageFilterPermissive || {}, recipient, false) &&
- (!handler.filterMessage || handler.filterMessage(sender, recipient))) {
- yield handler;
- }
- }
- }
-
- /**
- * Registers a handler for the given message.
- *
- * @param {string} messageName
- * The internal message name for which to register the handler.
- * @param {object} handler
- * An opaque handler object. The object may have a
- * `messageFilterStrict` and/or a `messageFilterPermissive`
- * property and/or a `filterMessage` method on which to filter messages.
- *
- * Final dispatching is handled by the message callback passed to
- * the constructor.
- */
- addHandler(messageName, handler) {
- if (!this.handlers.has(messageName)) {
- this.handlers.set(messageName, new Set());
- }
-
- this.handlers.get(messageName).add(handler);
- }
-
- /**
- * Unregisters a handler for the given message.
- *
- * @param {string} messageName
- * The internal message name for which to unregister the handler.
- * @param {object} handler
- * The handler object to unregister.
- */
- removeHandler(messageName, handler) {
- this.handlers.get(messageName).delete(handler);
- }
-}
-
-/**
- * Manages mappings of message managers to their corresponding message
- * brokers. Brokers are lazily created for each message manager the
- * first time they are accessed. In the case of content frame message
- * managers, they are also automatically destroyed when the frame
- * unload event fires.
- */
-class FilteringMessageManagerMap extends Map {
- // Unfortunately, we can't use a WeakMap for this, because message
- // managers do not support preserved wrappers.
-
- /**
- * @param {string} messageName
- * The native message name passed to `FilteringMessageManager` constructors.
- * @param {function} callback
- * The message callback function passed to
- * `FilteringMessageManager` constructors.
- */
- constructor(messageName, callback) {
- super();
-
- this.messageName = messageName;
- this.callback = callback;
- }
-
- /**
- * Returns, and possibly creates, a message broker for the given
- * message manager.
- *
- * @param {nsIMessageListenerManager} target
- * The message manager for which to return a broker.
- *
- * @returns {FilteringMessageManager}
- */
- get(target) {
- if (this.has(target)) {
- return super.get(target);
- }
-
- let broker = new FilteringMessageManager(this.messageName, this.callback, target);
- this.set(target, broker);
-
- if (target instanceof Ci.nsIDOMEventTarget) {
- let onUnload = event => {
- target.removeEventListener("unload", onUnload);
- this.delete(target);
- };
- target.addEventListener("unload", onUnload);
- }
-
- return broker;
- }
-}
-
-const MESSAGE_MESSAGE = "MessageChannel:Message";
-const MESSAGE_RESPONSE = "MessageChannel:Response";
-
-this.MessageChannel = {
- init() {
- Services.obs.addObserver(this, "message-manager-close", false);
- Services.obs.addObserver(this, "message-manager-disconnect", false);
-
- this.messageManagers = new FilteringMessageManagerMap(
- MESSAGE_MESSAGE, this._handleMessage.bind(this));
-
- this.responseManagers = new FilteringMessageManagerMap(
- MESSAGE_RESPONSE, this._handleResponse.bind(this));
-
- /**
- * Contains a list of pending responses, either waiting to be
- * received or waiting to be sent. @see _addPendingResponse
- */
- this.pendingResponses = new Set();
- },
-
- RESULT_SUCCESS: 0,
- RESULT_DISCONNECTED: 1,
- RESULT_NO_HANDLER: 2,
- RESULT_MULTIPLE_HANDLERS: 3,
- RESULT_ERROR: 4,
- RESULT_NO_RESPONSE: 5,
-
- REASON_DISCONNECTED: {
- result: this.RESULT_DISCONNECTED,
- message: "Message manager disconnected",
- },
-
- /**
- * Specifies that only a single listener matching the specified
- * recipient tag may be listening for the given message, at the other
- * end of the target message manager.
- *
- * If no matching listeners exist, a RESULT_NO_HANDLER error will be
- * returned. If multiple matching listeners exist, a
- * RESULT_MULTIPLE_HANDLERS error will be returned.
- */
- RESPONSE_SINGLE: 0,
-
- /**
- * If multiple message managers matching the specified recipient tag
- * are listening for a message, all listeners are notified, but only
- * the first response or error is returned.
- *
- * Only handlers which return a value other than `undefined` are
- * considered to have responded. Returning a Promise which evaluates
- * to `undefined` is interpreted as an explicit response.
- *
- * If no matching listeners exist, a RESULT_NO_HANDLER error will be
- * returned. If no listeners return a response, a RESULT_NO_RESPONSE
- * error will be returned.
- */
- RESPONSE_FIRST: 1,
-
- /**
- * If multiple message managers matching the specified recipient tag
- * are listening for a message, all listeners are notified, and all
- * responses are returned as an array, once all listeners have
- * replied.
- */
- RESPONSE_ALL: 2,
-
- /**
- * Fire-and-forget: The sender of this message does not expect a reply.
- */
- RESPONSE_NONE: 3,
-
- /**
- * Initializes message handlers for the given message managers if needed.
- *
- * @param {Array<nsIMessageListenerManager>} messageManagers
- */
- setupMessageManagers(messageManagers) {
- for (let mm of messageManagers) {
- // This call initializes a FilteringMessageManager for |mm| if needed.
- // The FilteringMessageManager must be created to make sure that senders
- // of messages that expect a reply, such as MessageChannel:Message, do
- // actually receive a default reply even if there are no explicit message
- // handlers.
- this.messageManagers.get(mm);
- }
- },
-
- /**
- * Returns true if the properties of the `data` object match those in
- * the `filter` object. Matching is done on a strict equality basis,
- * and the behavior varies depending on the value of the `strict`
- * parameter.
- *
- * @param {object} filter
- * The filter object to match against.
- * @param {object} data
- * The data object being matched.
- * @param {boolean} [strict=false]
- * If true, all properties in the `filter` object have a
- * corresponding property in `data` with the same value. If
- * false, properties present in both objects must have the same
- * value.
- * @returns {boolean} True if the objects match.
- */
- matchesFilter(filter, data, strict = true) {
- if (strict) {
- return Object.keys(filter).every(key => {
- return key in data && data[key] === filter[key];
- });
- }
- return Object.keys(filter).every(key => {
- return !(key in data) || data[key] === filter[key];
- });
- },
-
- /**
- * Adds a message listener to the given message manager.
- *
- * @param {nsIMessageListenerManager|Array<nsIMessageListenerManager>} targets
- * The message managers on which to listen.
- * @param {string|number} messageName
- * The name of the message to listen for.
- * @param {MessageReceiver} handler
- * The handler to dispatch to. Must be an object with the following
- * properties:
- *
- * receiveMessage:
- * A method which is called for each message received by the
- * listener. The method takes one argument, an object, with the
- * following properties:
- *
- * messageName:
- * The internal message name, as passed to `sendMessage`.
- *
- * target:
- * The message manager which received this message.
- *
- * channelId:
- * The internal ID of the transaction, used to map responses to
- * the original sender.
- *
- * sender:
- * An object describing the sender, as passed to `sendMessage`.
- *
- * recipient:
- * An object describing the recipient, as passed to
- * `sendMessage`.
- *
- * data:
- * The contents of the message, as passed to `sendMessage`.
- *
- * The method may return any structured-clone-compatible
- * object, which will be returned as a response to the message
- * sender. It may also instead return a `Promise`, the
- * resolution or rejection value of which will likewise be
- * returned to the message sender.
- *
- * messageFilterStrict:
- * An object containing arbitrary properties on which to filter
- * received messages. Messages will only be dispatched to this
- * object if the `recipient` object passed to `sendMessage`
- * matches this filter, as determined by `matchesFilter` with
- * `strict=true`.
- *
- * messageFilterPermissive:
- * An object containing arbitrary properties on which to filter
- * received messages. Messages will only be dispatched to this
- * object if the `recipient` object passed to `sendMessage`
- * matches this filter, as determined by `matchesFilter` with
- * `strict=false`.
- *
- * filterMessage:
- * An optional function that prevents the handler from handling a
- * message by returning `false`. See `getHandlers` for the parameters.
- */
- addListener(targets, messageName, handler) {
- for (let target of [].concat(targets)) {
- this.messageManagers.get(target).addHandler(messageName, handler);
- }
- },
-
- /**
- * Removes a message listener from the given message manager.
- *
- * @param {nsIMessageListenerManager|Array<nsIMessageListenerManager>} targets
- * The message managers on which to stop listening.
- * @param {string|number} messageName
- * The name of the message to stop listening for.
- * @param {MessageReceiver} handler
- * The handler to stop dispatching to.
- */
- removeListener(targets, messageName, handler) {
- for (let target of [].concat(targets)) {
- if (this.messageManagers.has(target)) {
- this.messageManagers.get(target).removeHandler(messageName, handler);
- }
- }
- },
-
- /**
- * Sends a message via the given message manager. Returns a promise which
- * resolves or rejects with the return value of the message receiver.
- *
- * The promise also rejects if there is no matching listener, or the other
- * side of the message manager disconnects before the response is received.
- *
- * @param {nsIMessageSender} target
- * The message manager on which to send the message.
- * @param {string} messageName
- * The name of the message to send, as passed to `addListener`.
- * @param {object} data
- * A structured-clone-compatible object to send to the message
- * recipient.
- * @param {object} [options]
- * An object containing any of the following properties:
- * @param {object} [options.recipient]
- * A structured-clone-compatible object to identify the message
- * recipient. The object must match the `messageFilterStrict` and
- * `messageFilterPermissive` filters defined by recipients in order
- * for the message to be received.
- * @param {object} [options.sender]
- * A structured-clone-compatible object to identify the message
- * sender. This object may also be used to avoid delivering the
- * message to the sender, and as a filter to prematurely
- * abort responses when the sender is being destroyed.
- * @see `abortResponses`.
- * @param {integer} [options.responseType=RESPONSE_SINGLE]
- * Specifies the type of response expected. See the `RESPONSE_*`
- * contents for details.
- * @returns {Promise}
- */
- sendMessage(target, messageName, data, options = {}) {
- let sender = options.sender || {};
- let recipient = options.recipient || {};
- let responseType = options.responseType || this.RESPONSE_SINGLE;
-
- let channelId = ExtensionUtils.getUniqueId();
- let message = {messageName, channelId, sender, recipient, data, responseType};
-
- if (responseType == this.RESPONSE_NONE) {
- try {
- target.sendAsyncMessage(MESSAGE_MESSAGE, message);
- } catch (e) {
- // Caller is not expecting a reply, so dump the error to the console.
- Cu.reportError(e);
- return Promise.reject(e);
- }
- return Promise.resolve(); // Not expecting any reply.
- }
-
- let deferred = PromiseUtils.defer();
- deferred.sender = recipient;
- deferred.messageManager = target;
-
- this._addPendingResponse(deferred);
-
- // The channel ID is used as the message name when routing responses.
- // Add a message listener to the response broker, and remove it once
- // we've gotten (or canceled) a response.
- let broker = this.responseManagers.get(target);
- broker.addHandler(channelId, deferred);
-
- let cleanup = () => {
- broker.removeHandler(channelId, deferred);
- };
- deferred.promise.then(cleanup, cleanup);
-
- try {
- target.sendAsyncMessage(MESSAGE_MESSAGE, message);
- } catch (e) {
- deferred.reject(e);
- }
- return deferred.promise;
- },
-
- _callHandlers(handlers, data) {
- let responseType = data.responseType;
-
- // At least one handler is required for all response types but
- // RESPONSE_ALL.
- if (handlers.length == 0 && responseType != this.RESPONSE_ALL) {
- return Promise.reject({result: MessageChannel.RESULT_NO_HANDLER,
- message: "No matching message handler"});
- }
-
- if (responseType == this.RESPONSE_SINGLE) {
- if (handlers.length > 1) {
- return Promise.reject({result: MessageChannel.RESULT_MULTIPLE_HANDLERS,
- message: `Multiple matching handlers for ${data.messageName}`});
- }
-
- // Note: We use `new Promise` rather than `Promise.resolve` here
- // so that errors from the handler are trapped and converted into
- // rejected promises.
- return new Promise(resolve => {
- resolve(handlers[0].receiveMessage(data));
- });
- }
-
- let responses = handlers.map(handler => {
- try {
- return handler.receiveMessage(data);
- } catch (e) {
- return Promise.reject(e);
- }
- });
- responses = responses.filter(response => response !== undefined);
-
- switch (responseType) {
- case this.RESPONSE_FIRST:
- if (responses.length == 0) {
- return Promise.reject({result: MessageChannel.RESULT_NO_RESPONSE,
- message: "No handler returned a response"});
- }
-
- return Promise.race(responses);
-
- case this.RESPONSE_ALL:
- return Promise.all(responses);
- }
- return Promise.reject({message: "Invalid response type"});
- },
-
- /**
- * Handles dispatching message callbacks from the message brokers to their
- * appropriate `MessageReceivers`, and routing the responses back to the
- * original senders.
- *
- * Each handler object is a `MessageReceiver` object as passed to
- * `addListener`.
- *
- * @param {Array<MessageHandler>} handlers
- * @param {object} data
- * @param {nsIMessageSender|{messageManager:nsIMessageSender}} data.target
- */
- _handleMessage(handlers, data) {
- if (data.responseType == this.RESPONSE_NONE) {
- handlers.forEach(handler => {
- // The sender expects no reply, so dump any errors to the console.
- new Promise(resolve => {
- resolve(handler.receiveMessage(data));
- }).catch(e => {
- Cu.reportError(e.stack ? `${e}\n${e.stack}` : e.message || e);
- });
- });
- // Note: Unhandled messages are silently dropped.
- return;
- }
-
- let target = new MessageManagerProxy(data.target);
-
- let deferred = {
- sender: data.sender,
- messageManager: target,
- };
- deferred.promise = new Promise((resolve, reject) => {
- deferred.reject = reject;
-
- this._callHandlers(handlers, data).then(resolve, reject);
- }).then(
- value => {
- let response = {
- result: this.RESULT_SUCCESS,
- messageName: data.channelId,
- recipient: {},
- value,
- };
-
- target.sendAsyncMessage(MESSAGE_RESPONSE, response);
- },
- error => {
- let response = {
- result: this.RESULT_ERROR,
- messageName: data.channelId,
- recipient: {},
- error: {},
- };
-
- if (error && typeof(error) == "object") {
- if (error.result) {
- response.result = error.result;
- }
- // Error objects are not structured-clonable, so just copy
- // over the important properties.
- for (let key of ["fileName", "filename", "lineNumber",
- "columnNumber", "message", "stack", "result"]) {
- if (key in error) {
- response.error[key] = error[key];
- }
- }
- }
-
- target.sendAsyncMessage(MESSAGE_RESPONSE, response);
- }).catch(e => {
- Cu.reportError(e);
- }).then(() => {
- target.dispose();
- });
-
- this._addPendingResponse(deferred);
- },
-
- /**
- * Handles message callbacks from the response brokers.
- *
- * Each handler object is a deferred object created by `sendMessage`, and
- * should be resolved or rejected based on the contents of the response.
- *
- * @param {Array<MessageHandler>} handlers
- * @param {object} data
- * @param {nsIMessageSender|{messageManager:nsIMessageSender}} data.target
- */
- _handleResponse(handlers, data) {
- // If we have an error at this point, we have handler to report it to,
- // so just log it.
- if (handlers.length == 0) {
- Cu.reportError(`No matching message response handler for ${data.messageName}`);
- } else if (handlers.length > 1) {
- Cu.reportError(`Multiple matching response handlers for ${data.messageName}`);
- } else if (data.result === this.RESULT_SUCCESS) {
- handlers[0].resolve(data.value);
- } else {
- handlers[0].reject(data.error);
- }
- },
-
- /**
- * Adds a pending response to the the `pendingResponses` list.
- *
- * The response object must be a deferred promise with the following
- * properties:
- *
- * promise:
- * The promise object which resolves or rejects when the response
- * is no longer pending.
- *
- * reject:
- * A function which, when called, causes the `promise` object to be
- * rejected.
- *
- * sender:
- * A sender object, as passed to `sendMessage.
- *
- * messageManager:
- * The message manager the response will be sent or received on.
- *
- * When the promise resolves or rejects, it will be removed from the
- * list.
- *
- * These values are used to clear pending responses when execution
- * contexts are destroyed.
- *
- * @param {Deferred} deferred
- */
- _addPendingResponse(deferred) {
- let cleanup = () => {
- this.pendingResponses.delete(deferred);
- };
- this.pendingResponses.add(deferred);
- deferred.promise.then(cleanup, cleanup);
- },
-
- /**
- * Aborts any pending message responses to senders matching the given
- * filter.
- *
- * @param {object} sender
- * The object on which to filter senders, as determined by
- * `matchesFilter`.
- * @param {object} [reason]
- * An optional object describing the reason the response was aborted.
- * Will be passed to the promise rejection handler of all aborted
- * responses.
- */
- abortResponses(sender, reason = this.REASON_DISCONNECTED) {
- for (let response of this.pendingResponses) {
- if (this.matchesFilter(sender, response.sender)) {
- response.reject(reason);
- }
- }
- },
-
- /**
- * Aborts any pending message responses to the broker for the given
- * message manager.
- *
- * @param {nsIMessageListenerManager} target
- * The message manager for which to abort brokers.
- * @param {object} reason
- * An object describing the reason the responses were aborted.
- * Will be passed to the promise rejection handler of all aborted
- * responses.
- */
- abortMessageManager(target, reason) {
- for (let response of this.pendingResponses) {
- if (MessageManagerProxy.matches(response.messageManager, target)) {
- response.reject(reason);
- }
- }
- },
-
- observe(subject, topic, data) {
- switch (topic) {
- case "message-manager-close":
- case "message-manager-disconnect":
- try {
- if (this.responseManagers.has(subject)) {
- this.abortMessageManager(subject, this.REASON_DISCONNECTED);
- }
- } finally {
- this.responseManagers.delete(subject);
- this.messageManagers.delete(subject);
- }
- break;
- }
- },
-};
-
-MessageChannel.init();