--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -14,18 +14,23 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
"@mozilla.org/inspector/dom-utils;1",
"inIDOMUtils");
Cu.import("resource://gre/modules/EventEmitter.jsm");
var {
DefaultWeakMap,
+} = ExtensionUtils;
+
+Cu.import("resource://gre/modules/ExtensionParent.jsm");
+
+var {
IconDetails,
-} = ExtensionUtils;
+} = ExtensionParent;
const POPUP_PRELOAD_TIMEOUT_MS = 200;
var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
function isAncestorOrSelf(target, node) {
for (; node; node = node.parentNode) {
if (node === target) {
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -6,18 +6,23 @@ Cu.import("resource://gre/modules/Extens
Cu.import("resource://gre/modules/MatchPattern.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
var {
ExtensionError,
+} = ExtensionUtils;
+
+Cu.import("resource://gre/modules/ExtensionParent.jsm");
+
+var {
IconDetails,
-} = ExtensionUtils;
+} = ExtensionParent;
const ACTION_MENU_TOP_LEVEL_LIMIT = 6;
// Map[Extension -> Map[ID -> MenuItem]]
// Note: we want to enumerate all the menu items so
// this cannot be a weak map.
var gContextMenuMap = new Map();
--- a/browser/components/extensions/ext-devtools-panels.js
+++ b/browser/components/extensions/ext-devtools-panels.js
@@ -3,21 +3,21 @@
"use strict";
Cu.import("resource://gre/modules/ExtensionParent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
"resource:///modules/E10SUtils.jsm");
var {
+ IconDetails,
watchExtensionProxyContextLoad,
} = ExtensionParent;
var {
- IconDetails,
promiseEvent,
} = ExtensionUtils;
var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
/**
* Represents an addon devtools panel in the main process.
*
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -3,18 +3,23 @@
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "PanelPopup",
"resource:///modules/ExtensionPopups.jsm");
var {
DefaultWeakMap,
+} = ExtensionUtils;
+
+Cu.import("resource://gre/modules/ExtensionParent.jsm");
+
+var {
IconDetails,
-} = ExtensionUtils;
+} = ExtensionParent;
// WeakMap[Extension -> PageAction]
let pageActionMap = new WeakMap();
this.pageAction = class extends ExtensionAPI {
static for(extension) {
return pageActionMap.get(extension);
}
--- a/browser/components/extensions/ext-sidebarAction.js
+++ b/browser/components/extensions/ext-sidebarAction.js
@@ -4,20 +4,25 @@
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/ExtensionParent.jsm");
+
var {
ExtensionError,
+} = ExtensionUtils;
+
+var {
IconDetails,
-} = ExtensionUtils;
+} = ExtensionParent;
var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
// WeakMap[Extension -> SidebarAction]
let sidebarActionMap = new WeakMap();
const sidebarURL = "chrome://browser/content/webext-panels.xul";
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -7,21 +7,21 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
// Import the android PageActions module.
XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
"resource://gre/modules/PageActions.jsm");
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/ExtensionParent.jsm");
var {
IconDetails,
-} = ExtensionUtils;
+} = ExtensionParent;
// WeakMap[Extension -> PageAction]
var pageActionMap = new WeakMap();
function PageAction(options, extension) {
this.id = null;
this.extension = extension;
--- a/toolkit/components/extensions/.eslintrc.js
+++ b/toolkit/components/extensions/.eslintrc.js
@@ -8,16 +8,17 @@ module.exports = {
"Cr": true,
"Cu": true,
"TextDecoder": false,
"TextEncoder": false,
// Specific to WebExtensions:
"AppConstants": true,
"Extension": true,
"ExtensionAPI": true,
+ "ExtensionCommon": true,
"ExtensionManagement": true,
"ExtensionUtils": true,
"extensions": true,
"getContainerForCookieStoreId": true,
"getCookieStoreIdForContainer": true,
"global": true,
"isContainerCookieStoreId": true,
"isDefaultCookieStoreId": true,
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -87,23 +87,22 @@ Cu.import("resource://gre/modules/Extens
XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
var {
GlobalManager,
ParentAPIManager,
+ StartupCache,
apiManager: Management,
} = ExtensionParent;
const {
- classifyPermission,
EventEmitter,
- StartupCache,
getUniqueId,
} = ExtensionUtils;
XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
XPCOMUtils.defineLazyGetter(this, "LocaleData", () => ExtensionCommon.LocaleData);
@@ -135,16 +134,39 @@ function validateThemeManifest(manifestP
for (let propName of manifestProperties) {
if (propName != "theme" && !allowedThemeProperties.includes(propName)) {
invalidProps.push(propName);
}
}
return invalidProps;
}
+/**
+ * Classify an individual permission from a webextension manifest
+ * as a host/origin permission, an api permission, or a regular permission.
+ *
+ * @param {string} perm The permission string to classify
+ *
+ * @returns {object}
+ * An object with exactly one of the following properties:
+ * "origin" to indicate this is a host/origin permission.
+ * "api" to indicate this is an api permission
+ * (as used for webextensions experiments).
+ * "permission" to indicate this is a regular permission.
+ */
+function classifyPermission(perm) {
+ let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
+ if (!match) {
+ return {origin: perm};
+ } else if (match[1] == "experiments" && match[2]) {
+ return {api: match[2]};
+ }
+ return {permission: perm};
+}
+
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`
^
(
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -39,32 +39,51 @@ XPCOMUtils.defineLazyModuleGetter(this,
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
DefaultMap,
EventEmitter,
LimitedSet,
- SpreadArgs,
defineLazyGetter,
getMessageManager,
getUniqueId,
- injectAPI,
} = ExtensionUtils;
const {
LocalAPIImplementation,
LocaleData,
SchemaAPIInterface,
SingletonEventManager,
+ SpreadArgs,
} = ExtensionCommon;
const isContentProcess = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+// Copy an API object from |source| into the scope |dest|.
+function injectAPI(source, dest) {
+ for (let prop in source) {
+ // Skip names prefixed with '_'.
+ if (prop[0] == "_") {
+ continue;
+ }
+
+ let desc = Object.getOwnPropertyDescriptor(source, prop);
+ if (typeof(desc.value) == "function") {
+ Cu.exportFunction(desc.value, dest, {defineAs: prop});
+ } else if (typeof(desc.value) == "object") {
+ let obj = Cu.createObjectIn(dest, {defineAs: prop});
+ injectAPI(desc.value, obj);
+ } else {
+ Object.defineProperty(dest, prop, desc);
+ }
+ }
+}
+
/**
* Abstraction for a Port object in the extension API.
*
* @param {BaseContext} context The context that owns this port.
* @param {nsIMessageSender} senderMM The message manager to send messages to.
* @param {Array<nsIMessageListenerManager>} receiverMMs Message managers to
* listen on.
* @param {string} name Arbitrary port name as defined by the addon.
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -24,35 +24,47 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
+ "@mozilla.org/content/style-sheet-service;1",
+ "nsIStyleSheetService");
+
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
DefaultMap,
DefaultWeakMap,
EventEmitter,
ExtensionError,
- SpreadArgs,
defineLazyGetter,
getConsole,
getInnerWindowID,
getUniqueId,
runSafeSync,
runSafeSyncWithoutClone,
instanceOf,
} = ExtensionUtils;
XPCOMUtils.defineLazyGetter(this, "console", getConsole);
+var ExtensionCommon;
+
+class SpreadArgs extends Array {
+ constructor(args) {
+ super();
+ this.push(...args);
+ }
+}
+
class BaseContext {
constructor(envType, extension) {
this.envType = envType;
this.onClose = new Set();
this.checkedLastError = false;
this._lastError = null;
this.contextId = getUniqueId();
this.unloaded = false;
@@ -1062,17 +1074,17 @@ class SchemaAPIManager extends EventEmit
* @returns {object} A sandbox that is used as the global by `loadScript`.
*/
_createExtGlobal() {
let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
wantXrays: false,
sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`,
});
- Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, ChromeWorker, extensions: this});
+ Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, ChromeWorker, ExtensionCommon, extensions: this});
Cu.import("resource://gre/modules/AppConstants.jsm", global);
Cu.import("resource://gre/modules/ExtensionAPI.jsm", global);
XPCOMUtils.defineLazyGetter(global, "console", getConsole);
XPCOMUtils.defineLazyModuleGetter(global, "ExtensionUtils",
"resource://gre/modules/ExtensionUtils.jsm");
@@ -1438,18 +1450,47 @@ SingletonEventManager.prototype = {
addListener: (...args) => this.addListener(...args),
removeListener: (...args) => this.removeListener(...args),
hasListener: (...args) => this.hasListener(...args),
[Schemas.REVOKE]: () => this.revoke(),
};
},
};
+// Simple API for event listeners where events never fire.
+function ignoreEvent(context, name) {
+ return {
+ 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 scriptError = Cc["@mozilla.org/scripterror;1"]
+ .createInstance(Ci.nsIScriptError);
+ scriptError.init(msg, frame.filename, null, frame.lineNumber,
+ frame.columnNumber, Ci.nsIScriptError.warningFlag,
+ "content javascript");
+ let consoleService = Cc["@mozilla.org/consoleservice;1"]
+ .getService(Ci.nsIConsoleService);
+ consoleService.logMessage(scriptError);
+ },
+ removeListener: function(callback) {},
+ hasListener: function(callback) {},
+ };
+}
-const ExtensionCommon = {
+
+const stylesheetMap = new DefaultMap(url => {
+ let uri = Services.io.newURI(url);
+ return styleSheetService.preloadSheet(uri, styleSheetService.AGENT_SHEET);
+});
+
+ExtensionCommon = {
BaseContext,
CanOfAPIs,
LocalAPIImplementation,
LocaleData,
SchemaAPIInterface,
SchemaAPIManager,
SingletonEventManager,
+ SpreadArgs,
+ ignoreEvent,
+ stylesheetMap,
};
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -19,16 +19,18 @@ Cu.import("resource://gre/modules/Servic
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
"resource:///modules/E10SUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "IndexedDB",
+ "resource://gre/modules/IndexedDB.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
"resource://gre/modules/NativeMessaging.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
@@ -41,22 +43,23 @@ XPCOMUtils.defineLazyServiceGetter(this,
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
BaseContext,
CanOfAPIs,
SchemaAPIManager,
+ SpreadArgs,
} = ExtensionCommon;
var {
DefaultWeakMap,
+ ExtensionError,
MessageManagerProxy,
- SpreadArgs,
defineLazyGetter,
promiseDocumentLoaded,
promiseEvent,
promiseObserved,
} = ExtensionUtils;
const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
@@ -1067,21 +1070,318 @@ function extensionNameFromURI(uri) {
} catch (ex) {
if (ex.name != "NS_ERROR_XPC_BAD_CONVERT_JS") {
Cu.reportError("Extension cannot be found in AddonPolicyService.");
}
}
return GlobalManager.getExtension(id).name;
}
+const INTEGER = /^[1-9]\d*$/;
+
+// Manages icon details for toolbar buttons in the |pageAction| and
+// |browserAction| APIs.
+let IconDetails = {
+ // WeakMap<Extension -> Map<url-string -> object>>
+ iconCache: new DefaultWeakMap(() => new Map()),
+
+ // 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) {
+ if (!details.imageData && typeof details.path === "string") {
+ let icons = this.iconCache.get(extension);
+
+ let baseURI = context ? context.uri : extension.baseURI;
+ let url = baseURI.resolve(details.path);
+
+ let icon = icons.get(url);
+ if (!icon) {
+ icon = this._normalize(details, extension, context);
+ icons.set(url, icon);
+ }
+ return icon;
+ }
+
+ return this._normalize(details, extension, context);
+ },
+
+ _normalize(details, extension, context = null) {
+ let result = {};
+
+ try {
+ if (details.imageData) {
+ let imageData = details.imageData;
+
+ if (typeof imageData == "string") {
+ imageData = {"19": imageData};
+ }
+
+ for (let size of Object.keys(imageData)) {
+ if (!INTEGER.test(size)) {
+ throw new ExtensionError(`Invalid icon size ${size}, must be an integer`);
+ }
+ result[size] = imageData[size];
+ }
+ }
+
+ 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 ExtensionError(`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.
+ try {
+ Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
+ extension.principal, url,
+ Services.scriptSecurityManager.DISALLOW_SCRIPT);
+ } catch (e) {
+ throw new ExtensionError(`Illegal URL ${url}`);
+ }
+
+ 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.
+ getPreferredIcon(icons, extension = null, size = 16) {
+ 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, contentWindow, browserWindow, size = 18) {
+ return new Promise((resolve, reject) => {
+ let image = new contentWindow.Image();
+ image.onload = function() {
+ let canvas = 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;
+ }
+
+ canvas.width = dSize;
+ canvas.height = dSize;
+ ctx.drawImage(this, 0, 0, this.width, this.height, dx, dy, dWidth, dHeight);
+ resolve(canvas.toDataURL("image/png"));
+ };
+ image.onerror = reject;
+ image.src = imageURL;
+ });
+ },
+
+ // These URLs should already be properly escaped, but make doubly sure CSS
+ // string escape characters are escaped here, since they could lead to a
+ // sandbox break.
+ escapeUrl(url) {
+ return url.replace(/[\\\s"]/g, encodeURIComponent);
+ },
+};
+
+let StartupCache = {
+ DB_NAME: "ExtensionStartupCache",
+
+ SCHEMA_VERSION: 2,
+
+ STORE_NAMES: Object.freeze(["locales", "manifests", "schemas"]),
+
+ dbPromise: null,
+
+ cacheInvalidated: 0,
+
+ initDB(db) {
+ for (let name of StartupCache.STORE_NAMES) {
+ try {
+ db.deleteObjectStore(name);
+ } catch (e) {
+ // Don't worry if the store doesn't already exist.
+ }
+ db.createObjectStore(name, {keyPath: "key"});
+ }
+ },
+
+ clearAddonData(id) {
+ let range = IDBKeyRange.bound([id], [id, "\uFFFF"]);
+
+ return Promise.all([
+ this.locales.delete(range),
+ this.manifests.delete(range),
+ ]).catch(e => {
+ // Ignore the error. It happens when we try to flush the add-on
+ // data after the AddonManager has flushed the entire startup cache.
+ });
+ },
+
+ async reallyOpen(invalidate = false) {
+ if (this.dbPromise) {
+ let db = await this.dbPromise;
+ db.close();
+ }
+
+ if (invalidate) {
+ this.cacheInvalidated = ExtensionManagement.cacheInvalidated;
+
+ if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ IndexedDB.deleteDatabase(this.DB_NAME, {storage: "persistent"});
+ }
+ }
+
+ return IndexedDB.open(this.DB_NAME,
+ {storage: "persistent", version: this.SCHEMA_VERSION},
+ db => this.initDB(db));
+ },
+
+ async open() {
+ if (ExtensionManagement.cacheInvalidated > this.cacheInvalidated) {
+ this.dbPromise = this.reallyOpen(true);
+ } else if (!this.dbPromise) {
+ this.dbPromise = this.reallyOpen();
+ }
+
+ return this.dbPromise;
+ },
+
+ observe(subject, topic, data) {
+ if (topic === "startupcache-invalidate") {
+ this.dbPromise = this.reallyOpen(true).catch(e => {});
+ }
+ },
+};
+
+Services.obs.addObserver(StartupCache, "startupcache-invalidate");
+
+class CacheStore {
+ constructor(storeName) {
+ this.storeName = storeName;
+ }
+
+ async get(key, createFunc) {
+ let db;
+ let result;
+ try {
+ db = await StartupCache.open();
+
+ result = await db.objectStore(this.storeName)
+ .get(key);
+ } catch (e) {
+ Cu.reportError(e);
+
+ return createFunc(key);
+ }
+
+ if (result === undefined) {
+ let value = await createFunc(key);
+ result = {key, value};
+
+ db.objectStore(this.storeName, "readwrite")
+ .put(result);
+ }
+
+ return result && result.value;
+ }
+
+ async getAll() {
+ let result = new Map();
+ try {
+ let db = await StartupCache.open();
+
+ let results = await db.objectStore(this.storeName)
+ .getAll();
+ for (let {key, value} of results) {
+ result.set(key, value);
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ return result;
+ }
+
+ async delete(key) {
+ let db = await StartupCache.open();
+
+ return db.objectStore(this.storeName, "readwrite").delete(key);
+ }
+}
+
+for (let name of StartupCache.STORE_NAMES) {
+ StartupCache[name] = new CacheStore(name);
+}
+
const ExtensionParent = {
extensionNameFromURI,
GlobalManager,
HiddenExtensionPage,
+ IconDetails,
ParentAPIManager,
+ StartupCache,
apiManager,
get baseManifestProperties() {
if (gBaseManifestProperties) {
return gBaseManifestProperties;
}
let types = Schemas.schemaJSON.get(BASE_SCHEMA)[0].types;
let manifest = types.find(type => type.id === "WebExtensionManifest");
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -6,41 +6,25 @@
this.EXPORTED_SYMBOLS = ["ExtensionUtils"];
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
-const INTEGER = /^[1-9]\d*$/;
-
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
- "resource://gre/modules/AddonManager.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
- "resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI",
"resource://gre/modules/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
"resource://gre/modules/ExtensionManagement.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "IndexedDB",
- "resource://gre/modules/IndexedDB.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
- "resource://gre/modules/Preferences.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
- "resource://gre/modules/Schemas.jsm");
-
-XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
- "@mozilla.org/content/style-sheet-service;1",
- "nsIStyleSheetService");
function getConsole() {
return new ConsoleAPI({
maxLogLevelPref: "extensions.webextensions.log.level",
prefix: "WebExtensions",
});
}
@@ -48,145 +32,16 @@ XPCOMUtils.defineLazyGetter(this, "conso
let nextId = 0;
XPCOMUtils.defineLazyGetter(this, "uniqueProcessID", () => Services.appinfo.uniqueProcessID);
function getUniqueId() {
return `${nextId++}-${uniqueProcessID}`;
}
-let StartupCache = {
- DB_NAME: "ExtensionStartupCache",
-
- SCHEMA_VERSION: 2,
-
- STORE_NAMES: Object.freeze(["locales", "manifests", "schemas"]),
-
- dbPromise: null,
-
- cacheInvalidated: 0,
-
- initDB(db) {
- for (let name of StartupCache.STORE_NAMES) {
- try {
- db.deleteObjectStore(name);
- } catch (e) {
- // Don't worry if the store doesn't already exist.
- }
- db.createObjectStore(name, {keyPath: "key"});
- }
- },
-
- clearAddonData(id) {
- let range = IDBKeyRange.bound([id], [id, "\uFFFF"]);
-
- return Promise.all([
- this.locales.delete(range),
- this.manifests.delete(range),
- ]).catch(e => {
- // Ignore the error. It happens when we try to flush the add-on
- // data after the AddonManager has flushed the entire startup cache.
- });
- },
-
- async reallyOpen(invalidate = false) {
- if (this.dbPromise) {
- let db = await this.dbPromise;
- db.close();
- }
-
- if (invalidate) {
- this.cacheInvalidated = ExtensionManagement.cacheInvalidated;
-
- if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) {
- IndexedDB.deleteDatabase(this.DB_NAME, {storage: "persistent"});
- }
- }
-
- return IndexedDB.open(this.DB_NAME,
- {storage: "persistent", version: this.SCHEMA_VERSION},
- db => this.initDB(db));
- },
-
- async open() {
- if (ExtensionManagement.cacheInvalidated > this.cacheInvalidated) {
- this.dbPromise = this.reallyOpen(true);
- } else if (!this.dbPromise) {
- this.dbPromise = this.reallyOpen();
- }
-
- return this.dbPromise;
- },
-
- observe(subject, topic, data) {
- if (topic === "startupcache-invalidate") {
- this.dbPromise = this.reallyOpen(true).catch(e => {});
- }
- },
-};
-
-Services.obs.addObserver(StartupCache, "startupcache-invalidate");
-
-class CacheStore {
- constructor(storeName) {
- this.storeName = storeName;
- }
-
- async get(key, createFunc) {
- let db;
- let result;
- try {
- db = await StartupCache.open();
-
- result = await db.objectStore(this.storeName)
- .get(key);
- } catch (e) {
- Cu.reportError(e);
-
- return createFunc(key);
- }
-
- if (result === undefined) {
- let value = await createFunc(key);
- result = {key, value};
-
- db.objectStore(this.storeName, "readwrite")
- .put(result);
- }
-
- return result && result.value;
- }
-
- async getAll() {
- let result = new Map();
- try {
- let db = await StartupCache.open();
-
- let results = await db.objectStore(this.storeName)
- .getAll();
- for (let {key, value} of results) {
- result.set(key, value);
- }
- } catch (e) {
- Cu.reportError(e);
- }
-
- return result;
- }
-
- async delete(key) {
- let db = await StartupCache.open();
-
- return db.objectStore(this.storeName, "readwrite").delete(key);
- }
-}
-
-for (let name of StartupCache.STORE_NAMES) {
- StartupCache[name] = new CacheStore(name);
-}
/**
* An Error subclass for which complete error messages are always passed
* to extensions, rather than being interpreted as an unknown error.
*/
class ExtensionError extends Error {}
function filterStack(error) {
@@ -249,31 +104,16 @@ function runSafe(context, f, ...args) {
}
// Return true if the given value is an instance of the given
// native type.
function instanceOf(value, type) {
return {}.toString.call(value) == `[object ${type}]`;
}
-// 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;
-}
-
/**
* Similar to a WeakMap, but creates a new key with the given
* constructor if one is not present.
*/
class DefaultWeakMap extends WeakMap {
constructor(defaultConstructor, init) {
super(init);
this.defaultConstructor = defaultConstructor;
@@ -306,186 +146,16 @@ const _winUtils = new DefaultWeakMap(win
.getInterface(Ci.nsIDOMWindowUtils);
});
const getWinUtils = win => _winUtils.get(win);
function getInnerWindowID(window) {
return getWinUtils(window).currentInnerWindowID;
}
-class SpreadArgs extends Array {
- constructor(args) {
- super();
- this.push(...args);
- }
-}
-
-// Manages icon details for toolbar buttons in the |pageAction| and
-// |browserAction| APIs.
-let IconDetails = {
- // WeakMap<Extension -> Map<url-string -> object>>
- iconCache: new DefaultWeakMap(() => new Map()),
-
- // 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) {
- if (!details.imageData && typeof details.path === "string") {
- let icons = this.iconCache.get(extension);
-
- let baseURI = context ? context.uri : extension.baseURI;
- let url = baseURI.resolve(details.path);
-
- let icon = icons.get(url);
- if (!icon) {
- icon = this._normalize(details, extension, context);
- icons.set(url, icon);
- }
- return icon;
- }
-
- return this._normalize(details, extension, context);
- },
-
- _normalize(details, extension, context = null) {
- let result = {};
-
- try {
- if (details.imageData) {
- let imageData = details.imageData;
-
- if (typeof imageData == "string") {
- imageData = {"19": imageData};
- }
-
- for (let size of Object.keys(imageData)) {
- if (!INTEGER.test(size)) {
- throw new ExtensionError(`Invalid icon size ${size}, must be an integer`);
- }
- result[size] = imageData[size];
- }
- }
-
- 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 ExtensionError(`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.
- try {
- Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
- extension.principal, url,
- Services.scriptSecurityManager.DISALLOW_SCRIPT);
- } catch (e) {
- throw new ExtensionError(`Illegal URL ${url}`);
- }
-
- 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.
- getPreferredIcon(icons, extension = null, size = 16) {
- 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, contentWindow, browserWindow, size = 18) {
- return new Promise((resolve, reject) => {
- let image = new contentWindow.Image();
- image.onload = function() {
- let canvas = 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;
- }
-
- canvas.width = dSize;
- canvas.height = dSize;
- ctx.drawImage(this, 0, 0, this.width, this.height, dx, dy, dWidth, dHeight);
- resolve(canvas.toDataURL("image/png"));
- };
- image.onerror = reject;
- image.src = imageURL;
- });
- },
-
- // These URLs should already be properly escaped, but make doubly sure CSS
- // string escape characters are escaped here, since they could lead to a
- // sandbox break.
- escapeUrl(url) {
- return url.replace(/[\\\s"]/g, encodeURIComponent);
- },
-};
-
const LISTENERS = Symbol("listeners");
const ONCE_MAP = Symbol("onceMap");
class EventEmitter {
constructor() {
this[LISTENERS] = new Map();
this[ONCE_MAP] = new WeakMap();
}
@@ -569,57 +239,16 @@ class EventEmitter {
let promises = Array.from(listeners, listener => {
return runSafeSyncWithoutClone(listener, event, ...args);
});
return Promise.all(promises);
}
}
-// Simple API for event listeners where events never fire.
-function ignoreEvent(context, name) {
- return {
- 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 scriptError = Cc["@mozilla.org/scripterror;1"]
- .createInstance(Ci.nsIScriptError);
- scriptError.init(msg, frame.filename, null, frame.lineNumber,
- frame.columnNumber, Ci.nsIScriptError.warningFlag,
- "content javascript");
- let consoleService = Cc["@mozilla.org/consoleservice;1"]
- .getService(Ci.nsIConsoleService);
- consoleService.logMessage(scriptError);
- },
- removeListener: function(callback) {},
- hasListener: function(callback) {},
- };
-}
-
-// Copy an API object from |source| into the scope |dest|.
-function injectAPI(source, dest) {
- for (let prop in source) {
- // Skip names prefixed with '_'.
- if (prop[0] == "_") {
- continue;
- }
-
- let desc = Object.getOwnPropertyDescriptor(source, prop);
- if (typeof(desc.value) == "function") {
- Cu.exportFunction(desc.value, dest, {defineAs: prop});
- } else if (typeof(desc.value) == "object") {
- let obj = Cu.createObjectIn(dest, {defineAs: prop});
- injectAPI(desc.value, obj);
- } else {
- Object.defineProperty(dest, prop, desc);
- }
- }
-}
-
/**
* A set with a limited number of slots, which flushes older entries as
* newer ones are added.
*/
class LimitedSet extends Set {
constructor(limit, iterable = undefined) {
super(iterable);
this.limit = limit;
@@ -742,38 +371,16 @@ function getMessageManager(target) {
}
return target.QueryInterface(Ci.nsIMessageSender);
}
function flushJarCache(jarPath) {
Services.obs.notifyObservers(null, "flush-cache-entry", jarPath);
}
-function PlatformInfo() {
- return 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;
- })(),
- });
-}
-
/**
* Convert any of several different representations of a date/time to a Date object.
* Accepts several formats:
* a Date object, an ISO8601 string, or a number of milliseconds since the epoch as
* either a number or a string.
*
* @param {Date|string|number} date
* The date to convert.
@@ -782,21 +389,16 @@ function PlatformInfo() {
*/
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.
return new Date((typeof date == "string" && /^\d+$/.test(date))
? parseInt(date, 10) : date);
}
-const stylesheetMap = new DefaultMap(url => {
- let uri = Services.io.newURI(url);
- return styleSheetService.preloadSheet(uri, styleSheetService.AGENT_SHEET);
-});
-
/**
* Defines a lazy getter for the given property on the given object. The
* first time the property is accessed, the return value of the getter
* is defined on the current `this` object with the given property name.
* Importantly, this means that a lazy getter defined on an object
* prototype will be invoked separately for each object instance that
* it's accessed on.
*
@@ -828,36 +430,16 @@ function defineLazyGetter(object, prop,
},
set(value) {
redefine(this, value);
},
});
}
-function findPathInObject(obj, path, printErrors = true) {
- let parent;
- for (let elt of path.split(".")) {
- if (!obj || !(elt in obj)) {
- if (printErrors) {
- Cu.reportError(`WebExtension API ${path} not found (it may be unimplemented by Firefox).`);
- }
- return null;
- }
-
- parent = obj;
- obj = obj[elt];
- }
-
- if (typeof obj === "function") {
- return obj.bind(parent);
- }
- return obj;
-}
-
/**
* Acts as a proxy for a message manager or message manager owner, and
* tracks docShell swaps so that messages are always sent to the same
* receiver, even if it is moved to a different <browser>.
*
* @param {nsIMessageSender|Element} target
* The target message manager on which to send messages, or the
* <browser> element which owns it.
@@ -1025,68 +607,34 @@ class MessageManagerProxy {
handleEvent(event) {
if (event.type == "SwapDocShells") {
this.removeListeners(this.eventTarget);
this.addListeners(event.detail);
}
}
}
-/**
- * Classify an individual permission from a webextension manifest
- * as a host/origin permission, an api permission, or a regular permission.
- *
- * @param {string} perm The permission string to classify
- *
- * @returns {object}
- * An object with exactly one of the following properties:
- * "origin" to indicate this is a host/origin permission.
- * "api" to indicate this is an api permission
- * (as used for webextensions experiments).
- * "permission" to indicate this is a regular permission.
- */
-function classifyPermission(perm) {
- let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
- if (!match) {
- return {origin: perm};
- } else if (match[1] == "experiments" && match[2]) {
- return {api: match[2]};
- }
- return {permission: perm};
-}
-
this.ExtensionUtils = {
- classifyPermission,
defineLazyGetter,
- extend,
- findPathInObject,
flushJarCache,
getConsole,
getInnerWindowID,
getMessageManager,
getUniqueId,
filterStack,
getWinUtils,
- ignoreEvent,
- injectAPI,
instanceOf,
normalizeTime,
promiseDocumentLoaded,
promiseDocumentReady,
promiseEvent,
promiseObserved,
runSafe,
runSafeSync,
runSafeSyncWithoutClone,
runSafeWithoutClone,
- stylesheetMap,
DefaultMap,
DefaultWeakMap,
EventEmitter,
ExtensionError,
- IconDetails,
LimitedSet,
MessageManagerProxy,
- SpreadArgs,
- StartupCache,
};
-
-XPCOMUtils.defineLazyGetter(this.ExtensionUtils, "PlatformInfo", PlatformInfo);
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -16,26 +16,29 @@ Cu.importGlobalProperties(["URL"]);
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
DefaultMap,
DefaultWeakMap,
- StartupCache,
instanceOf,
} = ExtensionUtils;
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
+ "resource://gre/modules/ExtensionParent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "contentPolicyService",
"@mozilla.org/addons/content-policy;1",
"nsIAddonContentPolicy");
+XPCOMUtils.defineLazyGetter(this, "StartupCache", () => ExtensionParent.StartupCache);
+
this.EXPORTED_SYMBOLS = ["Schemas"];
const {DEBUG} = AppConstants;
const isParentProcess = Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
function readJSON(url) {
return new Promise((resolve, reject) => {
--- a/toolkit/components/extensions/ext-browser-content.js
+++ b/toolkit/components/extensions/ext-browser-content.js
@@ -4,27 +4,28 @@
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
"resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionCommon",
+ "resource://gre/modules/ExtensionCommon.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "require",
"resource://devtools/shared/Loader.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
getWinUtils,
- stylesheetMap,
} = ExtensionUtils;
/* eslint-env mozilla/frame-script */
// Minimum time between two resizes.
const RESIZE_TIMEOUT = 100;
/**
@@ -113,17 +114,17 @@ const BrowserListener = {
}
}
},
loadStylesheets() {
let winUtils = getWinUtils(content);
for (let url of this.stylesheets) {
- winUtils.addSheet(stylesheetMap.get(url), winUtils.AGENT_SHEET);
+ winUtils.addSheet(ExtensionCommon.stylesheetMap.get(url), winUtils.AGENT_SHEET);
}
},
handleEvent(event) {
switch (event.type) {
case "DOMDocElementInserted":
if (this.blockingPromise) {
event.target.blockParsing(this.blockingPromise);
--- a/toolkit/components/extensions/ext-downloads.js
+++ b/toolkit/components/extensions/ext-downloads.js
@@ -1,28 +1,32 @@
"use strict";
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
"resource://gre/modules/DownloadPaths.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://gre/modules/EventEmitter.jsm");
var {
+ normalizeTime,
+} = ExtensionUtils;
+
+var {
ignoreEvent,
- normalizeTime,
- PlatformInfo,
-} = ExtensionUtils;
+} = ExtensionCommon;
const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
"danger", "mime", "startTime", "endTime",
"estimatedEndTime", "state",
"paused", "canResume", "error",
"bytesReceived", "totalBytes",
"fileSize", "exists",
"byExtensionId", "byExtensionName"];
@@ -387,17 +391,17 @@ function queryHelper(query) {
this.downloads = class extends ExtensionAPI {
getAPI(context) {
let {extension} = context;
return {
downloads: {
download(options) {
let {filename} = options;
- if (filename && PlatformInfo.os === "win") {
+ if (filename && AppConstants.platform === "win") {
// cross platform javascript code uses "/"
filename = filename.replace(/\//g, "\\");
}
if (filename != null) {
if (filename.length == 0) {
return Promise.reject({message: "filename must not be empty"});
}
--- a/toolkit/components/extensions/ext-notifications.js
+++ b/toolkit/components/extensions/ext-notifications.js
@@ -1,16 +1,16 @@
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://gre/modules/EventEmitter.jsm");
var {
ignoreEvent,
-} = ExtensionUtils;
+} = ExtensionCommon;
// WeakMap[Extension -> Map[id -> Notification]]
let notificationsMap = new WeakMap();
// Manages a notification popup (notifications API) created by the extension.
function Notification(extension, id, options) {
this.extension = extension;
this.id = id;
--- a/toolkit/components/extensions/ext-runtime.js
+++ b/toolkit/components/extensions/ext-runtime.js
@@ -6,16 +6,38 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Extension",
"resource://gre/modules/Extension.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
"resource://gre/modules/ExtensionManagement.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyGetter(this, "PlatformInfo", () => {
+ return 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;
+ })(),
+ });
+});
+
this.runtime = class extends ExtensionAPI {
getAPI(context) {
let {extension} = context;
return {
runtime: {
onStartup: new SingletonEventManager(context, "runtime.onStartup", fire => {
if (context.incognito) {
// This event should not fire if we are operating in a private profile.
@@ -97,17 +119,17 @@ this.runtime = class extends ExtensionAP
getBrowserInfo: function() {
const {name, vendor, version, appBuildID} = Services.appinfo;
const info = {name, vendor, version, buildID: appBuildID};
return Promise.resolve(info);
},
getPlatformInfo: function() {
- return Promise.resolve(ExtensionUtils.PlatformInfo);
+ return Promise.resolve(PlatformInfo);
},
openOptionsPage: function() {
if (!extension.manifest.options_ui) {
return Promise.reject({message: "No `options_ui` declared"});
}
return openOptionsPage(extension).then(() => {});