--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -94,17 +94,16 @@ ExtensionManagement.registerSchema("chro
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
BaseContext,
LocaleData,
Messenger,
injectAPI,
instanceOf,
- extend,
flushJarCache,
} = ExtensionUtils;
const LOGGER_ID_BASE = "addons.webextension.";
const COMMENT_REGEXP = new RegExp(String.raw`
^
(
@@ -706,46 +705,48 @@ function getExtensionUUID(id) {
// Represents the data contained in an extension, contained either
// in a directory or a zip file, which may or may not be installed.
// This class implements the functionality of the Extension class,
// primarily related to manifest parsing and localization, which is
// useful prior to extension installation or initialization.
//
// No functionality of this class is guaranteed to work before
// |readManifest| has been called, and completed.
-this.ExtensionData = function(rootURI) {
- this.rootURI = rootURI;
+this.ExtensionData = class {
+ constructor(rootURI) {
+ this.rootURI = rootURI;
- this.manifest = null;
- this.id = null;
- this.uuid = null;
- this.localeData = null;
- this._promiseLocales = null;
+ this.manifest = null;
+ this.id = null;
+ this.uuid = null;
+ this.localeData = null;
+ this._promiseLocales = null;
- this.errors = [];
-};
+ this.errors = [];
+ }
-ExtensionData.prototype = {
- builtinMessages: null,
+ get builtinMessages() {
+ return null;
+ }
get logger() {
let id = this.id || "<unknown>";
return Log.repository.getLogger(LOGGER_ID_BASE + id);
- },
+ }
// Report an error about the extension's manifest file.
manifestError(message) {
this.packagingError(`Reading manifest: ${message}`);
- },
+ }
// Report an error about the extension's general packaging.
packagingError(message) {
this.errors.push(message);
this.logger.error(`Loading extension '${this.id}': ${message}`);
- },
+ }
/**
* Returns the moz-extension: URL for the given path within this
* extension.
*
* Must not be called unless either the `id` or `uuid` property has
* already been set.
*
@@ -755,85 +756,87 @@ ExtensionData.prototype = {
getURL(path = "") {
if (!(this.id || this.uuid)) {
throw new Error("getURL may not be called before an `id` or `uuid` has been set");
}
if (!this.uuid) {
this.uuid = getExtensionUUID(this.id);
}
return `moz-extension://${this.uuid}/${path}`;
- },
+ }
- readDirectory: Task.async(function* (path) {
- if (this.rootURI instanceof Ci.nsIFileURL) {
- let uri = NetUtil.newURI(this.rootURI.resolve("./" + path));
- let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
+ readDirectory(path) {
+ return Task.spawn(function* () {
+ if (this.rootURI instanceof Ci.nsIFileURL) {
+ let uri = NetUtil.newURI(this.rootURI.resolve("./" + path));
+ let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
- let iter = new OS.File.DirectoryIterator(fullPath);
- let results = [];
+ let iter = new OS.File.DirectoryIterator(fullPath);
+ let results = [];
- try {
- yield iter.forEach(entry => {
- results.push(entry);
- });
- } catch (e) {
- // Always return a list, even if the directory does not exist (or is
- // not a directory) for symmetry with the ZipReader behavior.
+ try {
+ yield iter.forEach(entry => {
+ results.push(entry);
+ });
+ } catch (e) {
+ // Always return a list, even if the directory does not exist (or is
+ // not a directory) for symmetry with the ZipReader behavior.
+ }
+ iter.close();
+
+ return results;
}
- iter.close();
- return results;
- }
+ if (!(this.rootURI instanceof Ci.nsIJARURI &&
+ this.rootURI.JARFile instanceof Ci.nsIFileURL)) {
+ // This currently happens for app:// URLs passed to us by
+ // UserCustomizations.jsm
+ return [];
+ }
- if (!(this.rootURI instanceof Ci.nsIJARURI &&
- this.rootURI.JARFile instanceof Ci.nsIFileURL)) {
- // This currently happens for app:// URLs passed to us by
- // UserCustomizations.jsm
- return [];
- }
+ // FIXME: We need a way to do this without main thread IO.
+
+ let file = this.rootURI.JARFile.file;
+ let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
+ try {
+ zipReader.open(file);
- // FIXME: We need a way to do this without main thread IO.
+ let results = [];
- let file = this.rootURI.JARFile.file;
- let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
- try {
- zipReader.open(file);
+ // Normalize the directory path.
+ path = path.replace(/\/\/+/g, "/").replace(/^\/|\/$/g, "") + "/";
+
+ // Escape pattern metacharacters.
+ let pattern = path.replace(/[[\]()?*~|$\\]/g, "\\$&");
- let results = [];
-
- // Normalize the directory path.
- path = path.replace(/\/\/+/g, "/").replace(/^\/|\/$/g, "") + "/";
+ let enumerator = zipReader.findEntries(pattern + "*");
+ while (enumerator.hasMore()) {
+ let name = enumerator.getNext();
+ if (!name.startsWith(path)) {
+ throw new Error("Unexpected ZipReader entry");
+ }
- // Escape pattern metacharacters.
- let pattern = path.replace(/[[\]()?*~|$\\]/g, "\\$&");
-
- let enumerator = zipReader.findEntries(pattern + "*");
- while (enumerator.hasMore()) {
- let name = enumerator.getNext();
- if (!name.startsWith(path)) {
- throw new Error("Unexpected ZipReader entry");
+ // The enumerator returns the full path of all entries.
+ // Trim off the leading path, and filter out entries from
+ // subdirectories.
+ name = name.slice(path.length);
+ if (name && !/\/./.test(name)) {
+ results.push({
+ name: name.replace("/", ""),
+ isDir: name.endsWith("/"),
+ });
+ }
}
- // The enumerator returns the full path of all entries.
- // Trim off the leading path, and filter out entries from
- // subdirectories.
- name = name.slice(path.length);
- if (name && !/\/./.test(name)) {
- results.push({
- name: name.replace("/", ""),
- isDir: name.endsWith("/"),
- });
- }
+ return results;
+ } finally {
+ zipReader.close();
}
-
- return results;
- } finally {
- zipReader.close();
- }
- }),
+ }.bind(this));
+ }
readJSON(path) {
return new Promise((resolve, reject) => {
let uri = this.rootURI.resolve(`./${path}`);
NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
if (!Components.isSuccessCode(status)) {
reject(new Error(status));
@@ -846,17 +849,17 @@ ExtensionData.prototype = {
text = text.replace(COMMENT_REGEXP, "$1");
resolve(JSON.parse(text));
} catch (e) {
reject(e);
}
});
});
- },
+ }
// Reads the extension's |manifest.json| file, and stores its
// parsed contents in |this.manifest|.
readManifest() {
return Promise.all([
this.readJSON("manifest.json"),
Management.lazyInit(),
]).then(([manifest]) => {
@@ -893,58 +896,60 @@ ExtensionData.prototype = {
try {
this.id = this.manifest.applications.gecko.id;
} catch (e) {
// Errors are handled by the type checks above.
}
return this.manifest;
});
- },
+ }
localizeMessage(...args) {
return this.localeData.localizeMessage(...args);
- },
+ }
localize(...args) {
return this.localeData.localize(...args);
- },
+ }
// If a "default_locale" is specified in that manifest, returns it
// as a Gecko-compatible locale string. Otherwise, returns null.
get defaultLocale() {
if (this.manifest.default_locale != null) {
return this.normalizeLocaleCode(this.manifest.default_locale);
}
return null;
- },
+ }
// Normalizes a Chrome-compatible locale code to the appropriate
// Gecko-compatible variant. Currently, this means simply
// replacing underscores with hyphens.
normalizeLocaleCode(locale) {
return String.replace(locale, /_/g, "-");
- },
+ }
// Reads the locale file for the given Gecko-compatible locale code, and
// stores its parsed contents in |this.localeMessages.get(locale)|.
- readLocaleFile: Task.async(function* (locale) {
- let locales = yield this.promiseLocales();
- let dir = locales.get(locale) || locale;
- let file = `_locales/${dir}/messages.json`;
+ readLocaleFile(locale) {
+ return Task.spawn(function* () {
+ let locales = yield this.promiseLocales();
+ let dir = locales.get(locale) || locale;
+ let file = `_locales/${dir}/messages.json`;
- try {
- let messages = yield this.readJSON(file);
- return this.localeData.addLocale(locale, messages, this);
- } catch (e) {
- this.packagingError(`Loading locale file ${file}: ${e}`);
- return new Map();
- }
- }),
+ try {
+ let messages = yield this.readJSON(file);
+ return this.localeData.addLocale(locale, messages, this);
+ } catch (e) {
+ this.packagingError(`Loading locale file ${file}: ${e}`);
+ return new Map();
+ }
+ }.bind(this));
+ }
// Reads the list of locales available in the extension, and returns a
// Promise which resolves to a Map upon completion.
// Each map key is a Gecko-compatible locale code, and each value is the
// "_locales" subdirectory containing that locale:
//
// Map(gecko-locale-code -> locale-directory-name)
promiseLocales() {
@@ -966,341 +971,346 @@ ExtensionData.prototype = {
builtinMessages: this.builtinMessages,
});
return locales;
}.bind(this));
}
return this._promiseLocales;
- },
+ }
// Reads the locale messages for all locales, and returns a promise which
// resolves to a Map of locale messages upon completion. Each key in the map
// is a Gecko-compatible locale code, and each value is a locale data object
// as returned by |readLocaleFile|.
- initAllLocales: Task.async(function* () {
- let locales = yield this.promiseLocales();
+ initAllLocales() {
+ return Task.spawn(function* () {
+ let locales = yield this.promiseLocales();
- yield Promise.all(Array.from(locales.keys(),
- locale => this.readLocaleFile(locale)));
+ yield Promise.all(Array.from(locales.keys(),
+ locale => this.readLocaleFile(locale)));
- let defaultLocale = this.defaultLocale;
- if (defaultLocale) {
- if (!locales.has(defaultLocale)) {
- this.manifestError('Value for "default_locale" property must correspond to ' +
- 'a directory in "_locales/". Not found: ' +
- JSON.stringify(`_locales/${this.manifest.default_locale}/`));
+ let defaultLocale = this.defaultLocale;
+ if (defaultLocale) {
+ if (!locales.has(defaultLocale)) {
+ this.manifestError('Value for "default_locale" property must correspond to ' +
+ 'a directory in "_locales/". Not found: ' +
+ JSON.stringify(`_locales/${this.manifest.default_locale}/`));
+ }
+ } else if (locales.size) {
+ this.manifestError('The "default_locale" property is required when a ' +
+ '"_locales/" directory is present.');
}
- } else if (locales.size) {
- this.manifestError('The "default_locale" property is required when a ' +
- '"_locales/" directory is present.');
- }
- return this.localeData.messages;
- }),
+ return this.localeData.messages;
+ }.bind(this));
+ }
// Reads the locale file for the given Gecko-compatible locale code, or the
// default locale if no locale code is given, and sets it as the currently
// selected locale on success.
//
// Pre-loads the default locale for fallback message processing, regardless
// of the locale specified.
//
// If no locales are unavailable, resolves to |null|.
- initLocale: Task.async(function* (locale = this.defaultLocale) {
- if (locale == null) {
- return null;
- }
-
- let promises = [this.readLocaleFile(locale)];
-
- let {defaultLocale} = this;
- if (locale != defaultLocale && !this.localeData.has(defaultLocale)) {
- promises.push(this.readLocaleFile(defaultLocale));
- }
+ initLocale(locale = this.defaultLocale) {
+ return Task.spawn(function* () {
+ if (locale == null) {
+ return null;
+ }
- let results = yield Promise.all(promises);
-
- this.localeData.selectedLocale = locale;
- return results[0];
- }),
-};
-
-// We create one instance of this class per extension. |addonData|
-// comes directly from bootstrap.js when initializing.
-this.Extension = function(addonData) {
- ExtensionData.call(this, addonData.resourceURI);
-
- this.uuid = getExtensionUUID(addonData.id);
+ let promises = [this.readLocaleFile(locale)];
- if (addonData.cleanupFile) {
- Services.obs.addObserver(this, "xpcom-shutdown", false);
- this.cleanupFile = addonData.cleanupFile || null;
- delete addonData.cleanupFile;
- }
-
- this.addonData = addonData;
- this.id = addonData.id;
- this.baseURI = NetUtil.newURI(this.getURL("")).QueryInterface(Ci.nsIURL);
- this.principal = this.createPrincipal();
-
- this.views = new Set();
+ let {defaultLocale} = this;
+ if (locale != defaultLocale && !this.localeData.has(defaultLocale)) {
+ promises.push(this.readLocaleFile(defaultLocale));
+ }
- this.onStartup = null;
-
- this.hasShutdown = false;
- this.onShutdown = new Set();
+ let results = yield Promise.all(promises);
- this.uninstallURL = null;
-
- this.permissions = new Set();
- this.whiteListedHosts = null;
- this.webAccessibleResources = null;
-
- this.emitter = new EventEmitter();
+ this.localeData.selectedLocale = locale;
+ return results[0];
+ }.bind(this));
+ }
};
-/**
- * This code is designed to make it easy to test a WebExtension
- * without creating a bunch of files. Everything is contained in a
- * single JSON blob.
- *
- * Properties:
- * "background": "<JS code>"
- * A script to be loaded as the background script.
- * The "background" section of the "manifest" property is overwritten
- * if this is provided.
- * "manifest": {...}
- * Contents of manifest.json
- * "files": {"filename1": "contents1", ...}
- * Data to be included as files. Can be referenced from the manifest.
- * If a manifest file is provided here, it takes precedence over
- * a generated one. Always use "/" as a directory separator.
- * Directories should appear here only implicitly (as a prefix
- * to file names)
- *
- * To make things easier, the value of "background" and "files"[] can
- * be a function, which is converted to source that is run.
- *
- * The generated extension is stored in the system temporary directory,
- * and an nsIFile object pointing to it is returned.
- *
- * @param {string} id
- * @param {object} data
- * @returns {nsIFile}
- */
-this.Extension.generateXPI = function(id, data) {
- let manifest = data.manifest;
- if (!manifest) {
- manifest = {};
- }
-
- let files = data.files;
- if (!files) {
- files = {};
- }
-
- function provide(obj, keys, value, override = false) {
- if (keys.length == 1) {
- if (!(keys[0] in obj) || override) {
- obj[keys[0]] = value;
- }
- } else {
- if (!(keys[0] in obj)) {
- obj[keys[0]] = {};
- }
- provide(obj[keys[0]], keys.slice(1), value, override);
- }
- }
-
- provide(manifest, ["applications", "gecko", "id"], id);
-
- provide(manifest, ["name"], "Generated extension");
- provide(manifest, ["manifest_version"], 2);
- provide(manifest, ["version"], "1.0");
-
- if (data.background) {
- let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
- let bgScript = uuidGenerator.generateUUID().number + ".js";
-
- provide(manifest, ["background", "scripts"], [bgScript], true);
- files[bgScript] = data.background;
- }
-
- provide(files, ["manifest.json"], manifest);
-
- let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
- let zipW = new ZipWriter();
-
- let file = FileUtils.getFile("TmpD", ["generated-extension.xpi"]);
- file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
-
- const MODE_WRONLY = 0x02;
- const MODE_TRUNCATE = 0x20;
- zipW.open(file, MODE_WRONLY | MODE_TRUNCATE);
-
- // Needs to be in microseconds for some reason.
- let time = Date.now() * 1000;
-
- function generateFile(filename) {
- let components = filename.split("/");
- let path = "";
- for (let component of components.slice(0, -1)) {
- path += component + "/";
- if (!zipW.hasEntry(path)) {
- zipW.addEntryDirectory(path, time, false);
- }
- }
- }
-
- for (let filename in files) {
- let script = files[filename];
- if (typeof(script) == "function") {
- script = "(" + script.toString() + ")()";
- } else if (instanceOf(script, "Object")) {
- script = JSON.stringify(script);
- }
-
- if (!instanceOf(script, "ArrayBuffer")) {
- script = new TextEncoder("utf-8").encode(script).buffer;
- }
-
- let stream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"].createInstance(Ci.nsIArrayBufferInputStream);
- stream.setData(script, 0, script.byteLength);
-
- generateFile(filename);
- zipW.addEntryStream(filename, time, 0, stream, false);
- }
-
- zipW.close();
-
- return file;
-};
/**
* A skeleton Extension-like object, used for testing, which installs an
* add-on via the add-on manager when startup() is called, and
* uninstalles it on shutdown().
*
* @param {string} id
* @param {nsIFile} file
* @param {nsIURI} rootURI
*/
-function MockExtension(id, file, rootURI) {
- this.id = id;
- this.file = file;
- this.rootURI = rootURI;
+class MockExtension {
+ constructor(id, file, rootURI) {
+ this.id = id;
+ this.file = file;
+ this.rootURI = rootURI;
- this._extension = null;
- this._extensionPromise = new Promise(resolve => {
- let onstartup = (msg, extension) => {
- if (extension.id == this.id) {
- Management.off("startup", onstartup);
+ this._extension = null;
+ this._extensionPromise = new Promise(resolve => {
+ let onstartup = (msg, extension) => {
+ if (extension.id == this.id) {
+ Management.off("startup", onstartup);
- this._extension = extension;
- resolve(extension);
- }
- };
- Management.on("startup", onstartup);
- });
-}
+ this._extension = extension;
+ resolve(extension);
+ }
+ };
+ Management.on("startup", onstartup);
+ });
+ }
-MockExtension.prototype = {
testMessage(...args) {
return this._extension.testMessage(...args);
- },
+ }
on(...args) {
this._extensionPromise.then(extension => {
extension.on(...args);
});
- },
+ }
off(...args) {
this._extensionPromise.then(extension => {
extension.off(...args);
});
- },
+ }
startup() {
return AddonManager.installTemporaryAddon(this.file).then(addon => {
this.addon = addon;
return this._extensionPromise;
});
- },
+ }
shutdown() {
this.addon.uninstall(true);
return this.cleanupGeneratedFile();
- },
+ }
cleanupGeneratedFile() {
flushJarCache(this.file);
return OS.File.remove(this.file.path);
- },
-};
+ }
+}
+
+// We create one instance of this class per extension. |addonData|
+// comes directly from bootstrap.js when initializing.
+this.Extension = class extends ExtensionData {
+ constructor(addonData) {
+ super(addonData.resourceURI);
+
+ this.uuid = getExtensionUUID(addonData.id);
+
+ if (addonData.cleanupFile) {
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ this.cleanupFile = addonData.cleanupFile || null;
+ delete addonData.cleanupFile;
+ }
-/**
- * Generates a new extension using |Extension.generateXPI|, and initializes a
- * new |Extension| instance which will execute it.
- *
- * @param {string} id
- * @param {object} data
- * @returns {Extension}
- */
-this.Extension.generate = function(id, data) {
- let file = this.generateXPI(id, data);
+ this.addonData = addonData;
+ this.id = addonData.id;
+ this.baseURI = NetUtil.newURI(this.getURL("")).QueryInterface(Ci.nsIURL);
+ this.principal = this.createPrincipal();
+
+ this.views = new Set();
+
+ this.onStartup = null;
- flushJarCache(file);
- Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
+ this.hasShutdown = false;
+ this.onShutdown = new Set();
+
+ this.uninstallURL = null;
- let fileURI = Services.io.newFileURI(file);
- let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
+ this.permissions = new Set();
+ this.whiteListedHosts = null;
+ this.webAccessibleResources = null;
- if (data.useAddonManager) {
- return new MockExtension(id, file, jarURI);
+ this.emitter = new EventEmitter();
}
- return new Extension({
- id,
- resourceURI: jarURI,
- cleanupFile: file,
- });
-};
+ /**
+ * This code is designed to make it easy to test a WebExtension
+ * without creating a bunch of files. Everything is contained in a
+ * single JSON blob.
+ *
+ * Properties:
+ * "background": "<JS code>"
+ * A script to be loaded as the background script.
+ * The "background" section of the "manifest" property is overwritten
+ * if this is provided.
+ * "manifest": {...}
+ * Contents of manifest.json
+ * "files": {"filename1": "contents1", ...}
+ * Data to be included as files. Can be referenced from the manifest.
+ * If a manifest file is provided here, it takes precedence over
+ * a generated one. Always use "/" as a directory separator.
+ * Directories should appear here only implicitly (as a prefix
+ * to file names)
+ *
+ * To make things easier, the value of "background" and "files"[] can
+ * be a function, which is converted to source that is run.
+ *
+ * The generated extension is stored in the system temporary directory,
+ * and an nsIFile object pointing to it is returned.
+ *
+ * @param {string} id
+ * @param {object} data
+ * @returns {nsIFile}
+ */
+ static generateXPI(id, data) {
+ let manifest = data.manifest;
+ if (!manifest) {
+ manifest = {};
+ }
+
+ let files = data.files;
+ if (!files) {
+ files = {};
+ }
+
+ function provide(obj, keys, value, override = false) {
+ if (keys.length == 1) {
+ if (!(keys[0] in obj) || override) {
+ obj[keys[0]] = value;
+ }
+ } else {
+ if (!(keys[0] in obj)) {
+ obj[keys[0]] = {};
+ }
+ provide(obj[keys[0]], keys.slice(1), value, override);
+ }
+ }
+
+ provide(manifest, ["applications", "gecko", "id"], id);
+
+ provide(manifest, ["name"], "Generated extension");
+ provide(manifest, ["manifest_version"], 2);
+ provide(manifest, ["version"], "1.0");
+
+ if (data.background) {
+ let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ let bgScript = uuidGenerator.generateUUID().number + ".js";
+
+ provide(manifest, ["background", "scripts"], [bgScript], true);
+ files[bgScript] = data.background;
+ }
+
+ provide(files, ["manifest.json"], manifest);
+
+ let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
+ let zipW = new ZipWriter();
-Extension.prototype = extend(Object.create(ExtensionData.prototype), {
+ let file = FileUtils.getFile("TmpD", ["generated-extension.xpi"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+ const MODE_WRONLY = 0x02;
+ const MODE_TRUNCATE = 0x20;
+ zipW.open(file, MODE_WRONLY | MODE_TRUNCATE);
+
+ // Needs to be in microseconds for some reason.
+ let time = Date.now() * 1000;
+
+ function generateFile(filename) {
+ let components = filename.split("/");
+ let path = "";
+ for (let component of components.slice(0, -1)) {
+ path += component + "/";
+ if (!zipW.hasEntry(path)) {
+ zipW.addEntryDirectory(path, time, false);
+ }
+ }
+ }
+
+ for (let filename in files) {
+ let script = files[filename];
+ if (typeof(script) == "function") {
+ script = "(" + script.toString() + ")()";
+ } else if (instanceOf(script, "Object")) {
+ script = JSON.stringify(script);
+ }
+
+ if (!instanceOf(script, "ArrayBuffer")) {
+ script = new TextEncoder("utf-8").encode(script).buffer;
+ }
+
+ let stream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"].createInstance(Ci.nsIArrayBufferInputStream);
+ stream.setData(script, 0, script.byteLength);
+
+ generateFile(filename);
+ zipW.addEntryStream(filename, time, 0, stream, false);
+ }
+
+ zipW.close();
+
+ return file;
+ }
+
+ /**
+ * Generates a new extension using |Extension.generateXPI|, and initializes a
+ * new |Extension| instance which will execute it.
+ *
+ * @param {string} id
+ * @param {object} data
+ * @returns {Extension}
+ */
+ static generate(id, data) {
+ let file = this.generateXPI(id, data);
+
+ flushJarCache(file);
+ Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
+
+ let fileURI = Services.io.newFileURI(file);
+ let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
+
+ if (data.useAddonManager) {
+ return new MockExtension(id, file, jarURI);
+ }
+
+ return new Extension({
+ id,
+ resourceURI: jarURI,
+ cleanupFile: file,
+ });
+ }
+
on(hook, f) {
return this.emitter.on(hook, f);
- },
+ }
off(hook, f) {
return this.emitter.off(hook, f);
- },
+ }
emit(...args) {
return this.emitter.emit(...args);
- },
+ }
testMessage(...args) {
Management.emit("test-message", this, ...args);
- },
+ }
createPrincipal(uri = this.baseURI) {
return Services.scriptSecurityManager.createCodebasePrincipal(
uri, {addonId: this.id});
- },
+ }
// Checks that the given URL is a child of our baseURI.
isExtensionURL(url) {
let uri = Services.io.newURI(url, null, null);
let common = this.baseURI.getCommonBaseSpec(uri);
return common == this.baseURI.spec;
- },
+ }
// Representation of the extension to send to content
// processes. This should include anything the content process might
// need.
serialize() {
return {
id: this.id,
uuid: this.uuid,
@@ -1308,17 +1318,17 @@ Extension.prototype = extend(Object.crea
resourceURL: this.addonData.resourceURI.spec,
baseURL: this.baseURI.spec,
content_scripts: this.manifest.content_scripts || [], // eslint-disable-line camelcase
webAccessibleResources: this.webAccessibleResources.serialize(),
whiteListedHosts: this.whiteListedHosts.serialize(),
localeData: this.localeData.serialize(),
permissions: this.permissions,
};
- },
+ }
broadcast(msg, data) {
return new Promise(resolve => {
let count = Services.ppmm.childCount;
if (AppConstants.MOZ_NUWA_PROCESS) {
// The nuwa process is frozen, so don't expect it to answer.
count--;
}
@@ -1326,17 +1336,17 @@ Extension.prototype = extend(Object.crea
count--;
if (count == 0) {
Services.ppmm.removeMessageListener(msg + "Complete", listener);
resolve();
}
});
Services.ppmm.broadcastAsyncMessage(msg, data);
});
- },
+ }
runManifest(manifest) {
let permissions = manifest.permissions || [];
let whitelist = [];
for (let perm of permissions) {
this.permissions.add(perm);
if (!/^\w+(\.\w+)*$/.test(perm)) {
@@ -1362,49 +1372,54 @@ Extension.prototype = extend(Object.crea
let data = Services.ppmm.initialProcessData;
if (!data["Extension:Extensions"]) {
data["Extension:Extensions"] = [];
}
let serial = this.serialize();
data["Extension:Extensions"].push(serial);
return this.broadcast("Extension:Startup", serial);
- },
+ }
callOnClose(obj) {
this.onShutdown.add(obj);
- },
+ }
forgetOnClose(obj) {
this.onShutdown.delete(obj);
- },
+ }
get builtinMessages() {
return new Map([
["@@extension_id", this.uuid],
]);
- },
+ }
// Reads the locale file for the given Gecko-compatible locale code, or if
// no locale is given, the available locale closest to the UI locale.
// Sets the currently selected locale on success.
- initLocale: Task.async(function* (locale = undefined) {
- if (locale === undefined) {
- let locales = yield this.promiseLocales();
+ initLocale(locale = undefined) {
+ // Ugh.
+ let super_ = super.initLocale.bind(this);
+
+ return Task.spawn(function* () {
+ if (locale === undefined) {
+ let locales = yield this.promiseLocales();
- let localeList = Array.from(locales.keys(), locale => {
- return {name: locale, locales: [locale]};
- });
+ let localeList = Array.from(locales.keys(), locale => {
+ return {name: locale, locales: [locale]};
+ });
- let match = Locale.findClosestLocale(localeList);
- locale = match ? match.name : this.defaultLocale;
- }
+ let match = Locale.findClosestLocale(localeList);
+ locale = match ? match.name : this.defaultLocale;
+ }
- return ExtensionData.prototype.initLocale.call(this, locale);
- }),
+ return super_(locale);
+ }.bind(this));
+ }
startup() {
let started = false;
return this.readManifest().then(() => {
ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
started = true;
if (!this.hasShutdown) {
@@ -1435,17 +1450,17 @@ Extension.prototype = extend(Object.crea
if (started) {
ExtensionManagement.shutdownExtension(this.uuid);
}
this.cleanupGeneratedFile();
throw e;
});
- },
+ }
cleanupGeneratedFile() {
if (!this.cleanupFile) {
return;
}
let file = this.cleanupFile;
this.cleanupFile = null;
@@ -1454,17 +1469,17 @@ Extension.prototype = extend(Object.crea
this.broadcast("Extension:FlushJarCache", {path: file.path}).then(() => {
// We can't delete this file until everyone using it has
// closed it (because Windows is dumb). So we wait for all the
// child processes (including the parent) to flush their JAR
// caches. These caches may keep the file open.
file.remove(false);
});
- },
+ }
shutdown() {
this.hasShutdown = true;
if (!this.manifest) {
ExtensionManagement.shutdownExtension(this.uuid);
this.cleanupGeneratedFile();
return;
@@ -1484,24 +1499,24 @@ Extension.prototype = extend(Object.crea
Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
MessageChannel.abortResponses({extensionId: this.id});
ExtensionManagement.shutdownExtension(this.uuid);
this.cleanupGeneratedFile();
- },
+ }
observe(subject, topic, data) {
if (topic == "xpcom-shutdown") {
this.cleanupGeneratedFile();
}
- },
+ }
hasPermission(perm) {
return this.permissions.has(perm);
- },
+ }
get name() {
return this.localize(this.manifest.name);
- },
-});
+ }
+};