Bug 1368189: Move more code out of ExtensionUtils.jsm. r=mixedpuppy
authorKris Maglione <maglione.k@gmail.com>
Fri, 26 May 2017 15:44:41 -0700
changeset 410418 92040a759ddd4c1496dc64e1eb9173805fe25516
parent 410417 08ba2d5f6d3bdfc53d0681e1f990dfc456b04f30
child 410419 26b098924b0a008104e48246dd27166bab2cdec5
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1368189
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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/base/content/webext-panels.js
browser/components/extensions/ExtensionPopups.jsm
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-commands.js
browser/components/extensions/ext-contextMenus.js
browser/components/extensions/ext-devtools-inspectedWindow.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
netwerk/dns/mdns/libmdns/nsDNSServiceDiscovery.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
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/internal/XPIInstall.jsm
--- a/browser/base/content/webext-panels.js
+++ b/browser/base/content/webext-panels.js
@@ -4,20 +4,20 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // Via webext-panels.xul
 /* import-globals-from browser.js */
 /* import-globals-from nsContextMenu.js */
 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
                                   "resource://gre/modules/ExtensionParent.jsm");
+
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
-  extensionStylesheets,
   promiseEvent,
 } = ExtensionUtils;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 function getBrowser(sidebar) {
   let browser = document.getElementById("webext-panels-browser");
   if (browser) {
@@ -56,17 +56,17 @@ function getBrowser(sidebar) {
     browser.messageManager.loadFrameScript("chrome://browser/content/content.js", false);
     ExtensionParent.apiManager.emit("extension-browser-inserted", browser);
 
     if (sidebar.browserStyle) {
       browser.messageManager.loadFrameScript(
         "chrome://extensions/content/ext-browser-content.js", false);
 
       browser.messageManager.sendAsyncMessage("Extension:InitBrowser", {
-        stylesheets: extensionStylesheets,
+        stylesheets: ExtensionParent.extensionStylesheets,
       });
     }
     return browser;
   });
 }
 
 function loadWebPanel() {
   let sidebarURI = new URL(location);
--- a/browser/components/extensions/ExtensionPopups.jsm
+++ b/browser/components/extensions/ExtensionPopups.jsm
@@ -19,17 +19,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
                                   "resource://gre/modules/Timer.jsm");
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
   DefaultWeakMap,
-  extensionStylesheets,
   promiseEvent,
 } = ExtensionUtils;
 
 
 const POPUP_LOAD_TIMEOUT_MS = 200;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
