Bug 1316396: Part 1 - Reorganize parent, child, common, and test code into more appropriate modules. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Wed, 09 Nov 2016 10:07:55 -0800
changeset 436928 b784aad96456a0176c7b36a2864aea3556c978d3
parent 436927 b913f0ce1e3e3bc94ce444b286768aed15efbfd4
child 436929 0f0f0a8a5740d49537ec2fea3dea24761955f512
push id35242
push usermaglione.k@gmail.com
push dateThu, 10 Nov 2016 02:11:26 +0000
reviewersaswan
bugs1316396
milestone52.0a1
Bug 1316396: Part 1 - Reorganize parent, child, common, and test code into more appropriate modules. r?aswan MozReview-Commit-ID: 5WMt69GoN3K
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionCommon.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/ExtensionTestCommon.jsm
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/LegacyExtensionsUtils.jsm
toolkit/components/extensions/NativeMessaging.jsm
toolkit/components/extensions/moz.build
toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js
toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js
toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js
toolkit/components/extensions/test/xpcshell/test_native_messaging.js
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -29,30 +29,28 @@ Cu.import("resource://gre/modules/Servic
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionAPIs",
                                   "resource://gre/modules/ExtensionAPI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
                                   "resource://gre/modules/ExtensionStorage.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
-                                  "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestCommon",
+                                  "resource://testing-common/ExtensionTestCommon.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Locale",
                                   "resource://gre/modules/Locale.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Log",
                                   "resource://gre/modules/Log.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
                                   "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
                                   "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
-                                  "resource://gre/modules/NativeMessaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
@@ -60,39 +58,32 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://devtools/shared/Loader.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 Cu.import("resource://gre/modules/ExtensionContent.jsm");
 Cu.import("resource://gre/modules/ExtensionManagement.jsm");
+Cu.import("resource://gre/modules/ExtensionParent.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
-const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
-const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
-const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
-
-let schemaURLs = new Set();
+var {
+  GlobalManager,
+  ParentAPIManager,
+  apiManager: Management,
+} = ExtensionParent;
 
-if (!AppConstants.RELEASE_OR_BETA) {
-  schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
-}
-
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-var {
-  BaseContext,
+const {
   EventEmitter,
   LocaleData,
-  SchemaAPIManager,
-  SpreadArgs,
-  defineLazyGetter,
   flushJarCache,
   instanceOf,
 } = ExtensionUtils;
 
 XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
 
 const LOGGER_ID_BASE = "addons.webextension.";
 const UUID_MAP_PREF = "extensions.webextensions.uuids";
@@ -106,485 +97,16 @@ const COMMENT_REGEXP = new RegExp(String
         [^"\n] |
         " (?:[^"\\\n] | \\.)* "
       )*?
     )
 
     //.*
   `.replace(/\s+/g, ""), "gm");
 
-var GlobalManager;
-var ParentAPIManager;
-
-// This object loads the ext-*.js scripts that define the extension API.
-var Management = new class extends SchemaAPIManager {
-  constructor() {
-    super("main");
-    this.initialized = null;
-  }
-
-  // Loads all the ext-*.js scripts currently registered.
-  lazyInit() {
-    if (this.initialized) {
-      return this.initialized;
-    }
-
-    // Load order matters here. The base manifest defines types which are
-    // extended by other schemas, so needs to be loaded first.
-    let promise = Schemas.load(BASE_SCHEMA).then(() => {
-      let promises = [];
-      for (let [/* name */, url] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) {
-        promises.push(Schemas.load(url));
-      }
-      for (let url of schemaURLs) {
-        promises.push(Schemas.load(url));
-      }
-      return Promise.all(promises);
-    });
-
-    for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) {
-      this.loadScript(value);
-    }
-
-    this.initialized = promise;
-    return this.initialized;
-  }
-
-  registerSchemaAPI(namespace, envType, getAPI) {
-    if (envType == "addon_parent" || envType == "content_parent") {
-      super.registerSchemaAPI(namespace, envType, getAPI);
-    }
-  }
-}();
-
-// Subscribes to messages related to the extension messaging API and forwards it
-// to the relevant message manager. The "sender" field for the `onMessage` and
-// `onConnect` events are updated if needed.
-let ProxyMessenger = {
-  _initialized: false,
-  init() {
-    if (this._initialized) {
-      return;
-    }
-    this._initialized = true;
-
-    // TODO(robwu): When addons move to a separate process, we should use the
-    // parent process manager(s) of the addon process(es) instead of the
-    // in-process one.
-    let pipmm = Services.ppmm.getChildAt(0);
-    // Listen on the global frame message manager because content scripts send
-    // and receive extension messages via their frame.
-    // Listen on the parent process message manager because `runtime.connect`
-    // and `runtime.sendMessage` requests must be delivered to all frames in an
-    // addon process (by the API contract).
-    // And legacy addons are not associated with a frame, so that is another
-    // reason for having a parent process manager here.
-    let messageManagers = [Services.mm, pipmm];
-
-    MessageChannel.addListener(messageManagers, "Extension:Connect", this);
-    MessageChannel.addListener(messageManagers, "Extension:Message", this);
-    MessageChannel.addListener(messageManagers, "Extension:Port:Disconnect", this);
-    MessageChannel.addListener(messageManagers, "Extension:Port:PostMessage", this);
-  },
-
-  receiveMessage({target, messageName, channelId, sender, recipient, data, responseType}) {
-    if (recipient.toNativeApp) {
-      let {childId, toNativeApp} = recipient;
-      if (messageName == "Extension:Message") {
-        let context = ParentAPIManager.getContextById(childId);
-        return new NativeApp(context, toNativeApp).sendMessage(data);
-      }
-      if (messageName == "Extension:Connect") {
-        let context = ParentAPIManager.getContextById(childId);
-        NativeApp.onConnectNative(context, target.messageManager, data.portId, sender, toNativeApp);
-        return true;
-      }
-      // "Extension:Port:Disconnect" and "Extension:Port:PostMessage" for
-      // native messages are handled by NativeApp.
-      return;
-    }
-    let extension = GlobalManager.extensionMap.get(sender.extensionId);
-    let receiverMM = this._getMessageManagerForRecipient(recipient);
-    if (!extension || !receiverMM) {
-      return Promise.reject({
-        result: MessageChannel.RESULT_NO_HANDLER,
-        message: "No matching message handler for the given recipient.",
-      });
-    }
-
-    if ((messageName == "Extension:Message" ||
-         messageName == "Extension:Connect") &&
-        Management.global.tabGetSender) {
-      // From ext-tabs.js, undefined on Android.
-      Management.global.tabGetSender(extension, target, sender);
-    }
-    return MessageChannel.sendMessage(receiverMM, messageName, data, {
-      sender,
-      recipient,
-      responseType,
-    });
-  },
-
-  /**
-   * @param {object} recipient An object that was passed to
-   *     `MessageChannel.sendMessage`.
-   * @returns {object|null} The message manager matching the recipient if found.
-   */
-  _getMessageManagerForRecipient(recipient) {
-    let {extensionId, tabId} = recipient;
-    // tabs.sendMessage / tabs.connect
-    if (tabId) {
-      // `tabId` being set implies that the tabs API is supported, so we don't
-      // need to check whether `TabManager` exists.
-      let tab = Management.global.TabManager.getTab(tabId, null, null);
-      return tab && tab.linkedBrowser.messageManager;
-    }
-
-    // runtime.sendMessage / runtime.connect
-    if (extensionId) {
-      // TODO(robwu): map the extensionId to the addon parent process's message
-      // manager when they run in a separate process.
-      return Services.ppmm.getChildAt(0);
-    }
-
-    return null;
-  },
-};
-
-class BrowserDocshellFollower {
-  /**
-   * Follows the <browser> belonging to the `xulBrowser`'s current docshell.
-   *
-   * @param {XULElement} xulBrowser A <browser> tag.
-   * @param {function} onBrowserChange Called when the <browser> changes.
-   */
-  constructor(xulBrowser, onBrowserChange) {
-    this.xulBrowser = xulBrowser;
-    this.onBrowserChange = onBrowserChange;
-
-    xulBrowser.addEventListener("SwapDocShells", this);
-  }
-
-  destroy() {
-    this.xulBrowser.removeEventListener("SwapDocShells", this);
-    this.xulBrowser = null;
-  }
-
-  handleEvent({detail: otherBrowser}) {
-    this.xulBrowser.removeEventListener("SwapDocShells", this);
-    this.xulBrowser = otherBrowser;
-    this.xulBrowser.addEventListener("SwapDocShells", this);
-    this.onBrowserChange(otherBrowser);
-  }
-}
-
-class ProxyContext extends BaseContext {
-  constructor(envType, extension, params, xulBrowser, principal) {
-    super(envType, extension);
-
-    this.uri = NetUtil.newURI(params.url);
-
-    this.incognito = params.incognito;
-
-    // This message manager is used by ParentAPIManager to send messages and to
-    // close the ProxyContext if the underlying message manager closes. This
-    // message manager object may change when `xulBrowser` swaps docshells, e.g.
-    // when a tab is moved to a different window.
-    this.currentMessageManager = xulBrowser.messageManager;
-    this._docShellTracker = new BrowserDocshellFollower(
-      xulBrowser, this.onBrowserChange.bind(this));
-
-    Object.defineProperty(this, "principal", {
-      value: principal, enumerable: true, configurable: true,
-    });
-
-    this.listenerProxies = new Map();
-
-    Management.emit("proxy-context-load", this);
-  }
-
-  get cloneScope() {
-    return this.sandbox;
-  }
-
-  onBrowserChange(browser) {
-    // Make sure that the message manager is set. Otherwise the ProxyContext may
-    // never be destroyed because the ParentAPIManager would fail to detect that
-    // the message manager is closed.
-    if (!browser.messageManager) {
-      throw new Error("BrowserDocshellFollower: The new browser has no message manager");
-    }
-
-    this.currentMessageManager = browser.messageManager;
-  }
-
-  shutdown() {
-    this.unload();
-  }
-
-  unload() {
-    if (this.unloaded) {
-      return;
-    }
-    this._docShellTracker.destroy();
-    super.unload();
-    Management.emit("proxy-context-unload", this);
-  }
-}
-
-defineLazyGetter(ProxyContext.prototype, "apiObj", function() {
-  let obj = {};
-  GlobalManager.injectInObject(this, false, obj);
-  return obj;
-});
-
-defineLazyGetter(ProxyContext.prototype, "sandbox", function() {
-  return Cu.Sandbox(this.principal);
-});
-
-// The parent ProxyContext of an ExtensionContext in ExtensionChild.jsm.
-class ExtensionChildProxyContext extends ProxyContext {
-  constructor(envType, extension, params, xulBrowser) {
-    super(envType, extension, params, xulBrowser, extension.principal);
-
-    this.viewType = params.viewType;
-    // WARNING: The xulBrowser may change when docShells are swapped, e.g. when
-    // the tab moves to a different window.
-    this.xulBrowser = xulBrowser;
-  }
-
-  // The window that contains this context. This may change due to moving tabs.
-  get xulWindow() {
-    return this.xulBrowser.ownerGlobal;
-  }
-
-  get windowId() {
-    if (!Management.global.WindowManager || this.viewType == "background") {
-      return;
-    }
-    // viewType popup or tab:
-    return Management.global.WindowManager.getId(this.xulWindow);
-  }
-
-  get tabId() {
-    if (!Management.global.TabManager) {
-      return;  // Not yet supported on Android.
-    }
-    let {gBrowser} = this.xulBrowser.ownerGlobal;
-    let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser);
-    return tab && Management.global.TabManager.getId(tab);
-  }
-
-  onBrowserChange(browser) {
-    super.onBrowserChange(browser);
-    this.xulBrowser = browser;
-  }
-
-  shutdown() {
-    Management.emit("page-shutdown", this);
-    super.shutdown();
-  }
-}
-
-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;
-}
-
-ParentAPIManager = {
-  proxyContexts: new Map(),
-
-  init() {
-    Services.obs.addObserver(this, "message-manager-close", false);
-
-    Services.mm.addMessageListener("API:CreateProxyContext", this);
-    Services.mm.addMessageListener("API:CloseProxyContext", this, true);
-    Services.mm.addMessageListener("API:Call", this);
-    Services.mm.addMessageListener("API:AddListener", this);
-    Services.mm.addMessageListener("API:RemoveListener", this);
-  },
-
-  // "message-manager-close" observer.
-  observe(subject, topic, data) {
-    let mm = subject;
-    for (let [childId, context] of this.proxyContexts) {
-      if (context.currentMessageManager == mm) {
-        this.closeProxyContext(childId);
-      }
-    }
-  },
-
-  shutdownExtension(extensionId) {
-    for (let [childId, context] of this.proxyContexts) {
-      if (context.extension.id == extensionId) {
-        context.shutdown();
-        this.proxyContexts.delete(childId);
-      }
-    }
-  },
-
-  receiveMessage({name, data, target}) {
-    switch (name) {
-      case "API:CreateProxyContext":
-        this.createProxyContext(data, target);
-        break;
-
-      case "API:CloseProxyContext":
-        this.closeProxyContext(data.childId);
-        break;
-
-      case "API:Call":
-        this.call(data, target);
-        break;
-
-      case "API:AddListener":
-        this.addListener(data, target);
-        break;
-
-      case "API:RemoveListener":
-        this.removeListener(data);
-        break;
-    }
-  },
-
-  createProxyContext(data, target) {
-    let {envType, extensionId, childId, principal} = data;
-    if (this.proxyContexts.has(childId)) {
-      throw new Error("A WebExtension context with the given ID already exists!");
-    }
-
-    let extension = GlobalManager.getExtension(extensionId);
-    if (!extension) {
-      throw new Error(`No WebExtension found with ID ${extensionId}`);
-    }
-
-    let context;
-    if (envType == "addon_parent") {
-      // Privileged addon contexts can only be loaded in documents whose main
-      // frame is also the same addon.
-      if (principal.URI.prePath != extension.baseURI.prePath ||
-          !target.contentPrincipal.subsumes(principal)) {
-        throw new Error(`Refused to create privileged WebExtension context for ${principal.URI.spec}`);
-      }
-      context = new ExtensionChildProxyContext(envType, extension, data, target);
-    } else if (envType == "content_parent") {
-      context = new ProxyContext(envType, extension, data, target, principal);
-    } else {
-      throw new Error(`Invalid WebExtension context envType: ${envType}`);
-    }
-    this.proxyContexts.set(childId, context);
-  },
-
-  closeProxyContext(childId) {
-    let context = this.proxyContexts.get(childId);
-    if (!context) {
-      return;
-    }
-    context.unload();
-    this.proxyContexts.delete(childId);
-  },
-
-  call(data, target) {
-    let context = this.getContextById(data.childId);
-    if (context.currentMessageManager !== target.messageManager) {
-      Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
-    }
-
-    try {
-      let args = Cu.cloneInto(data.args, context.sandbox);
-      let result = findPathInObject(context.apiObj, data.path)(...args);
-
-      if (data.callId) {
-        result = result || Promise.resolve();
-
-        result.then(result => {
-          result = result instanceof SpreadArgs ? [...result] : [result];
-
-          context.currentMessageManager.sendAsyncMessage("API:CallResult", {
-            childId: data.childId,
-            callId: data.callId,
-            result,
-          });
-        }, error => {
-          error = context.normalizeError(error);
-          context.currentMessageManager.sendAsyncMessage("API:CallResult", {
-            childId: data.childId,
-            callId: data.callId,
-            error: {message: error.message},
-          });
-        });
-      }
-    } catch (e) {
-      if (data.callId) {
-        let error = context.normalizeError(e);
-        context.currentMessageManager.sendAsyncMessage("API:CallResult", {
-          childId: data.childId,
-          callId: data.callId,
-          error: {message: error.message},
-        });
-      } else {
-        Cu.reportError(e);
-      }
-    }
-  },
-
-  addListener(data, target) {
-    let context = this.getContextById(data.childId);
-    if (context.currentMessageManager !== target.messageManager) {
-      Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
-    }
-
-    function listener(...listenerArgs) {
-      context.currentMessageManager.sendAsyncMessage("API:RunListener", {
-        childId: data.childId,
-        path: data.path,
-        args: listenerArgs,
-      });
-    }
-
-    context.listenerProxies.set(data.path, listener);
-
-    let args = Cu.cloneInto(data.args, context.sandbox);
-    findPathInObject(context.apiObj, data.path).addListener(listener, ...args);
-  },
-
-  removeListener(data) {
-    let context = this.getContextById(data.childId);
-    let listener = context.listenerProxies.get(data.path);
-    findPathInObject(context.apiObj, data.path).removeListener(listener);
-  },
-
-  getContextById(childId) {
-    let context = this.proxyContexts.get(childId);
-    if (!context) {
-      let error = new Error("WebExtension context not found!");
-      Cu.reportError(error);
-      throw error;
-    }
-    return context;
-  },
-};
-
-ParentAPIManager.init();
-
 // All moz-extension URIs use a machine-specific UUID rather than the
 // extension's own ID in the host component. This makes it more
 // difficult for web pages to detect whether a user has a given add-on
 // installed (by trying to load a moz-extension URI referring to a
 // web_accessible_resource from the extension). UUIDMap.get()
 // returns the UUID for a given add-on ID.
 var UUIDMap = {
   _read() {
@@ -680,62 +202,17 @@ var UninstallObserver = {
 
     if (!this.leaveUuid) {
       // Clear the entry in the UUID map
       UUIDMap.remove(addon.id);
     }
   },
 };
 
-// Responsible for loading extension APIs into the right globals.
-GlobalManager = {
-  // Map[extension ID -> Extension]. Determines which extension is
-  // responsible for content under a particular extension ID.
-  extensionMap: new Map(),
-  initialized: false,
-
-  init(extension) {
-    if (this.extensionMap.size == 0) {
-      UninstallObserver.init();
-      ProxyMessenger.init();
-      Management.on("extension-browser-inserted", this._onExtensionBrowser);
-      this.initialized = true;
-    }
-
-    this.extensionMap.set(extension.id, extension);
-  },
-
-  uninit(extension) {
-    this.extensionMap.delete(extension.id);
-
-    if (this.extensionMap.size == 0 && this.initialized) {
-      Management.off("extension-browser-inserted", this._onExtensionBrowser);
-      this.initialized = false;
-    }
-  },
-
-  _onExtensionBrowser(type, browser) {
-    browser.messageManager.loadFrameScript(`data:,
-      Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
-      ExtensionContent.init(this);
-      addEventListener("unload", function() {
-        ExtensionContent.uninit(this);
-      });
-    `, false);
-  },
-
-  getExtension(extensionId) {
-    return this.extensionMap.get(extensionId);
-  },
-
-  injectInObject(context, isChromeCompat, dest) {
-    Management.generateAPIs(context, dest);
-    SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
-  },
-};
+UninstallObserver.init();
 
 // Represents the data contained in an extension, contained either
 // in a directory or a zip file, which may or may not be installed.
 // This class implements the functionality of the Extension class,
 // primarily related to manifest parsing and localization, which is
 // useful prior to extension installation or initialization.
 //
 // No functionality of this class is guaranteed to work before
@@ -1082,105 +559,16 @@ this.ExtensionData = class {
       let results = yield Promise.all(promises);
 
       this.localeData.selectedLocale = locale;
       return results[0];
     }.bind(this));
   }
 };
 
-
-/**
- * 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
- * @param {nsIFile} file
- * @param {nsIURI} rootURI
- * @param {string} installType
- */
-class MockExtension {
-  constructor(file, rootURI, installType) {
-    this.id = null;
-    this.file = file;
-    this.rootURI = rootURI;
-    this.installType = installType;
-    this.addon = null;
-
-    let promiseEvent = eventName => new Promise(resolve => {
-      let onstartup = (msg, extension) => {
-        if (this.addon && extension.id == this.addon.id) {
-          Management.off(eventName, onstartup);
-
-          this.id = extension.id;
-          this._extension = extension;
-          resolve(extension);
-        }
-      };
-      Management.on(eventName, onstartup);
-    });
-
-    this._extension = null;
-    this._extensionPromise = promiseEvent("startup");
-    this._readyPromise = promiseEvent("ready");
-  }
-
-  testMessage(...args) {
-    return this._extension.testMessage(...args);
-  }
-
-  on(...args) {
-    this._extensionPromise.then(extension => {
-      extension.on(...args);
-    });
-  }
-
-  off(...args) {
-    this._extensionPromise.then(extension => {
-      extension.off(...args);
-    });
-  }
-
-  startup() {
-    if (this.installType == "temporary") {
-      return AddonManager.installTemporaryAddon(this.file).then(addon => {
-        this.addon = addon;
-        return this._readyPromise;
-      });
-    } else if (this.installType == "permanent") {
-      return new Promise((resolve, reject) => {
-        AddonManager.getInstallForFile(this.file, install => {
-          let listener = {
-            onInstallFailed: reject,
-            onInstallEnded: (install, newAddon) => {
-              this.addon = newAddon;
-              resolve(this._readyPromise);
-            },
-          };
-
-          install.addListener(listener);
-          install.install();
-        });
-      });
-    }
-    throw new Error("installType must be one of: temporary, permanent");
-  }
-
-  shutdown() {
-    this.addon.uninstall();
-    return this.cleanupGeneratedFile();
-  }
-
-  cleanupGeneratedFile() {
-    flushJarCache(this.file);
-    return OS.File.remove(this.file.path);
-  }
-}
-
 let _browserUpdated = false;
 
 // We create one instance of this class per extension. |addonData|
 // comes directly from bootstrap.js when initializing.
 this.Extension = class extends ExtensionData {
   constructor(addonData, startupReason) {
     super(addonData.resourceURI);
 
@@ -1216,217 +604,26 @@ this.Extension = class extends Extension
   static set browserUpdated(updated) {
     _browserUpdated = updated;
   }
 
   static get browserUpdated() {
     return _browserUpdated;
   }
 
-  /**
-   * This code is designed to make it easy to test a WebExtension
-   * without creating a bunch of files. Everything is contained in a
-   * single JSON blob.
-   *
-   * Properties:
-   *   "background": "<JS code>"
-   *     A script to be loaded as the background script.
-   *     The "background" section of the "manifest" property is overwritten
-   *     if this is provided.
-   *   "manifest": {...}
-   *     Contents of manifest.json
-   *   "files": {"filename1": "contents1", ...}
-   *     Data to be included as files. Can be referenced from the manifest.
-   *     If a manifest file is provided here, it takes precedence over
-   *     a generated one. Always use "/" as a directory separator.
-   *     Directories should appear here only implicitly (as a prefix
-   *     to file names)
-   *
-   * To make things easier, the value of "background" and "files"[] can
-   * be a function, which is converted to source that is run.
-   *
-   * The generated extension is stored in the system temporary directory,
-   * and an nsIFile object pointing to it is returned.
-   *
-   * @param {object} data
-   * @returns {nsIFile}
-   */
   static generateXPI(data) {
-    let manifest = data.manifest;
-    if (!manifest) {
-      manifest = {};
-    }
-
-    let files = data.files;
-    if (!files) {
-      files = {};
-    }
-
-    function provide(obj, keys, value, override = false) {
-      if (keys.length == 1) {
-        if (!(keys[0] in obj) || override) {
-          obj[keys[0]] = value;
-        }
-      } else {
-        if (!(keys[0] in obj)) {
-          obj[keys[0]] = {};
-        }
-        provide(obj[keys[0]], keys.slice(1), value, override);
-      }
-    }
-
-    provide(manifest, ["name"], "Generated extension");
-    provide(manifest, ["manifest_version"], 2);
-    provide(manifest, ["version"], "1.0");
-
-    if (data.background) {
-      let bgScript = uuidGen.generateUUID().number + ".js";
-
-      provide(manifest, ["background", "scripts"], [bgScript], true);
-      files[bgScript] = data.background;
-    }
-
-    provide(files, ["manifest.json"], manifest);
-
-    if (data.embedded) {
-      // Package this as a webextension embedded inside a legacy
-      // extension.
-
-      let xpiFiles = {
-        "install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
-          <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-               xmlns:em="http://www.mozilla.org/2004/em-rdf#">
-              <Description about="urn:mozilla:install-manifest"
-                  em:id="${manifest.applications.gecko.id}"
-                  em:name="${manifest.name}"
-                  em:type="2"
-                  em:version="${manifest.version}"
-                  em:description=""
-                  em:hasEmbeddedWebExtension="true"
-                  em:bootstrap="true">
-
-                  <!-- Firefox -->
-                  <em:targetApplication>
-                      <Description
-                          em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
-                          em:minVersion="51.0a1"
-                          em:maxVersion="*"/>
-                  </em:targetApplication>
-              </Description>
-          </RDF>
-        `,
-
-        "bootstrap.js": `
-          function install() {}
-          function uninstall() {}
-          function shutdown() {}
-
-          function startup(data) {
-            data.webExtension.startup();
-          }
-        `,
-      };
-
-      for (let [path, data] of Object.entries(files)) {
-        xpiFiles[`webextension/${path}`] = data;
-      }
-
-      files = xpiFiles;
-    }
-
-    return this.generateZipFile(files);
+    return ExtensionTestCommon.generateXPI(data);
   }
 
   static generateZipFile(files, baseName = "generated-extension.xpi") {
-    let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
-    let zipW = new ZipWriter();
-
-    let file = FileUtils.getFile("TmpD", [baseName]);
-    file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
-
-    const MODE_WRONLY = 0x02;
-    const MODE_TRUNCATE = 0x20;
-    zipW.open(file, MODE_WRONLY | MODE_TRUNCATE);
-
-    // Needs to be in microseconds for some reason.
-    let time = Date.now() * 1000;
-
-    function generateFile(filename) {
-      let components = filename.split("/");
-      let path = "";
-      for (let component of components.slice(0, -1)) {
-        path += component + "/";
-        if (!zipW.hasEntry(path)) {
-          zipW.addEntryDirectory(path, time, false);
-        }
-      }
-    }
-
-    for (let filename in files) {
-      let script = files[filename];
-      if (typeof(script) == "function") {
-        script = "(" + script.toString() + ")()";
-      } else if (instanceOf(script, "Object") || instanceOf(script, "Array")) {
-        script = JSON.stringify(script);
-      }
-
-      if (!instanceOf(script, "ArrayBuffer")) {
-        script = new TextEncoder("utf-8").encode(script).buffer;
-      }
-
-      let stream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"].createInstance(Ci.nsIArrayBufferInputStream);
-      stream.setData(script, 0, script.byteLength);
-
-      generateFile(filename);
-      zipW.addEntryStream(filename, time, 0, stream, false);
-    }
-
-    zipW.close();
-
-    return file;
+    return ExtensionTestCommon.generateZipFile(files, baseName);
   }
 
