Bug 1317697: Split extension page child and content script child code as much as possible. r?mixedpuppy draft
authorKris Maglione <maglione.k@gmail.com>
Thu, 13 Apr 2017 15:28:51 -0700
changeset 562400 7ca9c01d26684f36dbfbed28f6eb1ca056fcf09b
parent 562399 5400984045184797c789711791c5d74c2f8404bf
child 562401 e92b0ff2be481690ae7642bb1dc2bd43b6381564
push id54021
push usermaglione.k@gmail.com
push dateThu, 13 Apr 2017 22:30:15 +0000
reviewersmixedpuppy
bugs1317697
milestone55.0a1
Bug 1317697: Split extension page child and content script child code as much as possible. r?mixedpuppy MozReview-Commit-ID: 4lKFqoJHVFb
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ExtensionPageChild.jsm
toolkit/components/extensions/LegacyExtensionsUtils.jsm
toolkit/components/extensions/ProxyScriptContext.jsm
toolkit/components/extensions/extension-process-script.js
toolkit/components/extensions/moz.build
toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -1,14 +1,16 @@
 /* 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";
 
+/* exported ExtensionChild */
+
 this.EXPORTED_SYMBOLS = ["ExtensionChild"];
 
 /*
  * This file handles addon logic that is independent of the chrome process.
  * When addons run out-of-process, this is the main entry point.
  * Its primary function is managing addon globals.
  *
  * Don't put contentscript logic here, use ExtensionContent.jsm instead.
@@ -17,56 +19,50 @@ this.EXPORTED_SYMBOLS = ["ExtensionChild
 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");
 
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
-                                  "resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContent",
+                                  "resource://gre/modules/ExtensionContent.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, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
-                                  "resource://gre/modules/Schemas.jsm");
-
-const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
-const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
 
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 const {
   DefaultMap,
+  EventEmitter,
   LimitedSet,
+  LocaleData,
   SingletonEventManager,
   SpreadArgs,
   defineLazyGetter,
-  getInnerWindowID,
   getMessageManager,
   getUniqueId,
   injectAPI,
-  promiseEvent,
 } = ExtensionUtils;
 
 const {
-  BaseContext,
-  CanOfAPIs,
   LocalAPIImplementation,
   SchemaAPIInterface,
-  SchemaAPIManager,
 } = ExtensionCommon;
 
-var ExtensionChild;
-
 /**
  * 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.
@@ -458,47 +454,124 @@ class Messenger {
     return this._onConnect(name, sender => sender.id === this.sender.id);
   }
 
   onConnectExternal(name) {
     return this._onConnect(name, sender => sender.id !== this.sender.id);
   }
 }
 
-var apiManager = new class extends SchemaAPIManager {
-  constructor() {
-    super("addon");
-    this.initialized = false;
+// For test use only.
+var ExtensionManager = {
+  extensions: new Map(),
+};
+
+// Represents a browser extension in the content process.
+class BrowserExtensionContent extends EventEmitter {
+  constructor(data) {
+    super();
+
+    this.data = data;
+    this.id = data.id;
+    this.uuid = data.uuid;
+    this.instanceId = data.instanceId;
+
+    this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
+    Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
+
+    defineLazyGetter(this, "scripts", () => {
+      return data.content_scripts.map(scriptData => new ExtensionContent.Script(this, scriptData));
+    });
+
+    this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
+    this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
+    this.permissions = data.permissions;
+    this.optionalPermissions = data.optionalPermissions;
+    this.principal = data.principal;
+
+    this.localeData = new LocaleData(data.localeData);
+
+    this.manifest = data.manifest;
+    this.baseURI = Services.io.newURI(data.baseURL);
+
+    // Only used in addon processes.
+    this.views = new Set();
+
+    // Only used for devtools views.
+    this.devtoolsViews = new Set();
+
+    /* eslint-disable mozilla/balanced-listeners */
+    this.on("add-permissions", (ignoreEvent, permissions) => {
+      if (permissions.permissions.length > 0) {
+        for (let perm of permissions.permissions) {
+          this.permissions.add(perm);
+        }
+      }
+
+      if (permissions.origins.length > 0) {
+        this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...permissions.origins));
+      }
+    });
+
+    this.on("remove-permissions", (ignoreEvent, permissions) => {
+      if (permissions.permissions.length > 0) {
+        for (let perm of permissions.permissions) {
+          this.permissions.delete(perm);
+        }
+      }
+
+      if (permissions.origins.length > 0) {
+        for (let origin of permissions.origins) {
+          this.whiteListedHosts.removeOne(origin);
+        }
+      }
+    });
+    /* eslint-enable mozilla/balanced-listeners */
+
+    ExtensionManager.extensions.set(this.id, this);
   }
 
-  lazyInit() {
-    if (!this.initialized) {
-      this.initialized = true;
-      for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_ADDON)) {
-        this.loadScript(value);
-      }
+  shutdown() {
+    ExtensionManager.extensions.delete(this.id);
+    ExtensionContent.shutdownExtension(this);
+    Services.cpmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
+  }
+
+  getContext(window) {
+    return ExtensionContent.getContext(this, window);
+  }
+
+  emit(event, ...args) {
+    Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args});
+
+    super.emit(event, ...args);
+  }
+
+  receiveMessage({name, data}) {
+    if (name === this.MESSAGE_EMIT_EVENT) {
+      super.emit(data.event, ...data.args);
     }
   }
-}();
 