@@ -155,17 +154,17 @@ class BasePopup {
   get DESTROY_EVENT() {
     throw new Error("Not implemented");
   }
 
   get STYLESHEETS() {
     let sheets = [];
 
     if (this.browserStyle) {
-      sheets.push(...extensionStylesheets);
+      sheets.push(...ExtensionParent.extensionStylesheets);
     }
     if (!this.fixedWidth) {
       sheets.push(...standaloneStylesheets);
     }
 
     return sheets;
   }
 
--- 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-commands.js
+++ b/browser/components/extensions/ext-commands.js
@@ -1,15 +1,14 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-var {
-  PlatformInfo,
-} = ExtensionUtils;
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
+                                  "resource://gre/modules/ExtensionParent.jsm");
 
 var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 this.commands = class extends ExtensionAPI {
   onManifestEntry(entryName) {
     let {extension} = this;
 
     this.id = makeWidgetId(extension.id);
@@ -66,16 +65,17 @@ this.commands = class extends ExtensionA
    *
    * @param {Object} manifest The manifest JSON object.
    * @returns {Map<string, object>}
    */
   loadCommandsFromManifest(manifest) {
     let commands = new Map();
     // For Windows, chrome.runtime expects 'win' while chrome.commands
     // expects 'windows'.  We can special case this for now.
+    let {PlatformInfo} = ExtensionParent;
     let os = PlatformInfo.os == "win" ? "windows" : PlatformInfo.os;
     for (let [name, command] of Object.entries(manifest.commands)) {
       let suggested_key = command.suggested_key || {};
       let shortcut = suggested_key[os] || suggested_key.default;
       shortcut = shortcut ? shortcut.replace(/\s+/g, "") : null;
       commands.set(name, {
         description: command.description,
         shortcut,
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -4,18 +4,23 @@
 
 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-inspectedWindow.js
+++ b/browser/components/extensions/ext-devtools-inspectedWindow.js
@@ -1,17 +1,17 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 /* global getDevToolsTargetForContext */
 
 var {
   SpreadArgs,
-} = ExtensionUtils;
+} = ExtensionCommon;
 
 this.devtools_inspectedWindow = class extends ExtensionAPI {
   getAPI(context) {
     const {
       WebExtensionInspectedWindowFront,
     } = require("devtools/shared/fronts/webextension-inspected-window");
 
     // Lazily retrieve and store an inspectedWindow actor front per child context.
--- 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/netwerk/dns/mdns/libmdns/nsDNSServiceDiscovery.js
+++ b/netwerk/dns/mdns/libmdns/nsDNSServiceDiscovery.js
@@ -1,22 +1,20 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import('resource://gre/modules/Services.jsm');
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-let { PlatformInfo } = ExtensionUtils;
-
-if (PlatformInfo.os == "android" && !Services.prefs.getBoolPref("network.mdns.use_js_fallback")) {
+if (AppConstants.platform == "android" && !Services.prefs.getBoolPref("network.mdns.use_js_fallback")) {
   Cu.import("resource://gre/modules/MulticastDNSAndroid.jsm");
 } else {
   Cu.import("resource://gre/modules/MulticastDNS.jsm");
 }
 
 const DNSSERVICEDISCOVERY_CID = Components.ID("{f9346d98-f27a-4e89-b744-493843416480}");
 const DNSSERVICEDISCOVERY_CONTRACT_ID = "@mozilla.org/toolkit/components/mdnsresponder/dns-sd;1";
 const DNSSERVICEINFO_CONTRACT_ID = "@mozilla.org/toolkit/components/mdnsresponder/dns-info;1";
--- a/toolkit/components/extensions/.eslintrc.js
+++ b/toolkit/components/extensions/.eslintrc.js
@@ -15,16 +15,17 @@ module.exports = {
     "MatchPatternSet": false,
     "WebExtensionContentScript": false,
     "WebExtensionPolicy": 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
@@ -92,23 +92,22 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "nsIUUIDGenerator");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "useRemoteWebExtensions",
                                       "extensions.webextensions.remote", false);
 
 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);
 
 
@@ -140,16 +139,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
@@ -35,32 +35,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, MatchPattern, MatchPatternSet, extensions: this});
+    Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, ChromeWorker, ExtensionCommon, MatchPattern, MatchPatternSet, 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,48 @@ 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,315 @@ 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 ExtensionParent = {
+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,
+
+  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.
+      this.dbPromise = this.reallyOpen(true).catch(e => {});
+    });
+  },
+
+  async reallyOpen(invalidate = false) {
+    if (this.dbPromise) {
+      let db = await this.dbPromise;
+      db.close();
+    }
+
+    if (invalidate) {
+      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 (!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};
+
+      try {
+        db.objectStore(this.storeName, "readwrite")
+          .put(result);
+      } catch (e) {
+        Cu.reportError(e);
+      }
+    }
+
+    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);
+}
+
+var ExtensionParent = {
   extensionNameFromURI,
   GlobalManager,
   HiddenExtensionPage,
+  IconDetails,
   ParentAPIManager,
+  StartupCache,
   WebExtensionPolicy,
   apiManager,
   get baseManifestProperties() {
     if (gBaseManifestProperties) {
       return gBaseManifestProperties;
     }
 
     let types = Schemas.schemaJSON.get(BASE_SCHEMA)[0].types;
@@ -1092,8 +1389,44 @@ const ExtensionParent = {
 
     gBaseManifestProperties = Object.getOwnPropertyNames(manifest.properties);
     return gBaseManifestProperties;
   },
   promiseExtensionViewLoaded,
   watchExtensionProxyContextLoad,
   DebugUtils,
 };
+
+XPCOMUtils.defineLazyGetter(ExtensionParent, "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;
+    })(),
+  });
+});
+
+/**
+ * Retreives the browser_style stylesheets needed for extension popups and sidebars.
+ * @returns {Array<string>} an array of stylesheets needed for the current platform.
+ */
+XPCOMUtils.defineLazyGetter(ExtensionParent, "extensionStylesheets", () => {
+  let stylesheets = ["chrome://browser/content/extension.css"];
+
+  if (AppConstants.platform === "macosx") {
+    stylesheets.push("chrome://browser/content/extension-mac.css");
+  }
+
+  return stylesheets;
+});
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -6,39 +6,23 @@
 
 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, "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",
   });
 }
 