-  /**
-   * Generates a new extension using |Extension.generateXPI|, and initializes a
-   * new |Extension| instance which will execute it.
-   *
-   * @param {object} data
-   * @returns {Extension}
-   */
   static generate(data) {
-    let file = this.generateXPI(data);
-
-    flushJarCache(file);
-    Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
-
-    let fileURI = Services.io.newFileURI(file);
-    let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
-
-    // This may be "temporary" or "permanent".
-    if (data.useAddonManager) {
-      return new MockExtension(file, jarURI, data.useAddonManager);
-    }
-
-    let id;
-    if (data.manifest) {
-      if (data.manifest.applications && data.manifest.applications.gecko) {
-        id = data.manifest.applications.gecko.id;
-      } else if (data.manifest.browser_specific_settings && data.manifest.browser_specific_settings.gecko) {
-        id = data.manifest.browser_specific_settings.gecko.id;
-      }
-    }
-    if (!id) {
-      id = uuidGen.generateUUID().number;
-    }
-
-    return new Extension({
-      id,
-      resourceURI: jarURI,
-      cleanupFile: file,
-    });
+    return ExtensionTestCommon.generate(data);
   }
 
   on(hook, f) {
     return this.emitter.on(hook, f);
   }
 
   off(hook, f) {
     return this.emitter.off(hook, f);
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -14,44 +14,414 @@ this.EXPORTED_SYMBOLS = ["ExtensionChild
  * Don't put contentscript logic here, use ExtensionContent.jsm instead.
  */
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
+                                  "resource://gre/modules/ExtensionParent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+                                  "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 
+XPCOMUtils.defineLazyGetter(this, "ParentAPIManager",
+                            () => ExtensionParent.ParentAPIManager);
+
 const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
 
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-var {
+
+const {
+  EventManager,
+  SingletonEventManager,
+  SpreadArgs,
+  defineLazyGetter,
+  findPathInObject,
   getInnerWindowID,
-  BaseContext,
-  ChildAPIManager,
-  defineLazyGetter,
-  LocalAPIImplementation,
-  Messenger,
-  SchemaAPIManager,
+  getMessageManager,
+  injectAPI,
 } = ExtensionUtils;
 
-// There is a circular dependency between Extension.jsm and us.
-// Long-term this file should not reference Extension.jsm (because they would
-// live in different processes), but for now use lazy getters.
-XPCOMUtils.defineLazyGetter(this, "findPathInObject",
-  () => Cu.import("resource://gre/modules/Extension.jsm", {}).findPathInObject);
-XPCOMUtils.defineLazyGetter(this, "ParentAPIManager",
-  () => Cu.import("resource://gre/modules/Extension.jsm", {}).ParentAPIManager);
+const {
+  BaseContext,
+  LocalAPIImplementation,
+  SchemaAPIInterface,
+  SchemaAPIManager,
+} = ExtensionCommon;
+
+var ExtensionChild;
+
+let gNextPortId = 1;
+
+/**
+ * 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.
+ * @param {string} id An ID that uniquely identifies this port's channel.
+ * @param {object} sender The `port.sender` property.
+ * @param {object} recipient The recipient of messages sent from this port.
+ */
+class Port {
+  constructor(context, senderMM, receiverMMs, name, id, sender, recipient) {
+    this.context = context;
+    this.senderMM = senderMM;
+    this.receiverMMs = receiverMMs;
+    this.name = name;
+    this.id = id;
+    this.sender = sender;
+    this.recipient = recipient;
+    this.disconnected = false;
+    this.disconnectListeners = new Set();
+    this.unregisterMessageFuncs = new Set();
+
+    // Common options for onMessage and onDisconnect.
+    this.handlerBase = {
+      messageFilterStrict: {portId: id},
+
+      filterMessage: (sender, recipient) => {
+        return sender.contextId !== this.context.contextId;
+      },
+    };
+
+    this.disconnectHandler = Object.assign({
+      receiveMessage: ({data}) => this.disconnectByOtherEnd(data),
+    }, this.handlerBase);
+
+    MessageChannel.addListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
+
+    this.context.callOnClose(this);
+  }
+
+  api() {
+    let portObj = Cu.createObjectIn(this.context.cloneScope);
+
+    let portError = null;
+    let publicAPI = {
+      name: this.name,
+
+      disconnect: () => {
+        this.disconnect();
+      },
+
+      postMessage: json => {
+        this.postMessage(json);
+      },
+
+      onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => {
+        return this.registerOnDisconnect(error => {
+          portError = error && this.context.normalizeError(error);
+          fire.withoutClone(portObj);
+        });
+      }).api(),
+
+      onMessage: new EventManager(this.context, "Port.onMessage", fire => {
+        return this.registerOnMessage(msg => {
+          msg = Cu.cloneInto(msg, this.context.cloneScope);
+          fire.withoutClone(msg, portObj);
+        });
+      }).api(),
+
+      get error() {
+        return portError;
+      },
+    };
+
+    if (this.sender) {
+      publicAPI.sender = this.sender;
+    }
+
+    injectAPI(publicAPI, portObj);
+    return portObj;
+  }
+
+  postMessage(json) {
+    if (this.disconnected) {
+      throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port");
+    }
+
+    this._sendMessage("Extension:Port:PostMessage", json);
+  }
+
+  /**
+   * Register a callback that is called when the port is disconnected by the
+   * *other* end. The callback is automatically unregistered when the port or
+   * context is closed.
+   *
+   * @param {function} callback Called when the other end disconnects the port.
+   *     If the disconnect is caused by an error, the first parameter is an
+   *     object with a "message" string property that describes the cause.
+   * @returns {function} Function to unregister the listener.
+   */
+  registerOnDisconnect(callback) {
+    let listener = error => {
+      if (this.context.active && !this.disconnected) {
+        callback(error);
+      }
+    };
+    this.disconnectListeners.add(listener);
+    return () => {
+      this.disconnectListeners.delete(listener);
+    };
+  }
+
+  /**
+   * Register a callback that is called when a message is received. The callback
+   * is automatically unregistered when the port or context is closed.
+   *
+   * @param {function} callback Called when a message is received.
+   * @returns {function} Function to unregister the listener.
+   */
+  registerOnMessage(callback) {
+    let handler = Object.assign({
+      receiveMessage: ({data}) => {
+        if (this.context.active && !this.disconnected) {
+          callback(data);
+        }
+      },
+    }, this.handlerBase);
+
+    let unregister = () => {
+      this.unregisterMessageFuncs.delete(unregister);
+      MessageChannel.removeListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
+    };
+    MessageChannel.addListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
+    this.unregisterMessageFuncs.add(unregister);
+    return unregister;
+  }
+
+  _sendMessage(message, data) {
+    let options = {
+      recipient: Object.assign({}, this.recipient, {portId: this.id}),
+      responseType: MessageChannel.RESPONSE_NONE,
+    };
+
+    return this.context.sendMessage(this.senderMM, message, data, options);
+  }
+
+  handleDisconnection() {
+    MessageChannel.removeListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
+    for (let unregister of this.unregisterMessageFuncs) {
+      unregister();
+    }
+    this.context.forgetOnClose(this);
+    this.disconnected = true;
+  }
+
+  /**
+   * Disconnect the port from the other end (which may not even exist).
+   *
+   * @param {Error|{message: string}} [error] The reason for disconnecting,
+   *     if it is an abnormal disconnect.
+   */
+  disconnectByOtherEnd(error = null) {
+    if (this.disconnected) {
+      return;
+    }
+
+    for (let listener of this.disconnectListeners) {
+      listener(error);
+    }
+
+    this.handleDisconnection();
+  }
+
+  /**
+   * Disconnect the port from this end.
+   *
+   * @param {Error|{message: string}} [error] The reason for disconnecting,
+   *     if it is an abnormal disconnect.
+   */
+  disconnect(error = null) {
+    if (this.disconnected) {
+      // disconnect() may be called without side effects even after the port is
+      // closed - https://developer.chrome.com/extensions/runtime#type-Port
+      return;
+    }
+    this.handleDisconnection();
+    if (error) {
+      error = {message: this.context.normalizeError(error).message};
+    }
+    this._sendMessage("Extension:Port:Disconnect", error);
+  }
+
+  close() {
+    this.disconnect();
+  }
+}
+
+/**
+ * Each extension context gets its own Messenger object. It handles the
+ * basics of sendMessage, onMessage, connect and onConnect.
+ *
+ * @param {BaseContext} context The context to which this Messenger is tied.
+ * @param {Array<nsIMessageListenerManager>} messageManagers
+ *     The message managers used to receive messages (e.g. onMessage/onConnect
+ *     requests).
+ * @param {object} sender Describes this sender to the recipient. This object
+ *     is extended further by BaseContext's sendMessage method and appears as
+ *     the `sender` object to `onConnect` and `onMessage`.
+ *     Do not set the `extensionId`, `contextId` or `tab` properties. The former
+ *     two are added by BaseContext's sendMessage, while `sender.tab` is set by
+ *     the ProxyMessenger in the main process.
+ * @param {object} filter A recipient filter to apply to incoming messages from
+ *     the broker. Messages are only handled by this Messenger if all key-value
+ *     pairs match the `recipient` as specified by the sender of the message.
+ *     In other words, this filter defines the required fields of `recipient`.
+ * @param {object} [optionalFilter] An additional filter to apply to incoming
+ *     messages. Unlike `filter`, the keys from `optionalFilter` are allowed to
+ *     be omitted from `recipient`. Only keys that are present in both
+ *     `optionalFilter` and `recipient` are applied to filter incoming messages.
+ */
+class Messenger {
+  constructor(context, messageManagers, sender, filter, optionalFilter) {
+    this.context = context;
+    this.messageManagers = messageManagers;
+    this.sender = sender;
+    this.filter = filter;
+    this.optionalFilter = optionalFilter;
+  }
+
+  _sendMessage(messageManager, message, data, recipient) {
+    let options = {
+      recipient,
+      sender: this.sender,
+      responseType: MessageChannel.RESPONSE_FIRST,
+    };
+
+    return this.context.sendMessage(messageManager, message, data, options);
+  }
+
+  sendMessage(messageManager, msg, recipient, responseCallback) {
+    let promise = this._sendMessage(messageManager, "Extension:Message", msg, recipient)
+      .catch(error => {
+        if (error.result == MessageChannel.RESULT_NO_HANDLER) {
+          return Promise.reject({message: "Could not establish connection. Receiving end does not exist."});
+        } else if (error.result != MessageChannel.RESULT_NO_RESPONSE) {
+          return Promise.reject({message: error.message});
+        }
+      });
+
+    return this.context.wrapPromise(promise, responseCallback);
+  }
+
+  onMessage(name) {
+    return new SingletonEventManager(this.context, name, callback => {
+      let listener = {
+        messageFilterPermissive: this.optionalFilter,
+        messageFilterStrict: this.filter,
+
+        filterMessage: (sender, recipient) => {
+          // Ignore the message if it was sent by this Messenger.
+          return sender.contextId !== this.context.contextId;
+        },
+
+        receiveMessage: ({target, data: message, sender, recipient}) => {
+          if (!this.context.active) {
+            return;
+          }
+
+          let sendResponse;
+          let response = undefined;
+          let promise = new Promise(resolve => {
+            sendResponse = value => {
+              resolve(value);
+              response = promise;
+            };
+          });
+
+          message = Cu.cloneInto(message, this.context.cloneScope);
+          sender = Cu.cloneInto(sender, this.context.cloneScope);
+          sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
+
+          // Note: We intentionally do not use runSafe here so that any
+          // errors are propagated to the message sender.
+          let result = callback(message, sender, sendResponse);
+          if (result instanceof this.context.cloneScope.Promise) {
+            return result;
+          } else if (result === true) {
+            return promise;
+          }
+          return response;
+        },
+      };
+
+      MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
+      return () => {
+        MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
+      };
+    }).api();
+  }
+
+  connectGetRawPort(messageManager, name, recipient) {
+    let portId = `${gNextPortId++}-${Services.appinfo.uniqueProcessID}`;
+    let port = new Port(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
+    let msg = {name, portId};
+    this._sendMessage(messageManager, "Extension:Connect", msg, recipient)
+      .catch(e => {
+        if (e.result === MessageChannel.RESULT_NO_HANDLER) {
+          e = {message: "Could not establish connection. Receiving end does not exist."};
+        } else if (e.result === MessageChannel.RESULT_DISCONNECTED) {
+          e = null;
+        }
+        port.disconnectByOtherEnd(e);
+      });
+    return port;
+  }
+
+  connect(messageManager, name, recipient) {
+    let port = this.connectGetRawPort(messageManager, name, recipient);
+    return port.api();
+  }
+
+  onConnect(name) {
+    return new SingletonEventManager(this.context, name, callback => {
+      let listener = {
+        messageFilterPermissive: this.optionalFilter,
+        messageFilterStrict: this.filter,
+
+        filterMessage: (sender, recipient) => {
+          // Ignore the port if it was created by this Messenger.
+          return sender.contextId !== this.context.contextId;
+        },
+
+        receiveMessage: ({target, data: message, sender}) => {
+          let {name, portId} = message;
+          let mm = getMessageManager(target);
+          let recipient = Object.assign({}, sender);
+          if (recipient.tab) {
+            recipient.tabId = recipient.tab.id;
+            delete recipient.tab;
+          }
+          let port = new Port(this.context, mm, this.messageManagers, name, portId, sender, recipient);
+          this.context.runSafeWithoutClone(callback, port.api());
+          return true;
+        },
+      };
+
+      MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
+      return () => {
+        MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
+      };
+    }).api();
+  }
+}
 
 var apiManager = new class extends SchemaAPIManager {
   constructor() {
     super("addon");
     this.initialized = false;
   }
 
   generateAPIs(...args) {
@@ -66,16 +436,262 @@ var apiManager = new class extends Schem
 
   registerSchemaAPI(namespace, envType, getAPI) {
     if (envType == "addon_child") {
       super.registerSchemaAPI(namespace, envType, getAPI);
     }
   }
 }();
 
+/**
+ * An object that runs an remote implementation of an API.
+ */
+class ProxyAPIImplementation extends SchemaAPIInterface {
+  /**
+   * @param {string} namespace The full path to the namespace that contains the
+   *     `name` member. This may contain dots, e.g. "storage.local".
+   * @param {string} name The name of the method or property.
+   * @param {ChildAPIManager} childApiManager The owner of this implementation.
+   */
+  constructor(namespace, name, childApiManager) {
+    super();
+    this.path = `${namespace}.${name}`;
+    this.childApiManager = childApiManager;
+  }
+
+  callFunctionNoReturn(args) {
+    this.childApiManager.callParentFunctionNoReturn(this.path, args);
+  }
+
+  callAsyncFunction(args, callback) {
+    return this.childApiManager.callParentAsyncFunction(this.path, args, callback);
+  }
+
+  addListener(listener, args) {
+    let set = this.childApiManager.listeners.get(this.path);
+    if (!set) {
+      set = new Set();
+      this.childApiManager.listeners.set(this.path, set);
+    }
+
+    set.add(listener);
+
+    if (set.size == 1) {
+      args = args.slice(1);
+
+      this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", {
+        childId: this.childApiManager.id,
+        path: this.path,
+        args,
+      });
+    }
+  }
+
+  removeListener(listener) {
+    let set = this.childApiManager.listeners.get(this.path);
+    if (!set) {
+      return;
+    }
+    set.delete(listener);
+
+    if (set.size == 0) {
+      this.childApiManager.messageManager.sendAsyncMessage("API:RemoveListener", {
+        childId: this.childApiManager.id,
+        path: this.path,
+      });
+    }
+  }
+
+  hasListener(listener) {
+    let set = this.childApiManager.listeners.get(this.path);
+    return set ? set.has(listener) : false;
+  }
+}
+
+let nextId = 1;
+
+// We create one instance of this class for every extension context
+// that needs to use remote APIs. It uses the message manager to
+// communicate with the ParentAPIManager singleton in
+// Extension.jsm. It handles asynchronous function calls as well as
+// event listeners.
+class ChildAPIManager {
+  constructor(context, messageManager, localApis, contextData) {
+    this.context = context;
+    this.messageManager = messageManager;
+
+    // The root namespace of all locally implemented APIs. If an extension calls
+    // an API that does not exist in this object, then the implementation is
+    // delegated to the ParentAPIManager.
+    this.localApis = localApis;
+
+    let id = String(context.extension.id) + "." + String(context.contextId);
+    this.id = id;
+
+    let data = {childId: id, extensionId: context.extension.id, principal: context.principal};
+    Object.assign(data, contextData);
+
+    messageManager.addMessageListener("API:RunListener", this);
+    messageManager.addMessageListener("API:CallResult", this);
+
+    // Map[path -> Set[listener]]
+    // path is, e.g., "runtime.onMessage".
+    this.listeners = new Map();
+
+    // Map[callId -> Deferred]
+    this.callPromises = new Map();
+
+    this.createProxyContextInConstructor(data);
+  }
+
+  createProxyContextInConstructor(data) {
+    this.messageManager.sendAsyncMessage("API:CreateProxyContext", data);
+  }
+
+  receiveMessage({name, data}) {
+    if (data.childId != this.id) {
+      return;
+    }
+
+    switch (name) {
+      case "API:RunListener":
+        let listeners = this.listeners.get(data.path);
+        for (let callback of listeners) {
+          this.context.runSafe(callback, ...data.args);
+        }
+        break;
+
+      case "API:CallResult":
+        let deferred = this.callPromises.get(data.callId);
+        if ("error" in data) {
+          deferred.reject(data.error);
+        } else {
+          deferred.resolve(new SpreadArgs(data.result));
+        }
+        this.callPromises.delete(data.callId);
+        break;
+    }
+  }
+
+  /**
+   * Call a function in the parent process and ignores its return value.
+   *
+   * @param {string} path The full name of the method, e.g. "tabs.create".
+   * @param {Array} args The parameters for the function.
+   */
+  callParentFunctionNoReturn(path, args) {
+    this.messageManager.sendAsyncMessage("API:Call", {
+      childId: this.id,
+      path,
+      args,
+    });
+  }
+
+  /**
+   * Calls a function in the parent process and returns its result
+   * asynchronously.
+   *
+   * @param {string} path The full name of the method, e.g. "tabs.create".
+   * @param {Array} args The parameters for the function.
+   * @param {function(*)} [callback] The callback to be called when the function
+   *     completes.
+   * @returns {Promise|undefined} Must be void if `callback` is set, and a
+   *     promise otherwise. The promise is resolved when the function completes.
+   */
+  callParentAsyncFunction(path, args, callback) {
+    let callId = nextId++;
+    let deferred = PromiseUtils.defer();
+    this.callPromises.set(callId, deferred);
+
+    this.messageManager.sendAsyncMessage("API:Call", {
+      childId: this.id,
+      callId,
+      path,
+      args,
+    });
+
+    return this.context.wrapPromise(deferred.promise, callback);
+  }
+
+  /**
+   * Create a proxy for an event in the parent process. The returned event
+   * object shares its internal state with other instances. For instance, if
+   * `removeListener` is used on a listener that was added on another object
+   * through `addListener`, then the event is unregistered.
+   *
+   * @param {string} path The full name of the event, e.g. "tabs.onCreated".
+   * @returns {object} An object with the addListener, removeListener and
+   *   hasListener methods. See SchemaAPIInterface for documentation.
+   */
+  getParentEvent(path) {
+    let parsed = /^(.+)\.(on[A-Z][^.]+)$/.exec(path);
+    if (!parsed) {
+      throw new Error("getParentEvent: Invalid event name: " + path);
+    }
+    let [, namespace, name] = parsed;
+    let impl = new ProxyAPIImplementation(namespace, name, this);
+    return {
+      addListener: (listener, ...args) => impl.addListener(listener, args),
+      removeListener: (listener) => impl.removeListener(listener),
+      hasListener: (listener) => impl.hasListener(listener),
+    };
+  }
+
+  close() {
+    this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
+  }
+
+  get cloneScope() {
+    return this.context.cloneScope;
+  }
+
+  get principal() {
+    return this.context.principal;
+  }
+
+  shouldInject(namespace, name, allowedContexts) {
+    // Do not generate content script APIs, unless explicitly allowed.
+    if (this.context.envType === "content_child" &&
+        !allowedContexts.includes("content")) {
+      return false;
+    }
+    if (allowedContexts.includes("addon_parent_only")) {
+      return false;
+    }
+    return true;
+  }
+
+  getImplementation(namespace, name) {
+    let pathObj = this.localApis;
+    if (pathObj) {
+      for (let part of namespace.split(".")) {
+        pathObj = pathObj[part];
+        if (!pathObj) {
+          break;
+        }
+      }
+      if (pathObj && name in pathObj) {
+        return new LocalAPIImplementation(pathObj, name, this.context);
+      }
+    }
+
+    return this.getFallbackImplementation(namespace, name);
+  }
+
+  getFallbackImplementation(namespace, name) {
+    // No local API found, defer implementation to the parent.
+    return new ProxyAPIImplementation(namespace, name, this);
+  }
+
+  hasPermission(permission) {
+    return this.context.extension.hasPermission(permission);
+  }
+}
+
+
 // A class that behaves identical to a ChildAPIManager, except
 // 1) creation of the ProxyContext in the parent is synchronous, and
 // 2) APIs without a local implementation and marked as incompatible with the
 //    out-of-process model fall back to directly invoking the parent methods.
 // TODO(robwu): Remove this when all APIs have migrated.
 class PseudoChildAPIManager extends ChildAPIManager {
   createProxyContextInConstructor(originalData) {
     // Create a structured clone to simulate IPC.
@@ -356,17 +972,17 @@ class ContentGlobal {
         return;
       }
       this.global.removeEventListener("DOMContentLoaded", this);
       this.global.sendAsyncMessage("Extension:ExtensionViewLoaded");
     }
   }
 }
 
-this.ExtensionChild = {
+ExtensionChild = {
   // Map<nsIContentFrameMessageManager, ContentGlobal>
   contentGlobals: new Map(),
 
   // Map<innerWindowId, ExtensionContext>
   extensionContexts: new Map(),
 
   initOnce() {
     // This initializes the default message handler for messages targeted at
@@ -441,12 +1057,20 @@ this.ExtensionChild = {
     }
   },
 };
 
 // TODO(robwu): Change this condition when addons move to a separate process.
 if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
   Object.keys(ExtensionChild).forEach(function(key) {
     if (typeof ExtensionChild[key] == "function") {
+      // :/
       ExtensionChild[key] = () => {};
     }
   });
 }
+
+Object.assign(ExtensionChild, {
+  ChildAPIManager,
+  Messenger,
+  Port,
+});
+
copy from toolkit/components/extensions/ExtensionUtils.jsm
copy to toolkit/components/extensions/ExtensionCommon.jsm
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -1,193 +1,45 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-this.EXPORTED_SYMBOLS = ["ExtensionUtils"];
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-const Cr = Components.results;
+/* exported ExtensionCommon */
 
-const INTEGER = /^[1-9]\d*$/;
+this.EXPORTED_SYMBOLS = ["ExtensionCommon"];
 
 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, "LanguageDetector",
-                                  "resource:///modules/translation/LanguageDetector.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Locale",
-                                  "resource://gre/modules/Locale.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
-                                  "resource://gre/modules/NetUtil.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
-                                  "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
-                                  "resource://gre/modules/PromiseUtils.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");
 
-function getConsole() {
-  return new ConsoleAPI({
-    maxLogLevelPref: "extensions.webextensions.log.level",
-    prefix: "WebExtensions",
-  });
-}
+var {
+  EventEmitter,
+  ExtensionError,
+  SpreadArgs,
+  getConsole,
+  getInnerWindowID,
+  runSafeSync,
+  runSafeSyncWithoutClone,
+  instanceOf,
+} = ExtensionUtils;
 
 XPCOMUtils.defineLazyGetter(this, "console", getConsole);
 
-/**
- * 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) {
-  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);
-  }
-}
-
-// Run a function and report exceptions.
-function runSafeWithoutClone(f, ...args) {
-  if (typeof(f) != "function") {
-    dump(`Extension error: expected function\n${filterStack(Error())}`);
-    return;
-  }
-
-  Promise.resolve().then(() => {
-    runSafeSyncWithoutClone(f, ...args);
-  });
-}
-
-// Run a function, cloning arguments into context.cloneScope, and
-// report exceptions. |f| is expected to be in context.cloneScope.
-function runSafeSync(context, f, ...args) {
-  if (context.unloaded) {
-    Cu.reportError("runSafeSync called after context unloaded");
-    return;
-  }
-
-  try {
-    args = Cu.cloneInto(args, context.cloneScope);
-  } catch (e) {
-    Cu.reportError(e);
-    dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
-  }
-  return runSafeSyncWithoutClone(f, ...args);
-}
-
-// Run a function, cloning arguments into context.cloneScope, and
-// report exceptions. |f| is expected to be in context.cloneScope.
-function runSafe(context, f, ...args) {
-  try {
-    args = Cu.cloneInto(args, context.cloneScope);
-  } catch (e) {
-    Cu.reportError(e);
-    dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`);
-  }
-  if (context.unloaded) {
-    dump(`runSafe failure: context is already unloaded ${filterStack(new Error())}\n`);
-    return undefined;
-  }
-  return runSafeWithoutClone(f, ...args);
-}
-
-function getInnerWindowID(window) {
-  return window.QueryInterface(Ci.nsIInterfaceRequestor)
-    .getInterface(Ci.nsIDOMWindowUtils)
-    .currentInnerWindowID;
-}
-
-// 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;
-  }
-
-  get(key) {
-    if (!this.has(key)) {
-      this.set(key, this.defaultConstructor());
-    }
-    return super.get(key);
-  }
-}
-
-class DefaultMap extends Map {
-  constructor(defaultConstructor, init) {
-    super(init);
-    this.defaultConstructor = defaultConstructor;
-  }
-
-  get(key) {
-    if (!this.has(key)) {
-      this.set(key, this.defaultConstructor(key));
-    }
-    return super.get(key);
-  }
-}
-
-class SpreadArgs extends Array {
-  constructor(args) {
-    super();
-    this.push(...args);
-  }
-}
-
 let gContextId = 0;
 
 class BaseContext {
   constructor(envType, extension) {
     this.envType = envType;
     this.onClose = new Set();
     this.checkedLastError = false;
     this._lastError = null;
@@ -512,1112 +364,16 @@ class BaseContext {
   /**
    * A simple proxy for unload(), for use with callOnClose().
    */
   close() {
     this.unload();
   }
 }
 
