2015-06-04 01:34:44 +03:00
|
|
|
/* 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";
|
|
|
|
|
2015-08-15 02:55:09 +03:00
|
|
|
this.EXPORTED_SYMBOLS = ["ExtensionUtils"];
|
2015-06-04 01:34:44 +03:00
|
|
|
|
|
|
|
const Ci = Components.interfaces;
|
|
|
|
const Cc = Components.classes;
|
|
|
|
const Cu = Components.utils;
|
|
|
|
const Cr = Components.results;
|
|
|
|
|
2016-05-24 01:59:33 +03:00
|
|
|
const INTEGER = /^[1-9]\d*$/;
|
|
|
|
|
2015-06-04 01:34:44 +03:00
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
|
2016-05-24 01:59:33 +03:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
|
|
|
|
"resource://gre/modules/AddonManager.jsm");
|
2016-02-17 23:14:28 +03:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
|
|
|
|
"resource://gre/modules/AppConstants.jsm");
|
2016-02-24 06:01:11 +03:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
|
|
|
|
"resource:///modules/translation/LanguageDetector.jsm");
|
2015-11-21 23:07:14 +03:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Locale",
|
|
|
|
"resource://gre/modules/Locale.jsm");
|
2016-03-05 02:40:56 +03:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
|
|
|
|
"resource://gre/modules/MessageChannel.jsm");
|
2016-02-25 15:29:09 +03:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
|
|
|
|
"resource://gre/modules/Preferences.jsm");
|
2016-04-06 00:44:07 +03:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
|
|
|
|
"resource://gre/modules/PromiseUtils.jsm");
|
2015-11-21 23:07:14 +03:00
|
|
|
|
2016-01-20 08:57:34 +03:00
|
|
|
function filterStack(error) {
|
2016-01-20 23:08:56 +03:00
|
|
|
return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n");
|
2016-01-20 08:57:34 +03:00
|
|
|
}
|
|
|
|
|
2015-06-04 01:34:44 +03:00
|
|
|
// Run a function and report exceptions.
|
2015-12-03 03:58:53 +03:00
|
|
|
function runSafeSyncWithoutClone(f, ...args) {
|
2015-06-04 01:34:44 +03:00
|
|
|
try {
|
|
|
|
return f(...args);
|
|
|
|
} catch (e) {
|
2016-01-20 08:57:34 +03:00
|
|
|
dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`);
|
2015-06-04 01:34:44 +03:00
|
|
|
Cu.reportError(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-31 05:54:13 +03:00
|
|
|
// Run a function and report exceptions.
|
2015-12-03 03:58:53 +03:00
|
|
|
function runSafeWithoutClone(f, ...args) {
|
2015-08-31 05:54:13 +03:00
|
|
|
if (typeof(f) != "function") {
|
2016-01-20 08:57:34 +03:00
|
|
|
dump(`Extension error: expected function\n${filterStack(Error())}`);
|
2015-08-31 05:54:13 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-01-20 08:57:34 +03:00
|
|
|
Promise.resolve().then(() => {
|
2015-08-31 05:54:13 +03:00
|
|
|
runSafeSyncWithoutClone(f, ...args);
|
2016-01-20 08:57:34 +03:00
|
|
|
});
|
2015-08-31 05:54:13 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Run a function, cloning arguments into context.cloneScope, and
|
|
|
|
// report exceptions. |f| is expected to be in context.cloneScope.
|
2015-12-03 03:58:53 +03:00
|
|
|
function runSafeSync(context, f, ...args) {
|
2016-04-05 00:14:35 +03:00
|
|
|
if (context.unloaded) {
|
|
|
|
Cu.reportError("runSafeSync called after context unloaded");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-08-31 05:54:13 +03:00
|
|
|
try {
|
|
|
|
args = Cu.cloneInto(args, context.cloneScope);
|
|
|
|
} catch (e) {
|
2015-10-16 01:15:04 +03:00
|
|
|
Cu.reportError(e);
|
2016-01-20 08:57:34 +03:00
|
|
|
dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
|
2015-08-31 05:54:13 +03:00
|
|
|
}
|
|
|
|
return runSafeSyncWithoutClone(f, ...args);
|
|
|
|
}
|
|
|
|
|
2015-06-04 01:34:44 +03:00
|
|
|
// Run a function, cloning arguments into context.cloneScope, and
|
|
|
|
// report exceptions. |f| is expected to be in context.cloneScope.
|
2015-12-03 03:58:53 +03:00
|
|
|
function runSafe(context, f, ...args) {
|
2015-06-04 01:34:44 +03:00
|
|
|
try {
|
|
|
|
args = Cu.cloneInto(args, context.cloneScope);
|
|
|
|
} catch (e) {
|
2015-10-16 01:15:04 +03:00
|
|
|
Cu.reportError(e);
|
2016-01-20 08:57:34 +03:00
|
|
|
dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
|
2015-06-04 01:34:44 +03:00
|
|
|
}
|
2016-04-05 00:14:35 +03:00
|
|
|
if (context.unloaded) {
|
|
|
|
dump(`runSafe failure: context is already unloaded ${filterStack(new Error())}\n`);
|
|
|
|
return undefined;
|
|
|
|
}
|
2015-06-04 01:34:44 +03:00
|
|
|
return runSafeWithoutClone(f, ...args);
|
|
|
|
}
|
|
|
|
|
2015-11-16 03:54:41 +03:00
|
|
|
// Return true if the given value is an instance of the given
|
|
|
|
// native type.
|
|
|
|
function instanceOf(value, type) {
|
|
|
|
return {}.toString.call(value) == `[object ${type}]`;
|
|
|
|
}
|
|
|
|
|
2015-10-30 14:01:40 +03:00
|
|
|
// Extend the object |obj| with the property descriptors of each object in
|
|
|
|
// |args|.
|
|
|
|
function extend(obj, ...args) {
|
|
|
|
for (let arg of args) {
|
|
|
|
let props = [...Object.getOwnPropertyNames(arg),
|
|
|
|
...Object.getOwnPropertySymbols(arg)];
|
|
|
|
for (let prop of props) {
|
|
|
|
let descriptor = Object.getOwnPropertyDescriptor(arg, prop);
|
|
|
|
Object.defineProperty(obj, prop, descriptor);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
2015-06-04 01:34:44 +03:00
|
|
|
// Similar to a WeakMap, but returns a particular default value for
|
|
|
|
// |get| if a key is not present.
|
2015-12-03 03:58:53 +03:00
|
|
|
function DefaultWeakMap(defaultValue) {
|
2015-06-04 01:34:44 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2016-02-02 06:20:13 +03:00
|
|
|
class SpreadArgs extends Array {
|
|
|
|
constructor(args) {
|
|
|
|
super();
|
|
|
|
this.push(...args);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-05 02:40:56 +03:00
|
|
|
let gContextId = 0;
|
|
|
|
|
2016-01-30 05:39:29 +03:00
|
|
|
class BaseContext {
|
2016-04-06 00:44:07 +03:00
|
|
|
constructor(extensionId) {
|
2016-01-30 05:39:29 +03:00
|
|
|
this.onClose = new Set();
|
2016-01-30 05:38:08 +03:00
|
|
|
this.checkedLastError = false;
|
|
|
|
this._lastError = null;
|
2016-03-05 02:40:56 +03:00
|
|
|
this.contextId = ++gContextId;
|
2016-04-05 00:14:35 +03:00
|
|
|
this.unloaded = false;
|
2016-04-06 00:44:07 +03:00
|
|
|
this.extensionId = extensionId;
|
2016-06-16 18:30:58 +03:00
|
|
|
this.jsonSandbox = null;
|
2016-07-07 03:14:02 +03:00
|
|
|
this.active = true;
|
2016-01-30 05:39:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
get cloneScope() {
|
|
|
|
throw new Error("Not implemented");
|
|
|
|
}
|
|
|
|
|
|
|
|
get principal() {
|
|
|
|
throw new Error("Not implemented");
|
|
|
|
}
|
|
|
|
|
2016-04-05 00:14:35 +03:00
|
|
|
runSafe(...args) {
|
|
|
|
if (this.unloaded) {
|
|
|
|
Cu.reportError("context.runSafe called after context unloaded");
|
|
|
|
} else {
|
|
|
|
return runSafeSync(this, ...args);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
runSafeWithoutClone(...args) {
|
|
|
|
if (this.unloaded) {
|
|
|
|
Cu.reportError("context.runSafeWithoutClone called after context unloaded");
|
|
|
|
} else {
|
|
|
|
return runSafeSyncWithoutClone(...args);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-30 05:39:29 +03:00
|
|
|
checkLoadURL(url, options = {}) {
|
|
|
|
let ssm = Services.scriptSecurityManager;
|
|
|
|
|
|
|
|
let flags = ssm.STANDARD;
|
|
|
|
if (!options.allowScript) {
|
|
|
|
flags |= ssm.DISALLOW_SCRIPT;
|
|
|
|
}
|
|
|
|
if (!options.allowInheritsPrincipal) {
|
|
|
|
flags |= ssm.DISALLOW_INHERIT_PRINCIPAL;
|
|
|
|
}
|
2016-02-25 20:13:59 +03:00
|
|
|
if (options.dontReportErrors) {
|
|
|
|
flags |= ssm.DONT_REPORT_ERRORS;
|
|
|
|
}
|
2016-01-30 05:39:29 +03:00
|
|
|
|
|
|
|
try {
|
|
|
|
ssm.checkLoadURIStrWithPrincipal(this.principal, url, flags);
|
|
|
|
} catch (e) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-06-16 18:30:58 +03:00
|
|
|
/**
|
|
|
|
* Safely call JSON.stringify() on an object that comes from an
|
|
|
|
* extension.
|
|
|
|
*
|
|
|
|
* @param {array<any>} args Arguments for JSON.stringify()
|
|
|
|
* @returns {string} The stringified representation of obj
|
|
|
|
*/
|
|
|
|
jsonStringify(...args) {
|
|
|
|
if (!this.jsonSandbox) {
|
|
|
|
this.jsonSandbox = Cu.Sandbox(this.principal, {
|
|
|
|
sameZoneAs: this.cloneScope,
|
|
|
|
wantXrays: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args);
|
|
|
|
}
|
|
|
|
|
2016-01-30 05:39:29 +03:00
|
|
|
callOnClose(obj) {
|
|
|
|
this.onClose.add(obj);
|
|
|
|
}
|
|
|
|
|
|
|
|
forgetOnClose(obj) {
|
|
|
|
this.onClose.delete(obj);
|
|
|
|
}
|
|
|
|
|
2016-03-05 02:40:56 +03:00
|
|
|
/**
|
|
|
|
* A wrapper around MessageChannel.sendMessage which adds the extension ID
|
|
|
|
* to the recipient object, and ensures replies are not processed after the
|
|
|
|
* context has been unloaded.
|
2016-06-08 04:36:19 +03:00
|
|
|
*
|
|
|
|
* @param {nsIMessageManager} target
|
|
|
|
* @param {string} messageName
|
|
|
|
* @param {object} data
|
|
|
|
* @param {object} [options]
|
|
|
|
* @param {object} [options.sender]
|
|
|
|
* @param {object} [options.recipient]
|
|
|
|
*
|
|
|
|
* @returns {Promise}
|
2016-03-05 02:40:56 +03:00
|
|
|
*/
|
|
|
|
sendMessage(target, messageName, data, options = {}) {
|
|
|
|
options.recipient = options.recipient || {};
|
|
|
|
options.sender = options.sender || {};
|
|
|
|
|
|
|
|
options.recipient.extensionId = this.extension.id;
|
|
|
|
options.sender.extensionId = this.extension.id;
|
|
|
|
options.sender.contextId = this.contextId;
|
|
|
|
|
|
|
|
return MessageChannel.sendMessage(target, messageName, data, options);
|
|
|
|
}
|
|
|
|
|
2016-01-30 05:38:08 +03:00
|
|
|
get lastError() {
|
|
|
|
this.checkedLastError = true;
|
|
|
|
return this._lastError;
|
|
|
|
}
|
|
|
|
|
|
|
|
set lastError(val) {
|
|
|
|
this.checkedLastError = false;
|
|
|
|
this._lastError = val;
|
|
|
|
}
|
|
|
|
|
2016-03-10 04:26:27 +03:00
|
|
|
/**
|
|
|
|
* Normalizes the given error object for use by the target scope. If
|
|
|
|
* the target is an error object which belongs to that scope, it is
|
|
|
|
* returned as-is. If it is an ordinary object with a `message`
|
|
|
|
* property, it is converted into an error belonging to the target
|
|
|
|
* scope. If it is an Error object which does *not* belong to the
|
|
|
|
* clone scope, it is reported, and converted to an unexpected
|
|
|
|
* exception error.
|
2016-06-08 04:36:19 +03:00
|
|
|
*
|
|
|
|
* @param {Error|object} error
|
|
|
|
* @returns {Error}
|
2016-03-10 04:26:27 +03:00
|
|
|
*/
|
|
|
|
normalizeError(error) {
|
|
|
|
if (error instanceof this.cloneScope.Error) {
|
|
|
|
return error;
|
|
|
|
}
|
|
|
|
if (!instanceOf(error, "Object")) {
|
|
|
|
Cu.reportError(error);
|
|
|
|
error = {message: "An unexpected error occurred"};
|
|
|
|
}
|
|
|
|
return new this.cloneScope.Error(error.message);
|
|
|
|
}
|
|
|
|
|
2016-01-30 05:38:08 +03:00
|
|
|
/**
|
|
|
|
* Sets the value of `.lastError` to `error`, calls the given
|
|
|
|
* callback, and reports an error if the value has not been checked
|
|
|
|
* when the callback returns.
|
|
|
|
*
|
|
|
|
* @param {object} error An object with a `message` property. May
|
|
|
|
* optionally be an `Error` object belonging to the target scope.
|
|
|
|
* @param {function} callback The callback to call.
|
|
|
|
* @returns {*} The return value of callback.
|
|
|
|
*/
|
|
|
|
withLastError(error, callback) {
|
2016-03-10 04:26:27 +03:00
|
|
|
this.lastError = this.normalizeError(error);
|
2016-01-30 05:38:08 +03:00
|
|
|
try {
|
|
|
|
return callback();
|
|
|
|
} finally {
|
|
|
|
if (!this.checkedLastError) {
|
2016-03-10 04:26:27 +03:00
|
|
|
Cu.reportError(`Unchecked lastError value: ${this.lastError}`);
|
2016-01-30 05:38:08 +03:00
|
|
|
}
|
|
|
|
this.lastError = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Wraps the given promise so it can be safely returned to extension
|
|
|
|
* code in this context.
|
|
|
|
*
|
|
|
|
* If `callback` is provided, however, it is used as a completion
|
|
|
|
* function for the promise, and no promise is returned. In this case,
|
|
|
|
* the callback is called when the promise resolves or rejects. In the
|
|
|
|
* latter case, `lastError` is set to the rejection value, and the
|
2016-02-13 02:38:55 +03:00
|
|
|
* callback function must check `browser.runtime.lastError` or
|
2016-01-30 05:38:08 +03:00
|
|
|
* `extension.runtime.lastError` in order to prevent it being reported
|
|
|
|
* to the console.
|
|
|
|
*
|
|
|
|
* @param {Promise} promise The promise with which to wrap the
|
2016-02-02 06:20:13 +03:00
|
|
|
* callback. May resolve to a `SpreadArgs` instance, in which case
|
|
|
|
* each element will be used as a separate argument.
|
2016-01-30 05:38:08 +03:00
|
|
|
*
|
2016-02-03 06:14:34 +03:00
|
|
|
* Unless the promise object belongs to the cloneScope global, its
|
|
|
|
* resolution value is cloned into cloneScope prior to calling the
|
|
|
|
* `callback` function or resolving the wrapped promise.
|
|
|
|
*
|
2016-01-30 05:38:08 +03:00
|
|
|
* @param {function} [callback] The callback function to wrap
|
|
|
|
*
|
|
|
|
* @returns {Promise|undefined} If callback is null, a promise object
|
|
|
|
* belonging to the target scope. Otherwise, undefined.
|
|
|
|
*/
|
|
|
|
wrapPromise(promise, callback = null) {
|
2016-04-05 00:14:35 +03:00
|
|
|
let runSafe = this.runSafe.bind(this);
|
2016-07-21 01:44:16 +03:00
|
|
|
if (promise instanceof this.cloneScope.Promise) {
|
2016-04-05 00:14:35 +03:00
|
|
|
runSafe = this.runSafeWithoutClone.bind(this);
|
2016-02-03 06:14:34 +03:00
|
|
|
}
|
|
|
|
|
2016-01-30 05:38:08 +03:00
|
|
|
if (callback) {
|
|
|
|
promise.then(
|
|
|
|
args => {
|
2016-04-05 00:14:35 +03:00
|
|
|
if (this.unloaded) {
|
|
|
|
dump(`Promise resolved after context unloaded\n`);
|
|
|
|
} else if (args instanceof SpreadArgs) {
|
2016-02-03 06:14:34 +03:00
|
|
|
runSafe(callback, ...args);
|
2016-02-02 06:20:13 +03:00
|
|
|
} else {
|
2016-02-03 06:14:34 +03:00
|
|
|
runSafe(callback, args);
|
2016-02-02 06:20:13 +03:00
|
|
|
}
|
2016-01-30 05:38:08 +03:00
|
|
|
},
|
|
|
|
error => {
|
|
|
|
this.withLastError(error, () => {
|
2016-04-05 00:14:35 +03:00
|
|
|
if (this.unloaded) {
|
|
|
|
dump(`Promise rejected after context unloaded\n`);
|
|
|
|
} else {
|
|
|
|
this.runSafeWithoutClone(callback);
|
|
|
|
}
|
2016-01-30 05:38:08 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return new this.cloneScope.Promise((resolve, reject) => {
|
|
|
|
promise.then(
|
|
|
|
value => {
|
2016-04-05 00:14:35 +03:00
|
|
|
if (this.unloaded) {
|
|
|
|
dump(`Promise resolved after context unloaded\n`);
|
|
|
|
} else {
|
|
|
|
runSafe(resolve, value);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
value => {
|
|
|
|
if (this.unloaded) {
|
|
|
|
dump(`Promise rejected after context unloaded\n`);
|
|
|
|
} else {
|
|
|
|
this.runSafeWithoutClone(reject, this.normalizeError(value));
|
|
|
|
}
|
2016-01-30 05:38:08 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-30 05:39:29 +03:00
|
|
|
unload() {
|
2016-04-05 00:14:35 +03:00
|
|
|
this.unloaded = true;
|
|
|
|
|
2016-03-05 02:40:56 +03:00
|
|
|
MessageChannel.abortResponses({
|
2016-04-06 00:44:07 +03:00
|
|
|
extensionId: this.extensionId,
|
2016-03-05 02:40:56 +03:00
|
|
|
contextId: this.contextId,
|
|
|
|
});
|
|
|
|
|
2016-01-30 05:39:29 +03:00
|
|
|
for (let obj of this.onClose) {
|
|
|
|
obj.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-24 01:59:33 +03:00
|
|
|
// Manages icon details for toolbar buttons in the |pageAction| and
|
|
|
|
// |browserAction| APIs.
|
|
|
|
let IconDetails = {
|
|
|
|
// Normalizes the various acceptable input formats into an object
|
|
|
|
// with icon size as key and icon URL as value.
|
|
|
|
//
|
|
|
|
// If a context is specified (function is called from an extension):
|
|
|
|
// Throws an error if an invalid icon size was provided or the
|
|
|
|
// extension is not allowed to load the specified resources.
|
|
|
|
//
|
|
|
|
// If no context is specified, instead of throwing an error, this
|
|
|
|
// function simply logs a warning message.
|
|
|
|
normalize(details, extension, context = null) {
|
|
|
|
let result = {};
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (details.imageData) {
|
|
|
|
let imageData = details.imageData;
|
|
|
|
|
|
|
|
// The global might actually be from Schema.jsm, which
|
|
|
|
// normalizes most of our arguments. In that case it won't have
|
|
|
|
// an ImageData property. But Schema.jsm doesn't normalize
|
|
|
|
// actual ImageData objects, so they will come from a global
|
|
|
|
// with the right property.
|
|
|
|
if (instanceOf(imageData, "ImageData")) {
|
|
|
|
imageData = {"19": imageData};
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let size of Object.keys(imageData)) {
|
|
|
|
if (!INTEGER.test(size)) {
|
|
|
|
throw new Error(`Invalid icon size ${size}, must be an integer`);
|
|
|
|
}
|
|
|
|
result[size] = this.convertImageDataToDataURL(imageData[size], context);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (details.path) {
|
|
|
|
let path = details.path;
|
|
|
|
if (typeof path != "object") {
|
|
|
|
path = {"19": path};
|
|
|
|
}
|
|
|
|
|
|
|
|
let baseURI = context ? context.uri : extension.baseURI;
|
|
|
|
|
|
|
|
for (let size of Object.keys(path)) {
|
|
|
|
if (!INTEGER.test(size)) {
|
|
|
|
throw new Error(`Invalid icon size ${size}, must be an integer`);
|
|
|
|
}
|
|
|
|
|
|
|
|
let url = baseURI.resolve(path[size]);
|
|
|
|
|
|
|
|
// The Chrome documentation specifies these parameters as
|
|
|
|
// relative paths. We currently accept absolute URLs as well,
|
|
|
|
// which means we need to check that the extension is allowed
|
|
|
|
// to load them. This will throw an error if it's not allowed.
|
|
|
|
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
|
|
|
|
extension.principal, url,
|
|
|
|
Services.scriptSecurityManager.DISALLOW_SCRIPT);
|
|
|
|
|
|
|
|
result[size] = url;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
// Function is called from extension code, delegate error.
|
|
|
|
if (context) {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
// If there's no context, it's because we're handling this
|
|
|
|
// as a manifest directive. Log a warning rather than
|
|
|
|
// raising an error.
|
|
|
|
extension.manifestError(`Invalid icon data: ${e}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
},
|
|
|
|
|
|
|
|
// Returns the appropriate icon URL for the given icons object and the
|
|
|
|
// screen resolution of the given window.
|
2016-07-14 01:16:00 +03:00
|
|
|
getPreferredIcon(icons, extension = null, size = 16) {
|
2016-05-24 01:59:33 +03:00
|
|
|
const DEFAULT = "chrome://browser/content/extension.svg";
|
|
|
|
|
|
|
|
let bestSize = null;
|
|
|
|
if (icons[size]) {
|
|
|
|
bestSize = size;
|
|
|
|
} else if (icons[2 * size]) {
|
|
|
|
bestSize = 2 * size;
|
|
|
|
} else {
|
|
|
|
let sizes = Object.keys(icons)
|
|
|
|
.map(key => parseInt(key, 10))
|
|
|
|
.sort((a, b) => a - b);
|
|
|
|
|
|
|
|
bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (bestSize) {
|
|
|
|
return {size: bestSize, icon: icons[bestSize]};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {size, icon: DEFAULT};
|
|
|
|
},
|
|
|
|
|
|
|
|
convertImageURLToDataURL(imageURL, context, browserWindow, size = 18) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
let image = new context.contentWindow.Image();
|
|
|
|
image.onload = function() {
|
|
|
|
let canvas = context.contentWindow.document.createElement("canvas");
|
|
|
|
let ctx = canvas.getContext("2d");
|
|
|
|
let dSize = size * browserWindow.devicePixelRatio;
|
|
|
|
|
|
|
|
// Scales the image while maintaing width to height ratio.
|
|
|
|
// If the width and height differ, the image is centered using the
|
|
|
|
// smaller of the two dimensions.
|
|
|
|
let dWidth, dHeight, dx, dy;
|
|
|
|
if (this.width > this.height) {
|
|
|
|
dWidth = dSize;
|
|
|
|
dHeight = image.height * (dSize / image.width);
|
|
|
|
dx = 0;
|
|
|
|
dy = (dSize - dHeight) / 2;
|
|
|
|
} else {
|
|
|
|
dWidth = image.width * (dSize / image.height);
|
|
|
|
dHeight = dSize;
|
|
|
|
dx = (dSize - dWidth) / 2;
|
|
|
|
dy = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.drawImage(this, 0, 0, this.width, this.height, dx, dy, dWidth, dHeight);
|
|
|
|
resolve(canvas.toDataURL("image/png"));
|
|
|
|
};
|
|
|
|
image.onerror = reject;
|
|
|
|
image.src = imageURL;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
convertImageDataToDataURL(imageData, context) {
|
|
|
|
let document = context.contentWindow.document;
|
|
|
|
let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
|
|
|
canvas.width = imageData.width;
|
|
|
|
canvas.height = imageData.height;
|
|
|
|
canvas.getContext("2d").putImageData(imageData, 0, 0);
|
|
|
|
|
|
|
|
return canvas.toDataURL("image/png");
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2015-11-21 23:07:14 +03:00
|
|
|
function LocaleData(data) {
|
|
|
|
this.defaultLocale = data.defaultLocale;
|
|
|
|
this.selectedLocale = data.selectedLocale;
|
|
|
|
this.locales = data.locales || new Map();
|
2016-04-04 23:54:27 +03:00
|
|
|
this.warnedMissingKeys = new Set();
|
2015-11-21 23:07:14 +03:00
|
|
|
|
2016-01-09 03:26:22 +03:00
|
|
|
// Map(locale-name -> Map(message-key -> localized-string))
|
2015-11-21 23:07:14 +03:00
|
|
|
//
|
|
|
|
// Contains a key for each loaded locale, each of which is a
|
|
|
|
// Map of message keys to their localized strings.
|
|
|
|
this.messages = data.messages || new Map();
|
2016-01-09 03:26:22 +03:00
|
|
|
|
|
|
|
if (data.builtinMessages) {
|
|
|
|
this.messages.set(this.BUILTIN, data.builtinMessages);
|
|
|
|
}
|
2015-12-03 03:58:53 +03:00
|
|
|
}
|
2015-11-21 23:07:14 +03:00
|
|
|
|
2016-05-24 01:59:33 +03:00
|
|
|
|
2015-11-21 23:07:14 +03:00
|
|
|
LocaleData.prototype = {
|
|
|
|
// Representation of the object to send to content processes. This
|
|
|
|
// should include anything the content process might need.
|
|
|
|
serialize() {
|
|
|
|
return {
|
|
|
|
defaultLocale: this.defaultLocale,
|
|
|
|
selectedLocale: this.selectedLocale,
|
|
|
|
messages: this.messages,
|
|
|
|
locales: this.locales,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2016-01-09 03:26:22 +03:00
|
|
|
BUILTIN: "@@BUILTIN_MESSAGES",
|
|
|
|
|
2015-11-21 23:07:14 +03:00
|
|
|
has(locale) {
|
|
|
|
return this.messages.has(locale);
|
|
|
|
},
|
|
|
|
|
|
|
|
// https://developer.chrome.com/extensions/i18n
|
2016-04-04 23:54:27 +03:00
|
|
|
localizeMessage(message, substitutions = [], options = {}) {
|
|
|
|
let defaultOptions = {
|
|
|
|
locale: this.selectedLocale,
|
|
|
|
defaultValue: "",
|
|
|
|
cloneScope: null,
|
|
|
|
};
|
|
|
|
|
|
|
|
options = Object.assign(defaultOptions, options);
|
|
|
|
|
|
|
|
let locales = new Set([this.BUILTIN, options.locale, this.defaultLocale]
|
2015-11-21 23:07:14 +03:00
|
|
|
.filter(locale => this.messages.has(locale)));
|
|
|
|
|
|
|
|
// Message names are case-insensitive, so normalize them to lower-case.
|
|
|
|
message = message.toLowerCase();
|
|
|
|
for (let locale of locales) {
|
|
|
|
let messages = this.messages.get(locale);
|
|
|
|
if (messages.has(message)) {
|
2015-12-03 03:58:53 +03:00
|
|
|
let str = messages.get(message);
|
2015-11-21 23:07:14 +03:00
|
|
|
|
|
|
|
if (!Array.isArray(substitutions)) {
|
|
|
|
substitutions = [substitutions];
|
|
|
|
}
|
|
|
|
|
|
|
|
let replacer = (matched, index, dollarSigns) => {
|
|
|
|
if (index) {
|
|
|
|
// This is not quite Chrome-compatible. Chrome consumes any number
|
|
|
|
// of digits following the $, but only accepts 9 substitutions. We
|
|
|
|
// accept any number of substitutions.
|
2015-12-03 03:58:53 +03:00
|
|
|
index = parseInt(index, 10) - 1;
|
2015-11-21 23:07:14 +03:00
|
|
|
return index in substitutions ? substitutions[index] : "";
|
|
|
|
} else {
|
|
|
|
// For any series of contiguous `$`s, the first is dropped, and
|
|
|
|
// the rest remain in the output string.
|
|
|
|
return dollarSigns;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
return str.replace(/\$(?:([1-9]\d*)|(\$+))/g, replacer);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for certain pre-defined messages.
|
2016-01-09 03:26:22 +03:00
|
|
|
if (message == "@@ui_locale") {
|
2016-02-18 15:50:17 +03:00
|
|
|
return this.uiLocale;
|
2015-11-21 23:07:14 +03:00
|
|
|
} else if (message.startsWith("@@bidi_")) {
|
|
|
|
let registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry);
|
|
|
|
let rtl = registry.isLocaleRTL("global");
|
|
|
|
|
|
|
|
if (message == "@@bidi_dir") {
|
|
|
|
return rtl ? "rtl" : "ltr";
|
|
|
|
} else if (message == "@@bidi_reversed_dir") {
|
|
|
|
return rtl ? "ltr" : "rtl";
|
|
|
|
} else if (message == "@@bidi_start_edge") {
|
|
|
|
return rtl ? "right" : "left";
|
|
|
|
} else if (message == "@@bidi_end_edge") {
|
|
|
|
return rtl ? "left" : "right";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-04 23:54:27 +03:00
|
|
|
if (!this.warnedMissingKeys.has(message)) {
|
|
|
|
let error = `Unknown localization message ${message}`;
|
|
|
|
if (options.cloneScope) {
|
|
|
|
error = new options.cloneScope.Error(error);
|
|
|
|
}
|
|
|
|
Cu.reportError(error);
|
|
|
|
this.warnedMissingKeys.add(message);
|
|
|
|
}
|
|
|
|
return options.defaultValue;
|
2015-11-21 23:07:14 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
// Localize a string, replacing all |__MSG_(.*)__| tokens with the
|
|
|
|
// matching string from the current locale, as determined by
|
|
|
|
// |this.selectedLocale|.
|
|
|
|
//
|
|
|
|
// This may not be called before calling either |initLocale| or
|
|
|
|
// |initAllLocales|.
|
|
|
|
localize(str, locale = this.selectedLocale) {
|
|
|
|
if (!str) {
|
|
|
|
return str;
|
|
|
|
}
|
|
|
|
|
|
|
|
return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => {
|
2016-04-04 23:54:27 +03:00
|
|
|
return this.localizeMessage(message, [], {locale, defaultValue: matched});
|
2015-11-21 23:07:14 +03:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
// Validates the contents of a locale JSON file, normalizes the
|
|
|
|
// messages into a Map of message key -> localized string pairs.
|
|
|
|
addLocale(locale, messages, extension) {
|
|
|
|
let result = new Map();
|
|
|
|
|
|
|
|
// Chrome does not document the semantics of its localization
|
|
|
|
// system very well. It handles replacements by pre-processing
|
|
|
|
// messages, replacing |$[a-zA-Z0-9@_]+$| tokens with the value of their
|
|
|
|
// replacements. Later, it processes the resulting string for
|
|
|
|
// |$[0-9]| replacements.
|
|
|
|
//
|
|
|
|
// Again, it does not document this, but it accepts any number
|
|
|
|
// of sequential |$|s, and replaces them with that number minus
|
|
|
|
// 1. It also accepts |$| followed by any number of sequential
|
|
|
|
// digits, but refuses to process a localized string which
|
|
|
|
// provides more than 9 substitutions.
|
|
|
|
if (!instanceOf(messages, "Object")) {
|
|
|
|
extension.packagingError(`Invalid locale data for ${locale}`);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let key of Object.keys(messages)) {
|
|
|
|
let msg = messages[key];
|
|
|
|
|
|
|
|
if (!instanceOf(msg, "Object") || typeof(msg.message) != "string") {
|
|
|
|
extension.packagingError(`Invalid locale message data for ${locale}, message ${JSON.stringify(key)}`);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Substitutions are case-insensitive, so normalize all of their names
|
|
|
|
// to lower-case.
|
|
|
|
let placeholders = new Map();
|
|
|
|
if (instanceOf(msg.placeholders, "Object")) {
|
|
|
|
for (let key of Object.keys(msg.placeholders)) {
|
|
|
|
placeholders.set(key.toLowerCase(), msg.placeholders[key]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let replacer = (match, name) => {
|
|
|
|
let replacement = placeholders.get(name.toLowerCase());
|
|
|
|
if (instanceOf(replacement, "Object") && "content" in replacement) {
|
|
|
|
return replacement.content;
|
|
|
|
}
|
|
|
|
return "";
|
|
|
|
};
|
|
|
|
|
|
|
|
let value = msg.message.replace(/\$([A-Za-z0-9@_]+)\$/g, replacer);
|
|
|
|
|
|
|
|
// Message names are also case-insensitive, so normalize them to lower-case.
|
|
|
|
result.set(key.toLowerCase(), value);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.messages.set(locale, result);
|
|
|
|
return result;
|
|
|
|
},
|
2016-02-18 15:50:17 +03:00
|
|
|
|
2016-02-25 15:29:09 +03:00
|
|
|
get acceptLanguages() {
|
|
|
|
let result = Preferences.get("intl.accept_languages", "", Ci.nsIPrefLocalizedString);
|
2016-02-25 21:18:16 +03:00
|
|
|
return result.split(/\s*,\s*/g);
|
2016-02-25 15:29:09 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
|
2016-02-18 15:50:17 +03:00
|
|
|
get uiLocale() {
|
|
|
|
// Return the browser locale, but convert it to a Chrome-style
|
|
|
|
// locale code.
|
|
|
|
return Locale.getLocale().replace(/-/g, "_");
|
|
|
|
},
|
2015-11-21 23:07:14 +03:00
|
|
|
};
|
|
|
|
|
2015-06-04 01:34:44 +03:00
|
|
|
// 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
|
2016-04-04 21:41:00 +03:00
|
|
|
// ExtensionContext in the chrome process or ExtensionContext in a
|
2015-06-04 01:34:44 +03:00
|
|
|
// 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.
|
2015-12-03 03:58:53 +03:00
|
|
|
function EventManager(context, name, register) {
|
2015-06-04 01:34:44 +03:00
|
|
|
this.context = context;
|
|
|
|
this.name = name;
|
|
|
|
this.register = register;
|
|
|
|
this.unregister = null;
|
|
|
|
this.callbacks = new Set();
|
|
|
|
}
|
|
|
|
|
|
|
|
EventManager.prototype = {
|
|
|
|
addListener(callback) {
|
2015-08-31 05:54:13 +03:00
|
|
|
if (typeof(callback) != "function") {
|
|
|
|
dump(`Expected function\n${Error().stack}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-02-28 01:53:09 +03:00
|
|
|
if (!this.callbacks.size) {
|
2015-06-04 01:34:44 +03:00
|
|
|
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) {
|
2016-02-28 01:53:09 +03:00
|
|
|
if (!this.callbacks.size) {
|
2015-06-04 01:34:44 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.callbacks.delete(callback);
|
2016-02-28 01:53:09 +03:00
|
|
|
if (this.callbacks.size == 0) {
|
2015-06-04 01:34:44 +03:00
|
|
|
this.unregister();
|
|
|
|
|
|
|
|
this.context.forgetOnClose(this);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
hasListener(callback) {
|
|
|
|
return this.callbacks.has(callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
fire(...args) {
|
|
|
|
for (let callback of this.callbacks) {
|
2016-04-05 00:14:35 +03:00
|
|
|
Promise.resolve(callback).then(callback => {
|
|
|
|
if (this.context.unloaded) {
|
2016-04-05 18:59:47 +03:00
|
|
|
dump(`${this.name} event fired after context unloaded.\n`);
|
2016-04-05 00:14:35 +03:00
|
|
|
} else if (this.callbacks.has(callback)) {
|
|
|
|
this.context.runSafe(callback, ...args);
|
|
|
|
}
|
|
|
|
});
|
2015-06-04 01:34:44 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
fireWithoutClone(...args) {
|
|
|
|
for (let callback of this.callbacks) {
|
2016-04-05 00:14:35 +03:00
|
|
|
this.context.runSafeWithoutClone(callback, ...args);
|
2015-06-04 01:34:44 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
close() {
|
2016-02-28 01:53:09 +03:00
|
|
|
if (this.callbacks.size) {
|
|
|
|
this.unregister();
|
|
|
|
}
|
2016-04-05 18:59:47 +03:00
|
|
|
this.callbacks = Object.freeze([]);
|
2015-06-04 01:34:44 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
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.
|
2015-12-03 03:58:53 +03:00
|
|
|
function SingletonEventManager(context, name, register) {
|
2015-06-04 01:34:44 +03:00
|
|
|
this.context = context;
|
|
|
|
this.name = name;
|
|
|
|
this.register = register;
|
|
|
|
this.unregister = new Map();
|
|
|
|
context.callOnClose(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
SingletonEventManager.prototype = {
|
|
|
|
addListener(callback, ...args) {
|
2016-04-05 00:14:35 +03:00
|
|
|
let wrappedCallback = (...args) => {
|
|
|
|
if (this.context.unloaded) {
|
2016-04-05 18:59:47 +03:00
|
|
|
dump(`${this.name} event fired after context unloaded.\n`);
|
2016-04-05 00:14:35 +03:00
|
|
|
} else if (this.unregister.has(callback)) {
|
|
|
|
return callback(...args);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let unregister = this.register(wrappedCallback, ...args);
|
2015-06-04 01:34:44 +03:00
|
|
|
this.unregister.set(callback, unregister);
|
|
|
|
},
|
|
|
|
|
|
|
|
removeListener(callback) {
|
|
|
|
if (!this.unregister.has(callback)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let unregister = this.unregister.get(callback);
|
|
|
|
this.unregister.delete(callback);
|
2015-12-03 03:58:53 +03:00
|
|
|
unregister();
|
2015-06-04 01:34:44 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
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.
|
2015-12-03 03:58:53 +03:00
|
|
|
function ignoreEvent(context, name) {
|
2015-06-04 01:34:44 +03:00
|
|
|
return {
|
2015-11-11 01:13:02 +03:00
|
|
|
addListener: function(callback) {
|
|
|
|
let id = context.extension.id;
|
|
|
|
let frame = Components.stack.caller;
|
|
|
|
let msg = `In add-on ${id}, attempting to use listener "${name}", which is unimplemented.`;
|
|
|
|
let winID = context.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
|
|
.getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
|
|
|
|
let scriptError = Cc["@mozilla.org/scripterror;1"]
|
|
|
|
.createInstance(Ci.nsIScriptError);
|
|
|
|
scriptError.initWithWindowID(msg, frame.filename, null,
|
|
|
|
frame.lineNumber, frame.columnNumber,
|
|
|
|
Ci.nsIScriptError.warningFlag,
|
|
|
|
"content javascript", winID);
|
2015-12-03 03:58:53 +03:00
|
|
|
let consoleService = Cc["@mozilla.org/consoleservice;1"]
|
2015-11-11 01:13:02 +03:00
|
|
|
.getService(Ci.nsIConsoleService);
|
|
|
|
consoleService.logMessage(scriptError);
|
|
|
|
},
|
|
|
|
removeListener: function(callback) {},
|
|
|
|
hasListener: function(callback) {},
|
2015-06-04 01:34:44 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy an API object from |source| into the scope |dest|.
|
2015-12-03 03:58:53 +03:00
|
|
|
function injectAPI(source, dest) {
|
2015-06-04 01:34:44 +03:00
|
|
|
for (let prop in source) {
|
|
|
|
// Skip names prefixed with '_'.
|
2015-12-03 03:58:53 +03:00
|
|
|
if (prop[0] == "_") {
|
2015-06-04 01:34:44 +03:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2016-01-30 05:38:08 +03:00
|
|
|
let desc = Object.getOwnPropertyDescriptor(source, prop);
|
|
|
|
if (typeof(desc.value) == "function") {
|
|
|
|
Cu.exportFunction(desc.value, dest, {defineAs: prop});
|
|
|
|
} else if (typeof(desc.value) == "object") {
|
2015-06-04 01:34:44 +03:00
|
|
|
let obj = Cu.createObjectIn(dest, {defineAs: prop});
|
2016-01-30 05:38:08 +03:00
|
|
|
injectAPI(desc.value, obj);
|
2015-06-04 01:34:44 +03:00
|
|
|
} else {
|
2016-01-30 05:38:08 +03:00
|
|
|
Object.defineProperty(dest, prop, desc);
|
2015-06-04 01:34:44 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-27 00:20:28 +03:00
|
|
|
/**
|
|
|
|
* Returns a Promise which resolves when the given document's DOM has
|
|
|
|
* fully loaded.
|
|
|
|
*
|
|
|
|
* @param {Document} doc The document to await the load of.
|
|
|
|
* @returns {Promise<Document>}
|
|
|
|
*/
|
|
|
|
function promiseDocumentReady(doc) {
|
|
|
|
if (doc.readyState == "interactive" || doc.readyState == "complete") {
|
|
|
|
return Promise.resolve(doc);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Promise(resolve => {
|
|
|
|
doc.addEventListener("DOMContentLoaded", function onReady(event) {
|
|
|
|
if (event.target === event.currentTarget) {
|
|
|
|
doc.removeEventListener("DOMContentLoaded", onReady, true);
|
|
|
|
resolve(doc);
|
|
|
|
}
|
|
|
|
}, true);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-06-04 01:34:44 +03:00
|
|
|
/*
|
|
|
|
* Messaging primitives.
|
|
|
|
*/
|
|
|
|
|
2016-07-16 07:44:03 +03:00
|
|
|
let gNextPortId = 1;
|
2015-06-04 01:34:44 +03:00
|
|
|
|
|
|
|
// Abstraction for a Port object in the extension API. Each port has a unique ID.
|
2015-12-03 03:58:53 +03:00
|
|
|
function Port(context, messageManager, name, id, sender) {
|
2015-06-04 01:34:44 +03:00
|
|
|
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;
|
2015-09-01 22:20:07 +03:00
|
|
|
|
|
|
|
this.messageManager.addMessageListener(this.disconnectName, this, true);
|
|
|
|
this.disconnectListeners = new Set();
|
2015-06-04 01:34:44 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2015-12-03 03:58:53 +03:00
|
|
|
throw new this.context.contentWindow.Error("Attempt to postMessage on disconnected port");
|
2015-06-04 01:34:44 +03:00
|
|
|
}
|
|
|
|
this.messageManager.sendAsyncMessage(this.listenerName, json);
|
|
|
|
},
|
|
|
|
onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => {
|
|
|
|
let listener = () => {
|
|
|
|
if (!this.disconnected) {
|
|
|
|
fire();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-09-01 22:20:07 +03:00
|
|
|
this.disconnectListeners.add(listener);
|
2015-06-04 01:34:44 +03:00
|
|
|
return () => {
|
2015-09-01 22:20:07 +03:00
|
|
|
this.disconnectListeners.delete(listener);
|
2015-06-04 01:34:44 +03:00
|
|
|
};
|
|
|
|
}).api(),
|
|
|
|
onMessage: new EventManager(this.context, "Port.onMessage", fire => {
|
|
|
|
let listener = ({data}) => {
|
2016-07-07 03:14:02 +03:00
|
|
|
if (!this.context.active) {
|
|
|
|
// TODO: Send error as a response.
|
|
|
|
Cu.reportError("Message received on port for an inactive content script");
|
|
|
|
} else if (!this.disconnected) {
|
2015-06-04 01:34:44 +03:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
2015-09-01 22:20:07 +03:00
|
|
|
handleDisconnection() {
|
|
|
|
this.messageManager.removeMessageListener(this.disconnectName, this);
|
Backed out 12 changesets (bug 1202482, bug 1202483, bug 1202481, bug 1202486, bug 1202479, bug 1202478, bug 1197475, bug 1203331, bug 1139860, bug 1202501, bug 1199473, bug 1190662) for Mulet mochitest-5 timeouts
CLOSED TREE
Backed out changeset 6503123e95dd (bug 1139860)
Backed out changeset b83bc163064d (bug 1203331)
Backed out changeset 2f501bd57cd2 (bug 1202481)
Backed out changeset 37e6ac7beb42 (bug 1202486)
Backed out changeset f9b6e99e620e (bug 1202483)
Backed out changeset 466af9f9baee (bug 1202482)
Backed out changeset 6be690e265a2 (bug 1202479)
Backed out changeset 57ff88bfccf4 (bug 1197475)
Backed out changeset 7e8c04ff6049 (bug 1202478)
Backed out changeset 525227997274 (bug 1202501)
Backed out changeset da317cdb79d3 (bug 1199473)
Backed out changeset 73b8ddd6dac9 (bug 1190662)
--HG--
rename : browser/components/extensions/test/browser/browser_ext_simple.js => browser/components/extensions/test/browser/browser_extensions_simple.js
rename : toolkit/components/extensions/test/mochitest/file_sample.html => toolkit/components/extensions/test/mochitest/file_contentscript_page1.html
2015-09-23 05:29:51 +03:00
|
|
|
this.context.forgetOnClose(this);
|
2015-09-01 22:20:07 +03:00
|
|
|
this.disconnected = true;
|
|
|
|
},
|
|
|
|
|
|
|
|
receiveMessage(msg) {
|
|
|
|
if (msg.name == this.disconnectName) {
|
|
|
|
if (this.disconnected) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let listener of this.disconnectListeners) {
|
|
|
|
listener();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.handleDisconnection();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
disconnect() {
|
|
|
|
if (this.disconnected) {
|
2016-07-16 08:46:42 +03:00
|
|
|
// disconnect() may be called without side effects even after the port is
|
|
|
|
// closed - https://developer.chrome.com/extensions/runtime#type-Port
|
|
|
|
return;
|
2015-09-01 22:20:07 +03:00
|
|
|
}
|
|
|
|
this.handleDisconnection();
|
2015-06-04 01:34:44 +03:00
|
|
|
this.messageManager.sendAsyncMessage(this.disconnectName);
|
|
|
|
},
|
|
|
|
|
|
|
|
close() {
|
|
|
|
this.disconnect();
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2015-12-03 03:58:53 +03:00
|
|
|
function getMessageManager(target) {
|
2016-01-06 02:37:01 +03:00
|
|
|
if (target instanceof Ci.nsIFrameLoaderOwner) {
|
|
|
|
return target.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
|
2016-01-06 21:10:35 +03:00
|
|
|
}
|
2016-01-06 02:37:01 +03:00
|
|
|
return target;
|
2015-06-04 01:34:44 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Each extension scope gets its own Messenger object. It handles the
|
|
|
|
// basics of sendMessage, onMessage, connect, and onConnect.
|
|
|
|
//
|
|
|
|
// |context| is the extension scope.
|
2016-03-05 02:40:56 +03:00
|
|
|
// |messageManagers| is an array of MessageManagers used to receive messages.
|
2015-12-04 01:25:40 +03:00
|
|
|
// |sender| is an object describing the sender (usually giving its extension id, tabId, etc.)
|
2015-06-04 01:34:44 +03:00
|
|
|
// |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.
|
2016-03-05 02:40:56 +03:00
|
|
|
function Messenger(context, messageManagers, sender, filter, delegate) {
|
2015-06-04 01:34:44 +03:00
|
|
|
this.context = context;
|
2016-03-05 02:40:56 +03:00
|
|
|
this.messageManagers = messageManagers;
|
2015-06-04 01:34:44 +03:00
|
|
|
this.sender = sender;
|
|
|
|
this.filter = filter;
|
|
|
|
this.delegate = delegate;
|
|
|
|
}
|
|
|
|
|
|
|
|
Messenger.prototype = {
|
2016-03-05 02:40:56 +03:00
|
|
|
_sendMessage(messageManager, message, data, recipient) {
|
|
|
|
let options = {
|
|
|
|
recipient,
|
|
|
|
sender: this.sender,
|
|
|
|
responseType: MessageChannel.RESPONSE_FIRST,
|
|
|
|
};
|
|
|
|
|
|
|
|
return this.context.sendMessage(messageManager, message, data, options);
|
|
|
|
},
|
|
|
|
|
2015-06-04 01:34:44 +03:00
|
|
|
sendMessage(messageManager, msg, recipient, responseCallback) {
|
2016-03-05 02:40:56 +03:00
|
|
|
let promise = this._sendMessage(messageManager, "Extension:Message", msg, recipient)
|
|
|
|
.catch(error => {
|
|
|
|
if (error.result == MessageChannel.RESULT_NO_HANDLER) {
|
|
|
|
return Promise.reject({message: "Could not establish connection. Receiving end does not exist."});
|
|
|
|
} else if (error.result == MessageChannel.RESULT_NO_RESPONSE) {
|
|
|
|
if (responseCallback) {
|
|
|
|
// As a special case, we don't call the callback variant if we
|
|
|
|
// receive no response. So return a promise which will never
|
|
|
|
// resolve.
|
|
|
|
return new Promise(() => {});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return Promise.reject({message: error.message});
|
2016-02-16 04:37:19 +03:00
|
|
|
}
|
2016-03-05 02:40:56 +03:00
|
|
|
});
|
2016-02-16 04:37:19 +03:00
|
|
|
|
|
|
|
return this.context.wrapPromise(promise, responseCallback);
|
2015-06-04 01:34:44 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
onMessage(name) {
|
2015-08-31 17:54:12 +03:00
|
|
|
return new SingletonEventManager(this.context, name, callback => {
|
2016-03-05 02:40:56 +03:00
|
|
|
let listener = {
|
|
|
|
messageFilterPermissive: this.filter,
|
|
|
|
|
|
|
|
receiveMessage: ({target, data: message, sender, recipient}) => {
|
2016-07-07 03:14:02 +03:00
|
|
|
if (!this.context.active) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-03-05 02:40:56 +03:00
|
|
|
if (this.delegate) {
|
|
|
|
this.delegate.getSender(this.context, target, sender);
|
|
|
|
}
|
2015-06-04 01:34:44 +03:00
|
|
|
|
2016-03-05 02:40:56 +03:00
|
|
|
let sendResponse;
|
|
|
|
let response = undefined;
|
|
|
|
let promise = new Promise(resolve => {
|
|
|
|
sendResponse = value => {
|
|
|
|
resolve(value);
|
|
|
|
response = promise;
|
|
|
|
};
|
|
|
|
});
|
2015-06-04 01:34:44 +03:00
|
|
|
|
2016-03-05 02:40:56 +03:00
|
|
|
message = Cu.cloneInto(message, this.context.cloneScope);
|
|
|
|
sender = Cu.cloneInto(sender, this.context.cloneScope);
|
|
|
|
sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
|
2015-06-04 01:34:44 +03:00
|
|
|
|
2016-02-16 04:37:19 +03:00
|
|
|
// Note: We intentionally do not use runSafe here so that any
|
|
|
|
// errors are propagated to the message sender.
|
|
|
|
let result = callback(message, sender, sendResponse);
|
2016-03-09 16:25:11 +03:00
|
|
|
if (result instanceof this.context.cloneScope.Promise) {
|
2016-03-05 02:40:56 +03:00
|
|
|
return result;
|
|
|
|
} else if (result === true) {
|
|
|
|
return promise;
|
2015-08-31 17:54:12 +03:00
|
|
|
}
|
2016-03-05 02:40:56 +03:00
|
|
|
return response;
|
|
|
|
},
|
2015-06-04 01:34:44 +03:00
|
|
|
};
|
|
|
|
|
2016-03-05 02:40:56 +03:00
|
|
|
MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
|
2015-06-04 01:34:44 +03:00
|
|
|
return () => {
|
2016-03-05 02:40:56 +03:00
|
|
|
MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
|
2015-06-04 01:34:44 +03:00
|
|
|
};
|
|
|
|
}).api();
|
|
|
|
},
|
|
|
|
|
|
|
|
connect(messageManager, name, recipient) {
|
2016-07-16 07:44:03 +03:00
|
|
|
// TODO(robwu): Use a process ID instead of the process type. bugzil.la/1287626
|
|
|
|
let portId = `${gNextPortId++}-${Services.appinfo.processType}`;
|
2015-06-04 01:34:44 +03:00
|
|
|
let port = new Port(this.context, messageManager, name, portId, null);
|
|
|
|
let msg = {name, portId};
|
2016-03-05 02:40:56 +03:00
|
|
|
// TODO: Disconnect the port if no response?
|
|
|
|
this._sendMessage(messageManager, "Extension:Connect", msg, recipient);
|
2015-06-04 01:34:44 +03:00
|
|
|
return port.api();
|
|
|
|
},
|
|
|
|
|
|
|
|
onConnect(name) {
|
2016-03-05 02:40:56 +03:00
|
|
|
return new SingletonEventManager(this.context, name, callback => {
|
|
|
|
let listener = {
|
|
|
|
messageFilterPermissive: this.filter,
|
|
|
|
|
|
|
|
receiveMessage: ({target, data: 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);
|
2016-04-05 00:14:35 +03:00
|
|
|
this.context.runSafeWithoutClone(callback, port.api());
|
2016-03-05 02:40:56 +03:00
|
|
|
return true;
|
|
|
|
},
|
2015-06-04 01:34:44 +03:00
|
|
|
};
|
|
|
|
|
2016-03-05 02:40:56 +03:00
|
|
|
MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
|
2015-06-04 01:34:44 +03:00
|
|
|
return () => {
|
2016-03-05 02:40:56 +03:00
|
|
|
MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
|
2015-06-04 01:34:44 +03:00
|
|
|
};
|
|
|
|
}).api();
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2015-12-03 03:58:53 +03:00
|
|
|
function flushJarCache(jarFile) {
|
2015-08-28 02:29:24 +03:00
|
|
|
Services.obs.notifyObservers(jarFile, "flush-cache-entry", null);
|
|
|
|
}
|
|
|
|
|
2016-02-17 23:14:28 +03:00
|
|
|
const PlatformInfo = Object.freeze({
|
|
|
|
os: (function() {
|
|
|
|
let os = AppConstants.platform;
|
|
|
|
if (os == "macosx") {
|
|
|
|
os = "mac";
|
|
|
|
}
|
|
|
|
return os;
|
|
|
|
})(),
|
|
|
|
arch: (function() {
|
|
|
|
let abi = Services.appinfo.XPCOMABI;
|
|
|
|
let [arch] = abi.split("-");
|
|
|
|
if (arch == "x86") {
|
|
|
|
arch = "x86-32";
|
|
|
|
} else if (arch == "x86_64") {
|
|
|
|
arch = "x86-64";
|
|
|
|
}
|
|
|
|
return arch;
|
|
|
|
})(),
|
|
|
|
});
|
|
|
|
|
2016-02-24 06:01:11 +03:00
|
|
|
function detectLanguage(text) {
|
|
|
|
return LanguageDetector.detectLanguage(text).then(result => ({
|
|
|
|
isReliable: result.confident,
|
|
|
|
languages: result.languages.map(lang => {
|
|
|
|
return {
|
|
|
|
language: lang.languageCode,
|
|
|
|
percentage: lang.percent,
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2016-04-06 00:44:07 +03:00
|
|
|
let nextId = 1;
|
|
|
|
|
|
|
|
// We create one instance of this class for every extension context
|
|
|
|
// that needs to use remote APIs. It uses the message manager to
|
|
|
|
// communicate with the ParentAPIManager singleton in
|
|
|
|
// Extension.jsm. It handles asynchronous function calls as well as
|
|
|
|
// event listeners.
|
|
|
|
class ChildAPIManager {
|
|
|
|
constructor(context, messageManager, namespaces, contextData) {
|
|
|
|
this.context = context;
|
|
|
|
this.messageManager = messageManager;
|
|
|
|
this.namespaces = namespaces;
|
|
|
|
|
|
|
|
let id = String(context.extension.id) + "." + String(context.contextId);
|
|
|
|
this.id = id;
|
|
|
|
|
|
|
|
let data = {childId: id, extensionId: context.extension.id, principal: context.principal};
|
|
|
|
Object.assign(data, contextData);
|
|
|
|
messageManager.sendAsyncMessage("API:CreateProxyContext", data);
|
|
|
|
|
|
|
|
messageManager.addMessageListener("API:RunListener", this);
|
|
|
|
messageManager.addMessageListener("API:CallResult", this);
|
|
|
|
|
|
|
|
// Map[path -> Set[listener]]
|
|
|
|
// path is, e.g., "runtime.onMessage".
|
|
|
|
this.listeners = new Map();
|
|
|
|
|
|
|
|
// Map[callId -> Deferred]
|
|
|
|
this.callPromises = new Map();
|
|
|
|
}
|
|
|
|
|
|
|
|
receiveMessage({name, data}) {
|
|
|
|
if (data.childId != this.id) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (name) {
|
|
|
|
case "API:RunListener":
|
|
|
|
let ref = data.path.concat(data.name).join(".");
|
|
|
|
let listeners = this.listeners.get(ref);
|
|
|
|
for (let callback of listeners) {
|
|
|
|
runSafe(this.context, callback, ...data.args);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "API:CallResult":
|
|
|
|
let deferred = this.callPromises.get(data.callId);
|
|
|
|
if (data.lastError) {
|
|
|
|
deferred.reject({message: data.lastError});
|
|
|
|
} else {
|
|
|
|
deferred.resolve(new SpreadArgs(data.args));
|
|
|
|
}
|
|
|
|
this.callPromises.delete(data.callId);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
|
|
this.messageManager.sendAsyncMessage("Extension:CloseProxyContext", {childId: this.id});
|
|
|
|
}
|
|
|
|
|
|
|
|
get cloneScope() {
|
|
|
|
return this.context.cloneScope;
|
|
|
|
}
|
|
|
|
|
|
|
|
callFunction(path, name, args) {
|
|
|
|
throw new Error("Not implemented");
|
|
|
|
}
|
|
|
|
|
|
|
|
callFunctionNoReturn(path, name, args) {
|
|
|
|
this.messageManager.sendAsyncMessage("API:Call", {
|
|
|
|
childId: this.id,
|
|
|
|
path, name, args,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
callAsyncFunction(path, name, args, callback) {
|
|
|
|
let callId = nextId++;
|
|
|
|
let deferred = PromiseUtils.defer();
|
|
|
|
this.callPromises.set(callId, deferred);
|
|
|
|
|
|
|
|
this.messageManager.sendAsyncMessage("API:Call", {
|
|
|
|
childId: this.id,
|
|
|
|
callId,
|
|
|
|
path, name, args,
|
|
|
|
});
|
|
|
|
|
|
|
|
return this.context.wrapPromise(deferred.promise, callback);
|
|
|
|
}
|
|
|
|
|
2016-06-14 14:37:52 +03:00
|
|
|
shouldInject(namespace, name) {
|
|
|
|
return this.namespaces.includes(namespace);
|
2016-04-06 00:44:07 +03:00
|
|
|
}
|
|
|
|
|
2016-06-10 03:44:47 +03:00
|
|
|
hasPermission(permission) {
|
|
|
|
return this.context.extension.permissions.has(permission);
|
|
|
|
}
|
|
|
|
|
2016-04-06 00:44:07 +03:00
|
|
|
getProperty(path, name) {
|
|
|
|
throw new Error("Not implemented");
|
|
|
|
}
|
|
|
|
|
|
|
|
setProperty(path, name, value) {
|
|
|
|
throw new Error("Not implemented");
|
|
|
|
}
|
|
|
|
|
|
|
|
addListener(path, name, listener, args) {
|
|
|
|
let ref = path.concat(name).join(".");
|
|
|
|
let set;
|
|
|
|
if (this.listeners.has(ref)) {
|
|
|
|
set = this.listeners.get(ref);
|
|
|
|
} else {
|
|
|
|
set = new Set();
|
|
|
|
this.listeners.set(ref, set);
|
|
|
|
}
|
|
|
|
|
|
|
|
set.add(listener);
|
|
|
|
|
|
|
|
if (set.size == 1) {
|
|
|
|
args = args.slice(1);
|
|
|
|
|
|
|
|
this.messageManager.sendAsyncMessage("API:AddListener", {
|
|
|
|
childId: this.id,
|
|
|
|
path, name, args,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
removeListener(path, name, listener) {
|
|
|
|
let ref = path.concat(name).join(".");
|
|
|
|
let set = this.listeners.get(ref) || new Set();
|
|
|
|
set.remove(listener);
|
|
|
|
|
|
|
|
if (set.size == 0) {
|
|
|
|
this.messageManager.sendAsyncMessage("Extension:RemoveListener", {
|
|
|
|
childId: this.id,
|
|
|
|
path, name,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
hasListener(path, name, listener) {
|
|
|
|
let ref = path.concat(name).join(".");
|
|
|
|
let set = this.listeners.get(ref) || new Set();
|
|
|
|
return set.has(listener);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-13 17:18:04 +03:00
|
|
|
/**
|
2016-05-24 16:00:17 +03:00
|
|
|
* Convert any of several different representations of a date/time to a Date object.
|
|
|
|
* Accepts several formats:
|
2016-05-13 17:18:04 +03:00
|
|
|
* a Date object, an ISO8601 string, or a number of milliseconds since the epoch as
|
|
|
|
* either a number or a string.
|
|
|
|
*
|
2016-06-07 22:53:32 +03:00
|
|
|
* @param {Date|string|number} date
|
2016-05-13 17:18:04 +03:00
|
|
|
* The date to convert.
|
2016-06-07 22:53:32 +03:00
|
|
|
* @returns {Date}
|
2016-05-24 16:00:17 +03:00
|
|
|
* A Date object
|
2016-05-13 17:18:04 +03:00
|
|
|
*/
|
|
|
|
function normalizeTime(date) {
|
|
|
|
// Of all the formats we accept the "number of milliseconds since the epoch as a string"
|
|
|
|
// is an outlier, everything else can just be passed directly to the Date constructor.
|
2016-05-24 16:00:17 +03:00
|
|
|
return new Date((typeof date == "string" && /^\d+$/.test(date))
|
2016-05-13 17:18:04 +03:00
|
|
|
? parseInt(date, 10) : date);
|
|
|
|
}
|
|
|
|
|
2015-08-15 02:55:09 +03:00
|
|
|
this.ExtensionUtils = {
|
2016-03-01 06:04:03 +03:00
|
|
|
detectLanguage,
|
|
|
|
extend,
|
|
|
|
flushJarCache,
|
|
|
|
ignoreEvent,
|
|
|
|
injectAPI,
|
|
|
|
instanceOf,
|
2016-05-13 17:18:04 +03:00
|
|
|
normalizeTime,
|
2016-02-27 00:20:28 +03:00
|
|
|
promiseDocumentReady,
|
2015-06-04 01:34:44 +03:00
|
|
|
runSafe,
|
2015-08-31 05:54:13 +03:00
|
|
|
runSafeSync,
|
2016-03-01 06:04:03 +03:00
|
|
|
runSafeSyncWithoutClone,
|
|
|
|
runSafeWithoutClone,
|
2016-01-30 05:39:29 +03:00
|
|
|
BaseContext,
|
2015-06-04 01:34:44 +03:00
|
|
|
DefaultWeakMap,
|
|
|
|
EventManager,
|
2016-05-24 01:59:33 +03:00
|
|
|
IconDetails,
|
2015-11-21 23:07:14 +03:00
|
|
|
LocaleData,
|
2015-06-04 01:34:44 +03:00
|
|
|
Messenger,
|
2016-02-17 23:14:28 +03:00
|
|
|
PlatformInfo,
|
2016-03-01 06:04:03 +03:00
|
|
|
SingletonEventManager,
|
2016-02-02 06:20:13 +03:00
|
|
|
SpreadArgs,
|
2016-04-06 00:44:07 +03:00
|
|
|
ChildAPIManager,
|
2015-06-04 01:34:44 +03:00
|
|
|
};
|