gecko-dev/toolkit/components/extensions/ExtensionUtils.jsm

548 lines
14 KiB
JavaScript

/* 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.EXPORTED_SYMBOLS = ["ExtensionUtils"];
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");
// Run a function and report exceptions.
function runSafeWithoutClone(f, ...args)
{
try {
return f(...args);
} catch (e) {
dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n${e.stack}\n${Error().stack}`);
Cu.reportError(e);
}
}
// Run a function, cloning arguments into context.cloneScope, and
// report exceptions. |f| is expected to be in context.cloneScope.
function runSafe(context, f, ...args)
{
try {
args = Cu.cloneInto(args, context.cloneScope);
} catch (e) {
dump(`runSafe failure\n${context.cloneScope}\n${Error().stack}`);
}
return runSafeWithoutClone(f, ...args);
}
// Similar to a WeakMap, but returns a particular default value for
// |get| if a key is not present.
function DefaultWeakMap(defaultValue)
{
this.defaultValue = defaultValue;
this.weakmap = new WeakMap();
}
DefaultWeakMap.prototype = {
get(key) {
if (this.weakmap.has(key)) {
return this.weakmap.get(key);
}
return this.defaultValue;
},
set(key, value) {
if (key) {
this.weakmap.set(key, value);
} else {
this.defaultValue = value;
}
},
};
// This is a generic class for managing event listeners. Example usage:
//
// new EventManager(context, "api.subAPI", fire => {
// let listener = (...) => {
// // Fire any listeners registered with addListener.
// fire(arg1, arg2);
// };
// // Register the listener.
// SomehowRegisterListener(listener);
// return () => {
// // Return a way to unregister the listener.
// SomehowUnregisterListener(listener);
// };
// }).api()
//
// The result is an object with addListener, removeListener, and
// hasListener methods. |context| is an add-on scope (either an
// ExtensionPage in the chrome process or ExtensionContext in a
// content process). |name| is for debugging. |register| is a function
// to register the listener. |register| is only called once, event if
// multiple listeners are registered. |register| should return an
// unregister function that will unregister the listener.
function EventManager(context, name, register)
{
this.context = context;
this.name = name;
this.register = register;
this.unregister = null;
this.callbacks = new Set();
this.registered = false;
}
EventManager.prototype = {
addListener(callback) {
if (!this.registered) {
this.context.callOnClose(this);
let fireFunc = this.fire.bind(this);
let fireWithoutClone = this.fireWithoutClone.bind(this);
fireFunc.withoutClone = fireWithoutClone;
this.unregister = this.register(fireFunc);
}
this.callbacks.add(callback);
},
removeListener(callback) {
if (!this.registered) {
return;
}
this.callbacks.delete(callback);
if (this.callbacks.length == 0) {
this.unregister();
this.context.forgetOnClose(this);
}
},
hasListener(callback) {
return this.callbacks.has(callback);
},
fire(...args) {
for (let callback of this.callbacks) {
runSafe(this.context, callback, ...args);
}
},
fireWithoutClone(...args) {
for (let callback of this.callbacks) {
runSafeWithoutClone(callback, ...args);
}
},
close() {
this.unregister();
},
api() {
return {
addListener: callback => this.addListener(callback),
removeListener: callback => this.removeListener(callback),
hasListener: callback => this.hasListener(callback),
};
},
};
// Similar to EventManager, but it doesn't try to consolidate event
// notifications. Each addListener call causes us to register once. It
// allows extra arguments to be passed to addListener.
function SingletonEventManager(context, name, register)
{
this.context = context;
this.name = name;
this.register = register;
this.unregister = new Map();
context.callOnClose(this);
}
SingletonEventManager.prototype = {
addListener(callback, ...args) {
let unregister = this.register(callback, ...args);
this.unregister.set(callback, unregister);
},
removeListener(callback) {
if (!this.unregister.has(callback)) {
return;
}
let unregister = this.unregister.get(callback);
this.unregister.delete(callback);
this.unregister();
},
hasListener(callback) {
return this.unregister.has(callback);
},
close() {
for (let unregister of this.unregister.values()) {
unregister();
}
},
api() {
return {
addListener: (...args) => this.addListener(...args),
removeListener: (...args) => this.removeListener(...args),
hasListener: (...args) => this.hasListener(...args),
};
},
};
// Simple API for event listeners where events never fire.
function ignoreEvent()
{
return {
addListener: function(context, callback) {},
removeListener: function(context, callback) {},
hasListener: function(context, callback) {},
};
}
// Copy an API object from |source| into the scope |dest|.
function injectAPI(source, dest)
{
for (let prop in source) {
// Skip names prefixed with '_'.
if (prop[0] == '_') {
continue;
}
let value = source[prop];
if (typeof(value) == "function") {
Cu.exportFunction(value, dest, {defineAs: prop});
} else if (typeof(value) == "object") {
let obj = Cu.createObjectIn(dest, {defineAs: prop});
injectAPI(value, obj);
} else {
dest[prop] = value;
}
}
}
/*
* Messaging primitives.
*/
var nextBrokerId = 1;
var MESSAGES = [
"Extension:Message",
"Extension:Connect",
];
// Receives messages from multiple message managers and directs them
// to a set of listeners. On the child side: one broker per frame
// script. On the parent side: one broker total, covering both the
// global MM and the ppmm. Message must be tagged with a recipient,
// which is an object with properties. Listeners can filter for
// messages that have a certain value for a particular property in the
// recipient. (If a message doesn't specify the given property, it's
// considered a match.)
function MessageBroker(messageManagers)
{
this.messageManagers = messageManagers;
for (let mm of this.messageManagers) {
for (let message of MESSAGES) {
mm.addMessageListener(message, this);
}
}
this.listeners = {message: [], connect: []};
}
MessageBroker.prototype = {
uninit() {
for (let mm of this.messageManagers) {
for (let message of MESSAGES) {
mm.removeMessageListener(message, this);
}
}
this.listeners = null;
},
makeId() {
return nextBrokerId++;
},
addListener(type, listener, filter) {
this.listeners[type].push({filter, listener});
},
removeListener(type, listener) {
let index = -1;
for (let i = 0; i < this.listeners[type].length; i++) {
if (this.listeners[type][i].listener == listener) {
this.listeners[type].splice(i, 1);
return;
}
}
},
runListeners(type, target, data) {
let listeners = [];
for (let {listener, filter} of this.listeners[type]) {
let pass = true;
for (let prop in filter) {
if (prop in data.recipient && filter[prop] != data.recipient[prop]) {
pass = false;
break;
}
}
// Save up the list of listeners to call in case they modify the
// set of listeners.
if (pass) {
listeners.push(listener);
}
}
for (let listener of listeners) {
listener(type, target, data.message, data.sender, data.recipient);
}
},
receiveMessage({name, data, target}) {
switch (name) {
case "Extension:Message":
this.runListeners("message", target, data);
break;
case "Extension:Connect":
this.runListeners("connect", target, data);
break;
}
},
sendMessage(messageManager, type, message, sender, recipient) {
let data = {message, sender, recipient};
let names = {message: "Extension:Message", connect: "Extension:Connect"};
messageManager.sendAsyncMessage(names[type], data);
},
};
// Abstraction for a Port object in the extension API. Each port has a unique ID.
function Port(context, messageManager, name, id, sender)
{
this.context = context;
this.messageManager = messageManager;
this.name = name;
this.id = id;
this.listenerName = `Extension:Port-${this.id}`;
this.disconnectName = `Extension:Disconnect-${this.id}`;
this.sender = sender;
this.disconnected = false;
}
Port.prototype = {
api() {
let portObj = Cu.createObjectIn(this.context.cloneScope);
// We want a close() notification when the window is destroyed.
this.context.callOnClose(this);
let publicAPI = {
name: this.name,
disconnect: () => {
this.disconnect();
},
postMessage: json => {
if (this.disconnected) {
throw "Attempt to postMessage on disconnected port";
}
this.messageManager.sendAsyncMessage(this.listenerName, json);
},
onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => {
let listener = () => {
if (!this.disconnected) {
fire();
}
};
this.messageManager.addMessageListener(this.disconnectName, listener, true);
return () => {
this.messageManager.removeMessageListener(this.disconnectName, listener);
};
}).api(),
onMessage: new EventManager(this.context, "Port.onMessage", fire => {
let listener = ({data}) => {
if (!this.disconnected) {
fire(data);
}
};
this.messageManager.addMessageListener(this.listenerName, listener);
return () => {
this.messageManager.removeMessageListener(this.listenerName, listener);
};
}).api(),
};
if (this.sender) {
publicAPI.sender = this.sender;
}
injectAPI(publicAPI, portObj);
return portObj;
},
disconnect() {
this.context.forgetOnClose(this);
this.disconnect = true;
this.messageManager.sendAsyncMessage(this.disconnectName);
},
close() {
this.disconnect();
},
};
function getMessageManager(target)
{
if (target instanceof Ci.nsIDOMXULElement) {
return target.messageManager;
} else {
return target;
}
}
// Each extension scope gets its own Messenger object. It handles the
// basics of sendMessage, onMessage, connect, and onConnect.
//
// |context| is the extension scope.
// |broker| is a MessageBroker used to receive and send messages.
// |sender| is an object describing the sender (usually giving its extensionId, tabId, etc.)
// |filter| is a recipient filter to apply to incoming messages from the broker.
// |delegate| is an object that must implement a few methods:
// getSender(context, messageManagerTarget, sender): returns a MessageSender
// See https://developer.chrome.com/extensions/runtime#type-MessageSender.
function Messenger(context, broker, sender, filter, delegate)
{
this.context = context;
this.broker = broker;
this.sender = sender;
this.filter = filter;
this.delegate = delegate;
}
Messenger.prototype = {
sendMessage(messageManager, msg, recipient, responseCallback) {
let id = this.broker.makeId();
let replyName = `Extension:Reply-${id}`;
recipient.messageId = id;
this.broker.sendMessage(messageManager, "message", msg, this.sender, recipient);
let onClose;
let listener = ({data: response}) => {
messageManager.removeMessageListener(replyName, listener);
this.context.forgetOnClose(onClose);
if (response.gotData) {
// TODO: Handle failure to connect to the extension?
runSafe(this.context, responseCallback, response.data);
}
};
onClose = {
close() {
messageManager.removeMessageListener(replyName, listener);
}
};
if (responseCallback) {
messageManager.addMessageListener(replyName, listener);
this.context.callOnClose(onClose);
}
},
onMessage(name) {
return new EventManager(this.context, name, fire => {
let listener = (type, target, message, sender, recipient) => {
message = Cu.cloneInto(message, this.context.cloneScope);
if (this.delegate) {
this.delegate.getSender(this.context, target, sender);
}
sender = Cu.cloneInto(sender, this.context.cloneScope);
let mm = getMessageManager(target);
let replyName = `Extension:Reply-${recipient.messageId}`;
let valid = true, sent = false;
let sendResponse = data => {
if (!valid) {
return;
}
sent = true;
mm.sendAsyncMessage(replyName, {data, gotData: true});
};
sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
let result = fire.withoutClone(message, sender, sendResponse);
if (result !== true) {
valid = false;
}
if (!sent) {
mm.sendAsyncMessage(replyName, {gotData: false});
}
};
this.broker.addListener("message", listener, this.filter);
return () => {
this.broker.removeListener("message", listener);
};
}).api();
},
connect(messageManager, name, recipient) {
let portId = this.broker.makeId();
let port = new Port(this.context, messageManager, name, portId, null);
let msg = {name, portId};
this.broker.sendMessage(messageManager, "connect", msg, this.sender, recipient);
return port.api();
},
onConnect(name) {
return new EventManager(this.context, name, fire => {
let listener = (type, target, message, sender, recipient) => {
let {name, portId} = message;
let mm = getMessageManager(target);
if (this.delegate) {
this.delegate.getSender(this.context, target, sender);
}
let port = new Port(this.context, mm, name, portId, sender);
fire.withoutClone(port.api());
};
this.broker.addListener("connect", listener, this.filter);
return () => {
this.broker.removeListener("connect", listener);
};
}).api();
},
};
function flushJarCache(jarFile)
{
Services.obs.notifyObservers(jarFile, "flush-cache-entry", null);
}
this.ExtensionUtils = {
runSafeWithoutClone,
runSafe,
DefaultWeakMap,
EventManager,
SingletonEventManager,
ignoreEvent,
injectAPI,
MessageBroker,
Messenger,
flushJarCache,
};