-// Manages icon details for toolbar buttons in the |pageAction| and
-// |browserAction| APIs.
-let IconDetails = {
-  // 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) {
-    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;
-        }
-
-        ctx.drawImage(this, 0, 0, this.width, this.height, dx, dy, dWidth, dHeight);
-        resolve(canvas.toDataURL("image/png"));
-      };
-      image.onerror = reject;
-      image.src = imageURL;
-    });
-  },
-};
-
-const LISTENERS = Symbol("listeners");
-
-class EventEmitter {
-  constructor() {
-    this[LISTENERS] = new Map();
-  }
-
-  /**
-   * 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) {
-    if (!this[LISTENERS].has(event)) {
-      this[LISTENERS].set(event, new Set());
-    }
-
-    this[LISTENERS].get(event).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) {
-    if (this[LISTENERS].has(event)) {
-      let set = this[LISTENERS].get(event);
-
-      set.delete(listener);
-      if (!set.size) {
-        this[LISTENERS].delete(event);
-      }
-    }
-  }
-
-  /**
-   * Triggers all listeners for the given event, and returns a promise
-   * which resolves when all listeners have been called, and any
-   * promises they have returned have likewise resolved.
-   *
-   * @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) || new Set();
-
-    let promises = Array.from(listeners, listener => {
-      return runSafeSyncWithoutClone(listener, event, ...args);
-    });
-
-    return Promise.all(promises);
-  }
-}
-
-function LocaleData(data) {
-  this.defaultLocale = data.defaultLocale;
-  this.selectedLocale = data.selectedLocale;
-  this.locales = data.locales || new Map();
-  this.warnedMissingKeys = new Set();
-
-  // Map(locale-name -> Map(message-key -> localized-string))
-  //
-  // Contains a key for each loaded locale, each of which is a
-  // Map of message keys to their localized strings.
-  this.messages = data.messages || new Map();
-
-  if (data.builtinMessages) {
-    this.messages.set(this.BUILTIN, data.builtinMessages);
-  }
-}
-
-
-LocaleData.prototype = {
-  // Representation of the object to send to content processes. This
-  // should include anything the content process might need.
-  serialize() {
-    return {
-      defaultLocale: this.defaultLocale,
-      selectedLocale: this.selectedLocale,
-      messages: this.messages,
-      locales: this.locales,
-    };
-  },
-
-  BUILTIN: "@@BUILTIN_MESSAGES",
-
-  has(locale) {
-    return this.messages.has(locale);
-  },
-
-  // https://developer.chrome.com/extensions/i18n
-  localizeMessage(message, substitutions = [], options = {}) {
-    let defaultOptions = {
-      locale: this.selectedLocale,
-      defaultValue: "",
-      cloneScope: null,
-    };
-
-    options = Object.assign(defaultOptions, options);
-
-    let locales = new Set([this.BUILTIN, options.locale, this.defaultLocale]
-                          .filter(locale => this.messages.has(locale)));
-
-    // Message names are case-insensitive, so normalize them to lower-case.
-    message = message.toLowerCase();
-    for (let locale of locales) {
-      let messages = this.messages.get(locale);
-      if (messages.has(message)) {
-        let str = messages.get(message);
-
-        if (!Array.isArray(substitutions)) {
-          substitutions = [substitutions];
-        }
-
-        let replacer = (matched, index, dollarSigns) => {
-          if (index) {
-            // This is not quite Chrome-compatible. Chrome consumes any number
-            // of digits following the $, but only accepts 9 substitutions. We
-            // accept any number of substitutions.
-            index = parseInt(index, 10) - 1;
-            return index in substitutions ? substitutions[index] : "";
-          }
-          // For any series of contiguous `$`s, the first is dropped, and
-          // the rest remain in the output string.
-          return dollarSigns;
-        };
-        return str.replace(/\$(?:([1-9]\d*)|(\$+))/g, replacer);
-      }
-    }
-
-    // Check for certain pre-defined messages.
-    if (message == "@@ui_locale") {
-      return this.uiLocale;
-    } else if (message.startsWith("@@bidi_")) {
-      let registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry);
-      let rtl = registry.isLocaleRTL("global");
-
-      if (message == "@@bidi_dir") {
-        return rtl ? "rtl" : "ltr";
-      } else if (message == "@@bidi_reversed_dir") {
-        return rtl ? "ltr" : "rtl";
-      } else if (message == "@@bidi_start_edge") {
-        return rtl ? "right" : "left";
-      } else if (message == "@@bidi_end_edge") {
-        return rtl ? "left" : "right";
-      }
-    }
-
-    if (!this.warnedMissingKeys.has(message)) {
-      let error = `Unknown localization message ${message}`;
-      if (options.cloneScope) {
-        error = new options.cloneScope.Error(error);
-      }
-      Cu.reportError(error);
-      this.warnedMissingKeys.add(message);
-    }
-    return options.defaultValue;
-  },
-
-  // Localize a string, replacing all |__MSG_(.*)__| tokens with the
-  // matching string from the current locale, as determined by
-  // |this.selectedLocale|.
-  //
-  // This may not be called before calling either |initLocale| or
-  // |initAllLocales|.
-  localize(str, locale = this.selectedLocale) {
-    if (!str) {
-      return str;
-    }
-
-    return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => {
-      return this.localizeMessage(message, [], {locale, defaultValue: matched});
-    });
-  },
-
-  // Validates the contents of a locale JSON file, normalizes the
-  // messages into a Map of message key -> localized string pairs.
-  addLocale(locale, messages, extension) {
-    let result = new Map();
-
-    // Chrome does not document the semantics of its localization
-    // system very well. It handles replacements by pre-processing
-    // messages, replacing |$[a-zA-Z0-9@_]+$| tokens with the value of their
-    // replacements. Later, it processes the resulting string for
-    // |$[0-9]| replacements.
-    //
-    // Again, it does not document this, but it accepts any number
-    // of sequential |$|s, and replaces them with that number minus
-    // 1. It also accepts |$| followed by any number of sequential
-    // digits, but refuses to process a localized string which
-    // provides more than 9 substitutions.
-    if (!instanceOf(messages, "Object")) {
-      extension.packagingError(`Invalid locale data for ${locale}`);
-      return result;
-    }
-
-    for (let key of Object.keys(messages)) {
-      let msg = messages[key];
-
-      if (!instanceOf(msg, "Object") || typeof(msg.message) != "string") {
-        extension.packagingError(`Invalid locale message data for ${locale}, message ${JSON.stringify(key)}`);
-        continue;
-      }
-
-      // Substitutions are case-insensitive, so normalize all of their names
-      // to lower-case.
-      let placeholders = new Map();
-      if (instanceOf(msg.placeholders, "Object")) {
-        for (let key of Object.keys(msg.placeholders)) {
-          placeholders.set(key.toLowerCase(), msg.placeholders[key]);
-        }
-      }
-
-      let replacer = (match, name) => {
-        let replacement = placeholders.get(name.toLowerCase());
-        if (instanceOf(replacement, "Object") && "content" in replacement) {
-          return replacement.content;
-        }
-        return "";
-      };
-
-      let value = msg.message.replace(/\$([A-Za-z0-9@_]+)\$/g, replacer);
-
-      // Message names are also case-insensitive, so normalize them to lower-case.
-      result.set(key.toLowerCase(), value);
-    }
-
-    this.messages.set(locale, result);
-    return result;
-  },
-
-  get acceptLanguages() {
-    let result = Preferences.get("intl.accept_languages", "", Ci.nsIPrefLocalizedString);
-    return result.split(/\s*,\s*/g);
-  },
-
-
-  get uiLocale() {
-    // Return the browser locale, but convert it to a Chrome-style
-    // locale code.
-    return Locale.getLocale().replace(/-/g, "_");
-  },
-};
-
-// This is a generic class for managing event listeners. Example usage:
-//
-// new EventManager(context, "api.subAPI", fire => {
-//   let listener = (...) => {
-//     // Fire any listeners registered with addListener.
-//     fire(arg1, arg2);
-//   };
-//   // Register the listener.
-//   SomehowRegisterListener(listener);
-//   return () => {
-//     // Return a way to unregister the listener.
-//     SomehowUnregisterListener(listener);
-//   };
-// }).api()
-//
-// The result is an object with addListener, removeListener, and
-// hasListener methods. |context| is an add-on scope (either an
-// ExtensionContext in the chrome process or ExtensionContext in a
-// content process). |name| is for debugging. |register| is a function
-// to register the listener. |register| is only called once, even if
-// multiple listeners are registered. |register| should return an
-// unregister function that will unregister the listener.
-function EventManager(context, name, register) {
-  this.context = context;
-  this.name = name;
-  this.register = register;
-  this.unregister = null;
-  this.callbacks = new Set();
-}
-
-EventManager.prototype = {
-  addListener(callback) {
-    if (typeof(callback) != "function") {
-      dump(`Expected function\n${Error().stack}`);
-      return;
-    }
-    if (this.context.unloaded) {
-      dump(`Cannot add listener to ${this.name} after context unloaded`);
-      return;
-    }
-
-    if (!this.callbacks.size) {
-      this.context.callOnClose(this);
-
-      let fireFunc = this.fire.bind(this);
-      let fireWithoutClone = this.fireWithoutClone.bind(this);
-      fireFunc.withoutClone = fireWithoutClone;
-      this.unregister = this.register(fireFunc);
-    }
-    this.callbacks.add(callback);
-  },
-
-  removeListener(callback) {
-    if (!this.callbacks.size) {
-      return;
-    }
-
-    this.callbacks.delete(callback);
-    if (this.callbacks.size == 0) {
-      this.unregister();
-      this.unregister = null;
-
-      this.context.forgetOnClose(this);
-    }
-  },
-
-  hasListener(callback) {
-    return this.callbacks.has(callback);
-  },
-
-  fire(...args) {
-    this._fireCommon("runSafe", args);
-  },
-
-  fireWithoutClone(...args) {
-    this._fireCommon("runSafeWithoutClone", args);
-  },
-
-  _fireCommon(runSafeMethod, args) {
-    for (let callback of this.callbacks) {
-      Promise.resolve(callback).then(callback => {
-        if (this.context.unloaded) {
-          dump(`${this.name} event fired after context unloaded.\n`);
-        } else if (!this.context.active) {
-          dump(`${this.name} event fired while context is inactive.\n`);
-        } else if (this.callbacks.has(callback)) {
-          this.context[runSafeMethod](callback, ...args);
-        }
-      });
-    }
-  },
-
-  close() {
-    if (this.callbacks.size) {
-      this.unregister();
-    }
-    this.callbacks.clear();
-    this.register = null;
-    this.unregister = null;
-  },
-
-  api() {
-    return {
-      addListener: callback => this.addListener(callback),
-      removeListener: callback => this.removeListener(callback),
-      hasListener: callback => this.hasListener(callback),
-    };
-  },
-};
-
-// Similar to EventManager, but it doesn't try to consolidate event
-// notifications. Each addListener call causes us to register once. It
-// allows extra arguments to be passed to addListener.
-function SingletonEventManager(context, name, register) {
-  this.context = context;
-  this.name = name;
-  this.register = register;
-  this.unregister = new Map();
-}
-
-SingletonEventManager.prototype = {
-  addListener(callback, ...args) {
-    let wrappedCallback = (...args) => {
-      if (this.context.unloaded) {
-        dump(`${this.name} event fired after context unloaded.\n`);
-      } else if (this.unregister.has(callback)) {
-        return callback(...args);
-      }
-    };
-
-    let unregister = this.register(wrappedCallback, ...args);
-    this.unregister.set(callback, unregister);
-    this.context.callOnClose(this);
-  },
-
-  removeListener(callback) {
-    if (!this.unregister.has(callback)) {
-      return;
-    }
-
-    let unregister = this.unregister.get(callback);
-    this.unregister.delete(callback);
-    unregister();
-  },
-
-  hasListener(callback) {
-    return this.unregister.has(callback);
-  },
-
-  close() {
-    for (let unregister of this.unregister.values()) {
-      unregister();
-    }
-  },
-
-  api() {
-    return {
-      addListener: (...args) => this.addListener(...args),
-      removeListener: (...args) => this.removeListener(...args),
-      hasListener: (...args) => this.hasListener(...args),
-    };
-  },
-};
-
-// 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);
-    }
-  }
-}
-
-/**
- * Returns a Promise which resolves when the given document's DOM has
- * fully loaded.
- *
- * @param {Document} doc The document to await the load of.
- * @returns {Promise<Document>}
- */
-function promiseDocumentReady(doc) {
-  if (doc.readyState == "interactive" || doc.readyState == "complete") {
-    return Promise.resolve(doc);
-  }
-
-  return new Promise(resolve => {
-    doc.addEventListener("DOMContentLoaded", function onReady(event) {
-      if (event.target === event.currentTarget) {
-        doc.removeEventListener("DOMContentLoaded", onReady, true);
-        resolve(doc);
-      }
-    }, true);
-  });
-}
-
-/**
- * Returns a Promise which resolves when the given document is fully
- * loaded.
- *
- * @param {Document} doc The document to await the load of.
- * @returns {Promise<Document>}
- */
-function promiseDocumentLoaded(doc) {
-  if (doc.readyState == "complete") {
-    return Promise.resolve(doc);
-  }
-
-  return new Promise(resolve => {
-    doc.defaultView.addEventListener("load", function onReady(event) {
-      doc.defaultView.removeEventListener("load", onReady);
-      resolve(doc);
-    });
-  });
-}
-
-/**
- * Returns a Promise which resolves when the given event is dispatched to the
- * given element.
- *
- * @param {Element} element
- *        The element on which to listen.
- * @param {string} eventName
- *        The event to listen for.
- * @param {boolean} [useCapture = true]
- *        If true, listen for the even in the capturing rather than
- *        bubbling phase.
- * @param {Event} [test]
- *        An optional test function which, when called with the
- *        observer's subject and data, should return true if this is the
- *        expected event, false otherwise.
- * @returns {Promise<Event>}
- */
-function promiseEvent(element, eventName, useCapture = true, test = event => true) {
-  return new Promise(resolve => {
-    function listener(event) {
-      if (test(event)) {
-        element.removeEventListener(eventName, listener, useCapture);
-        resolve(event);
-      }
-    }
-    element.addEventListener(eventName, listener, useCapture);
-  });
-}
-
-/**
- * Returns a Promise which resolves the given observer topic has been
- * observed.
- *
- * @param {string} topic
- *        The topic to observe.
- * @param {function(nsISupports, string)} [test]
- *        An optional test function which, when called with the
- *        observer's subject and data, should return true if this is the
- *        expected notification, false otherwise.
- * @returns {Promise<object>}
- */
-function promiseObserved(topic, test = () => true) {
-  return new Promise(resolve => {
-    let observer = (subject, topic, data) => {
-      if (test(subject, data)) {
-        Services.obs.removeObserver(observer, topic);
-        resolve({subject, data});
-      }
-    };
-    Services.obs.addObserver(observer, topic, false);
-  });
-}
-
-
-/*
- * Messaging primitives.
- */
-
-let gNextPortId = 1;
-
-/**
- * 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.
- * @param {string} id An ID that uniquely identifies this port's channel.
- * @param {object} sender The `port.sender` property.
- * @param {object} recipient The recipient of messages sent from this port.
- */
-function Port(context, senderMM, receiverMMs, name, id, sender, recipient) {
-  this.context = context;
-  this.senderMM = senderMM;
-  this.receiverMMs = receiverMMs;
-  this.name = name;
-  this.id = id;
-  this.sender = sender;
-  this.recipient = recipient;
-  this.disconnected = false;
-  this.disconnectListeners = new Set();
-  this.unregisterMessageFuncs = new Set();
-
-  // Common options for onMessage and onDisconnect.
-  this.handlerBase = {
-    messageFilterStrict: {portId: id},
-    filterMessage: (sender, recipient) => {
-      if (!sender.contextId) {
-        Cu.reportError("Missing sender.contextId in message to Port");
-        return false;
-      }
-      return sender.contextId !== this.context.contextId;
-    },
-  };
-
-  this.disconnectHandler = Object.assign({
-    receiveMessage: ({data}) => this.disconnectByOtherEnd(data),
-  }, this.handlerBase);
-  MessageChannel.addListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
-  this.context.callOnClose(this);
-}
-
-Port.prototype = {
-  api() {
-    let portObj = Cu.createObjectIn(this.context.cloneScope);
-
-    let portError = null;
-    let publicAPI = {
-      name: this.name,
-      disconnect: () => {
-        this.disconnect();
-      },
-      postMessage: json => {
-        this.postMessage(json);
-      },
-      onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => {
-        return this.registerOnDisconnect(error => {
-          portError = error && this.context.normalizeError(error);
-          fire.withoutClone(portObj);
-        });
-      }).api(),
-      onMessage: new EventManager(this.context, "Port.onMessage", fire => {
-        return this.registerOnMessage(msg => {
-          msg = Cu.cloneInto(msg, this.context.cloneScope);
-          fire.withoutClone(msg, portObj);
-        });
-      }).api(),
-
-      get error() {
-        return portError;
-      },
-    };
-
-    if (this.sender) {
-      publicAPI.sender = this.sender;
-    }
-
-    injectAPI(publicAPI, portObj);
-    return portObj;
-  },
-
-  postMessage(json) {
-    if (this.disconnected) {
-      throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port");
-    }
-
-    this._sendMessage("Extension:Port:PostMessage", json);
-  },
-
-  /**
-   * Register a callback that is called when the port is disconnected by the
-   * *other* end. The callback is automatically unregistered when the port or
-   * context is closed.
-   *
-   * @param {function} callback Called when the other end disconnects the port.
-   *     If the disconnect is caused by an error, the first parameter is an
-   *     object with a "message" string property that describes the cause.
-   * @returns {function} Function to unregister the listener.
-   */
-  registerOnDisconnect(callback) {
-    let listener = error => {
-      if (this.context.active && !this.disconnected) {
-        callback(error);
-      }
-    };
-    this.disconnectListeners.add(listener);
-    return () => {
-      this.disconnectListeners.delete(listener);
-    };
-  },
-
-  /**
-   * Register a callback that is called when a message is received. The callback
-   * is automatically unregistered when the port or context is closed.
-   *
-   * @param {function} callback Called when a message is received.
-   * @returns {function} Function to unregister the listener.
-   */
-  registerOnMessage(callback) {
-    let handler = Object.assign({
-      receiveMessage: ({data}) => {
-        if (this.context.active && !this.disconnected) {
-          callback(data);
-        }
-      },
-    }, this.handlerBase);
-
-    let unregister = () => {
-      this.unregisterMessageFuncs.delete(unregister);
-      MessageChannel.removeListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
-    };
-    MessageChannel.addListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
-    this.unregisterMessageFuncs.add(unregister);
-    return unregister;
-  },
-
-  _sendMessage(message, data) {
-    let options = {
-      recipient: Object.assign({}, this.recipient, {portId: this.id}),
-      responseType: MessageChannel.RESPONSE_NONE,
-    };
-
-    return this.context.sendMessage(this.senderMM, message, data, options);
-  },
-
-  handleDisconnection() {
-    MessageChannel.removeListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
-    for (let unregister of this.unregisterMessageFuncs) {
-      unregister();
-    }
-    this.context.forgetOnClose(this);
-    this.disconnected = true;
-  },
-
-  /**
-   * Disconnect the port from the other end (which may not even exist).
-   *
-   * @param {Error|{message: string}} [error] The reason for disconnecting,
-   *     if it is an abnormal disconnect.
-   */
-  disconnectByOtherEnd(error = null) {
-    if (this.disconnected) {
-      return;
-    }
-
-    for (let listener of this.disconnectListeners) {
-      listener(error);
-    }
-
-    this.handleDisconnection();
-  },
-
-  /**
-   * Disconnect the port from this end.
-   *
-   * @param {Error|{message: string}} [error] The reason for disconnecting,
-   *     if it is an abnormal disconnect.
-   */
-  disconnect(error = null) {
-    if (this.disconnected) {
-      // disconnect() may be called without side effects even after the port is
-      // closed - https://developer.chrome.com/extensions/runtime#type-Port
-      return;
-    }
-    this.handleDisconnection();
-    if (error) {
-      error = {message: this.context.normalizeError(error).message};
-    }
-    this._sendMessage("Extension:Port:Disconnect", error);
-  },
-
-  close() {
-    this.disconnect();
-  },
-};
-
-function getMessageManager(target) {
-  if (target instanceof Ci.nsIFrameLoaderOwner) {
-    return target.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
-  }
-  return target.QueryInterface(Ci.nsIMessageSender);
-}
-
-/**
- * Each extension context gets its own Messenger object. It handles the
- * basics of sendMessage, onMessage, connect and onConnect.
- *
- * @param {BaseContext} context The context to which this Messenger is tied.
- * @param {Array<nsIMessageListenerManager>} messageManagers
- *     The message managers used to receive messages (e.g. onMessage/onConnect
- *     requests).
- * @param {object} sender Describes this sender to the recipient. This object
- *     is extended further by BaseContext's sendMessage method and appears as
- *     the `sender` object to `onConnect` and `onMessage`.
- *     Do not set the `extensionId`, `contextId` or `tab` properties. The former
- *     two are added by BaseContext's sendMessage, while `sender.tab` is set by
- *     the ProxyMessenger in the main process.
- * @param {object} filter A recipient filter to apply to incoming messages from
- *     the broker. Messages are only handled by this Messenger if all key-value
- *     pairs match the `recipient` as specified by the sender of the message.
- *     In other words, this filter defines the required fields of `recipient`.
- * @param {object} [optionalFilter] An additional filter to apply to incoming
- *     messages. Unlike `filter`, the keys from `optionalFilter` are allowed to
- *     be omitted from `recipient`. Only keys that are present in both
- *     `optionalFilter` and `recipient` are applied to filter incoming messages.
- */
-function Messenger(context, messageManagers, sender, filter, optionalFilter) {
-  this.context = context;
-  this.messageManagers = messageManagers;
-  this.sender = sender;
-  this.filter = filter;
-  this.optionalFilter = optionalFilter;
-}
-
-Messenger.prototype = {
-  _sendMessage(messageManager, message, data, recipient) {
-    let options = {
-      recipient,
-      sender: this.sender,
-      responseType: MessageChannel.RESPONSE_FIRST,
-    };
-
-    return this.context.sendMessage(messageManager, message, data, options);
-  },
-
-  sendMessage(messageManager, msg, recipient, responseCallback) {
-    let promise = this._sendMessage(messageManager, "Extension:Message", msg, recipient)
-      .catch(error => {
-        if (error.result == MessageChannel.RESULT_NO_HANDLER) {
-          return Promise.reject({message: "Could not establish connection. Receiving end does not exist."});
-        } else if (error.result != MessageChannel.RESULT_NO_RESPONSE) {
-          return Promise.reject({message: error.message});
-        }
-      });
-
-    return this.context.wrapPromise(promise, responseCallback);
-  },
-
-  onMessage(name) {
-    return new SingletonEventManager(this.context, name, callback => {
-      let listener = {
-        messageFilterPermissive: this.optionalFilter,
-        messageFilterStrict: this.filter,
-
-        filterMessage: (sender, recipient) => {
-          // Ignore the message if it was sent by this Messenger.
-          return sender.contextId !== this.context.contextId;
-        },
-
-        receiveMessage: ({target, data: message, sender, recipient}) => {
-          if (!this.context.active) {
-            return;
-          }
-
-          let sendResponse;
-          let response = undefined;
-          let promise = new Promise(resolve => {
-            sendResponse = value => {
-              resolve(value);
-              response = promise;
-            };
-          });
-
-          message = Cu.cloneInto(message, this.context.cloneScope);
-          sender = Cu.cloneInto(sender, this.context.cloneScope);
-          sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
-
-          // Note: We intentionally do not use runSafe here so that any
-          // errors are propagated to the message sender.
-          let result = callback(message, sender, sendResponse);
-          if (result instanceof this.context.cloneScope.Promise) {
-            return result;
-          } else if (result === true) {
-            return promise;
-          }
-          return response;
-        },
-      };
-
-      MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
-      return () => {
-        MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
-      };
-    }).api();
-  },
-
-  connectGetRawPort(messageManager, name, recipient) {
-    let portId = `${gNextPortId++}-${Services.appinfo.uniqueProcessID}`;
-    let port = new Port(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
-    let msg = {name, portId};
-    this._sendMessage(messageManager, "Extension:Connect", msg, recipient)
-      .catch(e => {
-        if (e.result === MessageChannel.RESULT_NO_HANDLER) {
-          e = {message: "Could not establish connection. Receiving end does not exist."};
-        } else if (e.result === MessageChannel.RESULT_DISCONNECTED) {
-          e = null;
-        }
-        port.disconnectByOtherEnd(e);
-      });
-    return port;
-  },
-
-  connect(messageManager, name, recipient) {
-    let port = this.connectGetRawPort(messageManager, name, recipient);
-    return port.api();
-  },
-
-  onConnect(name) {
-    return new SingletonEventManager(this.context, name, callback => {
-      let listener = {
-        messageFilterPermissive: this.optionalFilter,
-        messageFilterStrict: this.filter,
-
-        filterMessage: (sender, recipient) => {
-          // Ignore the port if it was created by this Messenger.
-          return sender.contextId !== this.context.contextId;
-        },
-
-        receiveMessage: ({target, data: message, sender}) => {
-          let {name, portId} = message;
-          let mm = getMessageManager(target);
-          let recipient = Object.assign({}, sender);
-          if (recipient.tab) {
-            recipient.tabId = recipient.tab.id;
-            delete recipient.tab;
-          }
-          let port = new Port(this.context, mm, this.messageManagers, name, portId, sender, recipient);
-          this.context.runSafeWithoutClone(callback, port.api());
-          return true;
-        },
-      };
-
-      MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
-      return () => {
-        MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
-      };
-    }).api();
-  },
-};
-
-function flushJarCache(jarFile) {
-  Services.obs.notifyObservers(jarFile, "flush-cache-entry", null);
-}
-
-const PlatformInfo = 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;
-  })(),
-});
-
-function detectLanguage(text) {
-  return LanguageDetector.detectLanguage(text).then(result => ({
-    isReliable: result.confident,
-    languages: result.languages.map(lang => {
-      return {
-        language: lang.languageCode,
-        percentage: lang.percent,
-      };
-    }),
-  }));
-}
-
 /**
  * An object that runs the implementation of a schema API. Instantiations of
  * this interfaces are used by Schemas.jsm.
  *
  * @interface
  */
 class SchemaAPIInterface {
   /**
@@ -1767,261 +523,16 @@ class LocalAPIImplementation extends Sch
     return this.pathObj[this.name].hasListener.call(null, listener);
   }
 
   removeListener(listener) {
     this.pathObj[this.name].removeListener.call(null, listener);
   }
 }
 
-let nextId = 1;
-
-/**
- * An object that runs an remote implementation of an API.
- */
-class ProxyAPIImplementation extends SchemaAPIInterface {
-  /**
-   * @param {string} namespace The full path to the namespace that contains the
-   *     `name` member. This may contain dots, e.g. "storage.local".
-   * @param {string} name The name of the method or property.
-   * @param {ChildAPIManager} childApiManager The owner of this implementation.
-   */
-  constructor(namespace, name, childApiManager) {
-    super();
-    this.path = `${namespace}.${name}`;
-    this.childApiManager = childApiManager;
-  }
-
-  callFunctionNoReturn(args) {
-    this.childApiManager.callParentFunctionNoReturn(this.path, args);
-  }
-
-  callAsyncFunction(args, callback) {
-    return this.childApiManager.callParentAsyncFunction(this.path, args, callback);
-  }
-
-  addListener(listener, args) {
-    let set = this.childApiManager.listeners.get(this.path);
-    if (!set) {
-      set = new Set();
-      this.childApiManager.listeners.set(this.path, set);
-    }
-
-    set.add(listener);
-
-    if (set.size == 1) {
-      args = args.slice(1);
-
-      this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", {
-        childId: this.childApiManager.id,
-        path: this.path,
-        args,
-      });
-    }
-  }
-
-  removeListener(listener) {
-    let set = this.childApiManager.listeners.get(this.path);
-    if (!set) {
-      return;
-    }
-    set.delete(listener);
-
-    if (set.size == 0) {
-      this.childApiManager.messageManager.sendAsyncMessage("API:RemoveListener", {
-        childId: this.childApiManager.id,
-        path: this.path,
-      });
-    }
-  }
-
-  hasListener(listener) {
-    let set = this.childApiManager.listeners.get(this.path);
-    return set ? set.has(listener) : false;
-  }
-}
-
-// We create one instance of this class for every extension context
-// that needs to use remote APIs. It uses the message manager to
-// communicate with the ParentAPIManager singleton in
-// Extension.jsm. It handles asynchronous function calls as well as
-// event listeners.
-class ChildAPIManager {
-  constructor(context, messageManager, localApis, contextData) {
-    this.context = context;
-    this.messageManager = messageManager;
-
-    // The root namespace of all locally implemented APIs. If an extension calls
-    // an API that does not exist in this object, then the implementation is
-    // delegated to the ParentAPIManager.
-    this.localApis = localApis;
-
-    let id = String(context.extension.id) + "." + String(context.contextId);
-    this.id = id;
-
-    let data = {childId: id, extensionId: context.extension.id, principal: context.principal};
-    Object.assign(data, contextData);
-
-    messageManager.addMessageListener("API:RunListener", this);
-    messageManager.addMessageListener("API:CallResult", this);
-
-    // Map[path -> Set[listener]]
-    // path is, e.g., "runtime.onMessage".
-    this.listeners = new Map();
-
-    // Map[callId -> Deferred]
-    this.callPromises = new Map();
-
-    this.createProxyContextInConstructor(data);
-  }
-
-  createProxyContextInConstructor(data) {
-    this.messageManager.sendAsyncMessage("API:CreateProxyContext", data);
-  }
-
-  receiveMessage({name, data}) {
-    if (data.childId != this.id) {
-      return;
-    }
-
-    switch (name) {
-      case "API:RunListener":
-        let listeners = this.listeners.get(data.path);
-        for (let callback of listeners) {
-          runSafe(this.context, callback, ...data.args);
-        }
-        break;
-
-      case "API:CallResult":
-        let deferred = this.callPromises.get(data.callId);
-        if ("error" in data) {
-          deferred.reject(data.error);
-        } else {
-          deferred.resolve(new SpreadArgs(data.result));
-        }
-        this.callPromises.delete(data.callId);
-        break;
-    }
-  }
-
-  /**
-   * Call a function in the parent process and ignores its return value.
-   *
-   * @param {string} path The full name of the method, e.g. "tabs.create".
-   * @param {Array} args The parameters for the function.
-   */
-  callParentFunctionNoReturn(path, args) {
-    this.messageManager.sendAsyncMessage("API:Call", {
-      childId: this.id,
-      path,
-      args,
-    });
-  }
-
-  /**
-   * Calls a function in the parent process and returns its result
-   * asynchronously.
-   *
-   * @param {string} path The full name of the method, e.g. "tabs.create".
-   * @param {Array} args The parameters for the function.
-   * @param {function(*)} [callback] The callback to be called when the function
-   *     completes.
-   * @returns {Promise|undefined} Must be void if `callback` is set, and a
-   *     promise otherwise. The promise is resolved when the function completes.
-   */
-  callParentAsyncFunction(path, args, callback) {
-    let callId = nextId++;
-    let deferred = PromiseUtils.defer();
-    this.callPromises.set(callId, deferred);
-
-    this.messageManager.sendAsyncMessage("API:Call", {
-      childId: this.id,
-      callId,
-      path,
-      args,
-    });
-
-    return this.context.wrapPromise(deferred.promise, callback);
-  }
-
-  /**
-   * Create a proxy for an event in the parent process. The returned event
-   * object shares its internal state with other instances. For instance, if
-   * `removeListener` is used on a listener that was added on another object
-   * through `addListener`, then the event is unregistered.
-   *
-   * @param {string} path The full name of the event, e.g. "tabs.onCreated".
-   * @returns {object} An object with the addListener, removeListener and
-   *   hasListener methods. See SchemaAPIInterface for documentation.
-   */
-  getParentEvent(path) {
-    let parsed = /^(.+)\.(on[A-Z][^.]+)$/.exec(path);
-    if (!parsed) {
-      throw new Error("getParentEvent: Invalid event name: " + path);
-    }
-    let [, namespace, name] = parsed;
-    let impl = new ProxyAPIImplementation(namespace, name, this);
-    return {
-      addListener: (listener, ...args) => impl.addListener(listener, args),
-      removeListener: (listener) => impl.removeListener(listener),
-      hasListener: (listener) => impl.hasListener(listener),
-    };
-  }
-
-  close() {
-    this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
-  }
-
-  get cloneScope() {
-    return this.context.cloneScope;
-  }
-
-  get principal() {
-    return this.context.principal;
-  }
-
-  shouldInject(namespace, name, allowedContexts) {
-    // Do not generate content script APIs, unless explicitly allowed.
-    if (this.context.envType === "content_child" &&
-        !allowedContexts.includes("content")) {
-      return false;
-    }
-    if (allowedContexts.includes("addon_parent_only")) {
-      return false;
-    }
-    return true;
-  }
-
-  getImplementation(namespace, name) {
-    let pathObj = this.localApis;
-    if (pathObj) {
-      for (let part of namespace.split(".")) {
-        pathObj = pathObj[part];
-        if (!pathObj) {
-          break;
-        }
-      }
-      if (pathObj && name in pathObj) {
-        return new LocalAPIImplementation(pathObj, name, this.context);
-      }
-    }
-
-    return this.getFallbackImplementation(namespace, name);
-  }
-
-  getFallbackImplementation(namespace, name) {
-    // No local API found, defer implementation to the parent.
-    return new ProxyAPIImplementation(namespace, name, this);
-  }
-
-  hasPermission(permission) {
-    return this.context.extension.hasPermission(permission);
-  }
-}
-
 /**
  * This object loads the ext-*.js scripts that define the extension API.
  *
  * This class instance is shared with the scripts that it loads, so that the
  * ext-*.js scripts and the instantiator can communicate with each other.
  */
 class SchemaAPIManager extends EventEmitter {
   /**
@@ -2154,110 +665,14 @@ class SchemaAPIManager extends EventEmit
       if (Schemas.checkPermissions(api.namespace, context.extension)) {
         api = api.getAPI(context);
         copy(obj, api);
       }
     }
   }
 }
 
-/**
- * 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);
-}
-
-const stylesheetMap = new DefaultMap(url => {
-  let uri = NetUtil.newURI(url);
-  return styleSheetService.preloadSheet(uri, styleSheetService.AGENT_SHEET);
-});
-
-/**
- * Defines a lazy getter for the given property on the given object. The
- * first time the property is accessed, the return value of the getter
- * is defined on the current `this` object with the given property name.
- * Importantly, this means that a lazy getter defined on an object
- * prototype will be invoked separately for each object instance that
- * it's accessed on.
- *
- * @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);
-    },
-  });
-}
-
-this.ExtensionUtils = {
-  defineLazyGetter,
-  detectLanguage,
-  extend,
-  flushJarCache,
-  getConsole,
-  getInnerWindowID,
-  ignoreEvent,
-  injectAPI,
-  instanceOf,
-  normalizeTime,
-  promiseDocumentLoaded,
-  promiseDocumentReady,
-  promiseEvent,
-  promiseObserved,
-  runSafe,
-  runSafeSync,
-  runSafeSyncWithoutClone,
-  runSafeWithoutClone,
-  stylesheetMap,
+const ExtensionCommon = {
   BaseContext,
-  DefaultMap,
-  DefaultWeakMap,
-  EventEmitter,
-  EventManager,
-  ExtensionError,
-  IconDetails,
   LocalAPIImplementation,
-  LocaleData,
-  Messenger,
-  Port,
-  PlatformInfo,
   SchemaAPIInterface,
-  SingletonEventManager,
-  SpreadArgs,
-  ChildAPIManager,
   SchemaAPIManager,
 };
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -40,30 +40,37 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
                                   "resource://gre/modules/WebNavigationFrames.jsm");
 
 Cu.import("resource://gre/modules/ExtensionChild.jsm");
-
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-var {
-  runSafeSyncWithoutClone,
+
+const {
+  LocaleData,
   defineLazyGetter,
-  BaseContext,
-  LocaleData,
-  Messenger,
   flushJarCache,
   getInnerWindowID,
   promiseDocumentReady,
-  ChildAPIManager,
+  runSafeSyncWithoutClone,
+} = ExtensionUtils;
+
+const {
+  BaseContext,
   SchemaAPIManager,
-} = ExtensionUtils;
+} = ExtensionCommon;
+
+const {
+  ChildAPIManager,
+  Messenger,
+} = ExtensionChild;
 
 XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
 
 const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
 
 function isWhenBeforeOrSame(when1, when2) {
   let table = {"document_start": 0,
                "document_end": 1,
copy from toolkit/components/extensions/Extension.jsm
copy to toolkit/components/extensions/ExtensionParent.jsm
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -1,126 +1,65 @@
 /* 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, utils: Cu, results: Cr} = Components;
 
-this.EXPORTED_SYMBOLS = ["Extension", "ExtensionData"];
-
-/* globals Extension ExtensionData */
+/* exported ExtensionParent */
 
-/*
- * This file is the main entry point for extensions. When an extension
- * loads, its bootstrap.js file creates a Extension instance
- * and calls .startup() on it. It calls .shutdown() when the extension
- * unloads. Extension manages any extension-specific state in
- * the chrome process.
- */
+this.EXPORTED_SYMBOLS = ["ExtensionParent"];
 
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-const Cr = Components.results;
-
-Cu.importGlobalProperties(["TextEncoder"]);
-
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionAPIs",
-                                  "resource://gre/modules/ExtensionAPI.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
-                                  "resource://gre/modules/ExtensionStorage.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
-                                  "resource://gre/modules/FileUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Locale",
-                                  "resource://gre/modules/Locale.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Log",
-                                  "resource://gre/modules/Log.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
-                                  "resource://gre/modules/MatchPattern.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
-                                  "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
                                   "resource://gre/modules/NativeMessaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "OS",
-                                  "resource://gre/modules/osfile.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
-                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
-                                  "resource://gre/modules/Preferences.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "require",
-                                  "resource://devtools/shared/Loader.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Task",
-                                  "resource://gre/modules/Task.jsm");
+
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
-Cu.import("resource://gre/modules/ExtensionContent.jsm");
-Cu.import("resource://gre/modules/ExtensionManagement.jsm");
+var {
+  BaseContext,
+  SchemaAPIManager,
+} = ExtensionCommon;
 
-XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
-                                   "@mozilla.org/uuid-generator;1",
-                                   "nsIUUIDGenerator");
+var {
+  SpreadArgs,
+  defineLazyGetter,
+  findPathInObject,
+} = ExtensionUtils;
 
 const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
 const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
 const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
 
 let schemaURLs = new Set();
 
 if (!AppConstants.RELEASE_OR_BETA) {
   schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
 }
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-var {
-  BaseContext,
-  EventEmitter,
-  LocaleData,
-  SchemaAPIManager,
-  SpreadArgs,
-  defineLazyGetter,
-  flushJarCache,
-  instanceOf,
-} = ExtensionUtils;
-
-XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
-
-const LOGGER_ID_BASE = "addons.webextension.";
-const UUID_MAP_PREF = "extensions.webextensions.uuids";
-const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
-const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
-
-const COMMENT_REGEXP = new RegExp(String.raw`
-    ^
-    (
-      (?:
-        [^"\n] |
-        " (?:[^"\\\n] | \\.)* "
-      )*?
-    )
-
-    //.*
-  `.replace(/\s+/g, ""), "gm");
-
-var GlobalManager;
-var ParentAPIManager;
+let GlobalManager;
+let ParentAPIManager;
+let ProxyMessenger;
 
 // This object loads the ext-*.js scripts that define the extension API.
