Bug 1368189: Move more code out of ExtensionUtils.jsm. r?mixedpuppy draft
authorKris Maglione <maglione.k@gmail.com>
Fri, 26 May 2017 15:44:41 -0700
changeset 585480 5ce551f9d5afb6a3636eb61998b14ad639446b6f
parent 585479 23c3721b94e901cddd295a378100e9cf98afadc0
child 630730 f2e5fec37ace24f890e31eaa55fe93fb766b644d
push id61122
push usermaglione.k@gmail.com
push dateFri, 26 May 2017 22:45:34 +0000
reviewersmixedpuppy
bugs1368189
milestone55.0a1
Bug 1368189: Move more code out of ExtensionUtils.jsm. r?mixedpuppy Also removes some dead code. A lot of the code in ExtensionUtils.jsm is not needed in all processes, and a lot of the rest isn't needed until extension code runs. Most of it winds up being loaded into all processes way earlier than necessary. MozReview-Commit-ID: CMRjCPOjRF2
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-contextMenus.js
browser/components/extensions/ext-devtools-panels.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/ext-sidebarAction.js
mobile/android/components/extensions/ext-pageAction.js
toolkit/components/extensions/.eslintrc.js
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionCommon.jsm
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/ext-browser-content.js
toolkit/components/extensions/ext-downloads.js
toolkit/components/extensions/ext-notifications.js
toolkit/components/extensions/ext-runtime.js
--- 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(() => {});