-var devtoolsAPIManager = new class extends SchemaAPIManager {
-  constructor() {
-    super("devtools");
-    this.initialized = false;
+  localizeMessage(...args) {
+    return this.localeData.localizeMessage(...args);
+  }
+
+  localize(...args) {
+    return this.localeData.localize(...args);
   }
 
-  lazyInit() {
-    if (!this.initialized) {
-      this.initialized = true;
-      for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS)) {
-        this.loadScript(value);
-      }
+  hasPermission(perm) {
+    let match = /^manifest:(.*)/.exec(perm);
+    if (match) {
+      return this.manifest[match[1]] != null;
     }
+    return this.permissions.has(perm);
   }
-}();
+}
 
 /**
  * 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".
@@ -793,385 +866,14 @@ class ChildAPIManager {
     return this.context.extension.optionalPermissions.includes(permission);
   }
 
   setPermissionsChangedCallback(callback) {
     this.permissionsChangedCallbacks.add(callback);
   }
 }
 
-class ExtensionBaseContextChild extends BaseContext {
-  /**
-   * This ExtensionBaseContextChild represents an addon execution environment
-   * that is running in an addon or devtools child process.
-   *
-   * @param {BrowserExtensionContent} extension This context's owner.
-   * @param {object} params
-   * @param {string} params.envType One of "addon_child" or "devtools_child".
-   * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
-   * @param {string} params.viewType One of "background", "popup", "tab",
-   *   "sidebar", "devtools_page" or "devtools_panel".
-   * @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
-   */
-  constructor(extension, params) {
-    if (!params.envType) {
-      throw new Error("Missing envType");
-    }
-
-    super(params.envType, extension);
-    let {viewType, uri, contentWindow, tabId} = params;
-    this.viewType = viewType;
-    this.uri = uri || extension.baseURI;
-
-    this.setContentWindow(contentWindow);
-
-    // This is the MessageSender property passed to extension.
-    // It can be augmented by the "page-open" hook.
-    let sender = {id: extension.id};
-    if (viewType == "tab") {
-      sender.tabId = tabId;
-      this.tabId = tabId;
-    }
-    if (uri) {
-      sender.url = uri.spec;
-    }
-    this.sender = sender;
-
-    Schemas.exportLazyGetter(contentWindow, "browser", () => {
-      let browserObj = Cu.createObjectIn(contentWindow);
-      Schemas.inject(browserObj, this.childManager);
-      return browserObj;
-    });
-
-    Schemas.exportLazyGetter(contentWindow, "chrome", () => {
-      let chromeApiWrapper = Object.create(this.childManager);
-      chromeApiWrapper.isChromeCompat = true;
-
-      let chromeObj = Cu.createObjectIn(contentWindow);
-      Schemas.inject(chromeObj, chromeApiWrapper);
-      return chromeObj;
-    });
-  }
-
-  get cloneScope() {
-    return this.contentWindow;
-  }
-
-  get principal() {
-    return this.contentWindow.document.nodePrincipal;
-  }
-
-  get windowId() {
-    if (["tab", "popup", "sidebar"].includes(this.viewType)) {
-      let globalView = ExtensionChild.contentGlobals.get(this.messageManager);
-      return globalView ? globalView.windowId : -1;
-    }
-  }
-
-  // Called when the extension shuts down.
-  shutdown() {
-    this.unload();
-  }
-
-  // This method is called when an extension page navigates away or
-  // its tab is closed.
-  unload() {
-    // Note that without this guard, we end up running unload code
-    // multiple times for tab pages closed by the "page-unload" handlers
-    // triggered below.
-    if (this.unloaded) {
-      return;
-    }
-
-    if (this.contentWindow) {
-      this.contentWindow.close();
-    }
-
-    super.unload();
-  }
-}
-
-defineLazyGetter(ExtensionBaseContextChild.prototype, "messenger", function() {
-  let filter = {extensionId: this.extension.id};
-  let optionalFilter = {};
-  // Addon-generated messages (not necessarily from the same process as the
-  // addon itself) are sent to the main process, which forwards them via the
-  // parent process message manager. Specific replies can be sent to the frame
-  // message manager.
-  return new Messenger(this, [Services.cpmm, this.messageManager], this.sender,
-                       filter, optionalFilter);
-});
-
-class ExtensionPageContextChild extends ExtensionBaseContextChild {
-  /**
-   * This ExtensionPageContextChild represents a privileged addon
-   * execution environment that has full access to the WebExtensions
-   * APIs (provided that the correct permissions have been requested).
-   *
-   * This is the child side of the ExtensionPageContextParent class
-   * defined in ExtensionParent.jsm.
-   *
-   * @param {BrowserExtensionContent} extension This context's owner.
-   * @param {object} params
-   * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
-   * @param {string} params.viewType One of "background", "popup", "sidebar" or "tab".
-   *     "background", "sidebar" and "tab" are used by `browser.extension.getViews`.
-   *     "popup" is only used internally to identify page action and browser
-   *     action popups and options_ui pages.
-   * @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
-   */
-  constructor(extension, params) {
-    super(extension, Object.assign(params, {envType: "addon_child"}));
-
-    this.extension.views.add(this);
-  }
-
-  unload() {
-    super.unload();
-    this.extension.views.delete(this);
-  }
-}
-
-defineLazyGetter(ExtensionPageContextChild.prototype, "childManager", function() {
-  apiManager.lazyInit();
-
-  let localApis = {};
-  let can = new CanOfAPIs(this, apiManager, localApis);
-
-  let childManager = new ChildAPIManager(this, this.messageManager, can, {
-    envType: "addon_parent",
-    viewType: this.viewType,
-    url: this.uri.spec,
-    incognito: this.incognito,
-  });
-
-  this.callOnClose(childManager);
-
-  if (this.viewType == "background") {
-    apiManager.global.initializeBackgroundPage(this.contentWindow);
-  }
-
-  return childManager;
-});
-
-class DevToolsContextChild extends ExtensionBaseContextChild {
-  /**
-   * This DevToolsContextChild represents a devtools-related addon execution
-   * environment that has access to the devtools API namespace and to the same subset
-   * of APIs available in a content script execution environment.
-   *
-   * @param {BrowserExtensionContent} extension This context's owner.
-   * @param {object} params
-   * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
-   * @param {string} params.viewType One of "devtools_page" or "devtools_panel".
-   * @param {object} [params.devtoolsToolboxInfo] This devtools toolbox's information,
-   *   used if viewType is "devtools_page" or "devtools_panel".
-   */
-  constructor(extension, params) {
-    super(extension, Object.assign(params, {envType: "devtools_child"}));
-
-    this.devtoolsToolboxInfo = params.devtoolsToolboxInfo;
-
-    this.extension.devtoolsViews.add(this);
-  }
-
-  unload() {
-    super.unload();
-    this.extension.devtoolsViews.delete(this);
-  }
-}
-
-defineLazyGetter(DevToolsContextChild.prototype, "childManager", function() {
-  devtoolsAPIManager.lazyInit();
-
-  let localApis = {};
-  let can = new CanOfAPIs(this, apiManager, localApis);
-
-  let childManager = new ChildAPIManager(this, this.messageManager, can, {
-    envType: "devtools_parent",
-    viewType: this.viewType,
-    url: this.uri.spec,
-    incognito: this.incognito,
-  });
-
-  this.callOnClose(childManager);
-
-  return childManager;
-});
-
-// All subframes in a tab, background page, popup, etc. have the same view type.
-// This class keeps track of such global state.
-// Note that this is created even for non-extension tabs because at present we
-// do not have a way to distinguish regular tabs from extension tabs at the
-// initialization of a frame script.
-class ContentGlobal {
-  /**
-   * @param {nsIContentFrameMessageManager} global The frame script's global.
-   */
-  constructor(global) {
-    this.global = global;
-    // Unless specified otherwise assume that the extension page is in a tab,
-    // because the majority of all class instances are going to be a tab. Any
-    // special views (background page, extension popup) will immediately send an
-    // Extension:InitExtensionView message to change the viewType.
-    this.viewType = "tab";
-    this.tabId = -1;
-    this.windowId = -1;
-    this.initialized = false;
-
-    this.global.addMessageListener("Extension:InitExtensionView", this);
-    this.global.addMessageListener("Extension:SetTabAndWindowId", this);
-  }
-
-  uninit() {
-    this.global.removeMessageListener("Extension:InitExtensionView", this);
-    this.global.removeMessageListener("Extension:SetTabAndWindowId", this);
-  }
-
-  ensureInitialized() {
-    if (!this.initialized) {
-      // Request tab and window ID in case "Extension:InitExtensionView" is not
-      // sent (e.g. when `viewType` is "tab").
-      let reply = this.global.sendSyncMessage("Extension:GetTabAndWindowId");
-      this.handleSetTabAndWindowId(reply[0] || {});
-    }
-    return this;
-  }
-
-  receiveMessage({name, data}) {
-    switch (name) {
-      case "Extension:InitExtensionView":
-        // The view type is initialized once and then fixed.
-        this.global.removeMessageListener("Extension:InitExtensionView", this);
-        this.viewType = data.viewType;
-
-        // Force external links to open in tabs.
-        if (["popup", "sidebar"].includes(this.viewType)) {
-          this.global.docShell.isAppTab = true;
-        }
-
-        if (data.devtoolsToolboxInfo) {
-          this.devtoolsToolboxInfo = data.devtoolsToolboxInfo;
-        }
-
-        promiseEvent(this.global, "DOMContentLoaded", true).then(() => {
-          let windowId = getInnerWindowID(this.global.content);
-          let context = ExtensionChild.extensionContexts.get(windowId);
-
-          this.global.sendAsyncMessage("Extension:ExtensionViewLoaded",
-                                       {childId: context && context.childManager.id});
-        });
-
-        /* FALLTHROUGH */
-      case "Extension:SetTabAndWindowId":
-        this.handleSetTabAndWindowId(data);
-        break;
-    }
-  }
-
-  handleSetTabAndWindowId(data) {
-    let {tabId, windowId} = data;
-
-    if (tabId) {
-      // Tab IDs are not expected to change.
-      if (this.tabId !== -1 && tabId !== this.tabId) {
-        throw new Error("Attempted to change a tabId after it was set");
-      }
-      this.tabId = tabId;
-    }
-
-    if (windowId !== undefined) {
-      // Window IDs may change if a tab is moved to a different location.
-      // Note: This is the ID of the browser window for the extension API.
-      // Do not confuse it with the innerWindowID of DOMWindows!
-      this.windowId = windowId;
-    }
-    this.initialized = true;
-  }
-}
-
-ExtensionChild = {
+var ExtensionChild = {
+  BrowserExtensionContent,
   ChildAPIManager,
   Messenger,
   Port,
-
-  // Map<nsIContentFrameMessageManager, ContentGlobal>
-  contentGlobals: new Map(),
-
-  // Map<innerWindowId, ExtensionPageContextChild>
-  extensionContexts: new Map(),
-
-  init(global) {
-    if (!ExtensionManagement.isExtensionProcess) {
-      throw new Error("Cannot init extension page global in current process");
-    }
-
-    this.contentGlobals.set(global, new ContentGlobal(global));
-  },
-
-  uninit(global) {
-    this.contentGlobals.get(global).uninit();
-    this.contentGlobals.delete(global);
-  },
-
-  /**
-   * Create a privileged context at document-element-inserted.
-   *
-   * @param {BrowserExtensionContent} extension
-   *     The extension for which the context should be created.
-   * @param {nsIDOMWindow} contentWindow The global of the page.
-   */
-  initExtensionContext(extension, contentWindow) {
-    if (!ExtensionManagement.isExtensionProcess) {
-      throw new Error("Cannot create an extension page context in current process");
-    }
-
-    let windowId = getInnerWindowID(contentWindow);
-    let context = this.extensionContexts.get(windowId);
-    if (context) {
-      if (context.extension !== extension) {
-        throw new Error("A different extension context already exists for this frame");
-      }
-      throw new Error("An extension context was already initialized for this frame");
-    }
-
-    let mm = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                          .getInterface(Ci.nsIDocShell)
-                          .QueryInterface(Ci.nsIInterfaceRequestor)
-                          .getInterface(Ci.nsIContentFrameMessageManager);
-
-    let {viewType, tabId, devtoolsToolboxInfo} = this.contentGlobals.get(mm).ensureInitialized();
-
-    let uri = contentWindow.document.documentURIObject;
-
-    if (devtoolsToolboxInfo) {
-      context = new DevToolsContextChild(extension, {
-        viewType, contentWindow, uri, tabId, devtoolsToolboxInfo,
-      });
-    } else {
-      context = new ExtensionPageContextChild(extension, {viewType, contentWindow, uri, tabId});
-    }
-
-    this.extensionContexts.set(windowId, context);
-  },
-
-  /**
-   * Close the ExtensionPageContextChild belonging to the given window, if any.
-   *
-   * @param {number} windowId The inner window ID of the destroyed context.
-   */
-  destroyExtensionContext(windowId) {
-    let context = this.extensionContexts.get(windowId);
-    if (context) {
-      context.unload();
-      this.extensionContexts.delete(windowId);
-    }
-  },
-
-  shutdownExtension(extensionId) {
-    for (let [windowId, context] of this.extensionContexts) {
-      if (context.extension.id == extensionId) {
-        context.shutdown();
-        this.extensionContexts.delete(windowId);
-      }
-    }
-  },
 };
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -10,20 +10,16 @@ this.EXPORTED_SYMBOLS = ["ExtensionConte
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
                                   "resource:///modules/translation/LanguageDetector.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, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
                                   "resource://gre/modules/WebNavigationFrames.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
@@ -38,39 +34,41 @@ const Timer = Components.Constructor("@m
 
 Cu.import("resource://gre/modules/ExtensionChild.jsm");
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 const {
   DefaultMap,
   DefaultWeakMap,
-  EventEmitter,
-  LocaleData,
   defineLazyGetter,
   getInnerWindowID,
   getWinUtils,
   promiseDocumentLoaded,
   promiseDocumentReady,
   runSafeSyncWithoutClone,
 } = ExtensionUtils;
 
 const {
   BaseContext,
   CanOfAPIs,
   SchemaAPIManager,
 } = ExtensionCommon;
 
 const {
+  BrowserExtensionContent,
   ChildAPIManager,
   Messenger,
 } = ExtensionChild;
 
 XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
 
+
+var DocumentManager;
+
 const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
 
 var apiManager = new class extends SchemaAPIManager {
   constructor() {
     super("content");
     this.initialized = false;
   }
 
@@ -171,16 +169,32 @@ class CSSCache extends CacheMap {
         return;
       }
     }
 
     super.delete(url);
   }
 }
 
+defineLazyGetter(BrowserExtensionContent.prototype, "staticScripts", () => {
+  return new ScriptCache({hasReturnValue: false});
+});
+
+defineLazyGetter(BrowserExtensionContent.prototype, "dynamicScripts", () => {
+  return new ScriptCache({hasReturnValue: true});
+});
+
+defineLazyGetter(BrowserExtensionContent.prototype, "userCSS", () => {
+  return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET);
+});
+
+defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", () => {
+  return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET);
+});
+
 // Represents a content script.
 class Script {
   constructor(extension, options) {
     this.extension = extension;
     this.options = options;
 
     this.runAt = this.options.run_at;
     this.js = this.options.js || [];
@@ -243,16 +257,17 @@ class Script {
    *
    * @param {BaseContext} context
    *        The content script context into which to inject the scripts.
    * @returns {Promise<any>}
    *        Resolves to the last value in the evaluated script, when
    *        execution is complete.
    */
   async inject(context) {
+    DocumentManager.lazyInit();
     if (this.requiresCleanup) {
       context.addScript(this);
     }
 
     let cssPromise;
     if (this.cssURLs.length) {
       let window = context.contentWindow;
       let winUtils = getWinUtils(window);
@@ -315,18 +330,16 @@ defineLazyGetter(Script.prototype, "cssU
 
   if (this.options.cssCode) {
     urls.push("data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode));
   }
 
   return urls;
 });
 
-var DocumentManager;
-
 /**
  * An execution context for semi-privileged extension content scripts.
  *
  * This is the child side of the ContentScriptContextParent class
  * defined in ExtensionParent.jsm.
  */
 class ContentScriptContextChild extends BaseContext {
   constructor(extension, contentWindow) {
@@ -480,21 +493,16 @@ defineLazyGetter(ContentScriptContextChi
     url: this.url,
   });
 
   this.callOnClose(childManager);
 
   return childManager;
 });
 
-// For test use only.
-var ExtensionManager = {
-  extensions: new Map(),
-};
-
 // Responsible for creating ExtensionContexts and injecting content
 // scripts into them when new documents are created.
 DocumentManager = {
   // Map[windowId -> Map[ExtensionChild -> ContentScriptContextChild]]
   contexts: new Map(),
 
   initialized: false,
 
@@ -523,18 +531,16 @@ DocumentManager = {
       if (this.contexts.has(windowId)) {
         let extensions = this.contexts.get(windowId);
         for (let context of extensions.values()) {
           context.close();
         }
 
         this.contexts.delete(windowId);
       }
-
-      ExtensionChild.destroyExtensionContext(windowId);
     },
     "memory-pressure"(subject, topic, data) {
       let timeout = data === "heap-minimize" ? 0 : undefined;
 
       for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(scriptCaches)) {
         cache.clear(timeout);
       }
     },
@@ -585,160 +591,47 @@ DocumentManager = {
     return [];
   },
 
   initExtensionContext(extension, window) {
     extension.getContext(window).injectAPI();
   },
 };
 
-// Represents a browser extension in the content process.
-class BrowserExtensionContent extends EventEmitter {
-  constructor(data) {
-    super();
-
-    this.data = data;
-    this.id = data.id;
-    this.uuid = data.uuid;
-    this.instanceId = data.instanceId;
-
-    this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
-    Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
-
-    defineLazyGetter(this, "scripts", () => {
-      return data.content_scripts.map(scriptData => new Script(this, scriptData));
-    });
-
-    this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
-    this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
-    this.permissions = data.permissions;
-    this.optionalPermissions = data.optionalPermissions;
-    this.principal = data.principal;
-
-    this.localeData = new LocaleData(data.localeData);
-
-    this.manifest = data.manifest;
-    this.baseURI = Services.io.newURI(data.baseURL);
-
-    // Only used in addon processes.
-    this.views = new Set();
-
-    // Only used for devtools views.
-    this.devtoolsViews = new Set();
-
-    /* eslint-disable mozilla/balanced-listeners */
-    this.on("add-permissions", (ignoreEvent, permissions) => {
-      if (permissions.permissions.length > 0) {
-        for (let perm of permissions.permissions) {
-          this.permissions.add(perm);
-        }
-      }
-
-      if (permissions.origins.length > 0) {
-        this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...permissions.origins));
-      }
-    });
-
-    this.on("remove-permissions", (ignoreEvent, permissions) => {
-      if (permissions.permissions.length > 0) {
-        for (let perm of permissions.permissions) {
-          this.permissions.delete(perm);
-        }
-      }
-
-      if (permissions.origins.length > 0) {
-        for (let origin of permissions.origins) {
-          this.whiteListedHosts.removeOne(origin);
-        }
-      }
-    });
-    /* eslint-enable mozilla/balanced-listeners */
-
-    ExtensionManager.extensions.set(this.id, this);
-    DocumentManager.lazyInit();
-  }
-
-  shutdown() {
-    ExtensionManager.extensions.delete(this.id);
-    DocumentManager.shutdownExtension(this);
-    Services.cpmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
-  }
-
-  getContext(window) {
-    let extensions = DocumentManager.getContexts(window);
-
-    let context = extensions.get(this);
-    if (!context) {
-      context = new ContentScriptContextChild(this, window);
-      extensions.set(this, context);
-    }
-    return context;
-  }
-
-  emit(event, ...args) {
-    Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args});
-
-    super.emit(event, ...args);
-  }
-
-  receiveMessage({name, data}) {
-    if (name === this.MESSAGE_EMIT_EVENT) {
-      super.emit(data.event, ...data.args);
-    }
-  }
-
-  localizeMessage(...args) {
-    return this.localeData.localizeMessage(...args);
-  }
-
-  localize(...args) {
-    return this.localeData.localize(...args);
-  }
-
-  hasPermission(perm) {
-    let match = /^manifest:(.*)/.exec(perm);
-    if (match) {
-      return this.manifest[match[1]] != null;
-    }
-    return this.permissions.has(perm);
-  }
-}
-
-defineLazyGetter(BrowserExtensionContent.prototype, "staticScripts", () => {
-  return new ScriptCache({hasReturnValue: false});
-});
-
-defineLazyGetter(BrowserExtensionContent.prototype, "dynamicScripts", () => {
-  return new ScriptCache({hasReturnValue: true});
-});
-
-defineLazyGetter(BrowserExtensionContent.prototype, "userCSS", () => {
-  return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET);
-});
-
-defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", () => {
-  return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET);
-});
-
 this.ExtensionContent = {
   BrowserExtensionContent,
   Script,
 
+  shutdownExtension(extension) {
+    DocumentManager.shutdownExtension(extension);
+  },
+
   // This helper is exported to be integrated in the devtools RDP actors,
   // that can use it to retrieve the existent WebExtensions ContentScripts
   // of a target window and be able to show the ContentScripts source in the
   // DevTools Debugger panel.
   getContentScriptGlobals(window) {
     return DocumentManager.getContentScriptGlobals(window);
   },
 
   initExtensionContext(extension, window) {
     DocumentManager.initExtensionContext(extension, window);
   },
 
+  getContext(extension, window) {
+    let extensions = DocumentManager.getContexts(window);
+
+    let context = extensions.get(this);
+    if (!context) {
+      context = new ContentScriptContextChild(extension, window);
+      extensions.set(extension, context);
+    }
+    return context;
+  },
+
   handleExtensionCapture(global, width, height, options) {
     let win = global.content;
 
     const XHTML_NS = "http://www.w3.org/1999/xhtml";
     let canvas = win.document.createElementNS(XHTML_NS, "canvas");
     canvas.width = width;
     canvas.height = height;
     canvas.mozOpaque = true;
copy from toolkit/components/extensions/ExtensionChild.jsm
copy to toolkit/components/extensions/ExtensionPageChild.jsm
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionPageChild.jsm
@@ -1,472 +1,61 @@
 /* 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 = ["ExtensionChild"];
+/* exported ExtensionPageChild */
+
+this.EXPORTED_SYMBOLS = ["ExtensionPageChild"];
 
-/*
- * This file handles addon logic that is independent of the chrome process.
- * When addons run out-of-process, this is the main entry point.
- * Its primary function is managing addon globals.
- *
- * Don't put contentscript logic here, use ExtensionContent.jsm instead.
+/**
+ * This file handles privileged extension page logic that runs in the
+ * child process.
  */
 
 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");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
                                   "resource://gre/modules/ExtensionManagement.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
-                                  "resource://gre/modules/MessageChannel.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
-                                  "resource://gre/modules/NativeMessaging.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
-                                  "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 
 const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
 const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
 
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
+Cu.import("resource://gre/modules/ExtensionChild.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 const {
-  DefaultMap,
-  LimitedSet,
-  SingletonEventManager,
-  SpreadArgs,
   defineLazyGetter,
   getInnerWindowID,
-  getMessageManager,
-  getUniqueId,
-  injectAPI,
   promiseEvent,
 } = ExtensionUtils;
 
 const {
   BaseContext,
   CanOfAPIs,
-  LocalAPIImplementation,
-  SchemaAPIInterface,
   SchemaAPIManager,
 } = ExtensionCommon;
 
-var ExtensionChild;
-
-/**
- * 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 SingletonEventManager(this.context, "Port.onDisconnect", fire => {
-        return this.registerOnDisconnect(error => {
-          portError = error && this.context.normalizeError(error);
-          fire.asyncWithoutClone(portObj);
-        });
-      }).api(),
-
-      onMessage: new SingletonEventManager(this.context, "Port.onMessage", fire => {
-        return this.registerOnMessage(msg => {
-          msg = Cu.cloneInto(msg, this.context.cloneScope);
-          fire.asyncWithoutClone(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);
-  }
+const {
+  ChildAPIManager,
+  Messenger,
+} = ExtensionChild;
 
-  close() {
-    this.disconnect();
-  }
-}
-
-class NativePort extends Port {
-  postMessage(data) {
-    data = NativeApp.encodeMessage(this.context, data);
-
-    return super.postMessage(data);
-  }
-}
-
-/**
- * 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);
-  }
-
-  sendNativeMessage(messageManager, msg, recipient, responseCallback) {
-    msg = NativeApp.encodeMessage(this.context, msg);
-    return this.sendMessage(messageManager, msg, recipient, responseCallback);
-  }
-
-  _onMessage(name, filter) {
-    return new SingletonEventManager(this.context, name, fire => {
-      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 &&
-                  filter(sender, recipient));
-        },
-
-        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 = fire.raw(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();
-  }
-
-  onMessage(name) {
-    return this._onMessage(name, sender => sender.id === this.sender.id);
-  }
-
-  onMessageExternal(name) {
-    return this._onMessage(name, sender => sender.id !== this.sender.id);
-  }
-
-  _connect(messageManager, port, recipient) {
-    let msg = {
-      name: port.name,
-      portId: port.id,
-    };
-
-    this._sendMessage(messageManager, "Extension:Connect", msg, recipient).catch(error => {
-      if (error.result === MessageChannel.RESULT_NO_HANDLER) {
-        error = {message: "Could not establish connection. Receiving end does not exist."};
-      } else if (error.result === MessageChannel.RESULT_DISCONNECTED) {
-        error = null;
-      }
-      port.disconnectByOtherEnd(error);
-    });
-
-    return port.api();
-  }
-
-  connect(messageManager, name, recipient) {
-    let portId = getUniqueId();
-
-    let port = new Port(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
-
-    return this._connect(messageManager, port, recipient);
-  }
-
-  connectNative(messageManager, name, recipient) {
-    let portId = getUniqueId();
-
-    let port = new NativePort(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
-
-    return this._connect(messageManager, port, recipient);
-  }
-
-  _onConnect(name, filter) {
-    return new SingletonEventManager(this.context, name, fire => {
-      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 &&
-                  filter(sender, recipient));
-        },
-
-        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);
-          fire.asyncWithoutClone(port.api());
-          return true;
-        },
-      };
-
-      MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
-      return () => {
-        MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
-      };
-    }).api();
-  }
-
-  onConnect(name) {
-    return this._onConnect(name, sender => sender.id === this.sender.id);
-  }
-
-  onConnectExternal(name) {
-    return this._onConnect(name, sender => sender.id !== this.sender.id);
-  }
-}
+var ExtensionPageChild;
 
 var apiManager = new class extends SchemaAPIManager {
   constructor() {
     super("addon");
     this.initialized = false;
   }
 
   lazyInit() {
@@ -490,319 +79,16 @@ var devtoolsAPIManager = new class exten
       this.initialized = true;
       for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS)) {
         this.loadScript(value);
       }
     }
   }
 }();
 
-/**
- * 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;
-  }
-
-  revoke() {
-    let map = this.childApiManager.listeners.get(this.path);
-    for (let listener of map.keys()) {
-      this.removeListener(listener);
-    }
-
-    this.path = null;
-    this.childApiManager = null;
-  }
-
-  callFunctionNoReturn(args) {
-    this.childApiManager.callParentFunctionNoReturn(this.path, args);
-  }
-
-  callAsyncFunction(args, callback) {
-    return this.childApiManager.callParentAsyncFunction(this.path, args, callback);
-  }
-
-  addListener(listener, args) {
-    let map = this.childApiManager.listeners.get(this.path);
-
-    if (map.listeners.has(listener)) {
-      // TODO: Called with different args?
-      return;
-    }
-
-    let id = getUniqueId();
-
-    map.ids.set(id, listener);
-    map.listeners.set(listener, id);
-
-    this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", {
-      childId: this.childApiManager.id,
-      listenerId: id,
-      path: this.path,
-      args,
-    });
-  }
-
-  removeListener(listener) {
-    let map = this.childApiManager.listeners.get(this.path);
-
-    if (!map.listeners.has(listener)) {
-      return;
-    }
-
-    let id = map.listeners.get(listener);
-    map.listeners.delete(listener);
-    map.ids.delete(id);
-    map.removedIds.add(id);
-
-    this.childApiManager.messageManager.sendAsyncMessage("API:RemoveListener", {
-      childId: this.childApiManager.id,
-      listenerId: id,
-      path: this.path,
-    });
-  }
-
-  hasListener(listener) {
-    let map = this.childApiManager.listeners.get(this.path);
-    return map.listeners.has(listener);
-  }
-}
-
-// 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 ExtensionParent.jsm. It
-// handles asynchronous function calls as well as event listeners.
-class ChildAPIManager {
-  constructor(context, messageManager, localAPICan, contextData) {
-    this.context = context;
-    this.messageManager = messageManager;
-    this.url = contextData.url;
-
-    // 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 = localAPICan.root;
-    this.apiCan = localAPICan;
-
-    this.id = `${context.extension.id}.${context.contextId}`;
-
-    MessageChannel.addListener(messageManager, "API:RunListener", this);
-    messageManager.addMessageListener("API:CallResult", this);
-
-    this.messageFilterStrict = {childId: this.id};
-
-    this.listeners = new DefaultMap(() => ({
-      ids: new Map(),
-      listeners: new Map(),
-      removedIds: new LimitedSet(10),
-    }));
-
-    // Map[callId -> Deferred]
-    this.callPromises = new Map();
-
-    let params = {
-      childId: this.id,
-      extensionId: context.extension.id,
-      principal: context.principal,
-    };
-    Object.assign(params, contextData);
-
-    this.messageManager.sendAsyncMessage("API:CreateProxyContext", params);
-
-    this.permissionsChangedCallbacks = new Set();
-    this.updatePermissions = null;
-    if (this.context.extension.optionalPermissions.length > 0) {
-      this.updatePermissions = () => {
-        for (let callback of this.permissionsChangedCallbacks) {
-          try {
-            callback();
-          } catch (err) {
-            Cu.reportError(err);
-          }
-        }
-      };
-      this.context.extension.on("add-permissions", this.updatePermissions);
-      this.context.extension.on("remove-permissions", this.updatePermissions);
-    }
-  }
-
-  receiveMessage({name, messageName, data}) {
-    if (data.childId != this.id) {
-      return;
-    }
-
-    switch (name || messageName) {
-      case "API:RunListener":
-        let map = this.listeners.get(data.path);
-        let listener = map.ids.get(data.listenerId);
-
-        if (listener) {
-          return this.context.runSafe(listener, ...data.args);
-        }
-        if (!map.removedIds.has(data.listenerId)) {
-          Services.console.logStringMessage(
-            `Unknown listener at childId=${data.childId} path=${data.path} listenerId=${data.listenerId}\n`);
-        }
-        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 = getUniqueId();
-    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) {
-    path = path.split(".");
-
-    let name = path.pop();
-    let namespace = path.join(".");
-
-    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});
-    if (this.updatePermissions) {
-      this.context.extension.off("add-permissions", this.updatePermissions);
-      this.context.extension.off("remove-permissions", this.updatePermissions);
-    }
-  }
-
-  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;
-    }
-
-    // Do not generate devtools APIs, unless explicitly allowed.
-    if (this.context.envType === "devtools_child" &&
-        !allowedContexts.includes("devtools")) {
-      return false;
-    }
-
-    // Do not generate devtools APIs, unless explicitly allowed.
-    if (this.context.envType !== "devtools_child" &&
-        allowedContexts.includes("devtools_only")) {
-      return false;
-    }
-
-    return true;
-  }
-
-  getImplementation(namespace, name) {
-    this.apiCan.findAPIPath(`${namespace}.${name}`);
-    let obj = this.apiCan.findAPIPath(namespace);
-
-    if (obj && name in obj) {
-      return new LocalAPIImplementation(obj, 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);
-  }
-
-  isPermissionRevokable(permission) {
-    return this.context.extension.optionalPermissions.includes(permission);
-  }
-
-  setPermissionsChangedCallback(callback) {
-    this.permissionsChangedCallbacks.add(callback);
-  }
-}
-
 class ExtensionBaseContextChild extends BaseContext {
   /**
    * This ExtensionBaseContextChild represents an addon execution environment
    * that is running in an addon or devtools child process.
    *
    * @param {BrowserExtensionContent} extension This context's owner.
    * @param {object} params
    * @param {string} params.envType One of "addon_child" or "devtools_child".
@@ -856,17 +142,17 @@ class ExtensionBaseContextChild extends 
   }
 
   get principal() {
     return this.contentWindow.document.nodePrincipal;
   }
 
   get windowId() {
     if (["tab", "popup", "sidebar"].includes(this.viewType)) {
-      let globalView = ExtensionChild.contentGlobals.get(this.messageManager);
+      let globalView = ExtensionPageChild.contentGlobals.get(this.messageManager);
       return globalView ? globalView.windowId : -1;
     }
   }
 
   // Called when the extension shuts down.
   shutdown() {
     this.unload();
   }
@@ -1049,17 +335,17 @@ class ContentGlobal {
         }
 
         if (data.devtoolsToolboxInfo) {
           this.devtoolsToolboxInfo = data.devtoolsToolboxInfo;
         }
 
         promiseEvent(this.global, "DOMContentLoaded", true).then(() => {
           let windowId = getInnerWindowID(this.global.content);
-          let context = ExtensionChild.extensionContexts.get(windowId);
+          let context = ExtensionPageChild.extensionContexts.get(windowId);
 
           this.global.sendAsyncMessage("Extension:ExtensionViewLoaded",
                                        {childId: context && context.childManager.id});
         });
 
         /* FALLTHROUGH */
       case "Extension:SetTabAndWindowId":
         this.handleSetTabAndWindowId(data);
@@ -1083,48 +369,69 @@ class ContentGlobal {
       // Note: This is the ID of the browser window for the extension API.
       // Do not confuse it with the innerWindowID of DOMWindows!
       this.windowId = windowId;
     }
     this.initialized = true;
   }
 }
 