-var Management = new class extends SchemaAPIManager {
+let apiManager = new class extends SchemaAPIManager {
   constructor() {
     super("main");
     this.initialized = null;
   }
 
   // Loads all the ext-*.js scripts currently registered.
   lazyInit() {
     if (this.initialized) {
@@ -153,17 +92,17 @@ var Management = new class extends Schem
       super.registerSchemaAPI(namespace, envType, getAPI);
     }
   }
 }();
 
 // Subscribes to messages related to the extension messaging API and forwards it
 // to the relevant message manager. The "sender" field for the `onMessage` and
 // `onConnect` events are updated if needed.
-let ProxyMessenger = {
+ProxyMessenger = {
   _initialized: false,
   init() {
     if (this._initialized) {
       return;
     }
     this._initialized = true;
 
     // TODO(robwu): When addons move to a separate process, we should use the
@@ -207,19 +146,19 @@ let ProxyMessenger = {
       return Promise.reject({
         result: MessageChannel.RESULT_NO_HANDLER,
         message: "No matching message handler for the given recipient.",
       });
     }
 
     if ((messageName == "Extension:Message" ||
          messageName == "Extension:Connect") &&
-        Management.global.tabGetSender) {
+        apiManager.global.tabGetSender) {
       // From ext-tabs.js, undefined on Android.
-      Management.global.tabGetSender(extension, target, sender);
+      apiManager.global.tabGetSender(extension, target, sender);
     }
     return MessageChannel.sendMessage(receiverMM, messageName, data, {
       sender,
       recipient,
       responseType,
     });
   },
 
@@ -229,31 +168,77 @@ let ProxyMessenger = {
    * @returns {object|null} The message manager matching the recipient if found.
    */
   _getMessageManagerForRecipient(recipient) {
     let {extensionId, tabId} = recipient;
     // tabs.sendMessage / tabs.connect
     if (tabId) {
       // `tabId` being set implies that the tabs API is supported, so we don't
       // need to check whether `TabManager` exists.
-      let tab = Management.global.TabManager.getTab(tabId, null, null);
+      let tab = apiManager.global.TabManager.getTab(tabId, null, null);
       return tab && tab.linkedBrowser.messageManager;
     }
 
     // runtime.sendMessage / runtime.connect
     if (extensionId) {
       // TODO(robwu): map the extensionId to the addon parent process's message
       // manager when they run in a separate process.
       return Services.ppmm.getChildAt(0);
     }
 
     return null;
   },
 };
 
+// Responsible for loading extension APIs into the right globals.
+GlobalManager = {
+  // Map[extension ID -> Extension]. Determines which extension is
+  // responsible for content under a particular extension ID.
+  extensionMap: new Map(),
+  initialized: false,
+
+  init(extension) {
+    if (this.extensionMap.size == 0) {
+      ProxyMessenger.init();
+      apiManager.on("extension-browser-inserted", this._onExtensionBrowser);
+      this.initialized = true;
+    }
+
+    this.extensionMap.set(extension.id, extension);
+  },
+
+  uninit(extension) {
+    this.extensionMap.delete(extension.id);
+
+    if (this.extensionMap.size == 0 && this.initialized) {
+      apiManager.off("extension-browser-inserted", this._onExtensionBrowser);
+      this.initialized = false;
+    }
+  },
+
+  _onExtensionBrowser(type, browser) {
+    browser.messageManager.loadFrameScript(`data:,
+      Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
+      ExtensionContent.init(this);
+      addEventListener("unload", function() {
+        ExtensionContent.uninit(this);
+      });
+    `, false);
+  },
+
+  getExtension(extensionId) {
+    return this.extensionMap.get(extensionId);
+  },
+
+  injectInObject(context, isChromeCompat, dest) {
+    apiManager.generateAPIs(context, dest);
+    SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
+  },
+};
+
 class BrowserDocshellFollower {
   /**
    * Follows the <browser> belonging to the `xulBrowser`'s current docshell.
    *
    * @param {XULElement} xulBrowser A <browser> tag.
    * @param {function} onBrowserChange Called when the <browser> changes.
    */
   constructor(xulBrowser, onBrowserChange) {
@@ -293,17 +278,17 @@ class ProxyContext extends BaseContext {
       xulBrowser, this.onBrowserChange.bind(this));
 
     Object.defineProperty(this, "principal", {
       value: principal, enumerable: true, configurable: true,
     });
 
     this.listenerProxies = new Map();
 
-    Management.emit("proxy-context-load", this);
+    apiManager.emit("proxy-context-load", this);
   }
 
   get cloneScope() {
     return this.sandbox;
   }
 
   onBrowserChange(browser) {
     // Make sure that the message manager is set. Otherwise the ProxyContext may
@@ -321,17 +306,17 @@ class ProxyContext extends BaseContext {
   }
 
   unload() {
     if (this.unloaded) {
       return;
     }
     this._docShellTracker.destroy();
     super.unload();
-    Management.emit("proxy-context-unload", this);
+    apiManager.emit("proxy-context-unload", this);
   }
 }
 
 defineLazyGetter(ProxyContext.prototype, "apiObj", function() {
   let obj = {};
   GlobalManager.injectInObject(this, false, obj);
   return obj;
 });
@@ -352,82 +337,63 @@ class ExtensionChildProxyContext extends
   }
 
   // The window that contains this context. This may change due to moving tabs.
   get xulWindow() {
     return this.xulBrowser.ownerGlobal;
   }
 
   get windowId() {
-    if (!Management.global.WindowManager || this.viewType == "background") {
+    if (!apiManager.global.WindowManager || this.viewType == "background") {
       return;
     }
     // viewType popup or tab:
-    return Management.global.WindowManager.getId(this.xulWindow);
+    return apiManager.global.WindowManager.getId(this.xulWindow);
   }
 
   get tabId() {
-    if (!Management.global.TabManager) {
+    if (!apiManager.global.TabManager) {
       return;  // Not yet supported on Android.
     }
     let {gBrowser} = this.xulBrowser.ownerGlobal;
     let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser);
-    return tab && Management.global.TabManager.getId(tab);
+    return tab && apiManager.global.TabManager.getId(tab);
   }
 
   onBrowserChange(browser) {
     super.onBrowserChange(browser);
     this.xulBrowser = browser;
   }
 
   shutdown() {
-    Management.emit("page-shutdown", this);
+    apiManager.emit("page-shutdown", this);
     super.shutdown();
   }
 }
 
-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;
-}
-
 ParentAPIManager = {
   proxyContexts: new Map(),
 
   init() {
     Services.obs.addObserver(this, "message-manager-close", false);
 
     Services.mm.addMessageListener("API:CreateProxyContext", this);
     Services.mm.addMessageListener("API:CloseProxyContext", this, true);
     Services.mm.addMessageListener("API:Call", this);
     Services.mm.addMessageListener("API:AddListener", this);
     Services.mm.addMessageListener("API:RemoveListener", this);
   },
 
-  // "message-manager-close" observer.
   observe(subject, topic, data) {
-    let mm = subject;
-    for (let [childId, context] of this.proxyContexts) {
-      if (context.currentMessageManager == mm) {
-        this.closeProxyContext(childId);
+    if (topic === "message-manager-close") {
+      let mm = subject;
+      for (let [childId, context] of this.proxyContexts) {
+        if (context.currentMessageManager === mm) {
+          this.closeProxyContext(childId);
+        }
       }
     }
   },
 
   shutdownExtension(extensionId) {
     for (let [childId, context] of this.proxyContexts) {
       if (context.extension.id == extensionId) {
         context.shutdown();
@@ -470,36 +436,35 @@ ParentAPIManager = {
     if (!extension) {
       throw new Error(`No WebExtension found with ID ${extensionId}`);
     }
 
     let context;
     if (envType == "addon_parent") {
       // Privileged addon contexts can only be loaded in documents whose main
       // frame is also the same addon.
-      if (principal.URI.prePath != extension.baseURI.prePath ||
+      if (principal.URI.prePath !== extension.baseURI.prePath ||
           !target.contentPrincipal.subsumes(principal)) {
         throw new Error(`Refused to create privileged WebExtension context for ${principal.URI.spec}`);
       }
       context = new ExtensionChildProxyContext(envType, extension, data, target);
     } else if (envType == "content_parent") {
       context = new ProxyContext(envType, extension, data, target, principal);
     } else {
       throw new Error(`Invalid WebExtension context envType: ${envType}`);
     }
     this.proxyContexts.set(childId, context);
   },
 
   closeProxyContext(childId) {
     let context = this.proxyContexts.get(childId);
-    if (!context) {
-      return;
+    if (context) {
+      context.unload();
+      this.proxyContexts.delete(childId);
     }
-    context.unload();
-    this.proxyContexts.delete(childId);
   },
 
   call(data, target) {
     let context = this.getContextById(data.childId);
     if (context.currentMessageManager !== target.messageManager) {
       Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
     }
 
@@ -575,1112 +540,14 @@ ParentAPIManager = {
       throw error;
     }
     return context;
   },
 };
 
 ParentAPIManager.init();
 
-// All moz-extension URIs use a machine-specific UUID rather than the
-// extension's own ID in the host component. This makes it more
-// difficult for web pages to detect whether a user has a given add-on
-// installed (by trying to load a moz-extension URI referring to a
-// web_accessible_resource from the extension). UUIDMap.get()
-// returns the UUID for a given add-on ID.
-var UUIDMap = {
-  _read() {
-    let pref = Preferences.get(UUID_MAP_PREF, "{}");
-    try {
-      return JSON.parse(pref);
-    } catch (e) {
-      Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`);
-      return {};
-    }
-  },
 
-  _write(map) {
-    Preferences.set(UUID_MAP_PREF, JSON.stringify(map));
-  },
-
-  get(id, create = true) {
-    let map = this._read();
-
-    if (id in map) {
-      return map[id];
-    }
-
-    let uuid = null;
-    if (create) {
-      uuid = uuidGen.generateUUID().number;
-      uuid = uuid.slice(1, -1); // Strip { and } off the UUID.
-
-      map[id] = uuid;
-      this._write(map);
-    }
-    return uuid;
-  },
-
-  remove(id) {
-    let map = this._read();
-    delete map[id];
-    this._write(map);
-  },
-};
-
-// This is the old interface that UUIDMap replaced, to be removed when
-// the references listed in bug 1291399 are updated.
-/* exported getExtensionUUID */
-function getExtensionUUID(id) {
-  return UUIDMap.get(id, true);
-}
-
-// For extensions that have called setUninstallURL(), send an event
-// so the browser can display the URL.
-var UninstallObserver = {
-  initialized: false,
-
-  init() {
-    if (!this.initialized) {
-      AddonManager.addAddonListener(this);
-      XPCOMUtils.defineLazyPreferenceGetter(this, "leaveStorage", LEAVE_STORAGE_PREF, false);
-      XPCOMUtils.defineLazyPreferenceGetter(this, "leaveUuid", LEAVE_UUID_PREF, false);
-      this.initialized = true;
-    }
-  },
-
-  onUninstalling(addon) {
-    let extension = GlobalManager.extensionMap.get(addon.id);
-    if (extension) {
-      // Let any other interested listeners respond
-      // (e.g., display the uninstall URL)
-      Management.emit("uninstall", extension);
-    }
-  },
-
-  onUninstalled(addon) {
-    let uuid = UUIDMap.get(addon.id, false);
-    if (!uuid) {
-      return;
-    }
-
-    if (!this.leaveStorage) {
-      // Clear browser.local.storage
-      ExtensionStorage.clear(addon.id);
-
-      // Clear any IndexedDB storage created by the extension
-      let baseURI = NetUtil.newURI(`moz-extension://${uuid}/`);
-      let principal = Services.scriptSecurityManager.createCodebasePrincipal(
-        baseURI, {addonId: addon.id}
-      );
-      Services.qms.clearStoragesForPrincipal(principal);
-
-      // Clear localStorage created by the extension
-      let attrs = JSON.stringify({addonId: addon.id});
-      Services.obs.notifyObservers(null, "clear-origin-attributes-data", attrs);
-    }
-
-    if (!this.leaveUuid) {
-      // Clear the entry in the UUID map
-      UUIDMap.remove(addon.id);
-    }
-  },
-};
-
-// Responsible for loading extension APIs into the right globals.
-GlobalManager = {
-  // Map[extension ID -> Extension]. Determines which extension is
-  // responsible for content under a particular extension ID.
-  extensionMap: new Map(),
-  initialized: false,
-
-  init(extension) {
-    if (this.extensionMap.size == 0) {
-      UninstallObserver.init();
-      ProxyMessenger.init();
-      Management.on("extension-browser-inserted", this._onExtensionBrowser);
-      this.initialized = true;
-    }
-
-    this.extensionMap.set(extension.id, extension);
-  },
-
-  uninit(extension) {
-    this.extensionMap.delete(extension.id);
-
-    if (this.extensionMap.size == 0 && this.initialized) {
-      Management.off("extension-browser-inserted", this._onExtensionBrowser);
-      this.initialized = false;
-    }
-  },
-
-  _onExtensionBrowser(type, browser) {
-    browser.messageManager.loadFrameScript(`data:,
-      Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
-      ExtensionContent.init(this);
-      addEventListener("unload", function() {
-        ExtensionContent.uninit(this);
-      });
-    `, false);
-  },
-
-  getExtension(extensionId) {
-    return this.extensionMap.get(extensionId);
-  },
-
-  injectInObject(context, isChromeCompat, dest) {
-    Management.generateAPIs(context, dest);
-    SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
-  },
-};
-
-// Represents the data contained in an extension, contained either
-// in a directory or a zip file, which may or may not be installed.
-// This class implements the functionality of the Extension class,
-// primarily related to manifest parsing and localization, which is
-// useful prior to extension installation or initialization.
-//
-// No functionality of this class is guaranteed to work before
-// |readManifest| has been called, and completed.
-this.ExtensionData = class {
-  constructor(rootURI) {
-    this.rootURI = rootURI;
-
-    this.manifest = null;
-    this.id = null;
-    this.uuid = null;
-    this.localeData = null;
-    this._promiseLocales = null;
-
-    this.apiNames = new Set();
-    this.dependencies = new Set();
-    this.permissions = new Set();
-
-    this.errors = [];
-  }
-
-  get builtinMessages() {
-    return null;
-  }
-
-  get logger() {
-    let id = this.id || "<unknown>";
-    return Log.repository.getLogger(LOGGER_ID_BASE + id);
-  }
-
-  // Report an error about the extension's manifest file.
-  manifestError(message) {
-    this.packagingError(`Reading manifest: ${message}`);
-  }
-
-  // Report an error about the extension's general packaging.
-  packagingError(message) {
-    this.errors.push(message);
-    this.logger.error(`Loading extension '${this.id}': ${message}`);
-  }
-
-  /**
-   * Returns the moz-extension: URL for the given path within this
-   * extension.
-   *
-   * Must not be called unless either the `id` or `uuid` property has
-   * already been set.
-   *
-   * @param {string} path The path portion of the URL.
-   * @returns {string}
-   */
-  getURL(path = "") {
-    if (!(this.id || this.uuid)) {
-      throw new Error("getURL may not be called before an `id` or `uuid` has been set");
-    }
-    if (!this.uuid) {
-      this.uuid = UUIDMap.get(this.id);
-    }
-    return `moz-extension://${this.uuid}/${path}`;
-  }
-
-  readDirectory(path) {
-    return Task.spawn(function* () {
-      if (this.rootURI instanceof Ci.nsIFileURL) {
-        let uri = NetUtil.newURI(this.rootURI.resolve("./" + path));
-        let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
-
-        let iter = new OS.File.DirectoryIterator(fullPath);
-        let results = [];
-
-        try {
-          yield iter.forEach(entry => {
-            results.push(entry);
-          });
-        } catch (e) {
-          // Always return a list, even if the directory does not exist (or is
-          // not a directory) for symmetry with the ZipReader behavior.
-        }
-        iter.close();
-
-        return results;
-      }
-
-      // FIXME: We need a way to do this without main thread IO.
-
-      let uri = this.rootURI.QueryInterface(Ci.nsIJARURI);
-
-      let file = uri.JARFile.QueryInterface(Ci.nsIFileURL).file;
-      let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
-      zipReader.open(file);
-      try {
-        let results = [];
-
-        // Normalize the directory path.
-        path = `${uri.JAREntry}/${path}`;
-        path = path.replace(/\/\/+/g, "/").replace(/^\/|\/$/g, "") + "/";
-
-        // Escape pattern metacharacters.
-        let pattern = path.replace(/[[\]()?*~|$\\]/g, "\\$&");
-
-        let enumerator = zipReader.findEntries(pattern + "*");
-        while (enumerator.hasMore()) {
-          let name = enumerator.getNext();
-          if (!name.startsWith(path)) {
-            throw new Error("Unexpected ZipReader entry");
-          }
-
-          // The enumerator returns the full path of all entries.
-          // Trim off the leading path, and filter out entries from
-          // subdirectories.
-          name = name.slice(path.length);
-          if (name && !/\/./.test(name)) {
-            results.push({
-              name: name.replace("/", ""),
-              isDir: name.endsWith("/"),
-            });
-          }
-        }
-
-        return results;
-      } finally {
-        zipReader.close();
-      }
-    }.bind(this));
-  }
-
-  readJSON(path) {
-    return new Promise((resolve, reject) => {
-      let uri = this.rootURI.resolve(`./${path}`);
-
-      NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
-        if (!Components.isSuccessCode(status)) {
-          // Convert status code to a string
-          let e = Components.Exception("", status);
-          reject(new Error(`Error while loading '${uri}' (${e.name})`));
-          return;
-        }
-        try {
-          let text = NetUtil.readInputStreamToString(inputStream, inputStream.available(),
-                                                     {charset: "utf-8"});
-
-          text = text.replace(COMMENT_REGEXP, "$1");
-
-          resolve(JSON.parse(text));
-        } catch (e) {
-          reject(e);
-        }
-      });
-    });
-  }
-
-  // Reads the extension's |manifest.json| file, and stores its
-  // parsed contents in |this.manifest|.
-  readManifest() {
-    return Promise.all([
-      this.readJSON("manifest.json"),
-      Management.lazyInit(),
-    ]).then(([manifest]) => {
-      this.manifest = manifest;
-      this.rawManifest = manifest;
-
-      if (manifest && manifest.default_locale) {
-        return this.initLocale();
-      }
-    }).then(() => {
-      let context = {
-        url: this.baseURI && this.baseURI.spec,
-
-        principal: this.principal,
-
-        logError: error => {
-          this.logger.warn(`Loading extension '${this.id}': Reading manifest: ${error}`);
-        },
-
-        preprocessors: {},
-      };
-
-      if (this.localeData) {
-        context.preprocessors.localize = (value, context) => this.localize(value);
-      }
-
-      let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context);
-      if (normalized.error) {
-        this.manifestError(normalized.error);
-      } else {
-        this.manifest = normalized.value;
-      }
-
-      try {
-        // Do not override the add-on id that has been already assigned.
-        if (!this.id && this.manifest.applications.gecko.id) {
-          this.id = this.manifest.applications.gecko.id;
-        }
-      } catch (e) {
-        // Errors are handled by the type checks above.
-      }
-
-      let permissions = this.manifest.permissions || [];
-
-      let whitelist = [];
-      for (let perm of permissions) {
-        this.permissions.add(perm);
-
-        let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
-        if (!match) {
-          whitelist.push(perm);
-        } else if (match[1] == "experiments" && match[2]) {
-          this.apiNames.add(match[2]);
-        }
-      }
-      this.whiteListedHosts = new MatchPattern(whitelist);
-
-      for (let api of this.apiNames) {
-        this.dependencies.add(`${api}@experiments.addons.mozilla.org`);
-      }
-
-      return this.manifest;
-    });
-  }
-
-  localizeMessage(...args) {
-    return this.localeData.localizeMessage(...args);
-  }
-
-  localize(...args) {
-    return this.localeData.localize(...args);
-  }
-
-  // If a "default_locale" is specified in that manifest, returns it
-  // as a Gecko-compatible locale string. Otherwise, returns null.
-  get defaultLocale() {
-    if (this.manifest.default_locale != null) {
-      return this.normalizeLocaleCode(this.manifest.default_locale);
-    }
-
-    return null;
-  }
-
-  // Normalizes a Chrome-compatible locale code to the appropriate
-  // Gecko-compatible variant. Currently, this means simply
-  // replacing underscores with hyphens.
-  normalizeLocaleCode(locale) {
-    return String.replace(locale, /_/g, "-");
-  }
-
-  // Reads the locale file for the given Gecko-compatible locale code, and
-  // stores its parsed contents in |this.localeMessages.get(locale)|.
-  readLocaleFile(locale) {
-    return Task.spawn(function* () {
-      let locales = yield this.promiseLocales();
-      let dir = locales.get(locale) || locale;
-      let file = `_locales/${dir}/messages.json`;
-
-      try {
-        let messages = yield this.readJSON(file);
-        return this.localeData.addLocale(locale, messages, this);
-      } catch (e) {
-        this.packagingError(`Loading locale file ${file}: ${e}`);
-        return new Map();
-      }
-    }.bind(this));
-  }
-
-  // Reads the list of locales available in the extension, and returns a
-  // Promise which resolves to a Map upon completion.
-  // Each map key is a Gecko-compatible locale code, and each value is the
-  // "_locales" subdirectory containing that locale:
-  //
-  // Map(gecko-locale-code -> locale-directory-name)
-  promiseLocales() {
-    if (!this._promiseLocales) {
-      this._promiseLocales = Task.spawn(function* () {
-        let locales = new Map();
-
-        let entries = yield this.readDirectory("_locales");
-        for (let file of entries) {
-          if (file.isDir) {
-            let locale = this.normalizeLocaleCode(file.name);
-            locales.set(locale, file.name);
-          }
-        }
-
-        this.localeData = new LocaleData({
-          defaultLocale: this.defaultLocale,
-          locales,
-          builtinMessages: this.builtinMessages,
-        });
-
-        return locales;
-      }.bind(this));
-    }
-
-    return this._promiseLocales;
-  }
-
-  // Reads the locale messages for all locales, and returns a promise which
-  // resolves to a Map of locale messages upon completion. Each key in the map
-  // is a Gecko-compatible locale code, and each value is a locale data object
-  // as returned by |readLocaleFile|.
-  initAllLocales() {
-    return Task.spawn(function* () {
-      let locales = yield this.promiseLocales();
-
-      yield Promise.all(Array.from(locales.keys(),
-                                   locale => this.readLocaleFile(locale)));
-
-      let defaultLocale = this.defaultLocale;
-      if (defaultLocale) {
-        if (!locales.has(defaultLocale)) {
-          this.manifestError('Value for "default_locale" property must correspond to ' +
-                             'a directory in "_locales/". Not found: ' +
-                             JSON.stringify(`_locales/${this.manifest.default_locale}/`));
-        }
-      } else if (locales.size) {
-        this.manifestError('The "default_locale" property is required when a ' +
-                           '"_locales/" directory is present.');
-      }
-
-      return this.localeData.messages;
-    }.bind(this));
-  }
-
-  // Reads the locale file for the given Gecko-compatible locale code, or the
-  // default locale if no locale code is given, and sets it as the currently
-  // selected locale on success.
-  //
-  // Pre-loads the default locale for fallback message processing, regardless
-  // of the locale specified.
-  //
-  // If no locales are unavailable, resolves to |null|.
-  initLocale(locale = this.defaultLocale) {
-    return Task.spawn(function* () {
-      if (locale == null) {
-        return null;
-      }
-
-      let promises = [this.readLocaleFile(locale)];
-
-      let {defaultLocale} = this;
-      if (locale != defaultLocale && !this.localeData.has(defaultLocale)) {
-        promises.push(this.readLocaleFile(defaultLocale));
-      }
-
-      let results = yield Promise.all(promises);
-
-      this.localeData.selectedLocale = locale;
-      return results[0];
-    }.bind(this));
-  }
+const ExtensionParent = {
+  GlobalManager,
+  ParentAPIManager,
+  apiManager,
 };
-
-
-/**
- * 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
- * @param {nsIFile} file
- * @param {nsIURI} rootURI
- * @param {string} installType
- */
-class MockExtension {
-  constructor(file, rootURI, installType) {
-    this.id = null;
-    this.file = file;
-    this.rootURI = rootURI;
-    this.installType = installType;
-    this.addon = null;
-
-    let promiseEvent = eventName => new Promise(resolve => {
-      let onstartup = (msg, extension) => {
-        if (this.addon && extension.id == this.addon.id) {
-          Management.off(eventName, onstartup);
-
-          this.id = extension.id;
-          this._extension = extension;
-          resolve(extension);
-        }
-      };
-      Management.on(eventName, onstartup);
-    });
-
-    this._extension = null;
-    this._extensionPromise = promiseEvent("startup");
-    this._readyPromise = promiseEvent("ready");
-  }
-
-  testMessage(...args) {
-    return this._extension.testMessage(...args);
-  }
-
-  on(...args) {
-    this._extensionPromise.then(extension => {
-      extension.on(...args);
-    });
-  }
-
-  off(...args) {
-    this._extensionPromise.then(extension => {
-      extension.off(...args);
-    });
-  }
-
-  startup() {
-    if (this.installType == "temporary") {
-      return AddonManager.installTemporaryAddon(this.file).then(addon => {
-        this.addon = addon;
-        return this._readyPromise;
-      });
-    } else if (this.installType == "permanent") {
-      return new Promise((resolve, reject) => {
-        AddonManager.getInstallForFile(this.file, install => {
-          let listener = {
-            onInstallFailed: reject,
-            onInstallEnded: (install, newAddon) => {
-              this.addon = newAddon;
-              resolve(this._readyPromise);
-            },
-          };
-
-          install.addListener(listener);
-          install.install();
-        });
-      });
-    }
-    throw new Error("installType must be one of: temporary, permanent");
-  }
-
-  shutdown() {
-    this.addon.uninstall();
-    return this.cleanupGeneratedFile();
-  }
-
-  cleanupGeneratedFile() {
-    flushJarCache(this.file);
-    return OS.File.remove(this.file.path);
-  }
-}
-
-let _browserUpdated = false;
-
-// We create one instance of this class per extension. |addonData|
-// comes directly from bootstrap.js when initializing.
-this.Extension = class extends ExtensionData {
-  constructor(addonData, startupReason) {
-    super(addonData.resourceURI);
-
-    this.uuid = UUIDMap.get(addonData.id);
-
-    if (addonData.cleanupFile) {
-      Services.obs.addObserver(this, "xpcom-shutdown", false);
-      this.cleanupFile = addonData.cleanupFile || null;
-      delete addonData.cleanupFile;
-    }
-
-    this.addonData = addonData;
-    this.startupReason = startupReason;
-
-    this.id = addonData.id;
-    this.baseURI = NetUtil.newURI(this.getURL("")).QueryInterface(Ci.nsIURL);
-    this.principal = this.createPrincipal();
-
-    this.onStartup = null;
-
-    this.hasShutdown = false;
-    this.onShutdown = new Set();
-
-    this.uninstallURL = null;
-
-    this.apis = [];
-    this.whiteListedHosts = null;
-    this.webAccessibleResources = null;
-
-    this.emitter = new EventEmitter();
-  }
-
-  static set browserUpdated(updated) {
-    _browserUpdated = updated;
-  }
-
-  static get browserUpdated() {
-    return _browserUpdated;
-  }
-
-  /**
-   * This code is designed to make it easy to test a WebExtension
-   * without creating a bunch of files. Everything is contained in a
-   * single JSON blob.
-   *
-   * Properties:
-   *   "background": "<JS code>"
-   *     A script to be loaded as the background script.
-   *     The "background" section of the "manifest" property is overwritten
-   *     if this is provided.
-   *   "manifest": {...}
-   *     Contents of manifest.json
-   *   "files": {"filename1": "contents1", ...}
-   *     Data to be included as files. Can be referenced from the manifest.
-   *     If a manifest file is provided here, it takes precedence over
-   *     a generated one. Always use "/" as a directory separator.
-   *     Directories should appear here only implicitly (as a prefix
-   *     to file names)
-   *
-   * To make things easier, the value of "background" and "files"[] can
-   * be a function, which is converted to source that is run.
-   *
-   * The generated extension is stored in the system temporary directory,
-   * and an nsIFile object pointing to it is returned.
-   *
-   * @param {object} data
-   * @returns {nsIFile}
-   */
-  static generateXPI(data) {
-    let manifest = data.manifest;
-    if (!manifest) {
-      manifest = {};
-    }
-
-    let files = data.files;
-    if (!files) {
-      files = {};
-    }
-
-    function provide(obj, keys, value, override = false) {
-      if (keys.length == 1) {
-        if (!(keys[0] in obj) || override) {
-          obj[keys[0]] = value;
-        }
-      } else {
-        if (!(keys[0] in obj)) {
-          obj[keys[0]] = {};
-        }
-        provide(obj[keys[0]], keys.slice(1), value, override);
-      }
-    }
-
-    provide(manifest, ["name"], "Generated extension");
-    provide(manifest, ["manifest_version"], 2);
-    provide(manifest, ["version"], "1.0");
-
-    if (data.background) {
-      let bgScript = uuidGen.generateUUID().number + ".js";
-
-      provide(manifest, ["background", "scripts"], [bgScript], true);
-      files[bgScript] = data.background;
-    }
-
-    provide(files, ["manifest.json"], manifest);
-
-    if (data.embedded) {
-      // Package this as a webextension embedded inside a legacy
-      // extension.
-
-      let xpiFiles = {
-        "install.rdf": `<?xml version="1.0" encoding="UTF-8"?>
-          <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-               xmlns:em="http://www.mozilla.org/2004/em-rdf#">
-              <Description about="urn:mozilla:install-manifest"
-                  em:id="${manifest.applications.gecko.id}"
-                  em:name="${manifest.name}"
-                  em:type="2"
-                  em:version="${manifest.version}"
-                  em:description=""
-                  em:hasEmbeddedWebExtension="true"
-                  em:bootstrap="true">
-
-                  <!-- Firefox -->
-                  <em:targetApplication>
-                      <Description
-                          em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
-                          em:minVersion="51.0a1"
-                          em:maxVersion="*"/>
-                  </em:targetApplication>
-              </Description>
-          </RDF>
-        `,
-
-        "bootstrap.js": `
-          function install() {}
-          function uninstall() {}
-          function shutdown() {}
-
-          function startup(data) {
-            data.webExtension.startup();
-          }
-        `,
-      };
-
-      for (let [path, data] of Object.entries(files)) {
-        xpiFiles[`webextension/${path}`] = data;
-      }
-
-      files = xpiFiles;
-    }
-
-    return this.generateZipFile(files);
-  }
-
-  static generateZipFile(files, baseName = "generated-extension.xpi") {
-    let ZipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");
-    let zipW = new ZipWriter();
-
-    let file = FileUtils.getFile("TmpD", [baseName]);
-    file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
-
-    const MODE_WRONLY = 0x02;
-    const MODE_TRUNCATE = 0x20;
-    zipW.open(file, MODE_WRONLY | MODE_TRUNCATE);
-
-    // Needs to be in microseconds for some reason.
-    let time = Date.now() * 1000;
-
-    function generateFile(filename) {
-      let components = filename.split("/");
-      let path = "";
-      for (let component of components.slice(0, -1)) {
-        path += component + "/";
-        if (!zipW.hasEntry(path)) {
-          zipW.addEntryDirectory(path, time, false);
-        }
-      }
-    }
-
-    for (let filename in files) {
-      let script = files[filename];
-      if (typeof(script) == "function") {
-        script = "(" + script.toString() + ")()";
-      } else if (instanceOf(script, "Object") || instanceOf(script, "Array")) {
-        script = JSON.stringify(script);
-      }
-
-      if (!instanceOf(script, "ArrayBuffer")) {
-        script = new TextEncoder("utf-8").encode(script).buffer;
-      }
-
-      let stream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"].createInstance(Ci.nsIArrayBufferInputStream);
-      stream.setData(script, 0, script.byteLength);
-
-      generateFile(filename);
-      zipW.addEntryStream(filename, time, 0, stream, false);
-    }
-
-    zipW.close();
-
-    return file;
-  }
-
-  /**
-   * Generates a new extension using |Extension.generateXPI|, and initializes a
-   * new |Extension| instance which will execute it.
-   *
-   * @param {object} data
-   * @returns {Extension}
-   */
-  static generate(data) {
-    let file = this.generateXPI(data);
-
-    flushJarCache(file);
-    Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
-
-    let fileURI = Services.io.newFileURI(file);
-    let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
-
-    // This may be "temporary" or "permanent".
-    if (data.useAddonManager) {
-      return new MockExtension(file, jarURI, data.useAddonManager);
-    }
-
-    let id;
-    if (data.manifest) {
-      if (data.manifest.applications && data.manifest.applications.gecko) {
-        id = data.manifest.applications.gecko.id;
-      } else if (data.manifest.browser_specific_settings && data.manifest.browser_specific_settings.gecko) {
-        id = data.manifest.browser_specific_settings.gecko.id;
-      }
-    }
-    if (!id) {
-      id = uuidGen.generateUUID().number;
-    }
-
-    return new Extension({
-      id,
-      resourceURI: jarURI,
-      cleanupFile: file,
-    });
-  }
-
-  on(hook, f) {
-    return this.emitter.on(hook, f);
-  }
-
-  off(hook, f) {
-    return this.emitter.off(hook, f);
-  }
-
-  emit(...args) {
-    return this.emitter.emit(...args);
-  }
-
-  testMessage(...args) {
-    Management.emit("test-message", this, ...args);
-  }
-
-  createPrincipal(uri = this.baseURI) {
-    return Services.scriptSecurityManager.createCodebasePrincipal(
-      uri, {addonId: this.id});
-  }
-
-  // Checks that the given URL is a child of our baseURI.
-  isExtensionURL(url) {
-    let uri = Services.io.newURI(url, null, null);
-
-    let common = this.baseURI.getCommonBaseSpec(uri);
-    return common == this.baseURI.spec;
-  }
-
-  readManifest() {
-    return super.readManifest().then(manifest => {
-      if (AppConstants.RELEASE_OR_BETA) {
-        return manifest;
-      }
-
-      // Load Experiments APIs that this extension depends on.
-      return Promise.all(
-        Array.from(this.apiNames, api => ExtensionAPIs.load(api))
-      ).then(apis => {
-        for (let API of apis) {
-          this.apis.push(new API(this));
-        }
-
-        return manifest;
-      });
-    });
-  }
-
-  // Representation of the extension to send to content
-  // processes. This should include anything the content process might
-  // need.
-  serialize() {
-    return {
-      id: this.id,
-      uuid: this.uuid,
-      manifest: this.manifest,
-      resourceURL: this.addonData.resourceURI.spec,
-      baseURL: this.baseURI.spec,
-      content_scripts: this.manifest.content_scripts || [],  // eslint-disable-line camelcase
-      webAccessibleResources: this.webAccessibleResources.serialize(),
-      whiteListedHosts: this.whiteListedHosts.serialize(),
-      localeData: this.localeData.serialize(),
-      permissions: this.permissions,
-      principal: this.principal,
-    };
-  }
-
-  broadcast(msg, data) {
-    return new Promise(resolve => {
-      let count = Services.ppmm.childCount;
-      Services.ppmm.addMessageListener(msg + "Complete", function listener() {
-        count--;
-        if (count == 0) {
-          Services.ppmm.removeMessageListener(msg + "Complete", listener);
-          resolve();
-        }
-      });
-      Services.ppmm.broadcastAsyncMessage(msg, data);
-    });
-  }
-
-  runManifest(manifest) {
-    // Strip leading slashes from web_accessible_resources.
-    let strippedWebAccessibleResources = [];
-    if (manifest.web_accessible_resources) {
-      strippedWebAccessibleResources = manifest.web_accessible_resources.map(path => path.replace(/^\/+/, ""));
-    }
-
-    this.webAccessibleResources = new MatchGlobs(strippedWebAccessibleResources);
-
-    let promises = [];
-    for (let directive in manifest) {
-      if (manifest[directive] !== null) {
-        promises.push(Management.emit(`manifest_${directive}`, directive, this, manifest));
-      }
-    }
-
-    let data = Services.ppmm.initialProcessData;
-    if (!data["Extension:Extensions"]) {
-      data["Extension:Extensions"] = [];
-    }
-    let serial = this.serialize();
-    data["Extension:Extensions"].push(serial);
-
-    return this.broadcast("Extension:Startup", serial).then(() => {
-      return Promise.all(promises);
-    });
-  }
-
-  callOnClose(obj) {
-    this.onShutdown.add(obj);
-  }
-
-  forgetOnClose(obj) {
-    this.onShutdown.delete(obj);
-  }
-
-  get builtinMessages() {
-    return new Map([
-      ["@@extension_id", this.uuid],
-    ]);
-  }
-
-  // Reads the locale file for the given Gecko-compatible locale code, or if
-  // no locale is given, the available locale closest to the UI locale.
-  // Sets the currently selected locale on success.
-  initLocale(locale = undefined) {
-    // Ugh.
-    let super_ = super.initLocale.bind(this);
-
-    return Task.spawn(function* () {
-      if (locale === undefined) {
-        let locales = yield this.promiseLocales();
-
-        let localeList = Array.from(locales.keys(), locale => {
-          return {name: locale, locales: [locale]};
-        });
-
-        let match = Locale.findClosestLocale(localeList);
-        locale = match ? match.name : this.defaultLocale;
-      }
-
-      return super_(locale);
-    }.bind(this));
-  }
-
-  startup() {
-    let started = false;
-    return this.readManifest().then(() => {
-      ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
-      started = true;
-
-      if (!this.hasShutdown) {
-        return this.initLocale();
-      }
-    }).then(() => {
-      if (this.errors.length) {
-        return Promise.reject({errors: this.errors});
-      }
-
-      if (this.hasShutdown) {
-        return;
-      }
-
-      GlobalManager.init(this);
-
-      // The "startup" Management event sent on the extension instance itself
-      // is emitted just before the Management "startup" event,
-      // and it is used to run code that needs to be executed before
-      // any of the "startup" listeners.
-      this.emit("startup", this);
-      Management.emit("startup", this);
-
-      return this.runManifest(this.manifest);
-    }).then(() => {
-      Management.emit("ready", this);
-    }).catch(e => {
-      dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
-      Cu.reportError(e);
-
-      if (started) {
-        ExtensionManagement.shutdownExtension(this.uuid);
-      }
-
-      this.cleanupGeneratedFile();
-
-      throw e;
-    });
-  }
-
-  cleanupGeneratedFile() {
-    if (!this.cleanupFile) {
-      return;
-    }
-
-    let file = this.cleanupFile;
-    this.cleanupFile = null;
-
-    Services.obs.removeObserver(this, "xpcom-shutdown");
-
-    this.broadcast("Extension:FlushJarCache", {path: file.path}).then(() => {
-      // We can't delete this file until everyone using it has
-      // closed it (because Windows is dumb). So we wait for all the
-      // child processes (including the parent) to flush their JAR
-      // caches. These caches may keep the file open.
-      file.remove(false);
-    });
-  }
-
-  shutdown() {
-    this.hasShutdown = true;
-    if (!this.manifest) {
-      ExtensionManagement.shutdownExtension(this.uuid);
-
-      this.cleanupGeneratedFile();
-      return;
-    }
-
-    GlobalManager.uninit(this);
-
-    for (let obj of this.onShutdown) {
-      obj.close();
-    }
-
-    for (let api of this.apis) {
-      api.destroy();
-    }
-
-    ParentAPIManager.shutdownExtension(this.id);
-
-    Management.emit("shutdown", this);
-
-    Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
-
-    MessageChannel.abortResponses({extensionId: this.id});
-
-    ExtensionManagement.shutdownExtension(this.uuid);
-
-    this.cleanupGeneratedFile();
-  }
-
-  observe(subject, topic, data) {
-    if (topic == "xpcom-shutdown") {
-      this.cleanupGeneratedFile();
-    }
-  }
-
-  hasPermission(perm) {
-    let match = /^manifest:(.*)/.exec(perm);
-    if (match) {
-      return this.manifest[match[1]] != null;
-    }
-
-    return this.permissions.has(perm);
-  }
-
-  get name() {
-    return this.manifest.name;
-  }
-};
copy from toolkit/components/extensions/Extension.jsm
copy to toolkit/components/extensions/ExtensionTestCommon.jsm
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/ExtensionTestCommon.jsm
@@ -1,1097 +1,54 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-this.EXPORTED_SYMBOLS = ["Extension", "ExtensionData"];
-
-/* globals Extension ExtensionData */
+/* exported ExtensionTestCommon, MockExtension */
 
-/*
- * This file is the main entry point for extensions. When an extension
- * loads, its bootstrap.js file creates a Extension instance
- * and calls .startup() on it. It calls .shutdown() when the extension
- * unloads. Extension manages any extension-specific state in
- * the chrome process.
- */
+this.EXPORTED_SYMBOLS = ["ExtensionTestCommon", "MockExtension"];
 
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-const Cr = Components.results;
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.importGlobalProperties(["TextEncoder"]);
 
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionAPIs",
-                                  "resource://gre/modules/ExtensionAPI.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
-                                  "resource://gre/modules/ExtensionStorage.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+                                  "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent",
+                                  "resource://gre/modules/ExtensionParent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Locale",
-                                  "resource://gre/modules/Locale.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Log",
-                                  "resource://gre/modules/Log.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
-                                  "resource://gre/modules/MatchPattern.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
-                                  "resource://gre/modules/MatchPattern.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
-                                  "resource://gre/modules/MessageChannel.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
-                                  "resource://gre/modules/NativeMessaging.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
-                                  "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
-                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
-                                  "resource://gre/modules/Preferences.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "require",
-                                  "resource://devtools/shared/Loader.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
-                                  "resource://gre/modules/Schemas.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Task",
-                                  "resource://gre/modules/Task.jsm");
 
-Cu.import("resource://gre/modules/ExtensionContent.jsm");
-Cu.import("resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyGetter(this, "apiManager",
+                            () => ExtensionParent.apiManager);
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
-const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
-const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
-const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
-
-let schemaURLs = new Set();
-
-if (!AppConstants.RELEASE_OR_BETA) {
-  schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
-}
-
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-var {
-  BaseContext,
-  EventEmitter,
-  LocaleData,
-  SchemaAPIManager,
-  SpreadArgs,
-  defineLazyGetter,
+const {
   flushJarCache,
   instanceOf,
 } = ExtensionUtils;
 
 XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
 
-const LOGGER_ID_BASE = "addons.webextension.";
-const UUID_MAP_PREF = "extensions.webextensions.uuids";
-const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
-const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
-
-const COMMENT_REGEXP = new RegExp(String.raw`
-    ^
-    (
-      (?:
-        [^"\n] |
-        " (?:[^"\\\n] | \\.)* "
-      )*?
-    )
-
-    //.*
-  `.replace(/\s+/g, ""), "gm");
-
-var GlobalManager;
-var ParentAPIManager;
-
-// This object loads the ext-*.js scripts that define the extension API.
-var Management = new class extends SchemaAPIManager {
-  constructor() {
-    super("main");
-    this.initialized = null;
-  }
-
-  // Loads all the ext-*.js scripts currently registered.
-  lazyInit() {
-    if (this.initialized) {
-      return this.initialized;
-    }
-
-    // Load order matters here. The base manifest defines types which are
-    // extended by other schemas, so needs to be loaded first.
-    let promise = Schemas.load(BASE_SCHEMA).then(() => {
-      let promises = [];
-      for (let [/* name */, url] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCHEMAS)) {
-        promises.push(Schemas.load(url));
-      }
-      for (let url of schemaURLs) {
-        promises.push(Schemas.load(url));
-      }
-      return Promise.all(promises);
-    });
-
-    for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) {
-      this.loadScript(value);
-    }
-
-    this.initialized = promise;
-    return this.initialized;
-  }
-
-  registerSchemaAPI(namespace, envType, getAPI) {
-    if (envType == "addon_parent" || envType == "content_parent") {
-      super.registerSchemaAPI(namespace, envType, getAPI);
-    }
-  }
-}();
-
-// Subscribes to messages related to the extension messaging API and forwards it
-// to the relevant message manager. The "sender" field for the `onMessage` and
-// `onConnect` events are updated if needed.
-let ProxyMessenger = {
-  _initialized: false,
-  init() {
-    if (this._initialized) {
-      return;
-    }
-    this._initialized = true;
-
-    // TODO(robwu): When addons move to a separate process, we should use the
-    // parent process manager(s) of the addon process(es) instead of the
-    // in-process one.
-    let pipmm = Services.ppmm.getChildAt(0);
-    // Listen on the global frame message manager because content scripts send
-    // and receive extension messages via their frame.
-    // Listen on the parent process message manager because `runtime.connect`
-    // and `runtime.sendMessage` requests must be delivered to all frames in an
-    // addon process (by the API contract).
-    // And legacy addons are not associated with a frame, so that is another
-    // reason for having a parent process manager here.
-    let messageManagers = [Services.mm, pipmm];
-
-    MessageChannel.addListener(messageManagers, "Extension:Connect", this);
-    MessageChannel.addListener(messageManagers, "Extension:Message", this);
-    MessageChannel.addListener(messageManagers, "Extension:Port:Disconnect", this);
-    MessageChannel.addListener(messageManagers, "Extension:Port:PostMessage", this);
-  },
-
-  receiveMessage({target, messageName, channelId, sender, recipient, data, responseType}) {
-    if (recipient.toNativeApp) {
-      let {childId, toNativeApp} = recipient;
-      if (messageName == "Extension:Message") {
-        let context = ParentAPIManager.getContextById(childId);
-        return new NativeApp(context, toNativeApp).sendMessage(data);
-      }
-      if (messageName == "Extension:Connect") {
-        let context = ParentAPIManager.getContextById(childId);
-        NativeApp.onConnectNative(context, target.messageManager, data.portId, sender, toNativeApp);
-        return true;
-      }
-      // "Extension:Port:Disconnect" and "Extension:Port:PostMessage" for
-      // native messages are handled by NativeApp.
-      return;
-    }
-    let extension = GlobalManager.extensionMap.get(sender.extensionId);
-    let receiverMM = this._getMessageManagerForRecipient(recipient);
-    if (!extension || !receiverMM) {
-      return Promise.reject({
-        result: MessageChannel.RESULT_NO_HANDLER,
-        message: "No matching message handler for the given recipient.",
-      });
-    }
-
-    if ((messageName == "Extension:Message" ||
-         messageName == "Extension:Connect") &&
-        Management.global.tabGetSender) {
-      // From ext-tabs.js, undefined on Android.
-      Management.global.tabGetSender(extension, target, sender);
-    }
-    return MessageChannel.sendMessage(receiverMM, messageName, data, {
-      sender,
-      recipient,
-      responseType,
-    });
-  },
-
-  /**
-   * @param {object} recipient An object that was passed to
-   *     `MessageChannel.sendMessage`.
-   * @returns {object|null} The message manager matching the recipient if found.
-   */
-  _getMessageManagerForRecipient(recipient) {
-    let {extensionId, tabId} = recipient;
-    // tabs.sendMessage / tabs.connect
-    if (tabId) {
-      // `tabId` being set implies that the tabs API is supported, so we don't
-      // need to check whether `TabManager` exists.
-      let tab = Management.global.TabManager.getTab(tabId, null, null);
-      return tab && tab.linkedBrowser.messageManager;
-    }
-
-    // runtime.sendMessage / runtime.connect
-    if (extensionId) {
-      // TODO(robwu): map the extensionId to the addon parent process's message
-      // manager when they run in a separate process.
-      return Services.ppmm.getChildAt(0);
-    }
-
-    return null;
-  },
-};
-
-class BrowserDocshellFollower {
-  /**
-   * Follows the <browser> belonging to the `xulBrowser`'s current docshell.
-   *
-   * @param {XULElement} xulBrowser A <browser> tag.
-   * @param {function} onBrowserChange Called when the <browser> changes.
-   */
-  constructor(xulBrowser, onBrowserChange) {
-    this.xulBrowser = xulBrowser;
-    this.onBrowserChange = onBrowserChange;
-
-    xulBrowser.addEventListener("SwapDocShells", this);
-  }
-
-  destroy() {
-    this.xulBrowser.removeEventListener("SwapDocShells", this);
-    this.xulBrowser = null;
-  }
-
-  handleEvent({detail: otherBrowser}) {
-    this.xulBrowser.removeEventListener("SwapDocShells", this);
-    this.xulBrowser = otherBrowser;
-    this.xulBrowser.addEventListener("SwapDocShells", this);
-    this.onBrowserChange(otherBrowser);
-  }
-}
-
-class ProxyContext extends BaseContext {
-  constructor(envType, extension, params, xulBrowser, principal) {
-    super(envType, extension);
-
-    this.uri = NetUtil.newURI(params.url);
-
-    this.incognito = params.incognito;
-
-    // This message manager is used by ParentAPIManager to send messages and to
-    // close the ProxyContext if the underlying message manager closes. This
-    // message manager object may change when `xulBrowser` swaps docshells, e.g.
-    // when a tab is moved to a different window.
-    this.currentMessageManager = xulBrowser.messageManager;
-    this._docShellTracker = new BrowserDocshellFollower(
-      xulBrowser, this.onBrowserChange.bind(this));
-
-    Object.defineProperty(this, "principal", {
-      value: principal, enumerable: true, configurable: true,
-    });
-
-    this.listenerProxies = new Map();
-
-    Management.emit("proxy-context-load", this);
-  }
-
-  get cloneScope() {
-    return this.sandbox;
-  }
-
-  onBrowserChange(browser) {
-    // Make sure that the message manager is set. Otherwise the ProxyContext may
-    // never be destroyed because the ParentAPIManager would fail to detect that
-    // the message manager is closed.
-    if (!browser.messageManager) {
-      throw new Error("BrowserDocshellFollower: The new browser has no message manager");
-    }
-
-    this.currentMessageManager = browser.messageManager;
-  }
-
-  shutdown() {
-    this.unload();
-  }
-
-  unload() {
-    if (this.unloaded) {
-      return;
-    }
-    this._docShellTracker.destroy();
-    super.unload();
-    Management.emit("proxy-context-unload", this);
-  }
-}
-
-defineLazyGetter(ProxyContext.prototype, "apiObj", function() {
-  let obj = {};
-  GlobalManager.injectInObject(this, false, obj);
-  return obj;
-});
-
-defineLazyGetter(ProxyContext.prototype, "sandbox", function() {
-  return Cu.Sandbox(this.principal);
-});
-
-// The parent ProxyContext of an ExtensionContext in ExtensionChild.jsm.
-class ExtensionChildProxyContext extends ProxyContext {
-  constructor(envType, extension, params, xulBrowser) {
-    super(envType, extension, params, xulBrowser, extension.principal);
-
-    this.viewType = params.viewType;
-    // WARNING: The xulBrowser may change when docShells are swapped, e.g. when
-    // the tab moves to a different window.
-    this.xulBrowser = xulBrowser;
-  }
-
-  // The window that contains this context. This may change due to moving tabs.
-  get xulWindow() {
-    return this.xulBrowser.ownerGlobal;
-  }
-
-  get windowId() {
-    if (!Management.global.WindowManager || this.viewType == "background") {
-      return;
-    }
-    // viewType popup or tab:
-    return Management.global.WindowManager.getId(this.xulWindow);
-  }
-
-  get tabId() {
-    if (!Management.global.TabManager) {
-      return;  // Not yet supported on Android.
-    }
-    let {gBrowser} = this.xulBrowser.ownerGlobal;
-    let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser);
-    return tab && Management.global.TabManager.getId(tab);
-  }
-
-  onBrowserChange(browser) {
-    super.onBrowserChange(browser);
-    this.xulBrowser = browser;
-  }
-
-  shutdown() {
-    Management.emit("page-shutdown", this);
-    super.shutdown();
-  }
-}
-
-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;
-}
-
-ParentAPIManager = {
-  proxyContexts: new Map(),
-
-  init() {
-    Services.obs.addObserver(this, "message-manager-close", false);
-
-    Services.mm.addMessageListener("API:CreateProxyContext", this);
-    Services.mm.addMessageListener("API:CloseProxyContext", this, true);
-    Services.mm.addMessageListener("API:Call", this);
-    Services.mm.addMessageListener("API:AddListener", this);
-    Services.mm.addMessageListener("API:RemoveListener", this);
-  },
-
-  // "message-manager-close" observer.
-  observe(subject, topic, data) {
-    let mm = subject;
-    for (let [childId, context] of this.proxyContexts) {
-      if (context.currentMessageManager == mm) {
-        this.closeProxyContext(childId);
-      }
-    }
-  },
-
-  shutdownExtension(extensionId) {
-    for (let [childId, context] of this.proxyContexts) {
-      if (context.extension.id == extensionId) {
-        context.shutdown();
-        this.proxyContexts.delete(childId);
-      }
-    }
-  },
-
-  receiveMessage({name, data, target}) {
-    switch (name) {
-      case "API:CreateProxyContext":
-        this.createProxyContext(data, target);
-        break;
-
-      case "API:CloseProxyContext":
-        this.closeProxyContext(data.childId);
-        break;
-
-      case "API:Call":
-        this.call(data, target);
-        break;
-
-      case "API:AddListener":
-        this.addListener(data, target);
-        break;
-
-      case "API:RemoveListener":
-        this.removeListener(data);
-        break;
-    }
-  },
-
-  createProxyContext(data, target) {
-    let {envType, extensionId, childId, principal} = data;
-    if (this.proxyContexts.has(childId)) {
-      throw new Error("A WebExtension context with the given ID already exists!");
-    }
-
-    let extension = GlobalManager.getExtension(extensionId);
-    if (!extension) {
-      throw new Error(`No WebExtension found with ID ${extensionId}`);
-    }
-
-    let context;
-    if (envType == "addon_parent") {
-      // Privileged addon contexts can only be loaded in documents whose main
-      // frame is also the same addon.
-      if (principal.URI.prePath != extension.baseURI.prePath ||
-          !target.contentPrincipal.subsumes(principal)) {
-        throw new Error(`Refused to create privileged WebExtension context for ${principal.URI.spec}`);
-      }
-      context = new ExtensionChildProxyContext(envType, extension, data, target);
-    } else if (envType == "content_parent") {
-      context = new ProxyContext(envType, extension, data, target, principal);
-    } else {
-      throw new Error(`Invalid WebExtension context envType: ${envType}`);
-    }
-    this.proxyContexts.set(childId, context);
-  },
-
-  closeProxyContext(childId) {
-    let context = this.proxyContexts.get(childId);
-    if (!context) {
-      return;
-    }
-    context.unload();
-    this.proxyContexts.delete(childId);
-  },
-
-  call(data, target) {
-    let context = this.getContextById(data.childId);
-    if (context.currentMessageManager !== target.messageManager) {
-      Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
-    }
-
-    try {
-      let args = Cu.cloneInto(data.args, context.sandbox);
-      let result = findPathInObject(context.apiObj, data.path)(...args);
-
-      if (data.callId) {
-        result = result || Promise.resolve();
-
-        result.then(result => {
-          result = result instanceof SpreadArgs ? [...result] : [result];
-
-          context.currentMessageManager.sendAsyncMessage("API:CallResult", {
-            childId: data.childId,
-            callId: data.callId,
-            result,
-          });
-        }, error => {
-          error = context.normalizeError(error);
-          context.currentMessageManager.sendAsyncMessage("API:CallResult", {
-            childId: data.childId,
-            callId: data.callId,
-            error: {message: error.message},
-          });
-        });
-      }
-    } catch (e) {
-      if (data.callId) {
-        let error = context.normalizeError(e);
-        context.currentMessageManager.sendAsyncMessage("API:CallResult", {
-          childId: data.childId,
-          callId: data.callId,
-          error: {message: error.message},
-        });
-      } else {
-        Cu.reportError(e);
-      }
-    }
-  },
-
-  addListener(data, target) {
-    let context = this.getContextById(data.childId);
-    if (context.currentMessageManager !== target.messageManager) {
-      Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
-    }
-
-    function listener(...listenerArgs) {
-      context.currentMessageManager.sendAsyncMessage("API:RunListener", {
-        childId: data.childId,
-        path: data.path,
-        args: listenerArgs,
-      });
-    }
-
-    context.listenerProxies.set(data.path, listener);
-
-    let args = Cu.cloneInto(data.args, context.sandbox);
-    findPathInObject(context.apiObj, data.path).addListener(listener, ...args);
-  },
-
-  removeListener(data) {
-    let context = this.getContextById(data.childId);
-    let listener = context.listenerProxies.get(data.path);
-    findPathInObject(context.apiObj, data.path).removeListener(listener);
-  },
-
-  getContextById(childId) {
-    let context = this.proxyContexts.get(childId);
-    if (!context) {
-      let error = new Error("WebExtension context not found!");
-      Cu.reportError(error);
-      throw error;
-    }
-    return context;
-  },
-};
-
-ParentAPIManager.init();
-
-// All moz-extension URIs use a machine-specific UUID rather than the
-// extension's own ID in the host component. This makes it more
-// difficult for web pages to detect whether a user has a given add-on
-// installed (by trying to load a moz-extension URI referring to a
-// web_accessible_resource from the extension). UUIDMap.get()
-// returns the UUID for a given add-on ID.
-var UUIDMap = {
-  _read() {
-    let pref = Preferences.get(UUID_MAP_PREF, "{}");
-    try {
-      return JSON.parse(pref);
-    } catch (e) {
-      Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`);
-      return {};
-    }
-  },
-
-  _write(map) {
-    Preferences.set(UUID_MAP_PREF, JSON.stringify(map));
-  },
-
-  get(id, create = true) {
-    let map = this._read();
-
-    if (id in map) {
-      return map[id];
-    }
-
-    let uuid = null;
-    if (create) {
-      uuid = uuidGen.generateUUID().number;
-      uuid = uuid.slice(1, -1); // Strip { and } off the UUID.
-
-      map[id] = uuid;
-      this._write(map);
-    }
-    return uuid;
-  },
-
-  remove(id) {
-    let map = this._read();
-    delete map[id];
-    this._write(map);
-  },
-};
-
-// This is the old interface that UUIDMap replaced, to be removed when
-// the references listed in bug 1291399 are updated.
-/* exported getExtensionUUID */
-function getExtensionUUID(id) {
-  return UUIDMap.get(id, true);
-}
-
-// For extensions that have called setUninstallURL(), send an event
-// so the browser can display the URL.
-var UninstallObserver = {
-  initialized: false,
-
-  init() {
-    if (!this.initialized) {
-      AddonManager.addAddonListener(this);
-      XPCOMUtils.defineLazyPreferenceGetter(this, "leaveStorage", LEAVE_STORAGE_PREF, false);
-      XPCOMUtils.defineLazyPreferenceGetter(this, "leaveUuid", LEAVE_UUID_PREF, false);
-      this.initialized = true;
-    }
-  },
-
-  onUninstalling(addon) {
-    let extension = GlobalManager.extensionMap.get(addon.id);
-    if (extension) {
-      // Let any other interested listeners respond
-      // (e.g., display the uninstall URL)
-      Management.emit("uninstall", extension);
-    }
-  },
-
-  onUninstalled(addon) {
-    let uuid = UUIDMap.get(addon.id, false);
-    if (!uuid) {
-      return;
-    }
-
-    if (!this.leaveStorage) {
-      // Clear browser.local.storage
-      ExtensionStorage.clear(addon.id);
-
-      // Clear any IndexedDB storage created by the extension
-      let baseURI = NetUtil.newURI(`moz-extension://${uuid}/`);
-      let principal = Services.scriptSecurityManager.createCodebasePrincipal(
-        baseURI, {addonId: addon.id}
-      );
-      Services.qms.clearStoragesForPrincipal(principal);
-
-      // Clear localStorage created by the extension
-      let attrs = JSON.stringify({addonId: addon.id});
-      Services.obs.notifyObservers(null, "clear-origin-attributes-data", attrs);
-    }
-
-    if (!this.leaveUuid) {
-      // Clear the entry in the UUID map
-      UUIDMap.remove(addon.id);
-    }
-  },
-};
-
-// Responsible for loading extension APIs into the right globals.
-GlobalManager = {
-  // Map[extension ID -> Extension]. Determines which extension is
-  // responsible for content under a particular extension ID.
-  extensionMap: new Map(),
-  initialized: false,
-
-  init(extension) {
-    if (this.extensionMap.size == 0) {
-      UninstallObserver.init();
-      ProxyMessenger.init();
-      Management.on("extension-browser-inserted", this._onExtensionBrowser);
-      this.initialized = true;
-    }
-
-    this.extensionMap.set(extension.id, extension);
-  },
-
-  uninit(extension) {
-    this.extensionMap.delete(extension.id);
-
-    if (this.extensionMap.size == 0 && this.initialized) {
-      Management.off("extension-browser-inserted", this._onExtensionBrowser);
-      this.initialized = false;
-    }
-  },
-
-  _onExtensionBrowser(type, browser) {
-    browser.messageManager.loadFrameScript(`data:,
-      Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
-      ExtensionContent.init(this);
-      addEventListener("unload", function() {
-        ExtensionContent.uninit(this);
-      });
-    `, false);
-  },
-
-  getExtension(extensionId) {
-    return this.extensionMap.get(extensionId);
-  },
-
-  injectInObject(context, isChromeCompat, dest) {
-    Management.generateAPIs(context, dest);
-    SchemaAPIManager.generateAPIs(context, context.extension.apis, dest);
-  },
-};
-
-// Represents the data contained in an extension, contained either
-// in a directory or a zip file, which may or may not be installed.
-// This class implements the functionality of the Extension class,
-// primarily related to manifest parsing and localization, which is
-// useful prior to extension installation or initialization.
-//
-// No functionality of this class is guaranteed to work before
-// |readManifest| has been called, and completed.
-this.ExtensionData = class {
-  constructor(rootURI) {
-    this.rootURI = rootURI;
-
-    this.manifest = null;
-    this.id = null;
-    this.uuid = null;
-    this.localeData = null;
-    this._promiseLocales = null;
-
-    this.apiNames = new Set();
-    this.dependencies = new Set();
-    this.permissions = new Set();
-
-    this.errors = [];
-  }
-
-  get builtinMessages() {
-    return null;
-  }
-
-  get logger() {
-    let id = this.id || "<unknown>";
-    return Log.repository.getLogger(LOGGER_ID_BASE + id);
-  }
-
-  // Report an error about the extension's manifest file.
-  manifestError(message) {
-    this.packagingError(`Reading manifest: ${message}`);
-  }
-
-  // Report an error about the extension's general packaging.
-  packagingError(message) {
-    this.errors.push(message);
-    this.logger.error(`Loading extension '${this.id}': ${message}`);
-  }
-
-  /**
-   * Returns the moz-extension: URL for the given path within this
-   * extension.
-   *
-   * Must not be called unless either the `id` or `uuid` property has
-   * already been set.
-   *
-   * @param {string} path The path portion of the URL.
-   * @returns {string}
-   */
-  getURL(path = "") {
-    if (!(this.id || this.uuid)) {
-      throw new Error("getURL may not be called before an `id` or `uuid` has been set");
-    }
-    if (!this.uuid) {
-      this.uuid = UUIDMap.get(this.id);
-    }
-    return `moz-extension://${this.uuid}/${path}`;
-  }
-
-  readDirectory(path) {
-    return Task.spawn(function* () {
-      if (this.rootURI instanceof Ci.nsIFileURL) {
-        let uri = NetUtil.newURI(this.rootURI.resolve("./" + path));
-        let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;
-
-        let iter = new OS.File.DirectoryIterator(fullPath);
-        let results = [];
-
-        try {
-          yield iter.forEach(entry => {
-            results.push(entry);
-          });
-        } catch (e) {
-          // Always return a list, even if the directory does not exist (or is
-          // not a directory) for symmetry with the ZipReader behavior.
-        }
-        iter.close();
-
-        return results;
-      }
-
-      // FIXME: We need a way to do this without main thread IO.
-
-      let uri = this.rootURI.QueryInterface(Ci.nsIJARURI);
-
-      let file = uri.JARFile.QueryInterface(Ci.nsIFileURL).file;
-      let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
-      zipReader.open(file);
-      try {
-        let results = [];
-
-        // Normalize the directory path.
-        path = `${uri.JAREntry}/${path}`;
-        path = path.replace(/\/\/+/g, "/").replace(/^\/|\/$/g, "") + "/";
-
-        // Escape pattern metacharacters.
-        let pattern = path.replace(/[[\]()?*~|$\\]/g, "\\$&");
-
-        let enumerator = zipReader.findEntries(pattern + "*");
-        while (enumerator.hasMore()) {
-          let name = enumerator.getNext();
-          if (!name.startsWith(path)) {
-            throw new Error("Unexpected ZipReader entry");
-          }
-
-          // The enumerator returns the full path of all entries.
-          // Trim off the leading path, and filter out entries from
-          // subdirectories.
-          name = name.slice(path.length);
-          if (name && !/\/./.test(name)) {
-            results.push({
-              name: name.replace("/", ""),
-              isDir: name.endsWith("/"),
-            });
-          }
-        }
-
-        return results;
-      } finally {
-        zipReader.close();
-      }
-    }.bind(this));
-  }
-
-  readJSON(path) {
-    return new Promise((resolve, reject) => {
-      let uri = this.rootURI.resolve(`./${path}`);
-
-      NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
-        if (!Components.isSuccessCode(status)) {
-          // Convert status code to a string
-          let e = Components.Exception("", status);
-          reject(new Error(`Error while loading '${uri}' (${e.name})`));
-          return;
-        }
-        try {
-          let text = NetUtil.readInputStreamToString(inputStream, inputStream.available(),
-                                                     {charset: "utf-8"});
-
-          text = text.replace(COMMENT_REGEXP, "$1");
-
-          resolve(JSON.parse(text));
-        } catch (e) {
-          reject(e);
-        }
-      });
-    });
-  }
-
-  // Reads the extension's |manifest.json| file, and stores its
-  // parsed contents in |this.manifest|.
-  readManifest() {
-    return Promise.all([
-      this.readJSON("manifest.json"),
-      Management.lazyInit(),
-    ]).then(([manifest]) => {
-      this.manifest = manifest;
-      this.rawManifest = manifest;
-
-      if (manifest && manifest.default_locale) {
-        return this.initLocale();
-      }
-    }).then(() => {
-      let context = {
-        url: this.baseURI && this.baseURI.spec,
-
-        principal: this.principal,
-
-        logError: error => {
-          this.logger.warn(`Loading extension '${this.id}': Reading manifest: ${error}`);
-        },
-
-        preprocessors: {},
-      };
-
-      if (this.localeData) {
-        context.preprocessors.localize = (value, context) => this.localize(value);
-      }
-
-      let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context);
-      if (normalized.error) {
-        this.manifestError(normalized.error);
-      } else {
-        this.manifest = normalized.value;
-      }
-
-      try {
-        // Do not override the add-on id that has been already assigned.
-        if (!this.id && this.manifest.applications.gecko.id) {
-          this.id = this.manifest.applications.gecko.id;
-        }
-      } catch (e) {
-        // Errors are handled by the type checks above.
-      }
-
-      let permissions = this.manifest.permissions || [];
-
-      let whitelist = [];
-      for (let perm of permissions) {
-        this.permissions.add(perm);
-
-        let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
-        if (!match) {
-          whitelist.push(perm);
-        } else if (match[1] == "experiments" && match[2]) {
-          this.apiNames.add(match[2]);
-        }
-      }
-      this.whiteListedHosts = new MatchPattern(whitelist);
-
-      for (let api of this.apiNames) {
-        this.dependencies.add(`${api}@experiments.addons.mozilla.org`);
-      }
-
-      return this.manifest;
-    });
-  }
-
-  localizeMessage(...args) {
-    return this.localeData.localizeMessage(...args);
-  }
-
-  localize(...args) {
-    return this.localeData.localize(...args);
-  }
-
-  // If a "default_locale" is specified in that manifest, returns it
-  // as a Gecko-compatible locale string. Otherwise, returns null.
-  get defaultLocale() {
-    if (this.manifest.default_locale != null) {
-      return this.normalizeLocaleCode(this.manifest.default_locale);
-    }
-
-    return null;
-  }
-
-  // Normalizes a Chrome-compatible locale code to the appropriate
-  // Gecko-compatible variant. Currently, this means simply
-  // replacing underscores with hyphens.
-  normalizeLocaleCode(locale) {
-    return String.replace(locale, /_/g, "-");
-  }
-
-  // Reads the locale file for the given Gecko-compatible locale code, and
-  // stores its parsed contents in |this.localeMessages.get(locale)|.
-  readLocaleFile(locale) {
-    return Task.spawn(function* () {
-      let locales = yield this.promiseLocales();
-      let dir = locales.get(locale) || locale;
-      let file = `_locales/${dir}/messages.json`;
-
-      try {
-        let messages = yield this.readJSON(file);
-        return this.localeData.addLocale(locale, messages, this);
-      } catch (e) {
-        this.packagingError(`Loading locale file ${file}: ${e}`);
-        return new Map();
-      }
-    }.bind(this));
-  }
-
-  // Reads the list of locales available in the extension, and returns a
-  // Promise which resolves to a Map upon completion.
-  // Each map key is a Gecko-compatible locale code, and each value is the
-  // "_locales" subdirectory containing that locale:
-  //
-  // Map(gecko-locale-code -> locale-directory-name)
-  promiseLocales() {
-    if (!this._promiseLocales) {
-      this._promiseLocales = Task.spawn(function* () {
-        let locales = new Map();
-
-        let entries = yield this.readDirectory("_locales");
-        for (let file of entries) {
-          if (file.isDir) {
-            let locale = this.normalizeLocaleCode(file.name);
-            locales.set(locale, file.name);
-          }
-        }
-
-        this.localeData = new LocaleData({
-          defaultLocale: this.defaultLocale,
-          locales,
-          builtinMessages: this.builtinMessages,
-        });
-
-        return locales;
-      }.bind(this));
-    }
-
-    return this._promiseLocales;
-  }
-
-  // Reads the locale messages for all locales, and returns a promise which
-  // resolves to a Map of locale messages upon completion. Each key in the map
-  // is a Gecko-compatible locale code, and each value is a locale data object
-  // as returned by |readLocaleFile|.
-  initAllLocales() {
-    return Task.spawn(function* () {
-      let locales = yield this.promiseLocales();
-
-      yield Promise.all(Array.from(locales.keys(),
-                                   locale => this.readLocaleFile(locale)));
-
-      let defaultLocale = this.defaultLocale;
-      if (defaultLocale) {
-        if (!locales.has(defaultLocale)) {
-          this.manifestError('Value for "default_locale" property must correspond to ' +
-                             'a directory in "_locales/". Not found: ' +
-                             JSON.stringify(`_locales/${this.manifest.default_locale}/`));
-        }
-      } else if (locales.size) {
-        this.manifestError('The "default_locale" property is required when a ' +
-                           '"_locales/" directory is present.');
-      }
-
-      return this.localeData.messages;
-    }.bind(this));
-  }
-
-  // Reads the locale file for the given Gecko-compatible locale code, or the
-  // default locale if no locale code is given, and sets it as the currently
-  // selected locale on success.
-  //
-  // Pre-loads the default locale for fallback message processing, regardless
-  // of the locale specified.
-  //
-  // If no locales are unavailable, resolves to |null|.
-  initLocale(locale = this.defaultLocale) {
-    return Task.spawn(function* () {
-      if (locale == null) {
-        return null;
-      }
-
-      let promises = [this.readLocaleFile(locale)];
-
-      let {defaultLocale} = this;
-      if (locale != defaultLocale && !this.localeData.has(defaultLocale)) {
-        promises.push(this.readLocaleFile(defaultLocale));
-      }
-
-      let results = yield Promise.all(promises);
-
-      this.localeData.selectedLocale = locale;
-      return results[0];
-    }.bind(this));
-  }
-};
-
 
 /**
  * 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
  * @param {nsIFile} file
@@ -1104,24 +61,24 @@ class MockExtension {
     this.file = file;
     this.rootURI = rootURI;
     this.installType = installType;
     this.addon = null;
 
     let promiseEvent = eventName => new Promise(resolve => {
       let onstartup = (msg, extension) => {
         if (this.addon && extension.id == this.addon.id) {
-          Management.off(eventName, onstartup);
+          apiManager.off(eventName, onstartup);
 
           this.id = extension.id;
           this._extension = extension;
           resolve(extension);
         }
       };
-      Management.on(eventName, onstartup);
+      apiManager.on(eventName, onstartup);
     });
 
     this._extension = null;
     this._extensionPromise = promiseEvent("startup");
     this._readyPromise = promiseEvent("ready");
   }
 
   testMessage(...args) {
@@ -1171,61 +128,17 @@ class MockExtension {
   }
 
   cleanupGeneratedFile() {
     flushJarCache(this.file);
     return OS.File.remove(this.file.path);
   }
 }
 
-let _browserUpdated = false;
-
-// We create one instance of this class per extension. |addonData|
-// comes directly from bootstrap.js when initializing.
-this.Extension = class extends ExtensionData {
-  constructor(addonData, startupReason) {
-    super(addonData.resourceURI);
-
-    this.uuid = UUIDMap.get(addonData.id);
-
-    if (addonData.cleanupFile) {
-      Services.obs.addObserver(this, "xpcom-shutdown", false);
-      this.cleanupFile = addonData.cleanupFile || null;
-      delete addonData.cleanupFile;
-    }
-
-    this.addonData = addonData;
-    this.startupReason = startupReason;
-
-    this.id = addonData.id;
-    this.baseURI = NetUtil.newURI(this.getURL("")).QueryInterface(Ci.nsIURL);
-    this.principal = this.createPrincipal();
-
-    this.onStartup = null;
-
-    this.hasShutdown = false;
-    this.onShutdown = new Set();
-
-    this.uninstallURL = null;
-
-    this.apis = [];
-    this.whiteListedHosts = null;
-    this.webAccessibleResources = null;
-
-    this.emitter = new EventEmitter();
-  }
-
-  static set browserUpdated(updated) {
-    _browserUpdated = updated;
-  }
-
-  static get browserUpdated() {
-    return _browserUpdated;
-  }
-
+class ExtensionTestCommon {
   /**
    * This code is designed to make it easy to test a WebExtension
    * without creating a bunch of files. Everything is contained in a
    * single JSON blob.
    *
    * Properties:
    *   "background": "<JS code>"
    *     A script to be loaded as the background script.
@@ -1418,269 +331,9 @@ this.Extension = class extends Extension
     }
 
     return new Extension({
       id,
       resourceURI: jarURI,
       cleanupFile: file,
     });
   }
-
-  on(hook, f) {
-    return this.emitter.on(hook, f);
-  }
-
-  off(hook, f) {
-    return this.emitter.off(hook, f);
-  }
-
-  emit(...args) {
-    return this.emitter.emit(...args);
-  }
-
-  testMessage(...args) {
-    Management.emit("test-message", this, ...args);
-  }
-
-  createPrincipal(uri = this.baseURI) {
-    return Services.scriptSecurityManager.createCodebasePrincipal(
-      uri, {addonId: this.id});
-  }
-
-  // Checks that the given URL is a child of our baseURI.
-  isExtensionURL(url) {
-    let uri = Services.io.newURI(url, null, null);
-
-    let common = this.baseURI.getCommonBaseSpec(uri);
-    return common == this.baseURI.spec;
-  }
-
-  readManifest() {
-    return super.readManifest().then(manifest => {
-      if (AppConstants.RELEASE_OR_BETA) {
-        return manifest;
-      }
-
-      // Load Experiments APIs that this extension depends on.
-      return Promise.all(
-        Array.from(this.apiNames, api => ExtensionAPIs.load(api))
-      ).then(apis => {
-        for (let API of apis) {
-          this.apis.push(new API(this));
-        }
-
-        return manifest;
-      });
-    });
-  }
-
-  // Representation of the extension to send to content
-  // processes. This should include anything the content process might
-  // need.
-  serialize() {
-    return {
-      id: this.id,
-      uuid: this.uuid,
-      manifest: this.manifest,
-      resourceURL: this.addonData.resourceURI.spec,
-      baseURL: this.baseURI.spec,
-      content_scripts: this.manifest.content_scripts || [],  // eslint-disable-line camelcase
-      webAccessibleResources: this.webAccessibleResources.serialize(),
-      whiteListedHosts: this.whiteListedHosts.serialize(),
-      localeData: this.localeData.serialize(),
-      permissions: this.permissions,
-      principal: this.principal,
-    };
-  }
-
-  broadcast(msg, data) {
-    return new Promise(resolve => {
-      let count = Services.ppmm.childCount;
-      Services.ppmm.addMessageListener(msg + "Complete", function listener() {
-        count--;
-        if (count == 0) {
-          Services.ppmm.removeMessageListener(msg + "Complete", listener);
-          resolve();
-        }
-      });
-      Services.ppmm.broadcastAsyncMessage(msg, data);
-    });
-  }
-
-  runManifest(manifest) {
-    // Strip leading slashes from web_accessible_resources.
-    let strippedWebAccessibleResources = [];
-    if (manifest.web_accessible_resources) {
-      strippedWebAccessibleResources = manifest.web_accessible_resources.map(path => path.replace(/^\/+/, ""));
-    }
-
-    this.webAccessibleResources = new MatchGlobs(strippedWebAccessibleResources);
-
-    let promises = [];
-    for (let directive in manifest) {
-      if (manifest[directive] !== null) {
-        promises.push(Management.emit(`manifest_${directive}`, directive, this, manifest));
-      }
-    }
-
-    let data = Services.ppmm.initialProcessData;
-    if (!data["Extension:Extensions"]) {
-      data["Extension:Extensions"] = [];
-    }
-    let serial = this.serialize();
-    data["Extension:Extensions"].push(serial);
-
-    return this.broadcast("Extension:Startup", serial).then(() => {
-      return Promise.all(promises);
-    });
-  }
-
-  callOnClose(obj) {
-    this.onShutdown.add(obj);
-  }
-
-  forgetOnClose(obj) {
-    this.onShutdown.delete(obj);
-  }
-
-  get builtinMessages() {
-    return new Map([
-      ["@@extension_id", this.uuid],
-    ]);
-  }
-
-  // Reads the locale file for the given Gecko-compatible locale code, or if
-  // no locale is given, the available locale closest to the UI locale.
-  // Sets the currently selected locale on success.
-  initLocale(locale = undefined) {
-    // Ugh.
-    let super_ = super.initLocale.bind(this);
-
-    return Task.spawn(function* () {
-      if (locale === undefined) {
-        let locales = yield this.promiseLocales();
-
-        let localeList = Array.from(locales.keys(), locale => {
-          return {name: locale, locales: [locale]};
-        });
-
-        let match = Locale.findClosestLocale(localeList);
-        locale = match ? match.name : this.defaultLocale;
-      }
-
-      return super_(locale);
-    }.bind(this));
-  }
-
-  startup() {
-    let started = false;
-    return this.readManifest().then(() => {
-      ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
-      started = true;
-
-      if (!this.hasShutdown) {
-        return this.initLocale();
-      }
-    }).then(() => {
-      if (this.errors.length) {
-        return Promise.reject({errors: this.errors});
-      }
-
-      if (this.hasShutdown) {
-        return;
-      }
-
-      GlobalManager.init(this);
-
-      // The "startup" Management event sent on the extension instance itself
-      // is emitted just before the Management "startup" event,
-      // and it is used to run code that needs to be executed before
-      // any of the "startup" listeners.
-      this.emit("startup", this);
-      Management.emit("startup", this);
-
-      return this.runManifest(this.manifest);
-    }).then(() => {
-      Management.emit("ready", this);
-    }).catch(e => {
-      dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
-      Cu.reportError(e);
-
-      if (started) {
-        ExtensionManagement.shutdownExtension(this.uuid);
-      }
-
-      this.cleanupGeneratedFile();
-
-      throw e;
-    });
-  }
-
-  cleanupGeneratedFile() {
-    if (!this.cleanupFile) {
-      return;
-    }
-
-    let file = this.cleanupFile;
-    this.cleanupFile = null;
-
-    Services.obs.removeObserver(this, "xpcom-shutdown");
-
-    this.broadcast("Extension:FlushJarCache", {path: file.path}).then(() => {
-      // We can't delete this file until everyone using it has
-      // closed it (because Windows is dumb). So we wait for all the
-      // child processes (including the parent) to flush their JAR
-      // caches. These caches may keep the file open.
-      file.remove(false);
-    });
-  }
-
-  shutdown() {
-    this.hasShutdown = true;
-    if (!this.manifest) {
-      ExtensionManagement.shutdownExtension(this.uuid);
-
-      this.cleanupGeneratedFile();
-      return;
-    }
-
-    GlobalManager.uninit(this);
-
-    for (let obj of this.onShutdown) {
-      obj.close();
-    }
-
-    for (let api of this.apis) {
-      api.destroy();
-    }
-
-    ParentAPIManager.shutdownExtension(this.id);
-
-    Management.emit("shutdown", this);
-
-    Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
-
-    MessageChannel.abortResponses({extensionId: this.id});
-
-    ExtensionManagement.shutdownExtension(this.uuid);
-
-    this.cleanupGeneratedFile();
-  }
-
-  observe(subject, topic, data) {
-    if (topic == "xpcom-shutdown") {
-      this.cleanupGeneratedFile();
-    }
-  }
-
-  hasPermission(perm) {
-    let match = /^manifest:(.*)/.exec(perm);
-    if (match) {
-      return this.manifest[match[1]] != null;
-    }
-
-    return this.permissions.has(perm);
-  }
-
-  get name() {
-    return this.manifest.name;
-  }
-};
+}
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -27,20 +27,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Locale",
                                   "resource://gre/modules/Locale.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
-                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
-                                  "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
                                    "@mozilla.org/content/style-sheet-service;1",
                                    "nsIStyleSheetService");
 
 function getConsole() {
@@ -178,350 +174,16 @@ class DefaultMap extends Map {
 
 class SpreadArgs extends Array {
   constructor(args) {
     super();
     this.push(...args);
   }
 }
 
-let gContextId = 0;
-
-class BaseContext {
-  constructor(envType, extension) {
-    this.envType = envType;
-    this.onClose = new Set();
-    this.checkedLastError = false;
-    this._lastError = null;
-    this.contextId = `${++gContextId}-${Services.appinfo.uniqueProcessID}`;
-    this.unloaded = false;
-    this.extension = extension;
-    this.jsonSandbox = null;
-    this.active = true;
-    this.incognito = null;
-    this.messageManager = null;
-    this.docShell = null;
-    this.contentWindow = null;
-    this.innerWindowID = 0;
-  }
-
-  setContentWindow(contentWindow) {
-    let {document} = contentWindow;
-    let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                                .getInterface(Ci.nsIDocShell);
-
-    this.innerWindowID = getInnerWindowID(contentWindow);
-    this.messageManager = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
-                                  .getInterface(Ci.nsIContentFrameMessageManager);
-
-    if (this.incognito == null) {
-      this.incognito = PrivateBrowsingUtils.isContentWindowPrivate(contentWindow);
-    }
-
-    MessageChannel.setupMessageManagers([this.messageManager]);
-
-    let onPageShow = event => {
-      if (!event || event.target === document) {
-        this.docShell = docShell;
-        this.contentWindow = contentWindow;
-        this.active = true;
-      }
-    };
-    let onPageHide = event => {
-      if (!event || event.target === document) {
-        // Put this off until the next tick.
-        Promise.resolve().then(() => {
-          this.docShell = null;
-          this.contentWindow = null;
-          this.active = false;
-        });
-      }
-    };
-
-    onPageShow();
-    contentWindow.addEventListener("pagehide", onPageHide, true);
-    contentWindow.addEventListener("pageshow", onPageShow, true);
-    this.callOnClose({
-      close: () => {
-        onPageHide();
-        if (this.active) {
-          contentWindow.removeEventListener("pagehide", onPageHide, true);
-          contentWindow.removeEventListener("pageshow", onPageShow, true);
-        }
-      },
-    });
-  }
-
-  get cloneScope() {
-    throw new Error("Not implemented");
-  }
-
-  get principal() {
-    throw new Error("Not implemented");
-  }
-
-  runSafe(...args) {
-    if (this.unloaded) {
-      Cu.reportError("context.runSafe called after context unloaded");
-    } else if (!this.active) {
-      Cu.reportError("context.runSafe called while context is inactive");
-    } else {
-      return runSafeSync(this, ...args);
-    }
-  }
-
-  runSafeWithoutClone(...args) {
-    if (this.unloaded) {
-      Cu.reportError("context.runSafeWithoutClone called after context unloaded");
-    } else if (!this.active) {
-      Cu.reportError("context.runSafeWithoutClone called while context is inactive");
-    } else {
-      return runSafeSyncWithoutClone(...args);
-    }
-  }
-
-  checkLoadURL(url, 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.checkLoadURIStrWithPrincipal(this.principal, url, flags);
-    } catch (e) {
-      return false;
-    }
-    return true;
-  }
-
-  /**
-   * 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
-   */
-  jsonStringify(...args) {
-    if (!this.jsonSandbox) {
-      this.jsonSandbox = Cu.Sandbox(this.principal, {
-        sameZoneAs: this.cloneScope,
-        wantXrays: false,
-      });
-    }
-
-    return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args);
-  }
-
-  callOnClose(obj) {
-    this.onClose.add(obj);
-  }
-
-  forgetOnClose(obj) {
-    this.onClose.delete(obj);
-  }
-
-  /**
-   * A wrapper around MessageChannel.sendMessage which adds the extension ID
-   * to the recipient object, and ensures replies are not processed after the
-   * context has been unloaded.
-   *
-   * @param {nsIMessageManager} target
-   * @param {string} messageName
-   * @param {object} data
-   * @param {object} [options]
-   * @param {object} [options.sender]
-   * @param {object} [options.recipient]
-   *
-   * @returns {Promise}
-   */
-  sendMessage(target, messageName, data, options = {}) {
-    options.recipient = options.recipient || {};
-    options.sender = options.sender || {};
-
-    // TODO(robwu): This should not unconditionally be overwritten once we
-    // support onMessageExternal / onConnectExternal (bugzil.la/1258360).
-    options.recipient.extensionId = this.extension.id;
-    options.sender.extensionId = this.extension.id;
-    options.sender.contextId = this.contextId;
-
-    return MessageChannel.sendMessage(target, messageName, data, options);
-  }
-
-  get lastError() {
-    this.checkedLastError = true;
-    return this._lastError;
-  }
-
-  set lastError(val) {
-    this.checkedLastError = false;
-    this._lastError = val;
-  }
-
-  /**
-   * Normalizes the given error object for use by the target scope. If
-   * the target is an error object which belongs to that scope, it is
-   * returned as-is. If it is an ordinary object with a `message`
-   * property, it is converted into an error belonging to the target
-   * scope. If it is an Error object which does *not* belong to the
-   * clone scope, it is reported, and converted to an unexpected
-   * exception error.
-   *
-   * @param {Error|object} error
-   * @returns {Error}
-   */
-  normalizeError(error) {
-    if (error instanceof this.cloneScope.Error) {
-      return error;
-    }
-    let message;
-    if (instanceOf(error, "Object") || error instanceof ExtensionError) {
-      message = error.message;
-    } else if (typeof error == "object" &&
-        this.principal.subsumes(Cu.getObjectPrincipal(error))) {
-      message = error.message;
-    } else {
-      Cu.reportError(error);
-    }
-    message = message || "An unexpected error occurred";
-    return new this.cloneScope.Error(message);
-  }
-
-  /**
-   * Sets the value of `.lastError` to `error`, calls the given
-   * callback, and reports an error if the value has not been checked
-   * when the callback returns.
-   *
-   * @param {object} error An object with a `message` property. May
-   *     optionally be an `Error` object belonging to the target scope.
-   * @param {function} callback The callback to call.
-   * @returns {*} The return value of callback.
-   */
-  withLastError(error, callback) {
-    this.lastError = this.normalizeError(error);
-    try {
-      return callback();
-    } finally {
-      if (!this.checkedLastError) {
-        Cu.reportError(`Unchecked lastError value: ${this.lastError}`);
-      }
-      this.lastError = null;
-    }
-  }
-
-  /**
-   * Wraps the given promise so it can be safely returned to extension
-   * code in this context.
-   *
-   * If `callback` is provided, however, it is used as a completion
-   * function for the promise, and no promise is returned. In this case,
-   * the callback is called when the promise resolves or rejects. In the
-   * latter case, `lastError` is set to the rejection value, and the
-   * callback function must check `browser.runtime.lastError` or
-   * `extension.runtime.lastError` in order to prevent it being reported
-   * to the console.
-   *
-   * @param {Promise} promise The promise with which to wrap the
-   *     callback. May resolve to a `SpreadArgs` instance, in which case
-   *     each element will be used as a separate argument.
-   *
-   *     Unless the promise object belongs to the cloneScope global, its
-   *     resolution value is cloned into cloneScope prior to calling the
-   *     `callback` function or resolving the wrapped promise.
-   *
-   * @param {function} [callback] The callback function to wrap
-   *
-   * @returns {Promise|undefined} If callback is null, a promise object
-   *     belonging to the target scope. Otherwise, undefined.
-   */
-  wrapPromise(promise, callback = null) {
-    let runSafe = this.runSafe.bind(this);
-    if (promise instanceof this.cloneScope.Promise) {
-      runSafe = this.runSafeWithoutClone.bind(this);
-    }
-
-    if (callback) {
-      promise.then(
-        args => {
-          if (this.unloaded) {
-            dump(`Promise resolved after context unloaded\n`);
-          } else if (!this.active) {
-            dump(`Promise resolved while context is inactive\n`);
-          } else if (args instanceof SpreadArgs) {
-            runSafe(callback, ...args);
-          } else {
-            runSafe(callback, args);
-          }
-        },
-        error => {
-          this.withLastError(error, () => {
-            if (this.unloaded) {
-              dump(`Promise rejected after context unloaded\n`);
-            } else if (!this.active) {
-              dump(`Promise rejected while context is inactive\n`);
-            } else {
-              this.runSafeWithoutClone(callback);
-            }
-          });
-        });
-    } else {
-      return new this.cloneScope.Promise((resolve, reject) => {
-        promise.then(
-          value => {
-            if (this.unloaded) {
-              dump(`Promise resolved after context unloaded\n`);
-            } else if (!this.active) {
-              dump(`Promise resolved while context is inactive\n`);
-            } else if (value instanceof SpreadArgs) {
-              runSafe(resolve, value.length == 1 ? value[0] : value);
-            } else {
-              runSafe(resolve, value);
-            }
-          },
-          value => {
-            if (this.unloaded) {
-              dump(`Promise rejected after context unloaded: ${value && value.message}\n`);
-            } else if (!this.active) {
-              dump(`Promise rejected while context is inactive: ${value && value.message}\n`);
-            } else {
-              this.runSafeWithoutClone(reject, this.normalizeError(value));
-            }
-          });
-      });
-    }
-  }
-
-  unload() {
-    this.unloaded = true;
-
-    MessageChannel.abortResponses({
-      extensionId: this.extension.id,
-      contextId: this.contextId,
-    });
-
-    for (let obj of this.onClose) {
-      obj.close();
-    }
-  }
-
-  /**
-   * A simple proxy for unload(), for use with callOnClose().
-   */
-  close() {
-    this.unload();
-  }
-}
-
 // Manages icon details for toolbar buttons in the |pageAction| and
 // |browserAction| APIs.
 let IconDetails = {
   // 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
@@ -1204,384 +866,23 @@ function promiseObserved(topic, test = (
         Services.obs.removeObserver(observer, topic);
         resolve({subject, data});
       }
     };
     Services.obs.addObserver(observer, topic, false);
   });
 }
 
