diff options
author | Matt A. Tobin <email@mattatobin.com> | 2018-02-09 11:10:00 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2018-02-09 11:10:00 -0500 |
commit | f164d9124708b50789dbb6959e1de96cc5697c48 (patch) | |
tree | 6dffd12e08c5383130df0252fb69cd6d6330794f /toolkit/components/webextensions/Extension.jsm | |
parent | 30de4018913f0cdaea19d1dd12ecd8209e2ed08e (diff) | |
download | UXP-f164d9124708b50789dbb6959e1de96cc5697c48.tar UXP-f164d9124708b50789dbb6959e1de96cc5697c48.tar.gz UXP-f164d9124708b50789dbb6959e1de96cc5697c48.tar.lz UXP-f164d9124708b50789dbb6959e1de96cc5697c48.tar.xz UXP-f164d9124708b50789dbb6959e1de96cc5697c48.zip |
Rename Toolkit's webextensions component directory to better reflect what it is.
Diffstat (limited to 'toolkit/components/webextensions/Extension.jsm')
-rw-r--r-- | toolkit/components/webextensions/Extension.jsm | 902 |
1 files changed, 902 insertions, 0 deletions
diff --git a/toolkit/components/webextensions/Extension.jsm b/toolkit/components/webextensions/Extension.jsm new file mode 100644 index 000000000..3468f2594 --- /dev/null +++ b/toolkit/components/webextensions/Extension.jsm @@ -0,0 +1,902 @@ +/* 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 = ["Extension", "ExtensionData"]; + +/* globals Extension ExtensionData */ + +/* + * This file is the main entry point for extensions. When an extension + * loads, its bootstrap.js file creates a Extension instance + * and calls .startup() on it. It calls .shutdown() when the extension + * unloads. Extension manages any extension-specific state in + * the chrome process. + */ + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.importGlobalProperties(["TextEncoder"]); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", + "resource://gre/modules/AddonManager.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", + "resource://gre/modules/AppConstants.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionAPIs", + "resource://gre/modules/ExtensionAPI.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage", + "resource://gre/modules/ExtensionStorage.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestCommon", + "resource://testing-common/ExtensionTestCommon.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Locale", + "resource://gre/modules/Locale.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Log", + "resource://gre/modules/Log.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs", + "resource://gre/modules/MatchPattern.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", + "resource://gre/modules/MatchPattern.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", + "resource://gre/modules/MessageChannel.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Preferences", + "resource://gre/modules/Preferences.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "require", + "resource://devtools/shared/Loader.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Schemas", + "resource://gre/modules/Schemas.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +Cu.import("resource://gre/modules/ExtensionContent.jsm"); +Cu.import("resource://gre/modules/ExtensionManagement.jsm"); +Cu.import("resource://gre/modules/ExtensionParent.jsm"); +Cu.import("resource://gre/modules/ExtensionUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "uuidGen", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); + +var { + GlobalManager, + ParentAPIManager, + apiManager: Management, +} = ExtensionParent; + +const { + EventEmitter, + LocaleData, + getUniqueId, +} = ExtensionUtils; + +XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole); + +const LOGGER_ID_BASE = "addons.webextension."; +const UUID_MAP_PREF = "extensions.webextensions.uuids"; +const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall"; +const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall"; + +const COMMENT_REGEXP = new RegExp(String.raw` + ^ + ( + (?: + [^"\n] | + " (?:[^"\\\n] | \\.)* " + )*? + ) + + //.* + `.replace(/\s+/g, ""), "gm"); + +// All moz-extension URIs use a machine-specific UUID rather than the +// extension's own ID in the host component. This makes it more +// difficult for web pages to detect whether a user has a given add-on +// installed (by trying to load a moz-extension URI referring to a +// web_accessible_resource from the extension). UUIDMap.get() +// returns the UUID for a given add-on ID. +var UUIDMap = { + _read() { + let pref = Preferences.get(UUID_MAP_PREF, "{}"); + try { + return JSON.parse(pref); + } catch (e) { + Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`); + return {}; + } + }, + + _write(map) { + Preferences.set(UUID_MAP_PREF, JSON.stringify(map)); + }, + + get(id, create = true) { + let map = this._read(); + + if (id in map) { + return map[id]; + } + + let uuid = null; + if (create) { + uuid = uuidGen.generateUUID().number; + uuid = uuid.slice(1, -1); // Strip { and } off the UUID. + + map[id] = uuid; + this._write(map); + } + return uuid; + }, + + remove(id) { + let map = this._read(); + delete map[id]; + this._write(map); + }, +}; + +// This is the old interface that UUIDMap replaced, to be removed when +// the references listed in bug 1291399 are updated. +/* exported getExtensionUUID */ +function getExtensionUUID(id) { + return UUIDMap.get(id, true); +} + +// For extensions that have called setUninstallURL(), send an event +// so the browser can display the URL. +var UninstallObserver = { + initialized: false, + + init() { + if (!this.initialized) { + AddonManager.addAddonListener(this); + XPCOMUtils.defineLazyPreferenceGetter(this, "leaveStorage", LEAVE_STORAGE_PREF, false); + XPCOMUtils.defineLazyPreferenceGetter(this, "leaveUuid", LEAVE_UUID_PREF, false); + this.initialized = true; + } + }, + + onUninstalling(addon) { + let extension = GlobalManager.extensionMap.get(addon.id); + if (extension) { + // Let any other interested listeners respond + // (e.g., display the uninstall URL) + Management.emit("uninstall", extension); + } + }, + + onUninstalled(addon) { + let uuid = UUIDMap.get(addon.id, false); + if (!uuid) { + return; + } + + if (!this.leaveStorage) { + // Clear browser.local.storage + ExtensionStorage.clear(addon.id); + + // Clear any IndexedDB storage created by the extension + let baseURI = NetUtil.newURI(`moz-extension://${uuid}/`); + let principal = Services.scriptSecurityManager.createCodebasePrincipal( + baseURI, {addonId: addon.id} + ); + Services.qms.clearStoragesForPrincipal(principal); + + // Clear localStorage created by the extension + let attrs = JSON.stringify({addonId: addon.id}); + Services.obs.notifyObservers(null, "clear-origin-attributes-data", attrs); + } + + if (!this.leaveUuid) { + // Clear the entry in the UUID map + UUIDMap.remove(addon.id); + } + }, +}; + +UninstallObserver.init(); + +// 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 = class { + constructor(rootURI) { + this.rootURI = rootURI; + + this.manifest = null; + this.id = null; + this.uuid = null; + this.localeData = null; + this._promiseLocales = null; + + this.apiNames = new Set(); + this.dependencies = new Set(); + this.permissions = new Set(); + + this.errors = []; + } + + 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. + * + * @param {string} path The path portion of the URL. + * @returns {string} + */ + 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 = UUIDMap.get(this.id); + } + return `moz-extension://${this.uuid}/${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 = []; + + 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; + } + + // FIXME: We need a way to do this without main thread IO. + + let uri = this.rootURI.QueryInterface(Ci.nsIJARURI); + + let file = uri.JARFile.QueryInterface(Ci.nsIFileURL).file; + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader); + zipReader.open(file); + try { + let results = []; + + // Normalize the directory path. + path = `${uri.JAREntry}/${path}`; + path = path.replace(/\/\/+/g, "/").replace(/^\/|\/$/g, "") + "/"; + + // 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("/"), + }); + } + } + + 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)) { + // Convert status code to a string + let e = Components.Exception("", status); + reject(new Error(`Error while loading '${uri}' (${e.name})`)); + return; + } + try { + let text = NetUtil.readInputStreamToString(inputStream, inputStream.available(), + {charset: "utf-8"}); + + 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]) => { + this.manifest = manifest; + this.rawManifest = manifest; + + if (manifest && manifest.default_locale) { + return this.initLocale(); + } + }).then(() => { + let context = { + url: this.baseURI && this.baseURI.spec, + + principal: this.principal, + + logError: error => { + this.logger.warn(`Loading extension '${this.id}': Reading manifest: ${error}`); + }, + + preprocessors: {}, + }; + + if (this.localeData) { + context.preprocessors.localize = (value, context) => this.localize(value); + } + + let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context); + if (normalized.error) { + this.manifestError(normalized.error); + } else { + this.manifest = normalized.value; + } + + try { + // Do not override the add-on id that has been already assigned. + if (!this.id && this.manifest.applications.gecko.id) { + this.id = this.manifest.applications.gecko.id; + } + } catch (e) { + // Errors are handled by the type checks above. + } + + let permissions = this.manifest.permissions || []; + + let whitelist = []; + for (let perm of permissions) { + this.permissions.add(perm); + + let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm); + if (!match) { + whitelist.push(perm); + } else if (match[1] == "experiments" && match[2]) { + this.apiNames.add(match[2]); + } + } + this.whiteListedHosts = new MatchPattern(whitelist); + + for (let api of this.apiNames) { + this.dependencies.add(`${api}@experiments.addons.mozilla.org`); + } + + 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(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(); + } + }.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() { + if (!this._promiseLocales) { + this._promiseLocales = Task.spawn(function* () { + let locales = new Map(); + + let entries = yield this.readDirectory("_locales"); + for (let file of entries) { + if (file.isDir) { + let locale = this.normalizeLocaleCode(file.name); + locales.set(locale, file.name); + } + } + + this.localeData = new LocaleData({ + defaultLocale: this.defaultLocale, + locales, + 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() { + return Task.spawn(function* () { + let locales = yield this.promiseLocales(); + + 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}/`)); + } + } else if (locales.size) { + this.manifestError('The "default_locale" property is required when a ' + + '"_locales/" directory is present.'); + } + + 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(locale = this.defaultLocale) { + return Task.spawn(function* () { + 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)); + } + + let results = yield Promise.all(promises); + + this.localeData.selectedLocale = locale; + return results[0]; + }.bind(this)); + } +}; + +let _browserUpdated = false; + +const PROXIED_EVENTS = new Set(["test-harness-message"]); + +// We create one instance of this class per extension. |addonData| +// comes directly from bootstrap.js when initializing. +this.Extension = class extends ExtensionData { + constructor(addonData, startupReason) { + super(addonData.resourceURI); + + this.uuid = UUIDMap.get(addonData.id); + this.instanceId = getUniqueId(); + + this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`; + Services.ppmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this); + + if (addonData.cleanupFile) { + Services.obs.addObserver(this, "xpcom-shutdown", false); + this.cleanupFile = addonData.cleanupFile || null; + delete addonData.cleanupFile; + } + + this.addonData = addonData; + this.startupReason = startupReason; + + this.id = addonData.id; + this.baseURI = NetUtil.newURI(this.getURL("")).QueryInterface(Ci.nsIURL); + this.principal = this.createPrincipal(); + + this.onStartup = null; + + this.hasShutdown = false; + this.onShutdown = new Set(); + + this.uninstallURL = null; + + this.apis = []; + this.whiteListedHosts = null; + this.webAccessibleResources = null; + + this.emitter = new EventEmitter(); + } + + static set browserUpdated(updated) { + _browserUpdated = updated; + } + + static get browserUpdated() { + return _browserUpdated; + } + + static generateXPI(data) { + return ExtensionTestCommon.generateXPI(data); + } + + static generateZipFile(files, baseName = "generated-extension.xpi") { + return ExtensionTestCommon.generateZipFile(files, baseName); + } + + static generate(data) { + return ExtensionTestCommon.generate(data); + } + + on(hook, f) { + return this.emitter.on(hook, f); + } + + off(hook, f) { + return this.emitter.off(hook, f); + } + + emit(event, ...args) { + if (PROXIED_EVENTS.has(event)) { + Services.ppmm.broadcastAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args}); + } + + return this.emitter.emit(event, ...args); + } + + receiveMessage({name, data}) { + if (name === this.MESSAGE_EMIT_EVENT) { + this.emitter.emit(data.event, ...data.args); + } + } + + testMessage(...args) { + this.emit("test-harness-message", ...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; + } + + readManifest() { + return super.readManifest().then(manifest => { + if (AppConstants.RELEASE_OR_BETA) { + return manifest; + } + + // Load Experiments APIs that this extension depends on. + return Promise.all( + Array.from(this.apiNames, api => ExtensionAPIs.load(api)) + ).then(apis => { + for (let API of apis) { + this.apis.push(new API(this)); + } + + return manifest; + }); + }); + } + + // 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, + instanceId: this.instanceId, + manifest: this.manifest, + 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, + principal: this.principal, + }; + } + + broadcast(msg, data) { + return new Promise(resolve => { + let count = Services.ppmm.childCount; + Services.ppmm.addMessageListener(msg + "Complete", function listener() { + count--; + if (count == 0) { + Services.ppmm.removeMessageListener(msg + "Complete", listener); + resolve(); + } + }); + Services.ppmm.broadcastAsyncMessage(msg, data); + }); + } + + runManifest(manifest) { + // Strip leading slashes from web_accessible_resources. + let strippedWebAccessibleResources = []; + if (manifest.web_accessible_resources) { + strippedWebAccessibleResources = manifest.web_accessible_resources.map(path => path.replace(/^\/+/, "")); + } + + this.webAccessibleResources = new MatchGlobs(strippedWebAccessibleResources); + + let promises = []; + for (let directive in manifest) { + if (manifest[directive] !== null) { + promises.push(Management.emit(`manifest_${directive}`, directive, this, manifest)); + } + } + + 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).then(() => { + return Promise.all(promises); + }); + } + + 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(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 match = Locale.findClosestLocale(localeList); + locale = match ? match.name : this.defaultLocale; + } + + 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) { + return this.initLocale(); + } + }).then(() => { + if (this.errors.length) { + return Promise.reject({errors: this.errors}); + } + + if (this.hasShutdown) { + return; + } + + GlobalManager.init(this); + + // The "startup" Management event sent on the extension instance itself + // is emitted just before the Management "startup" event, + // and it is used to run code that needs to be executed before + // any of the "startup" listeners. + this.emit("startup", this); + Management.emit("startup", this); + + return this.runManifest(this.manifest); + }).then(() => { + Management.emit("ready", this); + }).catch(e => { + dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`); + Cu.reportError(e); + + if (started) { + ExtensionManagement.shutdownExtension(this.uuid); + } + + this.cleanupGeneratedFile(); + + throw e; + }); + } + + cleanupGeneratedFile() { + if (!this.cleanupFile) { + return; + } + + let file = this.cleanupFile; + this.cleanupFile = null; + + Services.obs.removeObserver(this, "xpcom-shutdown"); + + 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; + + Services.ppmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this); + + if (!this.manifest) { + ExtensionManagement.shutdownExtension(this.uuid); + + this.cleanupGeneratedFile(); + return; + } + + GlobalManager.uninit(this); + + for (let obj of this.onShutdown) { + obj.close(); + } + + for (let api of this.apis) { + api.destroy(); + } + + ParentAPIManager.shutdownExtension(this.id); + + Management.emit("shutdown", this); + + 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) { + let match = /^manifest:(.*)/.exec(perm); + if (match) { + return this.manifest[match[1]] != null; + } + + return this.permissions.has(perm); + } + + get name() { + return this.manifest.name; + } +}; |