Bug 1471102: Move more code out of ExtensionUtils.jsm. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Mon, 25 Jun 2018 19:30:21 -0700
changeset 810530 e1cbd0328ce71fc3902beb917147f32d2beb8e2a
parent 810521 ae7a45271b0e531ec81b57df93573aca08e22147
child 810531 d48fa5c4c25a7c1d6fe6a1ad60a12319c5af86a5
push id114022
push usermaglione.k@gmail.com
push dateTue, 26 Jun 2018 02:35:47 +0000
reviewersaswan
bugs1471102
milestone63.0a1
Bug 1471102: Move more code out of ExtensionUtils.jsm. r?aswan MozReview-Commit-ID: Fqlv5BRuuW8
browser/components/extensions/ExtensionControlledPopup.jsm
browser/components/extensions/ExtensionPopups.jsm
browser/components/extensions/child/ext-devtools-panels.js
browser/components/extensions/child/ext-menus.js
browser/components/extensions/parent/ext-browser.js
browser/components/extensions/parent/ext-history.js
browser/components/extensions/test/xpcshell/test_ext_history.js
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionCommon.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ExtensionPageChild.jsm
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/ExtensionStorageSync.jsm
toolkit/components/extensions/ExtensionTestCommon.jsm
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/MessageChannel.jsm
toolkit/components/extensions/MessageManagerProxy.jsm
toolkit/components/extensions/ProxyScriptContext.jsm
toolkit/components/extensions/extension-process-script.js
toolkit/components/extensions/moz.build
toolkit/components/extensions/parent/ext-downloads.js
toolkit/components/extensions/parent/ext-idle.js
toolkit/components/extensions/parent/ext-management.js
toolkit/components/extensions/parent/ext-tabs-base.js
toolkit/components/extensions/parent/ext-toolkit.js
--- a/browser/components/extensions/ExtensionControlledPopup.jsm
+++ b/browser/components/extensions/ExtensionControlledPopup.jsm
@@ -16,28 +16,31 @@
  * view those pages after a change to the setting in each session until they confirm
  * the change by triggering the primary action.
  */
 
 var EXPORTED_SYMBOLS = ["ExtensionControlledPopup"];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
+ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "AddonManager",
                                "resource://gre/modules/AddonManager.jsm");
 ChromeUtils.defineModuleGetter(this, "BrowserUtils",
                                "resource://gre/modules/BrowserUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "CustomizableUI",
                                "resource:///modules/CustomizableUI.jsm");
 ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore",
                                "resource://gre/modules/ExtensionSettingsStore.jsm");
 