-
-/*
- * Messaging primitives.
- */
-
-let gNextPortId = 1;
-
-/**
- * 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.
- * @param {string} id An ID that uniquely identifies this port's channel.
- * @param {object} sender The `port.sender` property.
- * @param {object} recipient The recipient of messages sent from this port.
- */
-function Port(context, senderMM, receiverMMs, name, id, sender, recipient) {
-  this.context = context;
-  this.senderMM = senderMM;
-  this.receiverMMs = receiverMMs;
-  this.name = name;
-  this.id = id;
-  this.sender = sender;
-  this.recipient = recipient;
-  this.disconnected = false;
-  this.disconnectListeners = new Set();
-  this.unregisterMessageFuncs = new Set();
-
-  // Common options for onMessage and onDisconnect.
-  this.handlerBase = {
-    messageFilterStrict: {portId: id},
-    filterMessage: (sender, recipient) => {
-      if (!sender.contextId) {
-        Cu.reportError("Missing sender.contextId in message to Port");
-        return false;
-      }
-      return sender.contextId !== this.context.contextId;
-    },
-  };
-
-  this.disconnectHandler = Object.assign({
-    receiveMessage: ({data}) => this.disconnectByOtherEnd(data),
-  }, this.handlerBase);
-  MessageChannel.addListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
-  this.context.callOnClose(this);
-}
-
-Port.prototype = {
-  api() {
-    let portObj = Cu.createObjectIn(this.context.cloneScope);
-
-    let portError = null;
-    let publicAPI = {
-      name: this.name,
-      disconnect: () => {
-        this.disconnect();
-      },
-      postMessage: json => {
-        this.postMessage(json);
-      },
-      onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => {
-        return this.registerOnDisconnect(error => {
-          portError = error && this.context.normalizeError(error);
-          fire.withoutClone(portObj);
-        });
-      }).api(),
-      onMessage: new EventManager(this.context, "Port.onMessage", fire => {
-        return this.registerOnMessage(msg => {
-          msg = Cu.cloneInto(msg, this.context.cloneScope);
-          fire.withoutClone(msg, portObj);
-        });
-      }).api(),
-
-      get error() {
-        return portError;
-      },
-    };
-
-    if (this.sender) {
-      publicAPI.sender = this.sender;
-    }
-
-    injectAPI(publicAPI, portObj);
-    return portObj;
-  },
-
-  postMessage(json) {
-    if (this.disconnected) {
-      throw new this.context.cloneScope.Error("Attempt to postMessage on disconnected port");
-    }
-
-    this._sendMessage("Extension:Port:PostMessage", json);
-  },
-
-  /**
-   * Register a callback that is called when the port is disconnected by the
-   * *other* end. The callback is automatically unregistered when the port or
-   * context is closed.
-   *
-   * @param {function} callback Called when the other end disconnects the port.
-   *     If the disconnect is caused by an error, the first parameter is an
-   *     object with a "message" string property that describes the cause.
-   * @returns {function} Function to unregister the listener.
-   */
-  registerOnDisconnect(callback) {
-    let listener = error => {
-      if (this.context.active && !this.disconnected) {
-        callback(error);
-      }
-    };
-    this.disconnectListeners.add(listener);
-    return () => {
-      this.disconnectListeners.delete(listener);
-    };
-  },
-
-  /**
-   * Register a callback that is called when a message is received. The callback
-   * is automatically unregistered when the port or context is closed.
-   *
-   * @param {function} callback Called when a message is received.
-   * @returns {function} Function to unregister the listener.
-   */
-  registerOnMessage(callback) {
-    let handler = Object.assign({
-      receiveMessage: ({data}) => {
-        if (this.context.active && !this.disconnected) {
-          callback(data);
-        }
-      },
-    }, this.handlerBase);
-
-    let unregister = () => {
-      this.unregisterMessageFuncs.delete(unregister);
-      MessageChannel.removeListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
-    };
-    MessageChannel.addListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
-    this.unregisterMessageFuncs.add(unregister);
-    return unregister;
-  },
-
-  _sendMessage(message, data) {
-    let options = {
-      recipient: Object.assign({}, this.recipient, {portId: this.id}),
-      responseType: MessageChannel.RESPONSE_NONE,
-    };
-
-    return this.context.sendMessage(this.senderMM, message, data, options);
-  },
-
-  handleDisconnection() {
-    MessageChannel.removeListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
-    for (let unregister of this.unregisterMessageFuncs) {
-      unregister();
-    }
-    this.context.forgetOnClose(this);
-    this.disconnected = true;
-  },
-
-  /**
-   * Disconnect the port from the other end (which may not even exist).
-   *
-   * @param {Error|{message: string}} [error] The reason for disconnecting,
-   *     if it is an abnormal disconnect.
-   */
-  disconnectByOtherEnd(error = null) {
-    if (this.disconnected) {
-      return;
-    }
-
-    for (let listener of this.disconnectListeners) {
-      listener(error);
-    }
-
-    this.handleDisconnection();
-  },
-
-  /**
-   * Disconnect the port from this end.
-   *
-   * @param {Error|{message: string}} [error] The reason for disconnecting,
-   *     if it is an abnormal disconnect.
-   */
-  disconnect(error = null) {
-    if (this.disconnected) {
-      // disconnect() may be called without side effects even after the port is
-      // closed - https://developer.chrome.com/extensions/runtime#type-Port
-      return;
-    }
-    this.handleDisconnection();
-    if (error) {
-      error = {message: this.context.normalizeError(error).message};
-    }
-    this._sendMessage("Extension:Port:Disconnect", error);
-  },
-
-  close() {
-    this.disconnect();
-  },
-};
-
 function getMessageManager(target) {
   if (target instanceof Ci.nsIFrameLoaderOwner) {
     return target.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
   }
   return target.QueryInterface(Ci.nsIMessageSender);
 }
 