-ExtensionChild = {
-  ChildAPIManager,
-  Messenger,
-  Port,
-
+ExtensionPageChild = {
   // Map<nsIContentFrameMessageManager, ContentGlobal>
   contentGlobals: new Map(),
 
   // Map<innerWindowId, ExtensionPageContextChild>
   extensionContexts: new Map(),
 
+  initialized: false,
+
+  _init() {
+    if (this.initialized) {
+      return;
+    }
+    this.initialized = true;
+
+    Services.obs.addObserver(this, "inner-window-destroyed", false); // eslint-ignore-line mozilla/balanced-listeners
+  },
+
   init(global) {
     if (!ExtensionManagement.isExtensionProcess) {
       throw new Error("Cannot init extension page global in current process");
     }
 
-    this.contentGlobals.set(global, new ContentGlobal(global));
+    if (!this.contentGlobals.has(global)) {
+      this.contentGlobals.set(global, new ContentGlobal(global));
+    }
   },
 
   uninit(global) {
-    this.contentGlobals.get(global).uninit();
-    this.contentGlobals.delete(global);
+    if (this.contentGlobals.has(global)) {
+      this.contentGlobals.get(global).uninit();
+      this.contentGlobals.delete(global);
+    }
+  },
+
+  observe(subject, topic, data) {
+    if (topic === "inner-window-destroyed") {
+      let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+
+      this.destroyExtensionContext(windowId);
+    }
   },
 
   /**
    * Create a privileged context at document-element-inserted.
    *
    * @param {BrowserExtensionContent} extension
    *     The extension for which the context should be created.
    * @param {nsIDOMWindow} contentWindow The global of the page.
    */
   initExtensionContext(extension, contentWindow) {
+    this._init();
+
     if (!ExtensionManagement.isExtensionProcess) {
       throw new Error("Cannot create an extension page context in current process");
     }
 
     let windowId = getInnerWindowID(contentWindow);
     let context = this.extensionContexts.get(windowId);
     if (context) {
       if (context.extension !== extension) {
--- a/toolkit/components/extensions/LegacyExtensionsUtils.jsm
+++ b/toolkit/components/extensions/LegacyExtensionsUtils.jsm
@@ -14,30 +14,27 @@ this.EXPORTED_SYMBOLS = ["LegacyExtensio
  */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
+                                  "resource://gre/modules/ExtensionChild.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
-Cu.import("resource://gre/modules/ExtensionChild.jsm");
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 
 var {
   BaseContext,
 } = ExtensionCommon;
 
-var {
-  Messenger,
-} = 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.
    *
@@ -64,17 +61,17 @@ var LegacyExtensionContext = class exten
       {value: cloneScope, enumerable: true, configurable: true, writable: true}
     );
 
     let sender = {id: targetExtension.id};
     let filter = {extensionId: targetExtension.id};
     // Legacy addons live in the main process. Messages from other addons are
     // Messages from WebExtensions are sent to the main process and forwarded via
     // the parent process manager to the legacy extension.
-    this.messenger = new Messenger(this, [Services.cpmm], sender, filter);
+    this.messenger = new ExtensionChild.Messenger(this, [Services.cpmm], sender, filter);
 
     this.api = {
       browser: {
         runtime: {
           onConnect: this.messenger.onConnect("runtime.onConnect"),
           onMessage: this.messenger.onMessage("runtime.onMessage"),
         },
       },
--- a/toolkit/components/extensions/ProxyScriptContext.jsm
+++ b/toolkit/components/extensions/ProxyScriptContext.jsm
@@ -7,20 +7,21 @@
 this.EXPORTED_SYMBOLS = ["ProxyScriptContext"];
 
 /* exported ProxyScriptContext */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/ExtensionChild.jsm");
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
+                                  "resource://gre/modules/ExtensionChild.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "ProxyService",
                                    "@mozilla.org/network/protocol-proxy-service;1",
                                    "nsIProtocolProxyService");
 
 const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
 
@@ -33,20 +34,16 @@ const {
 
 const {
   BaseContext,
   CanOfAPIs,
   LocalAPIImplementation,
   SchemaAPIManager,
 } = ExtensionCommon;
 
-const {
-  Messenger,
-} = ExtensionChild;
-
 const PROXY_TYPES = Object.freeze({
   DIRECT: "direct",
   HTTPS: "https",
   PROXY: "proxy",
   SOCKS: "socks",
 });
 
 class ProxyScriptContext extends BaseContext {
@@ -272,17 +269,17 @@ class ProxyScriptInjectionContext {
   get principal() {
     return this.context.principal;
   }
 }
 
 defineLazyGetter(ProxyScriptContext.prototype, "messenger", function() {
   let sender = {id: this.extension.id, frameId: this.frameId, url: this.url};
   let filter = {extensionId: this.extension.id, toProxyScript: true};
-  return new Messenger(this, [this.messageManager], sender, filter);
+  return new ExtensionChild.Messenger(this, [this.messageManager], sender, filter);
 });
 
 let proxyScriptAPIManager = new ProxyScriptAPIManager();
 
 defineLazyGetter(ProxyScriptContext.prototype, "browserObj", function() {
   let localAPIs = {};
   let can = new CanOfAPIs(this, proxyScriptAPIManager, localAPIs);
   proxyScriptAPIManager.lazyInit();
--- a/toolkit/components/extensions/extension-process-script.js
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -25,16 +25,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/MessageChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
                                   "resource://gre/modules/WebNavigationFrames.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
                                   "resource://gre/modules/ExtensionChild.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContent",
                                   "resource://gre/modules/ExtensionContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPageChild",
+                                  "resource://gre/modules/ExtensionPageChild.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
                                   "resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
 XPCOMUtils.defineLazyGetter(this, "getInnerWindowID", () => ExtensionUtils.getInnerWindowID);
 
 const isContentProcess = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
 
@@ -264,40 +266,54 @@ DocumentManager = {
   // to observe "content-document-global-created".
   initAboutBlankMatchers() {
     Services.obs.addObserver(this, "content-document-global-created", false);
   },
   uninitAboutBlankMatchers() {
     Services.obs.removeObserver(this, "content-document-global-created");
   },
 
+  extensionProcessInitialized: false,
+  initExtensionProcess() {
+    if (this.extensionProcessInitialized || !ExtensionManagement.isExtensionProcess) {
+      return;
+    }
+    this.extensionProcessInitialized = true;
+
+    for (let global of this.globals.keys()) {
+      ExtensionPageChild.init(global);
+    }
+  },
+
   // Initialize a frame script global which extension contexts may be loaded
   // into.
   initGlobal(global) {
     // Note: {once: true} does not work as expected here.
     global.addEventListener("unload", event => { // eslint-disable-line mozilla/balanced-listeners
       this.uninitGlobal(global);
     });
 
     this.globals.set(global, new ExtensionGlobal(global));
-    if (ExtensionManagement.isExtensionProcess) {
-      ExtensionChild.init(global);
+    this.initExtensionProcess();
+    if (this.extensionProcessInitialized && ExtensionManagement.isExtensionProcess) {
+      ExtensionPageChild.init(global);
     }
   },
   uninitGlobal(global) {
-    if (ExtensionManagement.isExtensionProcess) {
-      ExtensionChild.uninit(global);
+    if (this.extensionProcessInitialized) {
+      ExtensionPageChild.uninit(global);
     }
     this.globals.get(global).uninit();
     this.globals.delete(global);
   },
 
   initExtension(extension) {
     if (this.extensionCount === 0) {
       this.init();
+      this.initExtensionProcess();
     }
     this.extensionCount++;
 
     for (let script of extension.scripts) {
       this.addContentScript(script);
     }
 
     this.injectExtensionScripts(extension);
@@ -448,17 +464,17 @@ DocumentManager = {
     }
 
     let apiLevel = ExtensionManagement.getAPILevelForWindow(window, extensionId);
     const levels = ExtensionManagement.API_LEVELS;
 
     if (apiLevel === levels.CONTENTSCRIPT_PRIVILEGES) {
       ExtensionContent.initExtensionContext(extension.realExtension, window);
     } else if (apiLevel === levels.FULL_PRIVILEGES) {
-      ExtensionChild.initExtensionContext(extension.realExtension, window);
+      ExtensionPageChild.initExtensionContext(extension.realExtension, window);
     } else {
       throw new Error(`Unexpected window with extension ID ${extensionId}`);
     }
   },
 
   // Helpers
 
   * enumerateWindows(docShell) {
@@ -511,17 +527,17 @@ class StubExtension {
     if (this._realExtension) {
       this._realExtension.shutdown();
     }
   }
 
   // Lazily create the real extension object when needed.
   get realExtension() {
     if (!this._realExtension) {
-      this._realExtension = new ExtensionContent.BrowserExtensionContent(this.data);
+      this._realExtension = new ExtensionChild.BrowserExtensionContent(this.data);
     }
     return this._realExtension;
   }
 
   // Forward functions needed by ExtensionManagement.
   hasPermission(...args) {
     return this.realExtension.hasPermission(...args);
   }
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -6,16 +6,17 @@
 
 EXTRA_JS_MODULES += [
     'Extension.jsm',
     'ExtensionAPI.jsm',
     'ExtensionChild.jsm',
     'ExtensionCommon.jsm',
     'ExtensionContent.jsm',
     'ExtensionManagement.jsm',
+    'ExtensionPageChild.jsm',
     'ExtensionParent.jsm',
     'ExtensionPermissions.jsm',
     'ExtensionPreferencesManager.jsm',
     'ExtensionSettingsStore.jsm',
     'ExtensionStorage.jsm',
     'ExtensionStorageSync.jsm',
     'ExtensionTabs.jsm',
     'ExtensionUtils.jsm',
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html
@@ -39,17 +39,17 @@ add_task(function* test_contentscript_ca
     },
   });
 
   yield extension.startup();
 
   let origin = yield extension.awaitMessage("origin");
   let scriptUrl = `${origin}/content_script.js`;
 
-  let {ExtensionManager} = SpecialPowers.Cu.import("resource://gre/modules/ExtensionContent.jsm", {});
+  let {ExtensionManager} = SpecialPowers.Cu.import("resource://gre/modules/ExtensionChild.jsm", {});
   let ext = ExtensionManager.extensions.get(extension.id);
 
   ext.staticScripts.expiryTimeout = 3000;
   is(ext.staticScripts.size, 0, "Should have no cached scripts");
 
   let win = window.open("http://example.com/");
   yield extension.awaitMessage("content-script-loaded");
 
@@ -59,17 +59,17 @@ add_task(function* test_contentscript_ca
   }
 
   let chromeScript, chromeScriptDone;
   let {appinfo} = SpecialPowers.Services;
   if (appinfo.processType === appinfo.PROCESS_TYPE_CONTENT) {
     /* globals addMessageListener, assert */
     chromeScript = SpecialPowers.loadChromeScript(() => {
       addMessageListener("check-script-cache", extensionId => {
-        let {ExtensionManager} = Components.utils.import("resource://gre/modules/ExtensionContent.jsm", {});
+        let {ExtensionManager} = Components.utils.import("resource://gre/modules/ExtensionChild.jsm", {});
         let ext = ExtensionManager.extensions.get(extensionId);
 
         if (ext) {
           assert.equal(ext.staticScripts.size, 0, "Should have no cached scripts in the parent process");
         }
 
         sendAsyncMessage("done");
       });