@@ -46,144 +30,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,
-
-  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.
-      this.dbPromise = this.reallyOpen(true).catch(e => {});
-    });
-  },
-
-  async reallyOpen(invalidate = false) {
-    if (this.dbPromise) {
-      let db = await this.dbPromise;
-      db.close();
-    }
-
-    if (invalidate) {
-      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 (!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};
-
-      try {
-        db.objectStore(this.storeName, "readwrite")
-          .put(result);
-      } catch (e) {
-        Cu.reportError(e);
-      }
-    }
-
-    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) {
@@ -246,31 +102,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;
@@ -303,186 +144,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();
   }
@@ -566,57 +237,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;
@@ -739,38 +369,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.
@@ -779,35 +387,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);
-});
-
-/**
- * Retreives the browser_style stylesheets needed for extension popups and sidebars.
- * @returns {Array<string>} an array of stylesheets needed for the current platform.
- */
-function extensionStylesheets() {
-  let stylesheets = ["chrome://browser/content/extension.css"];
-
-  if (AppConstants.platform === "macosx") {
-    stylesheets.push("chrome://browser/content/extension-mac.css");
-  }
-
-  return stylesheets;
-}
-
 /**
  * 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.
  *
@@ -839,36 +428,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.
@@ -1036,69 +605,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, "extensionStylesheets", extensionStylesheets);
-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;
 
 /**
@@ -119,17 +120,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
@@ -1,16 +1,18 @@
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
+                                  "resource://gre/modules/ExtensionParent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 
 this.runtime = class extends ExtensionAPI {
   getAPI(context) {
     let {extension} = context;
     return {
       runtime: {
@@ -95,17 +97,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(ExtensionParent.PlatformInfo);
         },
 
         openOptionsPage: function() {
           if (!extension.manifest.options_ui) {
             return Promise.reject({message: "No `options_ui` declared"});
           }
 
           return openOptionsPage(extension).then(() => {});
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -69,18 +69,18 @@ const SEARCH_SCORE_MATCH_SUBSTRING = 3;
 const UPDATES_RECENT_TIMESPAN = 2 * 24 * 3600000; // 2 days (in milliseconds)
 const UPDATES_RELEASENOTES_TRANSFORMFILE = "chrome://mozapps/content/extensions/updateinfo.xsl";
 
 const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
 
 var gViewDefault = "addons://discover/";
 
 XPCOMUtils.defineLazyGetter(this, "extensionStylesheets", () => {
-  const {ExtensionUtils} = Cu.import("resource://gre/modules/ExtensionUtils.jsm", {});
-  return ExtensionUtils.extensionStylesheets;
+  const {ExtensionParent} = Cu.import("resource://gre/modules/ExtensionParent.jsm", {});
+  return ExtensionParent.extensionStylesheets;
 });
 
 var gStrings = {};
 XPCOMUtils.defineLazyServiceGetter(gStrings, "bundleSvc",
                                    "@mozilla.org/intl/stringbundle;1",
                                    "nsIStringBundleService");
 
 XPCOMUtils.defineLazyGetter(gStrings, "brand", function() {
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -36,17 +36,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/ChromeManifestParser.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
                                   "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Locale",
                                   "resource://gre/modules/Locale.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyGetter(this, "IconDetails", () => {
-  return Cu.import("resource://gre/modules/ExtensionUtils.jsm", {}).ExtensionUtils.IconDetails;
+  return Cu.import("resource://gre/modules/ExtensionParent.jsm", {}).ExtensionParent.IconDetails;
 });
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",