-/**
- * Each extension context gets its own Messenger object. It handles the
- * basics of sendMessage, onMessage, connect and onConnect.
- *
- * @param {BaseContext} context The context to which this Messenger is tied.
- * @param {Array<nsIMessageListenerManager>} messageManagers
- *     The message managers used to receive messages (e.g. onMessage/onConnect
- *     requests).
- * @param {object} sender Describes this sender to the recipient. This object
- *     is extended further by BaseContext's sendMessage method and appears as
- *     the `sender` object to `onConnect` and `onMessage`.
- *     Do not set the `extensionId`, `contextId` or `tab` properties. The former
- *     two are added by BaseContext's sendMessage, while `sender.tab` is set by
- *     the ProxyMessenger in the main process.
- * @param {object} filter A recipient filter to apply to incoming messages from
- *     the broker. Messages are only handled by this Messenger if all key-value
- *     pairs match the `recipient` as specified by the sender of the message.
- *     In other words, this filter defines the required fields of `recipient`.
- * @param {object} [optionalFilter] An additional filter to apply to incoming
- *     messages. Unlike `filter`, the keys from `optionalFilter` are allowed to
- *     be omitted from `recipient`. Only keys that are present in both
- *     `optionalFilter` and `recipient` are applied to filter incoming messages.
- */
-function Messenger(context, messageManagers, sender, filter, optionalFilter) {
-  this.context = context;
-  this.messageManagers = messageManagers;
-  this.sender = sender;
-  this.filter = filter;
-  this.optionalFilter = optionalFilter;
-}
-
-Messenger.prototype = {
-  _sendMessage(messageManager, message, data, recipient) {
-    let options = {
-      recipient,
-      sender: this.sender,
-      responseType: MessageChannel.RESPONSE_FIRST,
-    };
-
-    return this.context.sendMessage(messageManager, message, data, options);
-  },
-
-  sendMessage(messageManager, msg, recipient, responseCallback) {
-    let promise = this._sendMessage(messageManager, "Extension:Message", msg, recipient)
-      .catch(error => {
-        if (error.result == MessageChannel.RESULT_NO_HANDLER) {
-          return Promise.reject({message: "Could not establish connection. Receiving end does not exist."});
-        } else if (error.result != MessageChannel.RESULT_NO_RESPONSE) {
-          return Promise.reject({message: error.message});
-        }
-      });
-
-    return this.context.wrapPromise(promise, responseCallback);
-  },
-
-  onMessage(name) {
-    return new SingletonEventManager(this.context, name, callback => {
-      let listener = {
-        messageFilterPermissive: this.optionalFilter,
-        messageFilterStrict: this.filter,
-
-        filterMessage: (sender, recipient) => {
-          // Ignore the message if it was sent by this Messenger.
-          return sender.contextId !== this.context.contextId;
-        },
-
-        receiveMessage: ({target, data: message, sender, recipient}) => {
-          if (!this.context.active) {
-            return;
-          }
-
-          let sendResponse;
-          let response = undefined;
-          let promise = new Promise(resolve => {
-            sendResponse = value => {
-              resolve(value);
-              response = promise;
-            };
-          });
-
-          message = Cu.cloneInto(message, this.context.cloneScope);
-          sender = Cu.cloneInto(sender, this.context.cloneScope);
-          sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
-
-          // Note: We intentionally do not use runSafe here so that any
-          // errors are propagated to the message sender.
-          let result = callback(message, sender, sendResponse);
-          if (result instanceof this.context.cloneScope.Promise) {
-            return result;
-          } else if (result === true) {
-            return promise;
-          }
-          return response;
-        },
-      };
-
-      MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
-      return () => {
-        MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
-      };
-    }).api();
-  },
-
-  connectGetRawPort(messageManager, name, recipient) {
-    let portId = `${gNextPortId++}-${Services.appinfo.uniqueProcessID}`;
-    let port = new Port(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
-    let msg = {name, portId};
-    this._sendMessage(messageManager, "Extension:Connect", msg, recipient)
-      .catch(e => {
-        if (e.result === MessageChannel.RESULT_NO_HANDLER) {
-          e = {message: "Could not establish connection. Receiving end does not exist."};
-        } else if (e.result === MessageChannel.RESULT_DISCONNECTED) {
-          e = null;
-        }
-        port.disconnectByOtherEnd(e);
-      });
-    return port;
-  },
-
-  connect(messageManager, name, recipient) {
-    let port = this.connectGetRawPort(messageManager, name, recipient);
-    return port.api();
-  },
-
-  onConnect(name) {
-    return new SingletonEventManager(this.context, name, callback => {
-      let listener = {
-        messageFilterPermissive: this.optionalFilter,
-        messageFilterStrict: this.filter,
-
-        filterMessage: (sender, recipient) => {
-          // Ignore the port if it was created by this Messenger.
-          return sender.contextId !== this.context.contextId;
-        },
-
-        receiveMessage: ({target, data: message, sender}) => {
-          let {name, portId} = message;
-          let mm = getMessageManager(target);
-          let recipient = Object.assign({}, sender);
-          if (recipient.tab) {
-            recipient.tabId = recipient.tab.id;
-            delete recipient.tab;
-          }
-          let port = new Port(this.context, mm, this.messageManagers, name, portId, sender, recipient);
-          this.context.runSafeWithoutClone(callback, port.api());
-          return true;
-        },
-      };
-
-      MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
-      return () => {
-        MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
-      };
-    }).api();
-  },
-};
-
 function flushJarCache(jarFile) {
   Services.obs.notifyObservers(jarFile, "flush-cache-entry", null);
 }
 
 const PlatformInfo = Object.freeze({
   os: (function() {
     let os = AppConstants.platform;
     if (os == "macosx") {
@@ -1609,562 +910,16 @@ function detectLanguage(text) {
         language: lang.languageCode,
         percentage: lang.percent,
       };
     }),
   }));
 }
 
 /**
- * An object that runs the implementation of a schema API. Instantiations of
- * this interfaces are used by Schemas.jsm.
- *
- * @interface
- */
-class SchemaAPIInterface {
-  /**
-   * Calls this as a function that returns its return value.
-   *
-   * @abstract
-   * @param {Array} args The parameters for the function.
-   * @returns {*} The return value of the invoked function.
-   */
-  callFunction(args) {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Calls this as a function and ignores its return value.
-   *
-   * @abstract
-   * @param {Array} args The parameters for the function.
-   */
-  callFunctionNoReturn(args) {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Calls this as a function that completes asynchronously.
-   *
-   * @abstract
-   * @param {Array} args The parameters for the function.
-   * @param {function(*)} [callback] The callback to be called when the function
-   *     completes.
-   * @returns {Promise|undefined} Must be void if `callback` is set, and a
-   *     promise otherwise. The promise is resolved when the function completes.
-   */
-  callAsyncFunction(args, callback) {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Retrieves the value of this as a property.
-   *
-   * @abstract
-   * @returns {*} The value of the property.
-   */
-  getProperty() {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Assigns the value to this as property.
-   *
-   * @abstract
-   * @param {string} value The new value of the property.
-   */
-  setProperty(value) {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Registers a `listener` to this as an event.
-   *
-   * @abstract
-   * @param {function} listener The callback to be called when the event fires.
-   * @param {Array} args Extra parameters for EventManager.addListener.
-   * @see EventManager.addListener
-   */
-  addListener(listener, args) {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Checks whether `listener` is listening to this as an event.
-   *
-   * @abstract
-   * @param {function} listener The event listener.
-   * @returns {boolean} Whether `listener` is registered with this as an event.
-   * @see EventManager.hasListener
-   */
-  hasListener(listener) {
-    throw new Error("Not implemented");
-  }
-
-  /**
-   * Unregisters `listener` from this as an event.
-   *
-   * @abstract
-   * @param {function} listener The event listener.
-   * @see EventManager.removeListener
-   */
-  removeListener(listener) {
-    throw new Error("Not implemented");
-  }
-}
-
-/**
- * An object that runs a locally implemented API.
- */
-class LocalAPIImplementation extends SchemaAPIInterface {
-  /**
-   * Constructs an implementation of the `name` method or property of `pathObj`.
-   *
-   * @param {object} pathObj The object containing the member with name `name`.
-   * @param {string} name The name of the implemented member.
-   * @param {BaseContext} context The context in which the schema is injected.
-   */
-  constructor(pathObj, name, context) {
-    super();
-    this.pathObj = pathObj;
-    this.name = name;
-    this.context = context;
-  }
-
-  callFunction(args) {
-    return this.pathObj[this.name](...args);
-  }
-
-  callFunctionNoReturn(args) {
-    this.pathObj[this.name](...args);
-  }
-
-  callAsyncFunction(args, callback) {
-    let promise;
-    try {
-      promise = this.pathObj[this.name](...args) || Promise.resolve();
-    } catch (e) {
-      promise = Promise.reject(e);
-    }
-    return this.context.wrapPromise(promise, callback);
-  }
-
-  getProperty() {
-    return this.pathObj[this.name];
-  }
-
-  setProperty(value) {
-    this.pathObj[this.name] = value;
-  }
-
-  addListener(listener, args) {
-    try {
-      this.pathObj[this.name].addListener.call(null, listener, ...args);
-    } catch (e) {
-      throw this.context.normalizeError(e);
-    }
-  }
-
-  hasListener(listener) {
-    return this.pathObj[this.name].hasListener.call(null, listener);
-  }
-
-  removeListener(listener) {
-    this.pathObj[this.name].removeListener.call(null, listener);
-  }
-}
-
-let nextId = 1;
-
-/**
- * An object that runs an remote implementation of an API.
- */
-class ProxyAPIImplementation extends SchemaAPIInterface {
-  /**
-   * @param {string} namespace The full path to the namespace that contains the
-   *     `name` member. This may contain dots, e.g. "storage.local".
-   * @param {string} name The name of the method or property.
-   * @param {ChildAPIManager} childApiManager The owner of this implementation.
-   */
-  constructor(namespace, name, childApiManager) {
-    super();
-    this.path = `${namespace}.${name}`;
-    this.childApiManager = childApiManager;
-  }
-
-  callFunctionNoReturn(args) {
-    this.childApiManager.callParentFunctionNoReturn(this.path, args);
-  }
-
-  callAsyncFunction(args, callback) {
-    return this.childApiManager.callParentAsyncFunction(this.path, args, callback);
-  }
-
-  addListener(listener, args) {
-    let set = this.childApiManager.listeners.get(this.path);
-    if (!set) {
-      set = new Set();
-      this.childApiManager.listeners.set(this.path, set);
-    }
-
-    set.add(listener);
-
-    if (set.size == 1) {
-      args = args.slice(1);
-
-      this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", {
-        childId: this.childApiManager.id,
-        path: this.path,
-        args,
-      });
-    }
-  }
-
-  removeListener(listener) {
-    let set = this.childApiManager.listeners.get(this.path);
-    if (!set) {
-      return;
-    }
-    set.delete(listener);
-
-    if (set.size == 0) {
-      this.childApiManager.messageManager.sendAsyncMessage("API:RemoveListener", {
-        childId: this.childApiManager.id,
-        path: this.path,
-      });
-    }
-  }
-
-  hasListener(listener) {
-    let set = this.childApiManager.listeners.get(this.path);
-    return set ? set.has(listener) : false;
-  }
-}
-
-// We create one instance of this class for every extension context
-// that needs to use remote APIs. It uses the message manager to
-// communicate with the ParentAPIManager singleton in
-// Extension.jsm. It handles asynchronous function calls as well as
-// event listeners.
-class ChildAPIManager {
-  constructor(context, messageManager, localApis, contextData) {
-    this.context = context;
-    this.messageManager = messageManager;
-
-    // The root namespace of all locally implemented APIs. If an extension calls
-    // an API that does not exist in this object, then the implementation is
-    // delegated to the ParentAPIManager.
-    this.localApis = localApis;
-
-    let id = String(context.extension.id) + "." + String(context.contextId);
-    this.id = id;
-
-    let data = {childId: id, extensionId: context.extension.id, principal: context.principal};
-    Object.assign(data, contextData);
-
-    messageManager.addMessageListener("API:RunListener", this);
-    messageManager.addMessageListener("API:CallResult", this);
-
-    // Map[path -> Set[listener]]
-    // path is, e.g., "runtime.onMessage".
-    this.listeners = new Map();
-
-    // Map[callId -> Deferred]
-    this.callPromises = new Map();
-
-    this.createProxyContextInConstructor(data);
-  }
-
-  createProxyContextInConstructor(data) {
-    this.messageManager.sendAsyncMessage("API:CreateProxyContext", data);
-  }
-
-  receiveMessage({name, data}) {
-    if (data.childId != this.id) {
-      return;
-    }
-
-    switch (name) {
-      case "API:RunListener":
-        let listeners = this.listeners.get(data.path);
-        for (let callback of listeners) {
-          runSafe(this.context, callback, ...data.args);
-        }
-        break;
-
-      case "API:CallResult":
-        let deferred = this.callPromises.get(data.callId);
-        if ("error" in data) {
-          deferred.reject(data.error);
-        } else {
-          deferred.resolve(new SpreadArgs(data.result));
-        }
-        this.callPromises.delete(data.callId);
-        break;
-    }
-  }
-
-  /**
-   * Call a function in the parent process and ignores its return value.
-   *
-   * @param {string} path The full name of the method, e.g. "tabs.create".
-   * @param {Array} args The parameters for the function.
-   */
-  callParentFunctionNoReturn(path, args) {
-    this.messageManager.sendAsyncMessage("API:Call", {
-      childId: this.id,
-      path,
-      args,
-    });
-  }
-
-  /**
-   * Calls a function in the parent process and returns its result
-   * asynchronously.
-   *
-   * @param {string} path The full name of the method, e.g. "tabs.create".
-   * @param {Array} args The parameters for the function.
-   * @param {function(*)} [callback] The callback to be called when the function
-   *     completes.
-   * @returns {Promise|undefined} Must be void if `callback` is set, and a
-   *     promise otherwise. The promise is resolved when the function completes.
-   */
-  callParentAsyncFunction(path, args, callback) {
-    let callId = nextId++;
-    let deferred = PromiseUtils.defer();
-    this.callPromises.set(callId, deferred);
-
-    this.messageManager.sendAsyncMessage("API:Call", {
-      childId: this.id,
-      callId,
-      path,
-      args,
-    });
-
-    return this.context.wrapPromise(deferred.promise, callback);
-  }
-
-  /**
-   * Create a proxy for an event in the parent process. The returned event
-   * object shares its internal state with other instances. For instance, if
-   * `removeListener` is used on a listener that was added on another object
-   * through `addListener`, then the event is unregistered.
-   *
-   * @param {string} path The full name of the event, e.g. "tabs.onCreated".
-   * @returns {object} An object with the addListener, removeListener and
-   *   hasListener methods. See SchemaAPIInterface for documentation.
-   */
-  getParentEvent(path) {
-    let parsed = /^(.+)\.(on[A-Z][^.]+)$/.exec(path);
-    if (!parsed) {
-      throw new Error("getParentEvent: Invalid event name: " + path);
-    }
-    let [, namespace, name] = parsed;
-    let impl = new ProxyAPIImplementation(namespace, name, this);
-    return {
-      addListener: (listener, ...args) => impl.addListener(listener, args),
-      removeListener: (listener) => impl.removeListener(listener),
-      hasListener: (listener) => impl.hasListener(listener),
-    };
-  }
-
-  close() {
-    this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
-  }
-
-  get cloneScope() {
-    return this.context.cloneScope;
-  }
-
-  get principal() {
-    return this.context.principal;
-  }
-
-  shouldInject(namespace, name, allowedContexts) {
-    // Do not generate content script APIs, unless explicitly allowed.
-    if (this.context.envType === "content_child" &&
-        !allowedContexts.includes("content")) {
-      return false;
-    }
-    if (allowedContexts.includes("addon_parent_only")) {
-      return false;
-    }
-    return true;
-  }
-
-  getImplementation(namespace, name) {
-    let pathObj = this.localApis;
-    if (pathObj) {
-      for (let part of namespace.split(".")) {
-        pathObj = pathObj[part];
-        if (!pathObj) {
-          break;
-        }
-      }
-      if (pathObj && name in pathObj) {
-        return new LocalAPIImplementation(pathObj, name, this.context);
-      }
-    }
-
-    return this.getFallbackImplementation(namespace, name);
-  }
-
-  getFallbackImplementation(namespace, name) {
-    // No local API found, defer implementation to the parent.
-    return new ProxyAPIImplementation(namespace, name, this);
-  }
-
-  hasPermission(permission) {
-    return this.context.extension.hasPermission(permission);
-  }
-}
-
-/**
- * This object loads the ext-*.js scripts that define the extension API.
- *
- * This class instance is shared with the scripts that it loads, so that the
- * ext-*.js scripts and the instantiator can communicate with each other.
- */
-class SchemaAPIManager extends EventEmitter {
-  /**
-   * @param {string} processType
-   *     "main" - The main, one and only chrome browser process.
-   *     "addon" - An addon process.
-   *     "content" - A content process.
-   */
-  constructor(processType) {
-    super();
-    this.processType = processType;
-    this.global = this._createExtGlobal();
-    this._scriptScopes = [];
-    this._schemaApis = {
-      addon_parent: [],
-      addon_child: [],
-      content_parent: [],
-      content_child: [],
-    };
-  }
-
-  /**
-   * Create a global object that is used as the shared global for all ext-*.js
-   * scripts that are loaded via `loadScript`.
-   *
-   * @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, extensions: this});
-
-    XPCOMUtils.defineLazyGetter(global, "console", getConsole);
-
-    XPCOMUtils.defineLazyModuleGetter(global, "require",
-                                      "resource://devtools/shared/Loader.jsm");
-
-    return global;
-  }
-
-  /**
-   * Load an ext-*.js script. The script runs in its own scope, if it wishes to
-   * share state with another script it can assign to the `global` variable. If
-   * it wishes to communicate with this API manager, use `extensions`.
-   *
-   * @param {string} scriptUrl The URL of the ext-*.js script.
-   */
-  loadScript(scriptUrl) {
-    // Create the object in the context of the sandbox so that the script runs
-    // in the sandbox's context instead of here.
-    let scope = Cu.createObjectIn(this.global);
-
-    Services.scriptloader.loadSubScript(scriptUrl, scope, "UTF-8");
-
-    // Save the scope to avoid it being garbage collected.
-    this._scriptScopes.push(scope);
-  }
-
-  /**
-   * Called by an ext-*.js script to register an API.
-   *
-   * @param {string} namespace The API namespace.
-   *     Intended to match the namespace of the generated API, but not used at
-   *     the moment - see bugzil.la/1295774.
-   * @param {string} envType Restricts the API to contexts that run in the
-   *    given environment. Must be one of the following:
-   *     - "addon_parent" - addon APIs that runs in the main process.
-   *     - "addon_child" - addon APIs that runs in an addon process.
-   *     - "content_parent" - content script APIs that runs in the main process.
-   *     - "content_child" - content script APIs that runs in a content process.
-   * @param {function(BaseContext)} getAPI A function that returns an object
-   *     that will be merged with |chrome| and |browser|. The next example adds
-   *     the create, update and remove methods to the tabs API.
-   *
-   *     registerSchemaAPI("tabs", "addon_parent", (context) => ({
-   *       tabs: { create, update },
-   *     }));
-   *     registerSchemaAPI("tabs", "addon_parent", (context) => ({
-   *       tabs: { remove },
-   *     }));
-   */
-  registerSchemaAPI(namespace, envType, getAPI) {
-    this._schemaApis[envType].push({namespace, getAPI});
-  }
-
-  /**
-   * Exports all registered scripts to `obj`.
-   *
-   * @param {BaseContext} context The context for which the API bindings are
-   *     generated.
-   * @param {object} obj The destination of the API.
-   */
-  generateAPIs(context, obj) {
-    let apis = this._schemaApis[context.envType];
-    if (!apis) {
-      Cu.reportError(`No APIs have been registered for ${context.envType}`);
-      return;
-    }
-    SchemaAPIManager.generateAPIs(context, apis, obj);
-  }
-
-  /**
-   * Mash together all the APIs from `apis` into `obj`.
-   *
-   * @param {BaseContext} context The context for which the API bindings are
-   *     generated.
-   * @param {Array} apis A list of objects, see `registerSchemaAPI`.
-   * @param {object} obj The destination of the API.
-   */
-  static generateAPIs(context, apis, obj) {
-    // Recursively copy properties from source to dest.
-    function copy(dest, source) {
-      for (let prop in source) {
-        let desc = Object.getOwnPropertyDescriptor(source, prop);
-        if (typeof(desc.value) == "object") {
-          if (!(prop in dest)) {
-            dest[prop] = {};
-          }
-          copy(dest[prop], source[prop]);
-        } else {
-          Object.defineProperty(dest, prop, desc);
-        }
-      }
-    }
-
-    for (let api of apis) {
-      if (Schemas.checkPermissions(api.namespace, context.extension)) {
-        api = api.getAPI(context);
-        copy(obj, api);
-      }
-    }
-  }
-}
-
-/**
  * 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}
@@ -2218,46 +973,61 @@ 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;
+}
+
 this.ExtensionUtils = {
   defineLazyGetter,
   detectLanguage,
   extend,
+  findPathInObject,
   flushJarCache,
   getConsole,
   getInnerWindowID,
+  getMessageManager,
   ignoreEvent,
   injectAPI,
   instanceOf,
   normalizeTime,
   promiseDocumentLoaded,
   promiseDocumentReady,
   promiseEvent,
   promiseObserved,
   runSafe,
   runSafeSync,
   runSafeSyncWithoutClone,
   runSafeWithoutClone,
   stylesheetMap,
-  BaseContext,
   DefaultMap,
   DefaultWeakMap,
   EventEmitter,
   EventManager,
   ExtensionError,
   IconDetails,
-  LocalAPIImplementation,
   LocaleData,
-  Messenger,
-  Port,
   PlatformInfo,
-  SchemaAPIInterface,
   SingletonEventManager,
   SpreadArgs,
-  ChildAPIManager,
-  SchemaAPIManager,
 };
--- a/toolkit/components/extensions/LegacyExtensionsUtils.jsm
+++ b/toolkit/components/extensions/LegacyExtensionsUtils.jsm
@@ -17,22 +17,26 @@ const {classes: Cc, interfaces: Ci, util
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/ExtensionChild.jsm");
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 
 var {
   BaseContext,
+} = ExtensionCommon;
+
+var {
   Messenger,
-} = ExtensionUtils;
+} = ExtensionChild;
 
 /**
  * Instances created from this class provide to a legacy extension
  * a simple API to exchange messages with a webextension.
  */
 var LegacyExtensionContext = class extends BaseContext {
   /**
    * Create a new LegacyExtensionContext given a target Extension instance.
--- a/toolkit/components/extensions/NativeMessaging.jsm
+++ b/toolkit/components/extensions/NativeMessaging.jsm
@@ -12,18 +12,18 @@ const {classes: Cc, interfaces: Ci, util
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
-                                  "resource://gre/modules/ExtensionUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
+                                  "resource://gre/modules/ExtensionChild.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Subprocess",
                                   "resource://gre/modules/Subprocess.jsm");
@@ -226,17 +226,17 @@ this.NativeApp = class extends EventEmit
    *     and receive messages from the port's creator.
    * @param {string} portId A unique internal ID that identifies the port.
    * @param {object} sender The object describing the creator of the connection
    *     request.
    * @param {string} application The name of the native messaging host.
    */
   static onConnectNative(context, messageManager, portId, sender, application) {
     let app = new NativeApp(context, application);
-    let port = new ExtensionUtils.Port(context, messageManager, [messageManager], "", portId, sender, sender);
+    let port = new ExtensionChild.Port(context, messageManager, [messageManager], "", portId, sender, sender);
     app.once("disconnect", (what, err) => port.disconnect(err));
 
     /* eslint-disable mozilla/balanced-listeners */
     app.on("message", (what, msg) => port.postMessage(msg));
     /* eslint-enable mozilla/balanced-listeners */
 
     port.registerOnMessage(msg => app.send(msg));
     port.registerOnDisconnect(msg => app.close());
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -3,31 +3,34 @@
 # 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/.
 
 EXTRA_JS_MODULES += [
     'Extension.jsm',
     'ExtensionAPI.jsm',
     'ExtensionChild.jsm',
+    'ExtensionCommon.jsm',
     'ExtensionContent.jsm',
     'ExtensionManagement.jsm',
+    'ExtensionParent.jsm',
     'ExtensionStorage.jsm',
     'ExtensionUtils.jsm',
     'LegacyExtensionsUtils.jsm',
     'MessageChannel.jsm',
     'NativeMessaging.jsm',
     'Schemas.jsm',
 ]
 
 EXTRA_COMPONENTS += [
     'extensions-toolkit.manifest',
 ]
 
 TESTING_JS_MODULES += [
+    'ExtensionTestCommon.jsm',
     'ExtensionXPCShellUtils.jsm',
 ]
 
 DIRS += ['schemas']
 
 JAR_MANIFESTS += ['jar.mn']
 
 MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
--- a/toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_apimanager.js
@@ -1,15 +1,19 @@
 /* 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";
 
-let {ExtensionUtils: {SchemaAPIManager}} = Cu.import("resource://gre/modules/ExtensionUtils.jsm", {});
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+
+const {
+  SchemaAPIManager,
+} = ExtensionCommon;
 
 this.unknownvar = "Some module-global var";
 
 var gUniqueId = 0;
 
 // SchemaAPIManager's loadScript uses loadSubScript to load a script. This
 // requires a local (resource://) URL. So create such a temporary URL for
 // testing.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
@@ -1,17 +1,22 @@
 "use strict";
 
 const global = this;
 
 Cu.import("resource://gre/modules/Timer.jsm");
 
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
 var {
   BaseContext,
+} = ExtensionCommon;
+
+var {
   EventManager,
   SingletonEventManager,
 } = ExtensionUtils;
 
 class StubContext extends BaseContext {
   constructor() {
     let fakeExtension = {id: "test@web.extension"};
     super("testEnv", fakeExtension);
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -1,15 +1,15 @@
 "use strict";
 
 Components.utils.import("resource://gre/modules/Schemas.jsm");
 Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
-Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
 
-let {LocalAPIImplementation, SchemaAPIInterface} = ExtensionUtils;
+let {LocalAPIImplementation, SchemaAPIInterface} = ExtensionCommon;
 
 let json = [
   {namespace: "testing",
 
    properties: {
      PROP1: {value: 20},
      prop2: {type: "string"},
      prop3: {
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_api_injection.js
@@ -1,17 +1,17 @@
 "use strict";
 
-Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
 Components.utils.import("resource://gre/modules/Schemas.jsm");
 
 let {
   BaseContext,
   SchemaAPIManager,
-} = ExtensionUtils;
+} = ExtensionCommon;
 
 let nestedNamespaceJson = [
   {
     "namespace": "backgroundAPI.testnamespace",
     "functions": [
       {
         "name": "create",
         "type": "function",
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_async.js
@@ -1,14 +1,14 @@
 "use strict";
 
-Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
 Components.utils.import("resource://gre/modules/Schemas.jsm");
 
-let {BaseContext, LocalAPIImplementation} = ExtensionUtils;
+let {BaseContext, LocalAPIImplementation} = ExtensionCommon;
 
 let schemaJson = [
   {
     namespace: "testnamespace",
     functions: [{
       name: "one_required",
       type: "function",
       parameters: [{
--- a/toolkit/components/extensions/test/xpcshell/test_native_messaging.js
+++ b/toolkit/components/extensions/test/xpcshell/test_native_messaging.js
@@ -1,14 +1,14 @@
 "use strict";
 
 /* global OS, HostManifestManager, NativeApp */
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/AsyncShutdown.jsm");
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/Schemas.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 const {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
 Cu.import("resource://gre/modules/NativeMessaging.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 
 let registry = null;
@@ -79,17 +79,17 @@ let context = {
   jsonStringify(...args) { return JSON.stringify(...args); },
   cloneScope: global,
   logError() {},
   preprocessors: {},
   callOnClose: () => {},
   forgetOnClose: () => {},
 };
 
-class MockContext extends ExtensionUtils.BaseContext {
+class MockContext extends ExtensionCommon.BaseContext {
   constructor(extensionId) {
     let fakeExtension = {id: extensionId};
     super("testEnv", fakeExtension);
     this.sandbox = Cu.Sandbox(global);
   }
 
   get cloneScope() {
     return global;