-let {makeWidgetId} = ExtensionUtils;
+let {
+  makeWidgetId,
+} = ExtensionCommon;
 
 XPCOMUtils.defineLazyGetter(this, "strBundle", function() {
   return Services.strings.createBundle("chrome://global/locale/extensions.properties");
 });
 
 const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon.";
 
 XPCOMUtils.defineLazyGetter(this, "distributionAddonsList", function() {
--- a/browser/components/extensions/ExtensionPopups.jsm
+++ b/browser/components/extensions/ExtensionPopups.jsm
@@ -13,24 +13,28 @@ ChromeUtils.defineModuleGetter(this, "Cu
 ChromeUtils.defineModuleGetter(this, "E10SUtils",
                                "resource://gre/modules/E10SUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "ExtensionParent",
                                "resource://gre/modules/ExtensionParent.jsm");
 ChromeUtils.defineModuleGetter(this, "setTimeout",
                                "resource://gre/modules/Timer.jsm");
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
   DefaultWeakMap,
-  makeWidgetId,
   promiseEvent,
 } = ExtensionUtils;
 
+const {
+  makeWidgetId,
+} = ExtensionCommon;
+
 
 const POPUP_LOAD_TIMEOUT_MS = 200;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 function promisePopupShown(popup) {
   return new Promise(resolve => {
     if (popup.state == "open") {
--- a/browser/components/extensions/child/ext-devtools-panels.js
+++ b/browser/components/extensions/child/ext-devtools-panels.js
@@ -13,17 +13,17 @@ var {
  * Represents an addon devtools panel in the child process.
  *
  * @param {DevtoolsExtensionContext}
  *   A devtools extension context running in a child process.
  * @param {object} panelOptions
  * @param {string} panelOptions.id
  *   The id of the addon devtools panel registered in the main process.
  */
-class ChildDevToolsPanel extends ExtensionUtils.EventEmitter {
+class ChildDevToolsPanel extends ExtensionCommon.EventEmitter {
   constructor(context, {id}) {
     super();
 
     this.context = context;
     this.context.callOnClose(this);
 
     this.id = id;
     this._panelContext = null;
@@ -139,17 +139,17 @@ class ChildDevToolsPanel extends Extensi
  * Represents an addon devtools inspector sidebar in the child process.
  *
  * @param {DevtoolsExtensionContext}
  *   A devtools extension context running in a child process.
  * @param {object} sidebarOptions
  * @param {string} sidebarOptions.id
  *   The id of the addon devtools sidebar registered in the main process.
  */
-class ChildDevToolsInspectorSidebar extends ExtensionUtils.EventEmitter {
+class ChildDevToolsInspectorSidebar extends ExtensionCommon.EventEmitter {
   constructor(context, {id}) {
     super();
 
     this.context = context;
     this.context.callOnClose(this);
 
     this.id = id;
 
--- a/browser/components/extensions/child/ext-menus.js
+++ b/browser/components/extensions/child/ext-menus.js
@@ -1,15 +1,15 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 var {
   withHandlingUserInput,
-} = ExtensionUtils;
+} = ExtensionCommon;
 
 // If id is not specified for an item we use an integer.
 // This ID need only be unique within a single addon. Since all addon code that
 // can use this API runs in the same process, this local variable suffices.
 var gNextMenuItemID = 0;
 
 // Map[Extension -> Map[string or id, ContextMenusClickPropHandler]]
 var gPropHandlers = new Map();
--- a/browser/components/extensions/parent/ext-browser.js
+++ b/browser/components/extensions/parent/ext-browser.js
@@ -8,36 +8,39 @@
 
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "BrowserWindowTracker",
                                "resource:///modules/BrowserWindowTracker.jsm");
 
 var {
   ExtensionError,
+} = ExtensionUtils;
+
+var {
   defineLazyGetter,
-} = ExtensionUtils;
+} = ExtensionCommon;
 
 const READER_MODE_PREFIX = "about:reader";
 
 let tabTracker;
 let windowTracker;
 
 // This function is pretty tightly tied to Extension.jsm.
 // Its job is to fill in the |tab| property of the sender.
 const getSender = (extension, target, sender) => {
   let tabId;
   if ("tabId" in sender) {
     // The message came from a privileged extension page running in a tab. In
     // that case, it should include a tabId property (which is filled in by the
     // page-open listener below).
     tabId = sender.tabId;
     delete sender.tabId;
-  } else if (ExtensionUtils.instanceOf(target, "XULElement") ||
-             ExtensionUtils.instanceOf(target, "HTMLIFrameElement")) {
+  } else if (ExtensionCommon.instanceOf(target, "XULElement") ||
+             ExtensionCommon.instanceOf(target, "HTMLIFrameElement")) {
     tabId = tabTracker.getBrowserData(target).tabId;
   }
 
   if (tabId) {
     let tab = extension.tabManager.get(tabId, null);
     if (tab) {
       sender.tab = tab.convert();
     }
--- a/browser/components/extensions/parent/ext-history.js
+++ b/browser/components/extensions/parent/ext-history.js
@@ -4,17 +4,17 @@
 
 ChromeUtils.defineModuleGetter(this, "PlacesUtils",
                                "resource://gre/modules/PlacesUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
 
 var {
   normalizeTime,
-} = ExtensionUtils;
+} = ExtensionCommon;
 
 let nsINavHistoryService = Ci.nsINavHistoryService;
 const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
   ["link", nsINavHistoryService.TRANSITION_LINK],
   ["typed", nsINavHistoryService.TRANSITION_TYPED],
   ["auto_bookmark", nsINavHistoryService.TRANSITION_BOOKMARK],
   ["auto_subframe", nsINavHistoryService.TRANSITION_EMBED],
   ["manual_subframe", nsINavHistoryService.TRANSITION_FRAMED_LINK],
--- a/browser/components/extensions/test/xpcshell/test_ext_history.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_history.js
@@ -1,18 +1,18 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 ChromeUtils.defineModuleGetter(this, "PlacesTestUtils",
                                "resource://testing-common/PlacesTestUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "PlacesUtils",
                                "resource://gre/modules/PlacesUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "ExtensionUtils",
-                               "resource://gre/modules/ExtensionUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "ExtensionCommon",
+                               "resource://gre/modules/ExtensionCommon.jsm");
 
 ChromeUtils.import("resource://testing-common/PromiseTestUtils.jsm");
 
 PromiseTestUtils.whitelistRejectionsGlobally(/Message manager disconnected/);
 
 add_task(async function test_delete() {
   function background() {
     let historyClearedCount = 0;
@@ -303,17 +303,17 @@ add_task(async function test_add_url() {
   ];
 
   async function checkUrl(results) {
     ok((await PlacesTestUtils.isPageInDB(results.details.url)), `${results.details.url} found in history database`);
     ok(PlacesUtils.isValidGuid(results.result.id), "URL was added with a valid id");
     equal(results.result.title, results.details.title, "URL was added with the correct title");
     if (results.details.visitTime) {
       equal(results.result.lastVisitTime,
-            Number(ExtensionUtils.normalizeTime(results.details.visitTime)),
+            Number(ExtensionCommon.normalizeTime(results.details.visitTime)),
             "URL was added with the correct date");
     }
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["history"],
     },
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -37,17 +37,16 @@ ChromeUtils.import("resource://gre/modul
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AddonManager: "resource://gre/modules/AddonManager.jsm",
   AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
   AddonSettings: "resource://gre/modules/addons/AddonSettings.jsm",
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
   ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.jsm",
-  ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm",
   ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm",
   ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
   ExtensionTestCommon: "resource://testing-common/ExtensionTestCommon.jsm",
   FileSource: "resource://gre/modules/L10nRegistry.jsm",
   L10nRegistry: "resource://gre/modules/L10nRegistry.jsm",
   Log: "resource://gre/modules/Log.jsm",
   MessageChannel: "resource://gre/modules/MessageChannel.jsm",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
@@ -72,16 +71,17 @@ XPCOMUtils.defineLazyGetter(
     return obj;
   });
 
 XPCOMUtils.defineLazyGetter(
   this, "resourceProtocol",
   () => Services.io.getProtocolHandler("resource")
           .QueryInterface(Ci.nsIResProtocolHandler));
 
+ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetters(this, {
   aomStartup: ["@mozilla.org/addons/addon-manager-startup;1", "amIAddonManagerStartup"],
   spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"],
   uuidGen: ["@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"],
 });
@@ -91,22 +91,25 @@ XPCOMUtils.defineLazyPreferenceGetter(th
 var {
   GlobalManager,
   ParentAPIManager,
   StartupCache,
   apiManager: Management,
 } = ExtensionParent;
 
 const {
-  EventEmitter,
   getUniqueId,
   promiseTimeout,
 } = ExtensionUtils;
 
-XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
+const {
+  EventEmitter,
+} = ExtensionCommon;
+
+XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole);
 
 XPCOMUtils.defineLazyGetter(this, "LocaleData", () => ExtensionCommon.LocaleData);
 
 // The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB
 // storage used by the browser.storage.local API is not directly accessible from the extension code).
 XPCOMUtils.defineLazyGetter(this, "WEBEXT_STORAGE_USER_CONTEXT_ID", () => {
   return ContextualIdentityService.getDefaultPrivateIdentity(
     "userContextIdInternal.webextStorageLocal").userContextId;
@@ -1425,17 +1428,17 @@ class Extension extends ExtensionData {
 
   checkLoadURL(url, options = {}) {
     // As an optimization, f the URL starts with the extension's base URL,
     // don't do any further checks. It's always allowed to load it.
     if (url.startsWith(this.baseURL)) {
       return true;
     }
 
-    return ExtensionUtils.checkLoadURL(url, this.principal, options);
+    return ExtensionCommon.checkLoadURL(url, this.principal, options);
   }
 
   async promiseLocales(locale) {
     let locales = await StartupCache.locales
       .get([this.id, "@@all_locales"], () => this._promiseLocaleMap());
 
     return this._setupLocaleData(locales);
   }
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -37,31 +37,31 @@ XPCOMUtils.defineLazyGetter(
   () => Cc["@mozilla.org/webextensions/extension-process-script;1"]
           .getService().wrappedJSObject);
 
 ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 const {
   DefaultMap,
-  EventEmitter,
   LimitedSet,
-  defineLazyGetter,
   getMessageManager,
   getUniqueId,
   getWinUtils,
-  withHandlingUserInput,
 } = ExtensionUtils;
 
 const {
+  EventEmitter,
   EventManager,
   LocalAPIImplementation,
   LocaleData,
   NoCloneSpreadArgs,
   SchemaAPIInterface,
+  defineLazyGetter,
+  withHandlingUserInput,
 } = 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 '_'.
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -17,48 +17,168 @@ var EXPORTED_SYMBOLS = ["ExtensionCommon
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
+  ConsoleAPI: "resource://gre/modules/Console.jsm",
   MessageChannel: "resource://gre/modules/MessageChannel.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   Schemas: "resource://gre/modules/Schemas.jsm",
   SchemaRoot: "resource://gre/modules/Schemas.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
                                    "@mozilla.org/content/style-sheet-service;1",
                                    "nsIStyleSheetService");
 
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
   DefaultMap,
   DefaultWeakMap,
-  EventEmitter,
   ExtensionError,
-  defineLazyGetter,
   filterStack,
-  getConsole,
   getInnerWindowID,
   getUniqueId,
   getWinUtils,
 } = ExtensionUtils;
 
+function getConsole() {
+  return new ConsoleAPI({
+    maxLogLevelPref: "extensions.webextensions.log.level",
+    prefix: "WebExtensions",
+  });
+}
+
 XPCOMUtils.defineLazyGetter(this, "console", getConsole);
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "DELAYED_BG_STARTUP",
                                       "extensions.webextensions.background-delayed-startup");
 
 var ExtensionCommon;
 
+// Run a function and report exceptions.
+function runSafeSyncWithoutClone(f, ...args) {
+  try {
+    return f(...args);
+  } catch (e) {
+    dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`);
+    Cu.reportError(e);
+  }
+}
+
+// Return true if the given value is an instance of the given
+// native type.
+function instanceOf(value, type) {
+  return (value && typeof value === "object" &&
+          ChromeUtils.getClassName(value) === type);
+}
+
+/**
+ * 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.
+ * @returns {Date}
+ *      A Date object
+ */
+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);
+}
+
+function withHandlingUserInput(window, callable) {
+  let handle = getWinUtils(window).setHandlingUserInput(true);
+  try {
+    return callable();
+  } finally {
+    handle.destruct();
+  }
+}
+
+/**
+ * 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.
+ *
+ * @param {object} object
+ *        The prototype object on which to define the getter.
+ * @param {string|Symbol} prop
+ *        The property name for which to define the getter.
+ * @param {function} getter
+ *        The function to call in order to generate the final property
+ *        value.
+ */
+function defineLazyGetter(object, prop, getter) {
+  let redefine = (obj, value) => {
+    Object.defineProperty(obj, prop, {
+      enumerable: true,
+      configurable: true,
+      writable: true,
+      value,
+    });
+    return value;
+  };
+
+  Object.defineProperty(object, prop, {
+    enumerable: true,
+    configurable: true,
+
+    get() {
+      return redefine(this, getter.call(this));
+    },
+
+    set(value) {
+      redefine(this, value);
+    },
+  });
+}
+
+function checkLoadURL(url, principal, options) {
+  let ssm = Services.scriptSecurityManager;
+
+  let flags = ssm.STANDARD;
+  if (!options.allowScript) {
+    flags |= ssm.DISALLOW_SCRIPT;
+  }
+  if (!options.allowInheritsPrincipal) {
+    flags |= ssm.DISALLOW_INHERIT_PRINCIPAL;
+  }
+  if (options.dontReportErrors) {
+    flags |= ssm.DONT_REPORT_ERRORS;
+  }
+
+  try {
+    ssm.checkLoadURIWithPrincipal(principal,
+                                  Services.io.newURI(url),
+                                  flags);
+  } catch (e) {
+    return false;
+  }
+  return true;
+}
+
+function makeWidgetId(id) {
+  id = id.toLowerCase();
+  // FIXME: This allows for collisions.
+  return id.replace(/[^a-z0-9_-]/g, "_");
+}
+
 /**
  * A sentinel class to indicate that an array of values should be
  * treated as an array when used as a promise resolution value, but as a
  * spread expression (...args) when passed to a callback.
  */
 class SpreadArgs extends Array {
   constructor(args) {
     super();
@@ -80,22 +200,129 @@ class NoCloneSpreadArgs {
     this.unwrappedValues = args;
   }
 
   [Symbol.iterator]() {
     return this.unwrappedValues[Symbol.iterator]();
   }
 }
 
+const LISTENERS = Symbol("listeners");
+const ONCE_MAP = Symbol("onceMap");
+
+class EventEmitter {
+  constructor() {
+    this[LISTENERS] = new Map();
+    this[ONCE_MAP] = new WeakMap();
+  }
+
+  /**
+   * Adds the given function as a listener for the given event.
+   *
+   * The listener function may optionally return a Promise which
+   * resolves when it has completed all operations which event
+   * dispatchers may need to block on.
+   *
+   * @param {string} event
+   *       The name of the event to listen for.
+   * @param {function(string, ...any)} listener
+   *        The listener to call when events are emitted.
+   */
+  on(event, listener) {
+    let listeners = this[LISTENERS].get(event);
+    if (!listeners) {
+      listeners = new Set();
+      this[LISTENERS].set(event, listeners);
+    }
+
+    listeners.add(listener);
+  }
+
+  /**
+   * Removes the given function as a listener for the given event.
+   *
+   * @param {string} event
+   *       The name of the event to stop listening for.
+   * @param {function(string, ...any)} listener
+   *        The listener function to remove.
+   */
+  off(event, listener) {
+    let set = this[LISTENERS].get(event);
+    if (set) {
+      set.delete(listener);
+      set.delete(this[ONCE_MAP].get(listener));
+      if (!set.size) {
+        this[LISTENERS].delete(event);
+      }
+    }
+  }
+
+  /**
+   * Adds the given function as a listener for the given event once.
+   *
+   * @param {string} event
+   *       The name of the event to listen for.
+   * @param {function(string, ...any)} listener
+   *        The listener to call when events are emitted.
+   */
+  once(event, listener) {
+    let wrapper = (...args) => {
+      this.off(event, wrapper);
+      this[ONCE_MAP].delete(listener);
+
+      return listener(...args);
+    };
+    this[ONCE_MAP].set(listener, wrapper);
+
+    this.on(event, wrapper);
+  }
+
+
+  /**
+   * Triggers all listeners for the given event. If any listeners return
+   * a value, returns a promise which resolves when all returned
+   * promises have resolved. Otherwise, returns undefined.
+   *
+   * @param {string} event
+   *       The name of the event to emit.
+   * @param {any} args
+   *        Arbitrary arguments to pass to the listener functions, after
+   *        the event name.
+   * @returns {Promise?}
+   */
+  emit(event, ...args) {
+    let listeners = this[LISTENERS].get(event);
+
+    if (listeners) {
+      let promises = [];
+
+      for (let listener of listeners) {
+        try {
+          let result = listener(event, ...args);
+          if (result !== undefined) {
+            promises.push(result);
+          }
+        } catch (e) {
+          Cu.reportError(e);
+        }
+      }
+
+      if (promises.length) {
+        return Promise.all(promises);
+      }
+    }
+  }
+}
+
 /**
  * Base class for WebExtension APIs.  Each API creates a new class
  * that inherits from this class, the derived class is instantiated
  * once for each extension that uses the API.
  */
-class ExtensionAPI extends ExtensionUtils.EventEmitter {
+class ExtensionAPI extends EventEmitter {
   constructor(extension) {
     super();
 
     this.extension = extension;
 
     extension.once("shutdown", () => {
       if (this.onShutdown) {
         this.onShutdown(extension.shutdownReason);
@@ -241,17 +468,17 @@ class BaseContext {
 
   checkLoadURL(url, options = {}) {
     // As an optimization, f the URL starts with the extension's base URL,
     // don't do any further checks. It's always allowed to load it.
     if (url.startsWith(this.extension.baseURL)) {
       return true;
     }
 
-    return ExtensionUtils.checkLoadURL(url, this.principal, options);
+    return checkLoadURL(url, this.principal, options);
   }
 
   /**
    * Safely call JSON.stringify() on an object that comes from an
    * extension.
    *
    * @param {array<any>} args Arguments for JSON.stringify()
    * @returns {string} The stringified representation of obj
@@ -2069,20 +2296,29 @@ const stylesheetMap = new DefaultMap(url
 });
 
 
 ExtensionCommon = {
   BaseContext,
   CanOfAPIs,
   EventManager,
   ExtensionAPI,
+  EventEmitter,
   LocalAPIImplementation,
   LocaleData,
   NoCloneSpreadArgs,
   SchemaAPIInterface,
   SchemaAPIManager,
   SpreadArgs,
+  checkLoadURL,
+  defineLazyGetter,
+  getConsole,
   ignoreEvent,
+  instanceOf,
+  makeWidgetId,
+  normalizeTime,
+  runSafeSyncWithoutClone,
   stylesheetMap,
+  withHandlingUserInput,
 
   MultiAPIManager,
   LazyAPIManager,
 };
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -34,38 +34,38 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["crypto", "TextEncoder"]);
 
 const {
   DefaultMap,
   DefaultWeakMap,
-  defineLazyGetter,
   getInnerWindowID,
   getWinUtils,
   promiseDocumentIdle,
   promiseDocumentLoaded,
   promiseDocumentReady,
-  runSafeSyncWithoutClone,
 } = ExtensionUtils;
 
 const {
   BaseContext,
   CanOfAPIs,
   SchemaAPIManager,
+  defineLazyGetter,
+  runSafeSyncWithoutClone,
 } = ExtensionCommon;
 
 const {
   BrowserExtensionContent,
   ChildAPIManager,
   Messenger,
 } = ExtensionChild;
 
-XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
+XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole);
 
 
 var DocumentManager;
 
 const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
 const CONTENT_SCRIPT_INJECTION_HISTOGRAM = "WEBEXT_CONTENT_SCRIPT_INJECTION_MS";
 
 var apiManager = new class extends SchemaAPIManager {
--- a/toolkit/components/extensions/ExtensionPageChild.jsm
+++ b/toolkit/components/extensions/ExtensionPageChild.jsm
@@ -32,25 +32,25 @@ XPCOMUtils.defineLazyGetter(
 const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
 const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
 
 ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionChild.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 const {
-  defineLazyGetter,
   getInnerWindowID,
   promiseEvent,
 } = ExtensionUtils;
 
 const {
   BaseContext,
   CanOfAPIs,
   SchemaAPIManager,
+  defineLazyGetter,
 } = ExtensionCommon;
 
 const {
   ChildAPIManager,
   Messenger,
 } = ExtensionChild;
 
 var ExtensionPageChild;
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -20,42 +20,42 @@ ChromeUtils.import("resource://gre/modul
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
   DeferredTask: "resource://gre/modules/DeferredTask.jsm",
   E10SUtils: "resource://gre/modules/E10SUtils.jsm",
   ExtensionData: "resource://gre/modules/Extension.jsm",
   MessageChannel: "resource://gre/modules/MessageChannel.jsm",
+  MessageManagerProxy: "resource://gre/modules/MessageManagerProxy.jsm",
+  NativeApp: "resource://gre/modules/NativeMessaging.jsm",
   OS: "resource://gre/modules/osfile.jsm",
-  NativeApp: "resource://gre/modules/NativeMessaging.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   Schemas: "resource://gre/modules/Schemas.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetters(this, {
   aomStartup: ["@mozilla.org/addons/addon-manager-startup;1", "amIAddonManagerStartup"],
 });
 
 ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
   BaseContext,
   CanOfAPIs,
   SchemaAPIManager,
   SpreadArgs,
+  defineLazyGetter,
 } = ExtensionCommon;
 
 var {
   DefaultMap,
   DefaultWeakMap,
   ExtensionError,
-  MessageManagerProxy,
-  defineLazyGetter,
   promiseDocumentLoaded,
   promiseEvent,
   promiseObserved,
 } = ExtensionUtils;
 
 const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
 const CATEGORY_EXTENSION_MODULES = "webextension-modules";
 const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
--- a/toolkit/components/extensions/ExtensionStorageSync.jsm
+++ b/toolkit/components/extensions/ExtensionStorageSync.jsm
@@ -43,16 +43,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   BulkKeyBundle: "resource://services-sync/keys.js",
   CollectionKeyManager: "resource://services-sync/record.js",
   CommonUtils: "resource://services-common/utils.js",
   CryptoUtils: "resource://services-crypto/utils.js",
+  ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm",
   fxAccounts: "resource://gre/modules/FxAccounts.jsm",
   KintoHttpClient: "resource://services-common/kinto-http-client.js",
   Kinto: "resource://services-common/kinto-offline-client.js",
   FirefoxAdapter: "resource://services-common/kinto-storage-adapter.js",
   Observers: "resource://services-common/observers.js",
   Utils: "resource://services-sync/util.js",
 });
 
@@ -63,17 +64,16 @@ XPCOMUtils.defineLazyPreferenceGetter(th
                                       KINTO_DEFAULT_SERVER_URL);
 XPCOMUtils.defineLazyGetter(this, "WeaveCrypto", function() {
   let {WeaveCrypto} = ChromeUtils.import("resource://services-crypto/WeaveCrypto.js", {});
   return new WeaveCrypto();
 });
 
 const {
   DefaultMap,
-  runSafeSyncWithoutClone,
 } = ExtensionUtils;
 
 // Map of Extensions to Set<Contexts> to track contexts that are still
 // "live" and use storage.sync.
 const extensionContexts = new DefaultMap(() => new Set());
 // Borrow logger from Sync.
 const log = Log.repository.getLogger("Sync.Engine.Extension-Storage");
 
@@ -1244,15 +1244,15 @@ class ExtensionStorageSync {
     }
   }
 
   notifyListeners(extension, changes) {
     Observers.notify("ext.storage.sync-changed");
     let listeners = this.listeners.get(extension) || new Set();
     if (listeners) {
       for (let listener of listeners) {
-        runSafeSyncWithoutClone(listener, changes);
+        ExtensionCommon.runSafeSyncWithoutClone(listener, changes);
       }
     }
   }
 }
 this.ExtensionStorageSync = ExtensionStorageSync;
 extensionStorageSync = new ExtensionStorageSync(_fxaService, Services.telemetry);
--- a/toolkit/components/extensions/ExtensionTestCommon.jsm
+++ b/toolkit/components/extensions/ExtensionTestCommon.jsm
@@ -28,28 +28,32 @@ ChromeUtils.defineModuleGetter(this, "Ex
 ChromeUtils.defineModuleGetter(this, "FileUtils",
                                "resource://gre/modules/FileUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "OS",
                                "resource://gre/modules/osfile.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "apiManager",
                             () => ExtensionParent.apiManager);
 
+ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 const {
   flushJarCache,
-  instanceOf,
 } = ExtensionUtils;
 
-XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
+const {
+  instanceOf,
+} = ExtensionCommon;
+
+XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionCommon.getConsole());
 
 
 /**
  * A skeleton Extension-like object, used for testing, which installs an
  * add-on via the add-on manager when startup() is called, and
  * uninstalles it on shutdown().
  *
  * @param {string} id
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -5,30 +5,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 var EXPORTED_SYMBOLS = ["ExtensionUtils"];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-ChromeUtils.defineModuleGetter(this, "ConsoleAPI",
-                               "resource://gre/modules/Console.jsm");
 ChromeUtils.defineModuleGetter(this, "setTimeout",
                                "resource://gre/modules/Timer.jsm");
 
-function getConsole() {
-  return new ConsoleAPI({
-    maxLogLevelPref: "extensions.webextensions.log.level",
-    prefix: "WebExtensions",
-  });
-}
-
-XPCOMUtils.defineLazyGetter(this, "console", getConsole);
-
 // xpcshell doesn't handle idle callbacks well.
 XPCOMUtils.defineLazyGetter(this, "idleTimeout",
                             () => Services.appinfo.name === "XPCShell" ? 500 : undefined);
 
 // It would be nicer to go through `Services.appinfo`, but some tests need to be
 // able to replace that field with a custom implementation before it is first
 // called.
 // eslint-disable-next-line mozilla/use-services
@@ -57,33 +46,16 @@ function promiseTimeout(delay) {
  * to extensions, rather than being interpreted as an unknown error.
  */
 class ExtensionError extends Error {}
 
 function filterStack(error) {
   return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n");
 }
 
-// Run a function and report exceptions.
-function runSafeSyncWithoutClone(f, ...args) {
-  try {
-    return f(...args);
-  } catch (e) {
-    dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`);
-    Cu.reportError(e);
-  }
-}
-
-// Return true if the given value is an instance of the given
-// native type.
-function instanceOf(value, type) {
-  return (value && typeof value === "object" &&
-          ChromeUtils.getClassName(value) === type);
-}
-
 /**
  * Similar to a WeakMap, but creates a new key with the given
  * constructor if one is not present.
  */
 class DefaultWeakMap extends WeakMap {
   constructor(defaultConstructor = undefined, init = undefined) {
     super(init);
     if (defaultConstructor) {
@@ -124,132 +96,16 @@ const _winUtils = new DefaultWeakMap(win
             .getInterface(Ci.nsIDOMWindowUtils);
 });
 const getWinUtils = win => _winUtils.get(win);
 
 function getInnerWindowID(window) {
   return getWinUtils(window).currentInnerWindowID;
 }
 
-function withHandlingUserInput(window, callable) {
-  let handle = getWinUtils(window).setHandlingUserInput(true);
-  try {
-    return callable();
-  } finally {
-    handle.destruct();
-  }
-}
-
-const LISTENERS = Symbol("listeners");
-const ONCE_MAP = Symbol("onceMap");
-
-class EventEmitter {
-  constructor() {
-    this[LISTENERS] = new Map();
-    this[ONCE_MAP] = new WeakMap();
-  }
-
-  /**
-   * Adds the given function as a listener for the given event.
-   *
-   * The listener function may optionally return a Promise which
-   * resolves when it has completed all operations which event
-   * dispatchers may need to block on.
-   *
-   * @param {string} event
-   *       The name of the event to listen for.
-   * @param {function(string, ...any)} listener
-   *        The listener to call when events are emitted.
-   */
-  on(event, listener) {
-    let listeners = this[LISTENERS].get(event);
-    if (!listeners) {
-      listeners = new Set();
-      this[LISTENERS].set(event, listeners);
-    }
-
-    listeners.add(listener);
-  }
-
-  /**
-   * Removes the given function as a listener for the given event.
-   *
-   * @param {string} event
-   *       The name of the event to stop listening for.
-   * @param {function(string, ...any)} listener
-   *        The listener function to remove.
-   */
-  off(event, listener) {
-    let set = this[LISTENERS].get(event);
-    if (set) {
-      set.delete(listener);
-      set.delete(this[ONCE_MAP].get(listener));
-      if (!set.size) {
-        this[LISTENERS].delete(event);
-      }
-    }
-  }
-
-  /**
-   * Adds the given function as a listener for the given event once.
-   *
-   * @param {string} event
-   *       The name of the event to listen for.
-   * @param {function(string, ...any)} listener
-   *        The listener to call when events are emitted.
-   */
-  once(event, listener) {
-    let wrapper = (...args) => {
-      this.off(event, wrapper);
-      this[ONCE_MAP].delete(listener);
-
-      return listener(...args);
-    };
-    this[ONCE_MAP].set(listener, wrapper);
-
-    this.on(event, wrapper);
-  }
-
-
-  /**
-   * Triggers all listeners for the given event. If any listeners return
-   * a value, returns a promise which resolves when all returned
-   * promises have resolved. Otherwise, returns undefined.
-   *
-   * @param {string} event
-   *       The name of the event to emit.
-   * @param {any} args
-   *        Arbitrary arguments to pass to the listener functions, after
-   *        the event name.
-   * @returns {Promise?}
-   */
-  emit(event, ...args) {
-    let listeners = this[LISTENERS].get(event);
-
-    if (listeners) {
-      let promises = [];
-
-      for (let listener of listeners) {
-        try {
-          let result = listener(event, ...args);
-          if (result !== undefined) {
-            promises.push(result);
-          }
-        } catch (e) {
-          Cu.reportError(e);
-        }
-      }
-
-      if (promises.length) {
-        return Promise.all(promises);
-      }
-    }
-  }
-}
-
 /**
  * A set with a limited number of slots, which flushes older entries as
  * newer ones are added.
  *
  * @param {integer} limit
  *        The maximum size to trim the set to after it grows too large.
  * @param {integer} [slop = limit * .25]
  *        The number of extra entries to allow in the set after it
@@ -399,308 +255,26 @@ function getMessageManager(target) {
   }
   return target;
 }
 
 function flushJarCache(jarPath) {
   Services.obs.notifyObservers(null, "flush-cache-entry", jarPath);
 }
 
-/**
- * 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.
- * @returns {Date}
- *      A Date object
- */
-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);
-}
-
-/**
- * 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.
- *
- * @param {object} object
- *        The prototype object on which to define the getter.
- * @param {string|Symbol} prop
- *        The property name for which to define the getter.
- * @param {function} getter
- *        The function to call in order to generate the final property
- *        value.
- */
-function defineLazyGetter(object, prop, getter) {
-  let redefine = (obj, value) => {
-    Object.defineProperty(obj, prop, {
-      enumerable: true,
-      configurable: true,
-      writable: true,
-      value,
-    });
-    return value;
-  };
-
-  Object.defineProperty(object, prop, {
-    enumerable: true,
-    configurable: true,
-
-    get() {
-      return redefine(this, getter.call(this));
-    },
-
-    set(value) {
-      redefine(this, value);
-    },
-  });
-}
-
-/**
- * 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.
- */
-class MessageManagerProxy {
-  constructor(target) {
-    this.listeners = new DefaultMap(() => new Map());
-    this.closed = false;
-
-    if (target instanceof Ci.nsIMessageSender) {
-      this.messageManager = target;
-    } else {
-      this.addListeners(target);
-    }
-
-    Services.obs.addObserver(this, "message-manager-close");
-  }
-
-  /**
-   * Disposes of the proxy object, removes event listeners, and drops
-   * all references to the underlying message manager.
-   *
-   * Must be called before the last reference to the proxy is dropped,
-   * unless the underlying message manager or <browser> is also being
-   * destroyed.
-   */
-  dispose() {
-    if (this.eventTarget) {
-      this.removeListeners(this.eventTarget);
-      this.eventTarget = null;
-    }
-    this.messageManager = null;
-
-    Services.obs.removeObserver(this, "message-manager-close");
-  }
-
-  observe(subject, topic, data) {
-    if (topic === "message-manager-close") {
-      if (subject === this.messageManager) {
-        this.closed = true;
-      }
-    }
-  }
-
-  /**
-   * Returns true if the given target is the same as, or owns, the given
-   * message manager.
-   *
-   * @param {nsIMessageSender|MessageManagerProxy|Element} target
-   *        The message manager, MessageManagerProxy, or <browser>
-   *        element against which to match.
-   * @param {nsIMessageSender} messageManager
-   *        The message manager against which to match `target`.
-   *
-   * @returns {boolean}
-   *        True if `messageManager` is the same object as `target`, or
-   *        `target` is a MessageManagerProxy or <browser> element that
-   *        is tied to it.
-   */
-  static matches(target, messageManager) {
-    return target === messageManager || target.messageManager === messageManager;
-  }
-
-  /**
-   * @property {nsIMessageSender|null} messageManager
-   *        The message manager that is currently being proxied. This
-   *        may change during the life of the proxy object, so should
-   *        not be stored elsewhere.
-   */
-
-  /**
-   * Sends a message on the proxied message manager.
-   *
-   * @param {array} args
-   *        Arguments to be passed verbatim to the underlying
-   *        sendAsyncMessage method.
-   * @returns {undefined}
-   */
-  sendAsyncMessage(...args) {
-    if (this.messageManager) {
-      return this.messageManager.sendAsyncMessage(...args);
-    }
-
-    Cu.reportError(`Cannot send message: Other side disconnected: ${uneval(args)}`);
-  }
-
-  get isDisconnected() {
-    return this.closed || !this.messageManager;
-  }
-
-  /**
-   * Adds a message listener to the current message manager, and
-   * transfers it to the new message manager after a docShell swap.
-   *
-   * @param {string} message
-   *        The name of the message to listen for.
-   * @param {nsIMessageListener} listener
-   *        The listener to add.
-   * @param {boolean} [listenWhenClosed = false]
-   *        If true, the listener will receive messages which were sent
-   *        after the remote side of the listener began closing.
-   */
-  addMessageListener(message, listener, listenWhenClosed = false) {
-    this.messageManager.addMessageListener(message, listener, listenWhenClosed);
-    this.listeners.get(message).set(listener, listenWhenClosed);
-  }
-
-  /**
-   * Adds a message listener from the current message manager.
-   *
-   * @param {string} message
-   *        The name of the message to stop listening for.
-   * @param {nsIMessageListener} listener
-   *        The listener to remove.
-   */
-  removeMessageListener(message, listener) {
-    this.messageManager.removeMessageListener(message, listener);
-
-    let listeners = this.listeners.get(message);
-    listeners.delete(listener);
-    if (!listeners.size) {
-      this.listeners.delete(message);
-    }
-  }
-
-  /**
-   * @private
-   * Iterates over all of the currently registered message listeners.
-   */
-  * iterListeners() {
-    for (let [message, listeners] of this.listeners) {
-      for (let [listener, listenWhenClosed] of listeners) {
-        yield {message, listener, listenWhenClosed};
-      }
-    }
-  }
-
-  /**
-   * @private
-   * Adds docShell swap listeners to the message manager owner.
-   *
-   * @param {Element} target
-   *        The target element.
-   */
-  addListeners(target) {
-    target.addEventListener("SwapDocShells", this);
-
-    this.eventTarget = target;
-    this.messageManager = target.messageManager;
-
-    for (let {message, listener, listenWhenClosed} of this.iterListeners()) {
-      this.messageManager.addMessageListener(message, listener, listenWhenClosed);
-    }
-  }
-
-  /**
-   * @private
-   * Removes docShell swap listeners to the message manager owner.
-   *
-   * @param {Element} target
-   *        The target element.
-   */
-  removeListeners(target) {
-    target.removeEventListener("SwapDocShells", this);
-
-    for (let {message, listener} of this.iterListeners()) {
-      this.messageManager.removeMessageListener(message, listener);
-    }
-  }
-
-  handleEvent(event) {
-    if (event.type == "SwapDocShells") {
-      this.removeListeners(this.eventTarget);
-      this.addListeners(event.detail);
-    }
-  }
-}
-
-function checkLoadURL(url, principal, options) {
-  let ssm = Services.scriptSecurityManager;
-
-  let flags = ssm.STANDARD;
-  if (!options.allowScript) {
-    flags |= ssm.DISALLOW_SCRIPT;
-  }
-  if (!options.allowInheritsPrincipal) {
-    flags |= ssm.DISALLOW_INHERIT_PRINCIPAL;
-  }
-  if (options.dontReportErrors) {
-    flags |= ssm.DONT_REPORT_ERRORS;
-  }
-
-  try {
-    ssm.checkLoadURIWithPrincipal(principal,
-                                  Services.io.newURI(url),
-                                  flags);
-  } catch (e) {
-    return false;
-  }
-  return true;
-}
-
-function makeWidgetId(id) {
-  id = id.toLowerCase();
-  // FIXME: This allows for collisions.
-  return id.replace(/[^a-z0-9_-]/g, "_");
-}
-
 var ExtensionUtils = {
-  checkLoadURL,
-  defineLazyGetter,
   flushJarCache,
-  getConsole,
   getInnerWindowID,
   getMessageManager,
   getUniqueId,
   filterStack,
   getWinUtils,
-  instanceOf,
-  makeWidgetId,
-  normalizeTime,
   promiseDocumentIdle,
   promiseDocumentLoaded,
   promiseDocumentReady,
   promiseEvent,
   promiseObserved,
   promiseTimeout,
-  runSafeSyncWithoutClone,
-  withHandlingUserInput,
   DefaultMap,
   DefaultWeakMap,
-  EventEmitter,
   ExtensionError,
   LimitedSet,
-  MessageManagerProxy,
 };
--- a/toolkit/components/extensions/MessageChannel.jsm
+++ b/toolkit/components/extensions/MessageChannel.jsm
@@ -100,19 +100,29 @@
 var EXPORTED_SYMBOLS = ["MessageChannel"];
 
 /* globals MessageChannel */
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-const {
-  MessageManagerProxy,
-} = ExtensionUtils;
+ChromeUtils.defineModuleGetter(this, "MessageManagerProxy",
+                               "resource://gre/modules/MessageManagerProxy.jsm");
+
+function getMessageManager(target) {
+  if (typeof target.sendAsyncMessage === "function") {
+    return target;
+  }
+  return new MessageManagerProxy(target);
+}
+
+function matches(target, messageManager) {
+  return target === messageManager || target.messageManager === messageManager;
+}
 
 const {DEBUG} = AppConstants;
 
 // Idle callback timeout for low-priority message dispatch.
 const LOW_PRIORITY_TIMEOUT_MS = 250;
 
 const MESSAGE_MESSAGES = "MessageChannel:Messages";
 const MESSAGE_RESPONSE = "MessageChannel:Response";
@@ -914,28 +924,30 @@ this.MessageChannel = {
           Cu.reportError(e.stack ? `${e}\n${e.stack}` : e.message || e);
         });
       });
       data = null;
       // Note: Unhandled messages are silently dropped.
       return;
     }
 
-    let target = new MessageManagerProxy(data.target);
+    let target = getMessageManager(data.target);
 
     let deferred = {
       sender: data.sender,
       messageManager: target,
       channelId: data.channelId,
       respondingSide: true,
     };
 
     let cleanup = () => {
       this.pendingResponses.delete(deferred);
-      target.dispose();
+      if (target.dispose) {
+        target.dispose();
+      }
     };
     this.pendingResponses.add(deferred);
 
     deferred.promise = new Promise((resolve, reject) => {
       deferred.reject = reject;
 
       this._callHandlers(handlers, data).then(resolve, reject);
       data = null;
@@ -1073,17 +1085,17 @@ this.MessageChannel = {
    *    The message manager for which to abort brokers.
    * @param {object} reason
    *    An object describing the reason the responses were aborted.
    *    Will be passed to the promise rejection handler of all aborted
    *    responses.
    */
   abortMessageManager(target, reason) {
     for (let response of this.pendingResponses) {
-      if (MessageManagerProxy.matches(response.messageManager, target)) {
+      if (matches(response.messageManager, target)) {
         this.abortedResponses.add(response.channelId);
         response.reject(reason);
       }
     }
   },
 
   observe(subject, topic, data) {
     switch (topic) {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/MessageManagerProxy.jsm
@@ -0,0 +1,198 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* 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";
+
+var EXPORTED_SYMBOLS = ["MessageManagerProxy"];
+
+ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const {
+  DefaultMap,
+} = ExtensionUtils;
+
+/**
+ * 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.
+ */
+class MessageManagerProxy {
+  constructor(target) {
+    this.listeners = new DefaultMap(() => new Map());
+    this.closed = false;
+
+    if (target instanceof Ci.nsIMessageSender) {
+      this.messageManager = target;
+    } else {
+      this.addListeners(target);
+    }
+
+    Services.obs.addObserver(this, "message-manager-close");
+  }
+
+  /**
+   * Disposes of the proxy object, removes event listeners, and drops
+   * all references to the underlying message manager.
+   *
+   * Must be called before the last reference to the proxy is dropped,
+   * unless the underlying message manager or <browser> is also being
+   * destroyed.
+   */
+  dispose() {
+    if (this.eventTarget) {
+      this.removeListeners(this.eventTarget);
+      this.eventTarget = null;
+    }
+    this.messageManager = null;
+
+    Services.obs.removeObserver(this, "message-manager-close");
+  }
+
+  observe(subject, topic, data) {
+    if (topic === "message-manager-close") {
+      if (subject === this.messageManager) {
+        this.closed = true;
+      }
+    }
+  }
+
+  /**
+   * Returns true if the given target is the same as, or owns, the given
+   * message manager.
+   *
+   * @param {nsIMessageSender|MessageManagerProxy|Element} target
+   *        The message manager, MessageManagerProxy, or <browser>
+   *        element against which to match.
+   * @param {nsIMessageSender} messageManager
+   *        The message manager against which to match `target`.
+   *
+   * @returns {boolean}
+   *        True if `messageManager` is the same object as `target`, or
+   *        `target` is a MessageManagerProxy or <browser> element that
+   *        is tied to it.
+   */
+  static matches(target, messageManager) {
+    return target === messageManager || target.messageManager === messageManager;
+  }
+
+  /**
+   * @property {nsIMessageSender|null} messageManager
+   *        The message manager that is currently being proxied. This
+   *        may change during the life of the proxy object, so should
+   *        not be stored elsewhere.
+   */
+
+  /**
+   * Sends a message on the proxied message manager.
+   *
+   * @param {array} args
+   *        Arguments to be passed verbatim to the underlying
+   *        sendAsyncMessage method.
+   * @returns {undefined}
+   */
+  sendAsyncMessage(...args) {
+    if (this.messageManager) {
+      return this.messageManager.sendAsyncMessage(...args);
+    }
+
+    Cu.reportError(`Cannot send message: Other side disconnected: ${uneval(args)}`);
+  }
+
+  get isDisconnected() {
+    return this.closed || !this.messageManager;
+  }
+
+  /**
+   * Adds a message listener to the current message manager, and
+   * transfers it to the new message manager after a docShell swap.
+   *
+   * @param {string} message
+   *        The name of the message to listen for.
+   * @param {nsIMessageListener} listener
+   *        The listener to add.
+   * @param {boolean} [listenWhenClosed = false]
+   *        If true, the listener will receive messages which were sent
+   *        after the remote side of the listener began closing.
+   */
+  addMessageListener(message, listener, listenWhenClosed = false) {
+    this.messageManager.addMessageListener(message, listener, listenWhenClosed);
+    this.listeners.get(message).set(listener, listenWhenClosed);
+  }
+
+  /**
+   * Adds a message listener from the current message manager.
+   *
+   * @param {string} message
+   *        The name of the message to stop listening for.
+   * @param {nsIMessageListener} listener
+   *        The listener to remove.
+   */
+  removeMessageListener(message, listener) {
+    this.messageManager.removeMessageListener(message, listener);
+
+    let listeners = this.listeners.get(message);
+    listeners.delete(listener);
+    if (!listeners.size) {
+      this.listeners.delete(message);
+    }
+  }
+
+  /**
+   * @private
+   * Iterates over all of the currently registered message listeners.
+   */
+  * iterListeners() {
+    for (let [message, listeners] of this.listeners) {
+      for (let [listener, listenWhenClosed] of listeners) {
+        yield {message, listener, listenWhenClosed};
+      }
+    }
+  }
+
+  /**
+   * @private
+   * Adds docShell swap listeners to the message manager owner.
+   *
+   * @param {Element} target
+   *        The target element.
+   */
+  addListeners(target) {
+    target.addEventListener("SwapDocShells", this);
+
+    this.eventTarget = target;
+    this.messageManager = target.messageManager;
+
+    for (let {message, listener, listenWhenClosed} of this.iterListeners()) {
+      this.messageManager.addMessageListener(message, listener, listenWhenClosed);
+    }
+  }
+
+  /**
+   * @private
+   * Removes docShell swap listeners to the message manager owner.
+   *
+   * @param {Element} target
+   *        The target element.
+   */
+  removeListeners(target) {
+    target.removeEventListener("SwapDocShells", this);
+
+    for (let {message, listener} of this.iterListeners()) {
+      this.messageManager.removeMessageListener(message, listener);
+    }
+  }
+
+  handleEvent(event) {
+    if (event.type == "SwapDocShells") {
+      this.removeListeners(this.eventTarget);
+      this.addListeners(event.detail);
+    }
+  }
+}
--- a/toolkit/components/extensions/ProxyScriptContext.jsm
+++ b/toolkit/components/extensions/ProxyScriptContext.jsm
@@ -33,24 +33,24 @@ const CATEGORY_EXTENSION_SCRIPTS_CONTENT
 // DNS is resolved on the SOCKS proxy server.
 const {TRANSPARENT_PROXY_RESOLVES_HOST} = Ci.nsIProxyInfo;
 
 // The length of time (seconds) to wait for a proxy to resolve before ignoring it.
 const PROXY_TIMEOUT_SEC = 10;
 
 const {
   ExtensionError,
-  defineLazyGetter,
 } = ExtensionUtils;
 
 const {
   BaseContext,
   CanOfAPIs,
   LocalAPIImplementation,
   SchemaAPIManager,
+  defineLazyGetter,
 } = ExtensionCommon;
 
 const PROXY_TYPES = Object.freeze({
   DIRECT: "direct",
   HTTPS: "https",
   PROXY: "http", // Synonym for PROXY_TYPES.HTTP
   HTTP: "http",
   SOCKS: "socks", // SOCKS5
--- a/toolkit/components/extensions/extension-process-script.js
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -11,23 +11,24 @@
  */
 
 ChromeUtils.import("resource://gre/modules/MessageChannel.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   ExtensionChild: "resource://gre/modules/ExtensionChild.jsm",
+  ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm",
   ExtensionContent: "resource://gre/modules/ExtensionContent.jsm",
   ExtensionPageChild: "resource://gre/modules/ExtensionPageChild.jsm",
 });
 
 ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
+XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionCommon.getConsole());
 
 const {
   DefaultWeakMap,
   getInnerWindowID,
 } = ExtensionUtils;
 
 // We need to avoid touching Services.appinfo here in order to prevent
 // the wrong version from being cached during xpcshell test startup.
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -21,16 +21,17 @@ EXTRA_JS_MODULES += [
     'ExtensionSettingsStore.jsm',
     'ExtensionStorage.jsm',
     'ExtensionStorageIDB.jsm',
     'ExtensionStorageSync.jsm',
     'ExtensionUtils.jsm',
     'FindContent.jsm',
     'LegacyExtensionsUtils.jsm',
     'MessageChannel.jsm',
+    'MessageManagerProxy.jsm',
     'NativeManifests.jsm',
     'NativeMessaging.jsm',
     'ProxyScriptContext.jsm',
     'Schemas.jsm',
 ]
 
 EXTRA_COMPONENTS += [
     'extension-process-script.js',
--- a/toolkit/components/extensions/parent/ext-downloads.js
+++ b/toolkit/components/extensions/parent/ext-downloads.js
@@ -8,20 +8,16 @@ ChromeUtils.defineModuleGetter(this, "Do
                                "resource://gre/modules/DownloadPaths.jsm");
 ChromeUtils.defineModuleGetter(this, "OS",
                                "resource://gre/modules/osfile.jsm");
 ChromeUtils.defineModuleGetter(this, "FileUtils",
                                "resource://gre/modules/FileUtils.jsm");
 
 var {
   EventEmitter,
-  normalizeTime,
-} = ExtensionUtils;
-
-var {
   ignoreEvent,
 } = ExtensionCommon;
 
 const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
                               "danger", "mime", "startTime", "endTime",
                               "estimatedEndTime", "state",
                               "paused", "canResume", "error",
                               "bytesReceived", "totalBytes",
@@ -257,17 +253,17 @@ const downloadQuery = query => {
       }
     }
   }
 
   function normalizeDownloadTime(arg, before) {
     if (arg == null) {
       return before ? Number.MAX_VALUE : 0;
     }
-    return normalizeTime(arg).getTime();
+    return ExtensionCommon.normalizeTime(arg).getTime();
   }
 
   const startedBefore = normalizeDownloadTime(query.startedBefore, true);
   const startedAfter = normalizeDownloadTime(query.startedAfter, false);
   // const endedBefore = normalizeDownloadTime(query.endedBefore, true);
   // const endedAfter = normalizeDownloadTime(query.endedAfter, false);
 
   const totalBytesGreater = query.totalBytesGreater || 0;
--- a/toolkit/components/extensions/parent/ext-idle.js
+++ b/toolkit/components/extensions/parent/ext-idle.js
@@ -27,17 +27,17 @@ const getIdleObserverInfo = (extension, 
   }
   return observerInfo;
 };
 
 const getIdleObserver = (extension, context) => {
   let observerInfo = getIdleObserverInfo(extension, context);
   let {observer, detectionInterval} = observerInfo;
   if (!observer) {
-    observer = new class extends ExtensionUtils.EventEmitter {
+    observer = new class extends ExtensionCommon.EventEmitter {
       observe(subject, topic, data) {
         if (topic == "idle" || topic == "active") {
           this.emit("stateChanged", topic);
         }
       }
     }();
     idleService.addIdleObserver(observer, detectionInterval);
     observerInfo.observer = observer;
--- a/toolkit/components/extensions/parent/ext-management.js
+++ b/toolkit/components/extensions/parent/ext-management.js
@@ -90,17 +90,17 @@ function checkAllowedAddon(addon) {
     return false;
   }
   if (addon.type == "extension" && !addon.isWebExtension) {
     return false;
   }
   return allowedTypes.includes(addon.type);
 }
 
-class AddonListener extends ExtensionUtils.EventEmitter {
+class AddonListener extends ExtensionCommon.EventEmitter {
   constructor() {
     super();
     AddonManager.addAddonListener(this);
   }
 
   release() {
     AddonManager.removeAddonListener(this);
   }
--- a/toolkit/components/extensions/parent/ext-tabs-base.js
+++ b/toolkit/components/extensions/parent/ext-tabs-base.js
@@ -13,20 +13,23 @@ ChromeUtils.defineModuleGetter(this, "Pr
                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
 
 var {
   DefaultMap,
   DefaultWeakMap,
   ExtensionError,
-  defineLazyGetter,
   getWinUtils,
 } = ExtensionUtils;
 
+var {
+  defineLazyGetter,
+} = ExtensionCommon;
+
 /**
  * The platform-specific type of native tab objects, which are wrapped by
  * TabBase instances.
  *
  * @typedef {Object|XULElement} NativeTab
  */
 
 /**
--- a/toolkit/components/extensions/parent/ext-toolkit.js
+++ b/toolkit/components/extensions/parent/ext-toolkit.js
@@ -14,17 +14,17 @@
 
 ChromeUtils.defineModuleGetter(this, "ContextualIdentityService",
                                "resource://gre/modules/ContextualIdentityService.jsm");
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
 
 ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 
-global.EventEmitter = ExtensionUtils.EventEmitter;
+global.EventEmitter = ExtensionCommon.EventEmitter;
 global.EventManager = ExtensionCommon.EventManager;
 
 /* globals DEFAULT_STORE, PRIVATE_STORE, CONTAINER_STORE */
 
 global.DEFAULT_STORE = "firefox-default";
 global.PRIVATE_STORE = "firefox-private";
 global.CONTAINER_STORE = "firefox-container-";