Bug 1317697: Split ExtensionContent.jsm into a stub process script. r?mixedpuppy draft
authorKris Maglione <maglione.k@gmail.com>
Fri, 07 Apr 2017 13:17:07 -0700
changeset 562399 5400984045184797c789711791c5d74c2f8404bf
parent 562398 62cb91d550190bee62b7452f84bfe1c6e6fd3bd0
child 562400 7ca9c01d26684f36dbfbed28f6eb1ca056fcf09b
push id54021
push usermaglione.k@gmail.com
push dateThu, 13 Apr 2017 22:30:15 +0000
reviewersmixedpuppy
bugs1317697
milestone55.0a1
Bug 1317697: Split ExtensionContent.jsm into a stub process script. r?mixedpuppy MozReview-Commit-ID: 4vn0ERZiBQd
browser/base/content/tab-content.js
devtools/server/actors/tab.js
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ExtensionManagement.jsm
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/ExtensionXPCShellUtils.jsm
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/extension-process-script.js
toolkit/components/extensions/jar.mn
toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html
toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html
toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html
toolkit/modules/addons/WebNavigationFrames.jsm
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -6,17 +6,16 @@
 /* This content script contains code that requires a tab browser. */
 
 /* eslint-env mozilla/frame-script */
 
 var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/ExtensionContent.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
   "resource:///modules/E10SUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AboutReader",
@@ -1040,19 +1039,19 @@ var UserContextIdNotifier = {
     let userContextId = loadContext.originAttributes.userContextId;
 
     sendAsyncMessage("Browser:WindowCreated", { userContextId });
   }
 };
 
 UserContextIdNotifier.init();
 
-ExtensionContent.init(this);
+Services.obs.notifyObservers(this, "tab-content-frameloader-created", "");
+
 addEventListener("unload", () => {
-  ExtensionContent.uninit(this);
   RefreshBlocker.uninit();
 });
 
 addMessageListener("AllowScriptsToClose", () => {
   content.QueryInterface(Ci.nsIInterfaceRequestor)
          .getInterface(Ci.nsIDOMWindowUtils)
          .allowScriptsToClose();
 });
--- a/devtools/server/actors/tab.js
+++ b/devtools/server/actors/tab.js
@@ -331,17 +331,17 @@ TabActor.prototype = {
 
   /**
    * Getter for the WebExtensions ContentScript globals related to the
    * current tab content's DOM window.
    */
   get webextensionsContentScriptGlobals() {
     // Ignore xpcshell runtime which spawn TabActors without a window.
     if (this.window) {
-      return ExtensionContent.getContentScriptGlobalsForWindow(this.window);
+      return ExtensionContent.getContentScriptGlobals(this.window);
     }
 
     return [];
   },
 
   /**
    * Getter for the list of all content DOM windows in this tabActor
    * @return {Array}
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -62,17 +62,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "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");
 Cu.import("resource://gre/modules/ExtensionParent.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -1094,24 +1094,16 @@ ExtensionChild = {
   Port,
 
   // Map<nsIContentFrameMessageManager, ContentGlobal>
   contentGlobals: new Map(),
 
   // Map<innerWindowId, ExtensionPageContextChild>
   extensionContexts: new Map(),
 
-  initOnce() {
-    // This initializes the default message handler for messages targeted at
-    // an addon process, in case the addon process receives a message before
-    // its Messenger has been instantiated. For example, if a content script
-    // sends a message while there is no background page.
-    MessageChannel.setupMessageManagers([Services.cpmm]);
-  },
-
   init(global) {
     if (!ExtensionManagement.isExtensionProcess) {
       throw new Error("Cannot init extension page global in current process");
     }
 
     this.contentGlobals.set(global, new ContentGlobal(global));
   },
 
@@ -1122,17 +1114,17 @@ ExtensionChild = {
 
   /**
    * 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.
    */
-  createExtensionContext(extension, contentWindow) {
+  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) {
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -3,71 +3,57 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["ExtensionContent"];
 
 /* globals ExtensionContent */
 
-/*
- * This file handles the content process side of extensions. It mainly
- * takes care of content script injection, content script APIs, and
- * messaging.
- *
- * This file is also the initial entry point for addon processes.
- * ExtensionChild.jsm is responsible for functionality specific to addon
- * processes.
- */
+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;
-
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/AppConstants.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
-                                  "resource://gre/modules/ExtensionManagement.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
                                   "resource:///modules/translation/LanguageDetector.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
+XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
                                   "resource://gre/modules/MatchPattern.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
+XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
                                   "resource://gre/modules/MatchPattern.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.defineLazyModuleGetter(this, "WebNavigationFrames",
                                   "resource://gre/modules/WebNavigationFrames.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
                                    "@mozilla.org/content/style-sheet-service;1",
                                    "nsIStyleSheetService");
 
+const DocumentEncoder = Components.Constructor(
+  "@mozilla.org/layout/documentEncoder;1?type=text/plain",
+  "nsIDocumentEncoder", "init");
+
 const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", "initWithCallback");
 
 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,
-  flushJarCache,
   getInnerWindowID,
   getWinUtils,
+  promiseDocumentLoaded,
   promiseDocumentReady,
   runSafeSyncWithoutClone,
 } = ExtensionUtils;
 
 const {
   BaseContext,
   CanOfAPIs,
   SchemaAPIManager,
@@ -77,23 +63,16 @@ 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,
-               "document_idle": 2};
-  return table[when1] <= table[when2];
-}
-
 var apiManager = new class extends SchemaAPIManager {
   constructor() {
     super("content");
     this.initialized = false;
   }
 
   lazyInit() {
     if (!this.initialized) {
@@ -194,314 +173,204 @@ class CSSCache extends CacheMap {
     }
 
     super.delete(url);
   }
 }
 
 // Represents a content script.
 class Script {
-  constructor(extension, options, deferred = PromiseUtils.defer()) {
+  constructor(extension, options) {
     this.extension = extension;
     this.options = options;
-    this.run_at = this.options.run_at;
+
+    this.runAt = this.options.run_at;
     this.js = this.options.js || [];
     this.css = this.options.css || [];
     this.remove_css = this.options.remove_css;
-    this.match_about_blank = this.options.match_about_blank;
     this.css_origin = this.options.css_origin;
 
-    this.deferred = deferred;
-
     this.cssCache = extension[this.css_origin === "user" ? "userCSS"
                                                          : "authorCSS"];
     this.scriptCache = extension[options.wantReturnValue ? "dynamicScripts"
                                                          : "staticScripts"];
 
     if (options.wantReturnValue) {
       this.compileScripts();
       this.loadCSS();
     }
 
-    this.matches_ = new MatchPattern(this.options.matches);
-    this.exclude_matches_ = new MatchPattern(this.options.exclude_matches || null);
-    // TODO: MatchPattern should pre-mangle host-only patterns so that we
-    // don't need to call a separate match function.
-    this.matches_host_ = new MatchPattern(this.options.matchesHost || null);
-    this.include_globs_ = new MatchGlobs(this.options.include_globs);
-    this.exclude_globs_ = new MatchGlobs(this.options.exclude_globs);
-
     this.requiresCleanup = !this.remove_css && (this.css.length > 0 || options.cssCode);
   }
 
   compileScripts() {
     return this.js.map(url => this.scriptCache.get(url));
   }
 
   loadCSS() {
     return this.cssURLs.map(url => this.cssCache.get(url));
   }
 
-  matchesLoadInfo(uri, loadInfo) {
-    if (!this.matchesURI(uri)) {
-      return false;
-    }
-
-    if (!this.options.all_frames && !loadInfo.isTopLevelLoad) {
-      return false;
-    }
-
-    return true;
-  }
-
-  matchesURI(uri) {
-    if (!(this.matches_.matches(uri) || this.matches_host_.matchesIgnoringPath(uri))) {
-      return false;
-    }
-
-    if (this.exclude_matches_.matches(uri)) {
-      return false;
-    }
-
-    if (this.options.include_globs != null) {
-      if (!this.include_globs_.matches(uri.spec)) {
-        return false;
-      }
-    }
-
-    if (this.exclude_globs_.matches(uri.spec)) {
-      return false;
-    }
-
-    return true;
-  }
-
-  matches(window) {
-    let uri = window.document.documentURIObject;
-    let principal = window.document.nodePrincipal;
-
-    // If mozAddonManager is present on this page, don't allow
-    // content scripts.
-    if (window.navigator.mozAddonManager !== undefined) {
-      return false;
-    }
-
-    if (this.match_about_blank) {
-      // When matching top-level about:blank documents,
-      // allow loading into any with a NullPrincipal.
-      if (uri.spec === "about:blank" && window === window.top && principal.isNullPrincipal) {
-        return true;
-      }
-
-      // When matching about:blank/srcdoc iframes, the checks below
-      // need to be performed against the "owner" document's URI.
-      if (["about:blank", "about:srcdoc"].includes(uri.spec)) {
-        uri = principal.URI;
-      }
-    }
-
-    // Documents from data: URIs also inherit the principal.
-    if (Services.netUtils.URIChainHasFlags(uri, Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT)) {
-      if (!this.match_about_blank) {
-        return false;
-      }
-      uri = principal.URI;
-    }
-
-    if (!this.matchesURI(uri)) {
-      return false;
-    }
-
-    if (this.options.frame_id != null) {
-      if (WebNavigationFrames.getFrameId(window) != this.options.frame_id) {
-        return false;
-      }
-    } else if (!this.options.all_frames && window.top != window) {
-      return false;
-    }
-
-    return true;
-  }
-
   cleanup(window) {
     if (!this.remove_css && this.cssURLs.length) {
       let winUtils = getWinUtils(window);
 
       let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
       for (let url of this.cssURLs) {
         this.cssCache.deleteDocument(url, window.document);
         runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
       }
 
       // Clear any sheets that were kept alive past their timeout as
       // a result of living in this document.
       this.cssCache.clear(CSS_EXPIRY_TIMEOUT_MS);
     }
   }
 
+  async injectInto(window) {
+    let context = this.extension.getContext(window);
+
+    if (this.runAt === "document_end") {
+      await promiseDocumentReady(window.document);
+    } else if (this.runAt === "document_idle") {
+      await promiseDocumentLoaded(window.document);
+    }
+
+    return this.inject(context);
+  }
+
   /**
    * Tries to inject this script into the given window and sandbox, if
    * there are pending operations for the window's current load state.
    *
-   * @param {Window} window
-   *        The DOM Window to inject the scripts and CSS into.
-   * @param {Sandbox} sandbox
-   *        A Sandbox inheriting from `window` in which to evaluate the
-   *        injected scripts.
-   * @param {function} shouldRun
-   *        A function which, when passed the document load state that a
-   *        script is expected to run at, returns `true` if we should
-   *        currently be injecting scripts for that load state.
-   *
-   *        For initial injection of a script, this function should
-   *        return true if the document is currently in or has already
-   *        passed through the given state. For injections triggered by
-   *        document state changes, it should only return true if the
-   *        given state exactly matches the state that triggered the
-   *        change.
-   * @param {string} when
-   *        The document's current load state, or if triggered by a
-   *        document state change, the new document state that triggered
-   *        the injection.
+   * @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.
    */
-  tryInject(window, sandbox, shouldRun, when) {
-    if (this.cssURLs.length && shouldRun("document_start")) {
+  async inject(context) {
+    if (this.requiresCleanup) {
+      context.addScript(this);
+    }
+
+    let cssPromise;
+    if (this.cssURLs.length) {
+      let window = context.contentWindow;
       let winUtils = getWinUtils(window);
 
-      let innerWindowID = winUtils.currentInnerWindowID;
-
       let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
 
       if (this.remove_css) {
         for (let url of this.cssURLs) {
           this.cssCache.deleteDocument(url, window.document);
 
           runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
         }
+      } else {
+        cssPromise = Promise.all(this.loadCSS()).then(sheets => {
+          let window = context.contentWindow;
+          if (!window) {
+            return;
+          }
 
-        this.deferred.resolve();
-      } else {
-        this.deferred.resolve(
-          Promise.all(this.loadCSS()).then(sheets => {
-            if (winUtils.currentInnerWindowID !== innerWindowID) {
-              return;
-            }
+          for (let {url, sheet} of sheets) {
+            this.cssCache.addDocument(url, window.document);
 
-            for (let {url, sheet} of sheets) {
-              this.cssCache.addDocument(url, window.document);
-
-              runSafeSyncWithoutClone(winUtils.addSheet, sheet, type);
-            }
-          }));
+            runSafeSyncWithoutClone(winUtils.addSheet, sheet, type);
+          }
+        });
       }
     }
 
-    let scheduled = this.run_at || "document_idle";
-    if (shouldRun(scheduled)) {
-      let scriptsPromise = Promise.all(this.compileScripts());
+    let scriptsPromise = Promise.all(this.compileScripts());
 
-      // If we're supposed to inject at the start of the document load,
-      // and we haven't already missed that point, block further parsing
-      // until the scripts have been loaded.
-      if (this.run_at === "document_start" && when === "document_start") {
-        window.document.blockParsing(scriptsPromise);
-      }
+    // If we're supposed to inject at the start of the document load,
+    // and we haven't already missed that point, block further parsing
+    // until the scripts have been loaded.
+    let {document} = context.contentWindow;
+    if (this.runAt === "document_start" && document.readyState !== "complete") {
+      document.blockParsing(scriptsPromise);
+    }
 
-      this.deferred.resolve(scriptsPromise.then(scripts => {
-        let result;
+    let scripts = await scriptsPromise;
+    let result;
 
-        // The evaluations below may throw, in which case the promise will be
-        // automatically rejected.
-        for (let script of scripts) {
-          result = script.executeInGlobal(sandbox);
-        }
+    // The evaluations below may throw, in which case the promise will be
+    // automatically rejected.
+    for (let script of scripts) {
+      result = script.executeInGlobal(context.cloneScope);
+    }
 
-        if (this.options.jsCode) {
-          result = Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
-        }
+    if (this.options.jsCode) {
+      result = Cu.evalInSandbox(this.options.jsCode, context.cloneScope, "latest");
+    }
 
-        return result;
-      }));
-    }
+    await cssPromise;
+    return result;
   }
 }
 
 defineLazyGetter(Script.prototype, "cssURLs", function() {
   // We can handle CSS urls (css) and CSS code (cssCode).
   let urls = this.css.slice();
 
   if (this.options.cssCode) {
     urls.push("data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode));
   }
 
   return urls;
 });
 
-
-function getWindowMessageManager(contentWindow) {
-  let ir = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                        .getInterface(Ci.nsIDocShell)
-                        .QueryInterface(Ci.nsIInterfaceRequestor);
-  try {
-    return ir.getInterface(Ci.nsIContentFrameMessageManager);
-  } catch (e) {
-    // Some windows don't support this interface (hidden window).
-    return null;
-  }
-}
-
 var DocumentManager;
-var ExtensionManager;
 
 /**
  * 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, contextOptions = {}) {
+  constructor(extension, contentWindow) {
     super("content_child", extension);
 
-    let {isExtensionPage} = contextOptions;
-
-    this.isExtensionPage = isExtensionPage;
-
     this.setContentWindow(contentWindow);
 
     let frameId = WebNavigationFrames.getFrameId(contentWindow);
     this.frameId = frameId;
 
     this.scripts = [];
 
     let contentPrincipal = contentWindow.document.nodePrincipal;
     let ssm = Services.scriptSecurityManager;
 
     // Copy origin attributes from the content window origin attributes to
     // preserve the user context id.
     let attrs = contentPrincipal.originAttributes;
     let extensionPrincipal = ssm.createCodebasePrincipal(this.extension.baseURI, attrs);
 
+    this.isExtensionPage = contentPrincipal.equals(extensionPrincipal);
+
     let principal;
     if (ssm.isSystemPrincipal(contentPrincipal)) {
       // Make sure we don't hand out the system principal by accident.
       // also make sure that the null principal has the right origin attributes
       principal = ssm.createNullPrincipal(attrs);
+    } else if (this.isExtensionPage) {
+      principal = contentPrincipal;
     } else {
       principal = [contentPrincipal, extensionPrincipal];
     }
 
-    if (isExtensionPage) {
-      if (ExtensionManagement.getAddonIdForWindow(this.contentWindow) != this.extension.id) {
-        throw new Error("Invalid target window for this extension context");
-      }
-      // This is an iframe with content script API enabled and its principal should be the
-      // contentWindow itself. (we create a sandbox with the contentWindow as principal and with X-rays disabled
-      // because it enables us to create the APIs object in this sandbox object and then copying it
-      // into the iframe's window, see Bug 1214658 for rationale)
+    if (this.isExtensionPage) {
+      // This is an iframe with content script API enabled and its principal
+      // should be the contentWindow itself. We create a sandbox with the
+      // contentWindow as principal and with X-rays disabled because it
+      // enables us to create the APIs object in this sandbox object and then
+      // copying it into the iframe's window.  See bug 1214658.
       this.sandbox = Cu.Sandbox(contentWindow, {
         sandboxPrototype: contentWindow,
         sameZoneAs: contentWindow,
         wantXrays: false,
         isWebExtensionContentScript: true,
       });
     } else {
       // This metadata is required by the Developer Tools, in order for
@@ -542,63 +411,46 @@ class ContentScriptContextChild extends 
       let chromeObj = Cu.createObjectIn(this.sandbox);
 
       Schemas.inject(chromeObj, this.childManager);
       return chromeObj;
     });
 
     Schemas.exportLazyGetter(this.sandbox, "browser", () => this.chromeObj);
     Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj);
+  }
+
+  injectAPI() {
+    if (!this.isExtensionPage) {
+      throw new Error("Cannot inject extension API into non-extension window");
+    }
 
     // This is an iframe with content script API enabled (bug 1214658)
-    if (isExtensionPage) {
-      Schemas.exportLazyGetter(this.contentWindow,
-                               "browser", () => this.chromeObj);
-      Schemas.exportLazyGetter(this.contentWindow,
-                               "chrome", () => this.chromeObj);
-    }
+    Schemas.exportLazyGetter(this.contentWindow,
+                             "browser", () => this.chromeObj);
+    Schemas.exportLazyGetter(this.contentWindow,
+                             "chrome", () => this.chromeObj);
   }
 
   get cloneScope() {
     return this.sandbox;
   }
 
-  execute(script, shouldRun, when) {
-    script.tryInject(this.contentWindow, this.sandbox, shouldRun, when);
-  }
-
-  addScript(script, when) {
-    let state = DocumentManager.getWindowState(this.contentWindow);
-    this.execute(script, scheduled => isWhenBeforeOrSame(scheduled, state), when);
-
-    // Save the script in case it has pending operations in later load
-    // states, but only if we're before document_idle, or require cleanup.
-    if (state != "document_idle" || script.requiresCleanup) {
+  addScript(script) {
+    if (script.requiresCleanup) {
       this.scripts.push(script);
     }
   }
 
-  triggerScripts(documentState) {
-    for (let script of this.scripts) {
-      this.execute(script, scheduled => scheduled == documentState, documentState);
-    }
-    if (documentState == "document_idle") {
-      // Don't bother saving scripts after document_idle.
-      this.scripts = this.scripts.filter(script => script.requiresCleanup);
-    }
-  }
-
   close() {
     super.unload();
 
     if (this.contentWindow) {
       for (let script of this.scripts) {
-        if (script.requiresCleanup) {
-          script.cleanup(this.contentWindow);
-        }
+        script.cleanup(this.contentWindow);
       }
 
       // Overwrite the content script APIs with an empty object if the APIs objects are still
       // defined in the content window (bug 1214658).
       if (this.isExtensionPage) {
         Cu.createObjectIn(this.contentWindow, {defineAs: "browser"});
         Cu.createObjectIn(this.contentWindow, {defineAs: "chrome"});
       }
@@ -628,356 +480,129 @@ 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 = {
-  extensionCount: 0,
+  // Map[windowId -> Map[ExtensionChild -> ContentScriptContextChild]]
+  contexts: new Map(),
 
-  // Map[windowId -> Map[extensionId -> ContentScriptContextChild]]
-  contentScriptWindows: new Map(),
+  initialized: false,
 
-  // Map[windowId -> ContentScriptContextChild]
-  extensionPageWindows: new Map(),
+  lazyInit() {
+    if (this.initalized) {
+      return;
+    }
+    this.initialized = true;
 
-  init() {
-    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-      Services.obs.addObserver(this, "http-on-opening-request", false);
-    }
-    Services.obs.addObserver(this, "content-document-global-created", false);
-    Services.obs.addObserver(this, "document-element-inserted", false);
     Services.obs.addObserver(this, "inner-window-destroyed", false);
     Services.obs.addObserver(this, "memory-pressure", false);
   },
 
   uninit() {
-    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-      Services.obs.removeObserver(this, "http-on-opening-request");
-    }
-    Services.obs.removeObserver(this, "content-document-global-created");
-    Services.obs.removeObserver(this, "document-element-inserted");
     Services.obs.removeObserver(this, "inner-window-destroyed");
     Services.obs.removeObserver(this, "memory-pressure");
   },
 
-  getWindowState(contentWindow) {
-    let readyState = contentWindow.document.readyState;
-    if (readyState == "complete") {
-      return "document_idle";
-    }
-    if (readyState == "interactive") {
-      return "document_end";
-    }
-    return "document_start";
-  },
-
-  loadInto(window) {
-    // Enable the content script APIs should be available in subframes' window
-    // if it is recognized as a valid addon id (see Bug 1214658 for rationale).
-    const {
-      NO_PRIVILEGES,
-      CONTENTSCRIPT_PRIVILEGES,
-      FULL_PRIVILEGES,
-    } = ExtensionManagement.API_LEVELS;
-    let extensionId = ExtensionManagement.getAddonIdForWindow(window);
-    let apiLevel = ExtensionManagement.getAPILevelForWindow(window, extensionId);
-
-    if (apiLevel != NO_PRIVILEGES) {
-      let extension = ExtensionManager.get(extensionId);
-      if (extension) {
-        if (apiLevel == CONTENTSCRIPT_PRIVILEGES) {
-          DocumentManager.getExtensionPageContext(extension, window);
-        } else if (apiLevel == FULL_PRIVILEGES) {
-          ExtensionChild.createExtensionContext(extension, window);
-        }
-      }
-    }
-  },
-
   observers: {
-    // For some types of documents (about:blank), we only see the first
-    // notification, for others (data: URIs) we only observe the second.
-    "content-document-global-created"(subject, topic, data) {
-      this.observers["document-element-inserted"].call(this, subject.document, topic, data);
-    },
-    "document-element-inserted"(subject, topic, data) {
-      let document = subject;
-      let window = document && document.defaultView;
-
-      if (!document || !document.location || !window) {
-        return;
-      }
-
-      // Make sure we only load into frames that ExtensionContent.init
-      // was called on (i.e., not frames for social or sidebars).
-      let mm = getWindowMessageManager(window);
-      if (!mm || !ExtensionContent.globals.has(mm)) {
-        return;
-      }
-
-      // Load on document-element-inserted, except for about:blank which doesn't
-      // see it, and needs special late handling on DOMContentLoaded event.
-      if (topic === "document-element-inserted") {
-        this.loadInto(window);
-        this.trigger("document_start", window);
-      }
-
-      /* eslint-disable mozilla/balanced-listeners */
-      window.addEventListener("DOMContentLoaded", this, true);
-      window.addEventListener("load", this, true);
-      /* eslint-enable mozilla/balanced-listeners */
-    },
     "inner-window-destroyed"(subject, topic, data) {
       let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
 
       MessageChannel.abortResponses({innerWindowID: windowId});
 
       // Close any existent content-script context for the destroyed window.
-      if (this.contentScriptWindows.has(windowId)) {
-        let extensions = this.contentScriptWindows.get(windowId);
-        for (let [, context] of extensions) {
+      if (this.contexts.has(windowId)) {
+        let extensions = this.contexts.get(windowId);
+        for (let context of extensions.values()) {
           context.close();
         }
 
-        this.contentScriptWindows.delete(windowId);
-      }
-
-      // Close any existent iframe extension page context for the destroyed window.
-      if (this.extensionPageWindows.has(windowId)) {
-        let context = this.extensionPageWindows.get(windowId);
-        context.close();
-        this.extensionPageWindows.delete(windowId);
+        this.contexts.delete(windowId);
       }
 
       ExtensionChild.destroyExtensionContext(windowId);
     },
-    "http-on-opening-request"(subject, topic, data) {
-      let {loadInfo} = subject.QueryInterface(Ci.nsIChannel);
-      if (loadInfo) {
-        let {externalContentPolicyType: type} = loadInfo;
-        if (type === Ci.nsIContentPolicy.TYPE_DOCUMENT ||
-            type === Ci.nsIContentPolicy.TYPE_SUBDOCUMENT) {
-          this.preloadScripts(subject.URI, loadInfo);
-        }
-      }
-    },
     "memory-pressure"(subject, topic, data) {
       let timeout = data === "heap-minimize" ? 0 : undefined;
 
       for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(scriptCaches)) {
         cache.clear(timeout);
       }
     },
   },
 
   observe(subject, topic, data) {
     this.observers[topic].call(this, subject, topic, data);
   },
 
-  handleEvent(event) {
-    let window = event.currentTarget;
-    if (event.target != window.document) {
-      // We use capturing listeners so we have precedence over content script
-      // listeners, but only care about events targeted to the element we're
-      // listening on.
-      return;
-    }
-    window.removeEventListener(event.type, this, true);
-
-    // Need to check if we're still on the right page? Greasemonkey does this.
-
-    if (event.type == "DOMContentLoaded") {
-      // By this time, we can be sure if this is an explicit about:blank
-      // document, and if it needs special late loading and fake trigger.
-      if (window.location.href === "about:blank") {
-        this.loadInto(window);
-        this.trigger("document_start", window);
+  shutdownExtension(extension) {
+    for (let extensions of this.contexts.values()) {
+      let context = extensions.get(extension);
+      if (context) {
+        context.close();
+        extensions.delete(extension);
       }
-      this.trigger("document_end", window);
-    } else if (event.type == "load") {
-      this.trigger("document_idle", window);
     }
   },
 
-  // Used to executeScript, insertCSS and removeCSS.
-  executeScript(global, extensionId, options) {
-    let extension = ExtensionManager.get(extensionId);
-
-    let executeInWin = (window) => {
-      let deferred = PromiseUtils.defer();
-      let script = new Script(extension, options, deferred);
-
-      if (script.matches(window)) {
-        let context = this.getContentScriptContext(extension, window);
-        context.addScript(script);
-        return deferred.promise;
-      }
-      return null;
-    };
+  getContexts(window) {
+    let winId = getInnerWindowID(window);
 
-    let promises = Array.from(this.enumerateWindows(global.docShell), executeInWin)
-                        .filter(promise => promise);
-
-    if (!promises.length) {
-      if (options.frame_id) {
-        return Promise.reject({message: `Frame not found, or missing host permission`});
-      }
+    let extensions = this.contexts.get(winId);
+    if (!extensions) {
+      extensions = new Map();
+      this.contexts.set(winId, extensions);
+    }
 
-      let frames = options.all_frames ? ", and any iframes" : "";
-      return Promise.reject({message: `Missing host permission for the tab${frames}`});
-    }
-    if (!options.all_frames && promises.length > 1) {
-      return Promise.reject({message: `Internal error: Script matched multiple windows`});
-    }
-    return Promise.all(promises);
+    return extensions;
   },
 
-  enumerateWindows: function* (docShell) {
-    let window = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindow);
-    yield window;
-
-    for (let i = 0; i < docShell.childCount; i++) {
-      let child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
-      yield* this.enumerateWindows(child);
+  // For test use only.
+  getContext(extensionId, window) {
+    for (let [extension, context] of this.getContexts(window)) {
+      if (extension.id === extensionId) {
+        return context;
+      }
     }
   },
 
-  getContentScriptGlobalsForWindow(window) {
-    let winId = getInnerWindowID(window);
-    let extensions = this.contentScriptWindows.get(winId);
+  getContentScriptGlobals(window) {
+    let extensions = this.contexts.get(getInnerWindowID(window));
 
     if (extensions) {
       return Array.from(extensions.values(), ctx => ctx.sandbox);
     }
 
     return [];
   },
 
-  getContentScriptContext(extension, window) {
-    let winId = getInnerWindowID(window);
-    if (!this.contentScriptWindows.has(winId)) {
-      this.contentScriptWindows.set(winId, new Map());
-    }
-
-    let extensions = this.contentScriptWindows.get(winId);
-    if (!extensions.has(extension.id)) {
-      let context = new ContentScriptContextChild(extension, window);
-      extensions.set(extension.id, context);
-    }
-
-    return extensions.get(extension.id);
-  },
-
-  getExtensionPageContext(extension, window) {
-    let winId = getInnerWindowID(window);
-
-    let context = this.extensionPageWindows.get(winId);
-    if (!context) {
-      let context = new ContentScriptContextChild(extension, window, {isExtensionPage: true});
-      this.extensionPageWindows.set(winId, context);
-    }
-
-    return context;
-  },
-
-  startupExtension(extensionId) {
-    if (this.extensionCount == 0) {
-      this.init();
-    }
-    this.extensionCount++;
-
-    let extension = ExtensionManager.get(extensionId);
-    for (let global of ExtensionContent.globals.keys()) {
-      // Note that we miss windows in the bfcache here. In theory we
-      // could execute content scripts on a pageshow event for that
-      // window, but that seems extreme.
-      for (let window of this.enumerateWindows(global.docShell)) {
-        for (let script of extension.scripts) {
-          if (script.matches(window)) {
-            let context = this.getContentScriptContext(extension, window);
-            context.addScript(script);
-          }
-        }
-      }
-    }
-  },
-
-  shutdownExtension(extensionId) {
-    // Clean up content-script contexts on extension shutdown.
-    for (let [, extensions] of this.contentScriptWindows) {
-      let context = extensions.get(extensionId);
-      if (context) {
-        context.close();
-        extensions.delete(extensionId);
-      }
-    }
-
-    // Clean up iframe extension page contexts on extension shutdown.
-    for (let [winId, context] of this.extensionPageWindows) {
-      if (context.extension.id == extensionId) {
-        context.close();
-        this.extensionPageWindows.delete(winId);
-      }
-    }
-
-    ExtensionChild.shutdownExtension(extensionId);
-
-    MessageChannel.abortResponses({extensionId});
-
-    this.extensionCount--;
-    if (this.extensionCount == 0) {
-      this.uninit();
-    }
-  },
-
-  preloadScripts(uri, loadInfo) {
-    for (let extension of ExtensionManager.extensions.values()) {
-      for (let script of extension.scripts) {
-        if (script.matchesLoadInfo(uri, loadInfo)) {
-          script.loadCSS();
-          script.compileScripts();
-        }
-      }
-    }
-  },
-
-  trigger(when, window) {
-    if (when === "document_start") {
-      for (let extension of ExtensionManager.extensions.values()) {
-        for (let script of extension.scripts) {
-          if (script.matches(window)) {
-            let context = this.getContentScriptContext(extension, window);
-            context.addScript(script, when);
-          }
-        }
-      }
-    } else {
-      let contexts = this.contentScriptWindows.get(getInnerWindowID(window)) || new Map();
-      for (let context of contexts.values()) {
-        context.triggerScripts(when);
-      }
-    }
+  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.data = data;
     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));
     });
@@ -994,23 +619,16 @@ class BrowserExtensionContent extends Ev
     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();
 
-    let uri = Services.io.newURI(data.resourceURL);
-
-    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-      // Extension.jsm takes care of this in the parent.
-      ExtensionManagement.startupExtension(this.uuid, uri, this);
-    }
-
     /* 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);
         }
       }
 
@@ -1028,24 +646,36 @@ class BrowserExtensionContent extends Ev
 
       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);
+  }
 
-    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-      ExtensionManagement.shutdownExtension(this.uuid);
+  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);
   }
 
@@ -1083,115 +713,34 @@ defineLazyGetter(BrowserExtensionContent
 defineLazyGetter(BrowserExtensionContent.prototype, "userCSS", () => {
   return new CSSCache(Ci.nsIStyleSheetService.USER_SHEET);
 });
 
 defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", () => {
   return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET);
 });
 
-ExtensionManager = {
-  // Map[extensionId, BrowserExtensionContent]
-  extensions: new Map(),
-
-  init() {
-    ExtensionChild.initOnce();
-
-    Services.cpmm.addMessageListener("Extension:Startup", this);
-    Services.cpmm.addMessageListener("Extension:Shutdown", this);
-    Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
+this.ExtensionContent = {
+  BrowserExtensionContent,
+  Script,
 
-    if (Services.cpmm.initialProcessData && "Extension:Extensions" in Services.cpmm.initialProcessData) {
-      let extensions = Services.cpmm.initialProcessData["Extension:Extensions"];
-      for (let data of extensions) {
-        this.extensions.set(data.id, new BrowserExtensionContent(data));
-        DocumentManager.startupExtension(data.id);
-      }
-    }
-  },
-
-  get(extensionId) {
-    return this.extensions.get(extensionId);
+  // 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);
   },
 
-  receiveMessage({name, data}) {
-    let extension;
-    switch (name) {
-      case "Extension:Startup": {
-        extension = new BrowserExtensionContent(data);
-
-        this.extensions.set(data.id, extension);
-
-        DocumentManager.startupExtension(data.id);
-
-        Services.cpmm.sendAsyncMessage("Extension:StartupComplete");
-        break;
-      }
-
-      case "Extension:Shutdown": {
-        extension = this.extensions.get(data.id);
-        extension.shutdown();
-
-        DocumentManager.shutdownExtension(data.id);
-
-        this.extensions.delete(data.id);
-        break;
-      }
-
-      case "Extension:FlushJarCache": {
-        flushJarCache(data.path);
-        Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete");
-        break;
-      }
-    }
+  initExtensionContext(extension, window) {
+    DocumentManager.initExtensionContext(extension, window);
   },
-};
-
-class ExtensionGlobal {
-  constructor(global) {
-    this.global = global;
 
-    MessageChannel.addListener(global, "Extension:Capture", this);
-    MessageChannel.addListener(global, "Extension:DetectLanguage", this);
-    MessageChannel.addListener(global, "Extension:Execute", this);
-    MessageChannel.addListener(global, "WebNavigation:GetFrame", this);
-    MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this);
-
-    this.windowId = getWinUtils(global.content).outerWindowID;
-
-    global.sendAsyncMessage("Extension:TopWindowID", {windowId: this.windowId});
-  }
-
-  uninit() {
-    this.global.sendAsyncMessage("Extension:RemoveTopWindowID", {windowId: this.windowId});
-  }
-
-  get messageFilterStrict() {
-    return {
-      innerWindowID: getInnerWindowID(this.global.content),
-    };
-  }
-
-  receiveMessage({target, messageName, recipient, data}) {
-    switch (messageName) {
-      case "Extension:Capture":
-        return this.handleExtensionCapture(data.width, data.height, data.options);
-      case "Extension:DetectLanguage":
-        return this.handleDetectLanguage(target);
-      case "Extension:Execute":
-        return this.handleExtensionExecute(target, recipient.extensionId, data.options);
-      case "WebNavigation:GetFrame":
-        return this.handleWebNavigationGetFrame(data.options);
-      case "WebNavigation:GetAllFrames":
-        return this.handleWebNavigationGetAllFrames();
-    }
-  }
-
-  handleExtensionCapture(width, height, options) {
-    let win = this.global.content;
+  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;
 
     let ctx = canvas.getContext("2d");
@@ -1199,19 +748,19 @@ class ExtensionGlobal {
     // We need to scale the image to the visible size of the browser,
     // in order for the result to appear as the user sees it when
     // settings like full zoom come into play.
     ctx.scale(canvas.width / win.innerWidth, canvas.height / win.innerHeight);
 
     ctx.drawWindow(win, win.scrollX, win.scrollY, win.innerWidth, win.innerHeight, "#fff");
 
     return canvas.toDataURL(`image/${options.format}`, options.quality / 100);
-  }
+  },
 
-  handleDetectLanguage(target) {
+  handleDetectLanguage(global, target) {
     let doc = target.content.document;
 
     return promiseDocumentReady(doc).then(() => {
       let elem = doc.documentElement;
 
       let language = (elem.getAttribute("xml:lang") || elem.getAttribute("lang") ||
                       doc.contentLanguage || null);
 
@@ -1220,73 +769,77 @@ class ExtensionGlobal {
       // values cause no harm.
       let tld = doc.location.hostname.match(/[a-z]*$/)[0];
 
       // The CLD2 library used by the language detector is capable of
       // analyzing raw HTML. Unfortunately, that takes much more memory,
       // and since it's hosted by emscripten, and therefore can't shrink
       // its heap after it's grown, it has a performance cost.
       // So we send plain text instead.
-      let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"].createInstance(Ci.nsIDocumentEncoder);
-      encoder.init(doc, "text/plain", encoder.SkipInvisibleContent);
+      let encoder = new DocumentEncoder(doc, "text/plain", Ci.nsIDocumentEncoder.SkipInvisibleContent);
       let text = encoder.encodeToStringWithMaxLength(60 * 1024);
 
       let encoding = doc.characterSet;
 
       return LanguageDetector.detectLanguage({language, tld, text, encoding})
         .then(result => result.language === "un" ? "und" : result.language);
     });
-  }
+  },
 
   // Used to executeScript, insertCSS and removeCSS.
-  handleExtensionExecute(target, extensionId, options) {
-    return DocumentManager.executeScript(target, extensionId, options).then(result => {
-      try {
-        // Make sure we can structured-clone the result value before
-        // we try to send it back over the message manager.
-        Cu.cloneInto(result, target);
-      } catch (e) {
-        const {js} = options;
-        const fileName = js.length ? js[js.length - 1] : "<anonymous code>";
-        const message = `Script '${fileName}' result is non-structured-clonable data`;
-        return Promise.reject({message, fileName});
+  async handleExtensionExecute(global, target, options, script) {
+    let executeInWin = (window) => {
+      if (script.matchesWindow(window)) {
+        return script.injectInto(window);
       }
-      return result;
-    });
-  }
+      return null;
+    };
+
+    let promises = Array.from(this.enumerateWindows(global.docShell), executeInWin)
+                        .filter(promise => promise);
+
+    if (!promises.length) {
+      if (options.frame_id) {
+        return Promise.reject({message: `Frame not found, or missing host permission`});
+      }
+
+      let frames = options.all_frames ? ", and any iframes" : "";
+      return Promise.reject({message: `Missing host permission for the tab${frames}`});
+    }
+    if (!options.all_frames && promises.length > 1) {
+      return Promise.reject({message: `Internal error: Script matched multiple windows`});
+    }
+
+    let result = await Promise.all(promises);
 
-  handleWebNavigationGetFrame({frameId}) {
-    return WebNavigationFrames.getFrame(this.global.docShell, frameId);
-  }
+    try {
+      // Make sure we can structured-clone the result value before
+      // we try to send it back over the message manager.
+      Cu.cloneInto(result, target);
+    } catch (e) {
+      const {js} = options;
+      const fileName = js.length ? js[js.length - 1] : "<anonymous code>";
+      const message = `Script '${fileName}' result is non-structured-clonable data`;
+      return Promise.reject({message, fileName});
+    }
+
+    return result;
+  },
 
-  handleWebNavigationGetAllFrames() {
-    return WebNavigationFrames.getAllFrames(this.global.docShell);
-  }
-}
+  handleWebNavigationGetFrame(global, {frameId}) {
+    return WebNavigationFrames.getFrame(global.docShell, frameId);
+  },
+
+  handleWebNavigationGetAllFrames(global) {
+    return WebNavigationFrames.getAllFrames(global.docShell);
+  },
 
-this.ExtensionContent = {
-  globals: new Map(),
+  // Helpers
 
-  init(global) {
-    this.globals.set(global, new ExtensionGlobal(global));
-    if (ExtensionManagement.isExtensionProcess) {
-      ExtensionChild.init(global);
+  * enumerateWindows(docShell) {
+    let enum_ = docShell.getDocShellEnumerator(docShell.typeContent,
+                                               docShell.ENUMERATE_FORWARDS);
+
+    for (let docShell of XPCOMUtils.IterSimpleEnumerator(enum_, Ci.nsIInterfaceRequestor)) {
+      yield docShell.getInterface(Ci.nsIDOMWindow);
     }
   },
-
-  uninit(global) {
-    if (ExtensionManagement.isExtensionProcess) {
-      ExtensionChild.uninit(global);
-    }
-    this.globals.get(global).uninit();
-    this.globals.delete(global);
-  },
-
-  // 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.
-  getContentScriptGlobalsForWindow(window) {
-    return DocumentManager.getContentScriptGlobalsForWindow(window);
-  },
 };
-
-ExtensionManager.init();
--- a/toolkit/components/extensions/ExtensionManagement.jsm
+++ b/toolkit/components/extensions/ExtensionManagement.jsm
@@ -6,32 +6,37 @@
 
 this.EXPORTED_SYMBOLS = ["ExtensionManagement"];
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
-Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
                                   "resource:///modules/E10SUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
                                   "resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
 
 XPCOMUtils.defineLazyGetter(this, "UUIDMap", () => {
   let {UUIDMap} = Cu.import("resource://gre/modules/Extension.jsm", {});
   return UUIDMap;
 });
 
+const {appinfo} = Services;
+const isParentProcess = appinfo.processType === appinfo.PROCESS_TYPE_DEFAULT;
+if (isParentProcess) {
+  Services.ppmm.loadProcessScript("chrome://extensions/content/extension-process-script.js", true);
+}
+
 var ExtensionManagement;
 
 /*
  * This file should be kept short and simple since it's loaded even
  * when no extensions are running.
  */
 
 // Keep track of frame IDs for content windows. Mostly we can just use
@@ -324,19 +329,19 @@ Services.obs.addObserver(onCacheInvalida
 
 ExtensionManagement = {
   get cacheInvalidated() {
     return cacheInvalidated;
   },
 
   get isExtensionProcess() {
     if (this.useRemoteWebExtensions) {
-      return Services.appinfo.remoteType === E10SUtils.EXTENSION_REMOTE_TYPE;
+      return appinfo.remoteType === E10SUtils.EXTENSION_REMOTE_TYPE;
     }
-    return Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
+    return isParentProcess;
   },
 
   startupExtension: Service.startupExtension.bind(Service),
   shutdownExtension: Service.shutdownExtension.bind(Service),
 
   registerAPI: APIs.register.bind(APIs),
   unregisterAPI: APIs.unregister.bind(APIs),
 
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -260,21 +260,19 @@ GlobalManager = {
     if (this.extensionMap.size == 0 && this.initialized) {
       apiManager.off("extension-browser-inserted", this._onExtensionBrowser);
       this.initialized = false;
     }
   },
 
   _onExtensionBrowser(type, browser, additionalData = {}) {
     browser.messageManager.loadFrameScript(`data:,
-      Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
-      ExtensionContent.init(this);
-      addEventListener("unload", function() {
-        ExtensionContent.uninit(this);
-      });
+      Components.utils.import("resource://gre/modules/Services.jsm");
+
+      Services.obs.notifyObservers(this, "tab-content-frameloader-created", "");
     `, false);
 
     let viewType = browser.getAttribute("webextension-view-type");
     if (viewType) {
       let data = {viewType};
 
       let {tabTracker} = apiManager.global;
       Object.assign(data, tabTracker.getBrowserData(browser), additionalData);
--- a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -50,22 +50,19 @@ let BASE_MANIFEST = Object.freeze({
   "manifest_version": 2,
 
   "name": "name",
   "version": "0",
 });
 
 
 function frameScript() {
-  Components.utils.import("resource://gre/modules/ExtensionContent.jsm");
+  Components.utils.import("resource://gre/modules/Services.jsm");
 
-  ExtensionContent.init(this);
-  this.addEventListener("unload", () => { // eslint-disable-line mozilla/balanced-listeners
-    ExtensionContent.uninit(this);
-  });
+  Services.obs.notifyObservers(this, "tab-content-frameloader-created", "");
 }
 
 const FRAME_SCRIPT = `data:text/javascript,(${encodeURI(frameScript)}).call(this)`;
 
 
 const XUL_URL = "data:application/vnd.mozilla.xul+xml;charset=utf-8," + encodeURI(
   `<?xml version="1.0"?>
   <window id="documentElement"/>`);
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -2528,16 +2528,17 @@ this.Schemas = {
     this.initialized = true;
 
     if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
       let data = Services.cpmm.initialProcessData;
       let schemas = data["Extension:Schemas"];
       if (schemas) {
         this.schemaJSON = schemas;
       }
+
       Services.cpmm.addMessageListener("Schema:Add", this);
     }
 
     this.flushSchemas();
   },
 
   receiveMessage(msg) {
     switch (msg.name) {
copy from toolkit/components/extensions/ExtensionContent.jsm
copy to toolkit/components/extensions/extension-process-script.js
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -1,1166 +1,201 @@
 /* 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 = ["ExtensionContent"];
-
-/* globals ExtensionContent */
-
-/*
- * This file handles the content process side of extensions. It mainly
- * takes care of content script injection, content script APIs, and
- * messaging.
- *
- * This file is also the initial entry point for addon processes.
- * ExtensionChild.jsm is responsible for functionality specific to addon
- * processes.
+/**
+ * This script contains the minimum, skeleton content process code that we need
+ * in order to lazily load other extension modules when they are first
+ * necessary. Anything which is not likely to be needed immediately, or shortly
+ * after startup, in *every* browser process live outside of this file.
  */
 
-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.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/AppConstants.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
                                   "resource://gre/modules/ExtensionManagement.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, "MatchGlobs",
-                                  "resource://gre/modules/MatchPattern.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.defineLazyModuleGetter(this, "WebNavigationFrames",
                                   "resource://gre/modules/WebNavigationFrames.jsm");
 
-XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
-                                   "@mozilla.org/content/style-sheet-service;1",
-                                   "nsIStyleSheetService");
-
-const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", "initWithCallback");
-
-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, "ExtensionContent",
+                                  "resource://gre/modules/ExtensionContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
+                                  "resource://gre/modules/ExtensionUtils.jsm");
 
-const {
-  DefaultMap,
-  DefaultWeakMap,
-  EventEmitter,
-  LocaleData,
-  defineLazyGetter,
-  flushJarCache,
-  getInnerWindowID,
-  getWinUtils,
-  promiseDocumentReady,
-  runSafeSyncWithoutClone,
-} = ExtensionUtils;
+XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
+XPCOMUtils.defineLazyGetter(this, "getInnerWindowID", () => ExtensionUtils.getInnerWindowID);
 
-const {
-  BaseContext,
-  CanOfAPIs,
-  SchemaAPIManager,
-} = ExtensionCommon;
-
-const {
-  ChildAPIManager,
-  Messenger,
-} = ExtensionChild;
+const isContentProcess = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
 
-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,
-               "document_idle": 2};
-  return table[when1] <= table[when2];
-}
 
-var apiManager = new class extends SchemaAPIManager {
-  constructor() {
-    super("content");
-    this.initialized = false;
-  }
+class ScriptMatcher {
+  constructor(extension, options) {
+    this.extension = extension;
+    this.options = options;
 
-  lazyInit() {
-    if (!this.initialized) {
-      this.initialized = true;
-      for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_CONTENT)) {
-        this.loadScript(value);
-      }
-    }
-  }
-}();
-
-const SCRIPT_EXPIRY_TIMEOUT_MS = 5 * 60 * 1000;
-const SCRIPT_CLEAR_TIMEOUT_MS = 5 * 1000;
-
-const CSS_EXPIRY_TIMEOUT_MS = 30 * 60 * 1000;
+    this._script = null;
 
-const scriptCaches = new WeakSet();
-const sheetCacheDocuments = new DefaultWeakMap(() => new WeakSet());
-
-class CacheMap extends DefaultMap {
-  constructor(timeout, getter) {
-    super(getter);
-
-    this.expiryTimeout = timeout;
-
-    scriptCaches.add(this);
-  }
+    this.allFrames = options.all_frames;
+    this.matchAboutBlank = options.match_about_blank;
+    this.frameId = options.frame_id;
+    this.runAt = options.run_at;
 
-  get(url) {
-    let promise = super.get(url);
-
-    promise.lastUsed = Date.now();
-    if (promise.timer) {
-      promise.timer.cancel();
-    }
-    promise.timer = Timer(this.delete.bind(this, url),
-                          this.expiryTimeout,
-                          Ci.nsITimer.TYPE_ONE_SHOT);
-
-    return promise;
+    this.matches = new MatchPattern(options.matches);
+    this.excludeMatches = new MatchPattern(options.exclude_matches || null);
+    // TODO: MatchPattern should pre-mangle host-only patterns so that we
+    // don't need to call a separate match function.
+    this.matchesHost = new MatchPattern(options.matchesHost || null);
+    this.includeGlobs = options.include_globs && new MatchGlobs(options.include_globs);
+    this.excludeGlobs = new MatchGlobs(options.exclude_globs);
   }
 
-  delete(url) {
-    if (this.has(url)) {
-      super.get(url).timer.cancel();
-    }
-
-    super.delete(url);
-  }
-
-  clear(timeout = SCRIPT_CLEAR_TIMEOUT_MS) {
-    let now = Date.now();
-    for (let [url, promise] of this.entries()) {
-      if (now - promise.lastUsed >= timeout) {
-        this.delete(url);
-      }
-    }
-  }
-}
-
-class ScriptCache extends CacheMap {
-  constructor(options) {
-    super(SCRIPT_EXPIRY_TIMEOUT_MS,
-          url => ChromeUtils.compileScript(url, options));
-  }
-}
-
-class CSSCache extends CacheMap {
-  constructor(sheetType) {
-    super(CSS_EXPIRY_TIMEOUT_MS, url => {
-      let uri = Services.io.newURI(url);
-      return styleSheetService.preloadSheetAsync(uri, sheetType).then(sheet => {
-        return {url, sheet};
-      });
-    });
-  }
-
-  addDocument(url, document) {
-    sheetCacheDocuments.get(this.get(url)).add(document);
-  }
-
-  deleteDocument(url, document) {
-    sheetCacheDocuments.get(this.get(url)).delete(document);
+  toString() {
+    return `[Script {js: [${this.options.js}], matchAboutBlank: ${this.matchAboutBlank}, runAt: ${this.runAt}, matches: ${this.options.matches}}]`;
   }
 
-  delete(url) {
-    if (this.has(url)) {
-      let promise = this.get(url);
-
-      // Never remove a sheet from the cache if it's still being used by a
-      // document. Rule processors can be shared between documents with the
-      // same preloaded sheet, so we only lose by removing them while they're
-      // still in use.
-      let docs = ChromeUtils.nondeterministicGetWeakSetKeys(sheetCacheDocuments.get(promise));
-      if (docs.length) {
-        return;
-      }
+  get script() {
+    if (!this._script) {
+      this._script = new ExtensionContent.Script(this.extension.realExtension,
+                                                 this.options);
     }
-
-    super.delete(url);
-  }
-}
-
-// Represents a content script.
-class Script {
-  constructor(extension, options, deferred = PromiseUtils.defer()) {
-    this.extension = extension;
-    this.options = options;
-    this.run_at = this.options.run_at;
-    this.js = this.options.js || [];
-    this.css = this.options.css || [];
-    this.remove_css = this.options.remove_css;
-    this.match_about_blank = this.options.match_about_blank;
-    this.css_origin = this.options.css_origin;
-
-    this.deferred = deferred;
-
-    this.cssCache = extension[this.css_origin === "user" ? "userCSS"
-                                                         : "authorCSS"];
-    this.scriptCache = extension[options.wantReturnValue ? "dynamicScripts"
-                                                         : "staticScripts"];
-
-    if (options.wantReturnValue) {
-      this.compileScripts();
-      this.loadCSS();
-    }
-
-    this.matches_ = new MatchPattern(this.options.matches);
-    this.exclude_matches_ = new MatchPattern(this.options.exclude_matches || null);
-    // TODO: MatchPattern should pre-mangle host-only patterns so that we
-    // don't need to call a separate match function.
-    this.matches_host_ = new MatchPattern(this.options.matchesHost || null);
-    this.include_globs_ = new MatchGlobs(this.options.include_globs);
-    this.exclude_globs_ = new MatchGlobs(this.options.exclude_globs);
-
-    this.requiresCleanup = !this.remove_css && (this.css.length > 0 || options.cssCode);
+    return this._script;
   }
 
-  compileScripts() {
-    return this.js.map(url => this.scriptCache.get(url));
-  }
+  preload() {
+    let {script} = this;
 
-  loadCSS() {
-    return this.cssURLs.map(url => this.cssCache.get(url));
+    script.loadCSS();
+    script.compileScripts();
   }
 
   matchesLoadInfo(uri, loadInfo) {
     if (!this.matchesURI(uri)) {
       return false;
     }
 
-    if (!this.options.all_frames && !loadInfo.isTopLevelLoad) {
+    if (!this.allFrames && !loadInfo.isTopLevelLoad) {
       return false;
     }
 
     return true;
   }
 
   matchesURI(uri) {
-    if (!(this.matches_.matches(uri) || this.matches_host_.matchesIgnoringPath(uri))) {
-      return false;
-    }
-
-    if (this.exclude_matches_.matches(uri)) {
+    if (!(this.matches.matches(uri) || this.matchesHost.matchesIgnoringPath(uri))) {
       return false;
     }
 
-    if (this.options.include_globs != null) {
-      if (!this.include_globs_.matches(uri.spec)) {
-        return false;
-      }
+    if (this.excludeMatches.matches(uri)) {
+      return false;
     }
 
-    if (this.exclude_globs_.matches(uri.spec)) {
+    if (this.includeGlobs != null && !this.includeGlobs.matches(uri.spec)) {
+      return false;
+    }
+
+    if (this.excludeGlobs.matches(uri.spec)) {
       return false;
     }
 
     return true;
   }
 
-  matches(window) {
+  matchesWindow(window) {
+    if (!this.allFrames && this.frameId == null && window.parent !== window) {
+      return false;
+    }
+
     let uri = window.document.documentURIObject;
     let principal = window.document.nodePrincipal;
 
-    // If mozAddonManager is present on this page, don't allow
-    // content scripts.
-    if (window.navigator.mozAddonManager !== undefined) {
-      return false;
-    }
-
-    if (this.match_about_blank) {
+    if (this.matchAboutBlank) {
       // When matching top-level about:blank documents,
       // allow loading into any with a NullPrincipal.
-      if (uri.spec === "about:blank" && window === window.top && principal.isNullPrincipal) {
+      if (uri.spec === "about:blank" && window === window.parent && principal.isNullPrincipal) {
         return true;
       }
 
       // When matching about:blank/srcdoc iframes, the checks below
       // need to be performed against the "owner" document's URI.
       if (["about:blank", "about:srcdoc"].includes(uri.spec)) {
         uri = principal.URI;
       }
     }
 
     // Documents from data: URIs also inherit the principal.
     if (Services.netUtils.URIChainHasFlags(uri, Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT)) {
-      if (!this.match_about_blank) {
+      if (!this.matchAboutBlank) {
         return false;
       }
       uri = principal.URI;
     }
 
     if (!this.matchesURI(uri)) {
       return false;
     }
 
-    if (this.options.frame_id != null) {
-      if (WebNavigationFrames.getFrameId(window) != this.options.frame_id) {
-        return false;
-      }
-    } else if (!this.options.all_frames && window.top != window) {
+    if (this.frameId != null && WebNavigationFrames.getFrameId(window) !== this.frameId) {
+      return false;
+    }
+
+    // If mozAddonManager is present on this page, don't allow
+    // content scripts.
+    if (window.navigator.mozAddonManager !== undefined) {
       return false;
     }
 
     return true;
   }
 
-  cleanup(window) {
-    if (!this.remove_css && this.cssURLs.length) {
-      let winUtils = getWinUtils(window);
-
-      let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
-      for (let url of this.cssURLs) {
-        this.cssCache.deleteDocument(url, window.document);
-        runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
-      }
-
-      // Clear any sheets that were kept alive past their timeout as
-      // a result of living in this document.
-      this.cssCache.clear(CSS_EXPIRY_TIMEOUT_MS);
-    }
-  }
-
-  /**
-   * Tries to inject this script into the given window and sandbox, if
-   * there are pending operations for the window's current load state.
-   *
-   * @param {Window} window
-   *        The DOM Window to inject the scripts and CSS into.
-   * @param {Sandbox} sandbox
-   *        A Sandbox inheriting from `window` in which to evaluate the
-   *        injected scripts.
-   * @param {function} shouldRun
-   *        A function which, when passed the document load state that a
-   *        script is expected to run at, returns `true` if we should
-   *        currently be injecting scripts for that load state.
-   *
-   *        For initial injection of a script, this function should
-   *        return true if the document is currently in or has already
-   *        passed through the given state. For injections triggered by
-   *        document state changes, it should only return true if the
-   *        given state exactly matches the state that triggered the
-   *        change.
-   * @param {string} when
-   *        The document's current load state, or if triggered by a
-   *        document state change, the new document state that triggered
-   *        the injection.
-   */
-  tryInject(window, sandbox, shouldRun, when) {
-    if (this.cssURLs.length && shouldRun("document_start")) {
-      let winUtils = getWinUtils(window);
-
-      let innerWindowID = winUtils.currentInnerWindowID;
-
-      let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
-
-      if (this.remove_css) {
-        for (let url of this.cssURLs) {
-          this.cssCache.deleteDocument(url, window.document);
-
-          runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
-        }
-
-        this.deferred.resolve();
-      } else {
-        this.deferred.resolve(
-          Promise.all(this.loadCSS()).then(sheets => {
-            if (winUtils.currentInnerWindowID !== innerWindowID) {
-              return;
-            }
-
-            for (let {url, sheet} of sheets) {
-              this.cssCache.addDocument(url, window.document);
-
-              runSafeSyncWithoutClone(winUtils.addSheet, sheet, type);
-            }
-          }));
-      }
-    }
-
-    let scheduled = this.run_at || "document_idle";
-    if (shouldRun(scheduled)) {
-      let scriptsPromise = Promise.all(this.compileScripts());
-
-      // If we're supposed to inject at the start of the document load,
-      // and we haven't already missed that point, block further parsing
-      // until the scripts have been loaded.
-      if (this.run_at === "document_start" && when === "document_start") {
-        window.document.blockParsing(scriptsPromise);
-      }
-
-      this.deferred.resolve(scriptsPromise.then(scripts => {
-        let result;
-
-        // The evaluations below may throw, in which case the promise will be
-        // automatically rejected.
-        for (let script of scripts) {
-          result = script.executeInGlobal(sandbox);
-        }
-
-        if (this.options.jsCode) {
-          result = Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
-        }
-
-        return result;
-      }));
-    }
+  injectInto(window) {
+    return this.script.injectInto(window);
   }
 }
 
-defineLazyGetter(Script.prototype, "cssURLs", function() {
-  // We can handle CSS urls (css) and CSS code (cssCode).
-  let urls = this.css.slice();
-
-  if (this.options.cssCode) {
-    urls.push("data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode));
-  }
-
-  return urls;
-});
-
-
-function getWindowMessageManager(contentWindow) {
-  let ir = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                        .getInterface(Ci.nsIDocShell)
-                        .QueryInterface(Ci.nsIInterfaceRequestor);
+function getMessageManager(contentWindow) {
+  let docShell = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIDocShell)
+                              .QueryInterface(Ci.nsIInterfaceRequestor);
   try {
-    return ir.getInterface(Ci.nsIContentFrameMessageManager);
+    return docShell.getInterface(Ci.nsIContentFrameMessageManager);
   } catch (e) {
     // Some windows don't support this interface (hidden window).
     return null;
   }
 }
 
 var DocumentManager;
 var ExtensionManager;
 
-/**
- * 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, contextOptions = {}) {
-    super("content_child", extension);
-
-    let {isExtensionPage} = contextOptions;
-
-    this.isExtensionPage = isExtensionPage;
-
-    this.setContentWindow(contentWindow);
-
-    let frameId = WebNavigationFrames.getFrameId(contentWindow);
-    this.frameId = frameId;
-
-    this.scripts = [];
-
-    let contentPrincipal = contentWindow.document.nodePrincipal;
-    let ssm = Services.scriptSecurityManager;
-
-    // Copy origin attributes from the content window origin attributes to
-    // preserve the user context id.
-    let attrs = contentPrincipal.originAttributes;
-    let extensionPrincipal = ssm.createCodebasePrincipal(this.extension.baseURI, attrs);
-
-    let principal;
-    if (ssm.isSystemPrincipal(contentPrincipal)) {
-      // Make sure we don't hand out the system principal by accident.
-      // also make sure that the null principal has the right origin attributes
-      principal = ssm.createNullPrincipal(attrs);
-    } else {
-      principal = [contentPrincipal, extensionPrincipal];
-    }
-
-    if (isExtensionPage) {
-      if (ExtensionManagement.getAddonIdForWindow(this.contentWindow) != this.extension.id) {
-        throw new Error("Invalid target window for this extension context");
-      }
-      // This is an iframe with content script API enabled and its principal should be the
-      // contentWindow itself. (we create a sandbox with the contentWindow as principal and with X-rays disabled
-      // because it enables us to create the APIs object in this sandbox object and then copying it
-      // into the iframe's window, see Bug 1214658 for rationale)
-      this.sandbox = Cu.Sandbox(contentWindow, {
-        sandboxPrototype: contentWindow,
-        sameZoneAs: contentWindow,
-        wantXrays: false,
-        isWebExtensionContentScript: true,
-      });
-    } else {
-      // This metadata is required by the Developer Tools, in order for
-      // the content script to be associated with both the extension and
-      // the tab holding the content page.
-      let metadata = {
-        "inner-window-id": this.innerWindowID,
-        addonId: extensionPrincipal.addonId,
-      };
-
-      this.sandbox = Cu.Sandbox(principal, {
-        metadata,
-        sandboxPrototype: contentWindow,
-        sameZoneAs: contentWindow,
-        wantXrays: true,
-        isWebExtensionContentScript: true,
-        wantExportHelpers: true,
-        wantGlobalProperties: ["XMLHttpRequest", "fetch"],
-        originAttributes: attrs,
-      });
-
-      Cu.evalInSandbox(`
-        window.JSON = JSON;
-        window.XMLHttpRequest = XMLHttpRequest;
-        window.fetch = fetch;
-      `, this.sandbox);
-    }
-
-    Object.defineProperty(this, "principal", {
-      value: Cu.getObjectPrincipal(this.sandbox),
-      enumerable: true,
-      configurable: true,
-    });
-
-    this.url = contentWindow.location.href;
-
-    defineLazyGetter(this, "chromeObj", () => {
-      let chromeObj = Cu.createObjectIn(this.sandbox);
-
-      Schemas.inject(chromeObj, this.childManager);
-      return chromeObj;
-    });
-
-    Schemas.exportLazyGetter(this.sandbox, "browser", () => this.chromeObj);
-    Schemas.exportLazyGetter(this.sandbox, "chrome", () => this.chromeObj);
-
-    // This is an iframe with content script API enabled (bug 1214658)
-    if (isExtensionPage) {
-      Schemas.exportLazyGetter(this.contentWindow,
-                               "browser", () => this.chromeObj);
-      Schemas.exportLazyGetter(this.contentWindow,
-                               "chrome", () => this.chromeObj);
-    }
-  }
-
-  get cloneScope() {
-    return this.sandbox;
-  }
-
-  execute(script, shouldRun, when) {
-    script.tryInject(this.contentWindow, this.sandbox, shouldRun, when);
-  }
-
-  addScript(script, when) {
-    let state = DocumentManager.getWindowState(this.contentWindow);
-    this.execute(script, scheduled => isWhenBeforeOrSame(scheduled, state), when);
-
-    // Save the script in case it has pending operations in later load
-    // states, but only if we're before document_idle, or require cleanup.
-    if (state != "document_idle" || script.requiresCleanup) {
-      this.scripts.push(script);
-    }
-  }
-
-  triggerScripts(documentState) {
-    for (let script of this.scripts) {
-      this.execute(script, scheduled => scheduled == documentState, documentState);
-    }
-    if (documentState == "document_idle") {
-      // Don't bother saving scripts after document_idle.
-      this.scripts = this.scripts.filter(script => script.requiresCleanup);
-    }
-  }
-
-  close() {
-    super.unload();
-
-    if (this.contentWindow) {
-      for (let script of this.scripts) {
-        if (script.requiresCleanup) {
-          script.cleanup(this.contentWindow);
-        }
-      }
-
-      // Overwrite the content script APIs with an empty object if the APIs objects are still
-      // defined in the content window (bug 1214658).
-      if (this.isExtensionPage) {
-        Cu.createObjectIn(this.contentWindow, {defineAs: "browser"});
-        Cu.createObjectIn(this.contentWindow, {defineAs: "chrome"});
-      }
-    }
-    Cu.nukeSandbox(this.sandbox);
-    this.sandbox = null;
-  }
-}
-
-defineLazyGetter(ContentScriptContextChild.prototype, "messenger", function() {
-  // The |sender| parameter is passed directly to the extension.
-  let sender = {id: this.extension.id, frameId: this.frameId, url: this.url};
-  let filter = {extensionId: this.extension.id};
-  let optionalFilter = {frameId: this.frameId};
-
-  return new Messenger(this, [this.messageManager], sender, filter, optionalFilter);
-});
-
-defineLazyGetter(ContentScriptContextChild.prototype, "childManager", function() {
-  apiManager.lazyInit();
-
-  let localApis = {};
-  let can = new CanOfAPIs(this, apiManager, localApis);
-
-  let childManager = new ChildAPIManager(this, this.messageManager, can, {
-    envType: "content_parent",
-    url: this.url,
-  });
-
-  this.callOnClose(childManager);
-
-  return childManager;
-});
-
-// Responsible for creating ExtensionContexts and injecting content
-// scripts into them when new documents are created.
-DocumentManager = {
-  extensionCount: 0,
-
-  // Map[windowId -> Map[extensionId -> ContentScriptContextChild]]
-  contentScriptWindows: new Map(),
-
-  // Map[windowId -> ContentScriptContextChild]
-  extensionPageWindows: new Map(),
-
-  init() {
-    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-      Services.obs.addObserver(this, "http-on-opening-request", false);
-    }
-    Services.obs.addObserver(this, "content-document-global-created", false);
-    Services.obs.addObserver(this, "document-element-inserted", false);
-    Services.obs.addObserver(this, "inner-window-destroyed", false);
-    Services.obs.addObserver(this, "memory-pressure", false);
-  },
-
-  uninit() {
-    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-      Services.obs.removeObserver(this, "http-on-opening-request");
-    }
-    Services.obs.removeObserver(this, "content-document-global-created");
-    Services.obs.removeObserver(this, "document-element-inserted");
-    Services.obs.removeObserver(this, "inner-window-destroyed");
-    Services.obs.removeObserver(this, "memory-pressure");
-  },
-
-  getWindowState(contentWindow) {
-    let readyState = contentWindow.document.readyState;
-    if (readyState == "complete") {
-      return "document_idle";
-    }
-    if (readyState == "interactive") {
-      return "document_end";
-    }
-    return "document_start";
-  },
-
-  loadInto(window) {
-    // Enable the content script APIs should be available in subframes' window
-    // if it is recognized as a valid addon id (see Bug 1214658 for rationale).
-    const {
-      NO_PRIVILEGES,
-      CONTENTSCRIPT_PRIVILEGES,
-      FULL_PRIVILEGES,
-    } = ExtensionManagement.API_LEVELS;
-    let extensionId = ExtensionManagement.getAddonIdForWindow(window);
-    let apiLevel = ExtensionManagement.getAPILevelForWindow(window, extensionId);
-
-    if (apiLevel != NO_PRIVILEGES) {
-      let extension = ExtensionManager.get(extensionId);
-      if (extension) {
-        if (apiLevel == CONTENTSCRIPT_PRIVILEGES) {
-          DocumentManager.getExtensionPageContext(extension, window);
-        } else if (apiLevel == FULL_PRIVILEGES) {
-          ExtensionChild.createExtensionContext(extension, window);
-        }
-      }
-    }
-  },
-
-  observers: {
-    // For some types of documents (about:blank), we only see the first
-    // notification, for others (data: URIs) we only observe the second.
-    "content-document-global-created"(subject, topic, data) {
-      this.observers["document-element-inserted"].call(this, subject.document, topic, data);
-    },
-    "document-element-inserted"(subject, topic, data) {
-      let document = subject;
-      let window = document && document.defaultView;
-
-      if (!document || !document.location || !window) {
-        return;
-      }
-
-      // Make sure we only load into frames that ExtensionContent.init
-      // was called on (i.e., not frames for social or sidebars).
-      let mm = getWindowMessageManager(window);
-      if (!mm || !ExtensionContent.globals.has(mm)) {
-        return;
-      }
-
-      // Load on document-element-inserted, except for about:blank which doesn't
-      // see it, and needs special late handling on DOMContentLoaded event.
-      if (topic === "document-element-inserted") {
-        this.loadInto(window);
-        this.trigger("document_start", window);
-      }
-
-      /* eslint-disable mozilla/balanced-listeners */
-      window.addEventListener("DOMContentLoaded", this, true);
-      window.addEventListener("load", this, true);
-      /* eslint-enable mozilla/balanced-listeners */
-    },
-    "inner-window-destroyed"(subject, topic, data) {
-      let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
-
-      MessageChannel.abortResponses({innerWindowID: windowId});
-
-      // Close any existent content-script context for the destroyed window.
-      if (this.contentScriptWindows.has(windowId)) {
-        let extensions = this.contentScriptWindows.get(windowId);
-        for (let [, context] of extensions) {
-          context.close();
-        }
-
-        this.contentScriptWindows.delete(windowId);
-      }
-
-      // Close any existent iframe extension page context for the destroyed window.
-      if (this.extensionPageWindows.has(windowId)) {
-        let context = this.extensionPageWindows.get(windowId);
-        context.close();
-        this.extensionPageWindows.delete(windowId);
-      }
-
-      ExtensionChild.destroyExtensionContext(windowId);
-    },
-    "http-on-opening-request"(subject, topic, data) {
-      let {loadInfo} = subject.QueryInterface(Ci.nsIChannel);
-      if (loadInfo) {
-        let {externalContentPolicyType: type} = loadInfo;
-        if (type === Ci.nsIContentPolicy.TYPE_DOCUMENT ||
-            type === Ci.nsIContentPolicy.TYPE_SUBDOCUMENT) {
-          this.preloadScripts(subject.URI, loadInfo);
-        }
-      }
-    },
-    "memory-pressure"(subject, topic, data) {
-      let timeout = data === "heap-minimize" ? 0 : undefined;
-
-      for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(scriptCaches)) {
-        cache.clear(timeout);
-      }
-    },
-  },
-
-  observe(subject, topic, data) {
-    this.observers[topic].call(this, subject, topic, data);
-  },
-
-  handleEvent(event) {
-    let window = event.currentTarget;
-    if (event.target != window.document) {
-      // We use capturing listeners so we have precedence over content script
-      // listeners, but only care about events targeted to the element we're
-      // listening on.
-      return;
-    }
-    window.removeEventListener(event.type, this, true);
-
-    // Need to check if we're still on the right page? Greasemonkey does this.
-
-    if (event.type == "DOMContentLoaded") {
-      // By this time, we can be sure if this is an explicit about:blank
-      // document, and if it needs special late loading and fake trigger.
-      if (window.location.href === "about:blank") {
-        this.loadInto(window);
-        this.trigger("document_start", window);
-      }
-      this.trigger("document_end", window);
-    } else if (event.type == "load") {
-      this.trigger("document_idle", window);
-    }
-  },
-
-  // Used to executeScript, insertCSS and removeCSS.
-  executeScript(global, extensionId, options) {
-    let extension = ExtensionManager.get(extensionId);
-
-    let executeInWin = (window) => {
-      let deferred = PromiseUtils.defer();
-      let script = new Script(extension, options, deferred);
-
-      if (script.matches(window)) {
-        let context = this.getContentScriptContext(extension, window);
-        context.addScript(script);
-        return deferred.promise;
-      }
-      return null;
-    };
-
-    let promises = Array.from(this.enumerateWindows(global.docShell), executeInWin)
-                        .filter(promise => promise);
-
-    if (!promises.length) {
-      if (options.frame_id) {
-        return Promise.reject({message: `Frame not found, or missing host permission`});
-      }
-
-      let frames = options.all_frames ? ", and any iframes" : "";
-      return Promise.reject({message: `Missing host permission for the tab${frames}`});
-    }
-    if (!options.all_frames && promises.length > 1) {
-      return Promise.reject({message: `Internal error: Script matched multiple windows`});
-    }
-    return Promise.all(promises);
-  },
-
-  enumerateWindows: function* (docShell) {
-    let window = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindow);
-    yield window;
-
-    for (let i = 0; i < docShell.childCount; i++) {
-      let child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
-      yield* this.enumerateWindows(child);
-    }
-  },
-
-  getContentScriptGlobalsForWindow(window) {
-    let winId = getInnerWindowID(window);
-    let extensions = this.contentScriptWindows.get(winId);
-
-    if (extensions) {
-      return Array.from(extensions.values(), ctx => ctx.sandbox);
-    }
-
-    return [];
-  },
-
-  getContentScriptContext(extension, window) {
-    let winId = getInnerWindowID(window);
-    if (!this.contentScriptWindows.has(winId)) {
-      this.contentScriptWindows.set(winId, new Map());
-    }
-
-    let extensions = this.contentScriptWindows.get(winId);
-    if (!extensions.has(extension.id)) {
-      let context = new ContentScriptContextChild(extension, window);
-      extensions.set(extension.id, context);
-    }
-
-    return extensions.get(extension.id);
-  },
-
-  getExtensionPageContext(extension, window) {
-    let winId = getInnerWindowID(window);
-
-    let context = this.extensionPageWindows.get(winId);
-    if (!context) {
-      let context = new ContentScriptContextChild(extension, window, {isExtensionPage: true});
-      this.extensionPageWindows.set(winId, context);
-    }
-
-    return context;
-  },
-
-  startupExtension(extensionId) {
-    if (this.extensionCount == 0) {
-      this.init();
-    }
-    this.extensionCount++;
-
-    let extension = ExtensionManager.get(extensionId);
-    for (let global of ExtensionContent.globals.keys()) {
-      // Note that we miss windows in the bfcache here. In theory we
-      // could execute content scripts on a pageshow event for that
-      // window, but that seems extreme.
-      for (let window of this.enumerateWindows(global.docShell)) {
-        for (let script of extension.scripts) {
-          if (script.matches(window)) {
-            let context = this.getContentScriptContext(extension, window);
-            context.addScript(script);
-          }
-        }
-      }
-    }
-  },
-
-  shutdownExtension(extensionId) {
-    // Clean up content-script contexts on extension shutdown.
-    for (let [, extensions] of this.contentScriptWindows) {
-      let context = extensions.get(extensionId);
-      if (context) {
-        context.close();
-        extensions.delete(extensionId);
-      }
-    }
-
-    // Clean up iframe extension page contexts on extension shutdown.
-    for (let [winId, context] of this.extensionPageWindows) {
-      if (context.extension.id == extensionId) {
-        context.close();
-        this.extensionPageWindows.delete(winId);
-      }
-    }
-
-    ExtensionChild.shutdownExtension(extensionId);
-
-    MessageChannel.abortResponses({extensionId});
-
-    this.extensionCount--;
-    if (this.extensionCount == 0) {
-      this.uninit();
-    }
-  },
-
-  preloadScripts(uri, loadInfo) {
-    for (let extension of ExtensionManager.extensions.values()) {
-      for (let script of extension.scripts) {
-        if (script.matchesLoadInfo(uri, loadInfo)) {
-          script.loadCSS();
-          script.compileScripts();
-        }
-      }
-    }
-  },
-
-  trigger(when, window) {
-    if (when === "document_start") {
-      for (let extension of ExtensionManager.extensions.values()) {
-        for (let script of extension.scripts) {
-          if (script.matches(window)) {
-            let context = this.getContentScriptContext(extension, window);
-            context.addScript(script, when);
-          }
-        }
-      }
-    } else {
-      let contexts = this.contentScriptWindows.get(getInnerWindowID(window)) || new Map();
-      for (let context of contexts.values()) {
-        context.triggerScripts(when);
-      }
-    }
-  },
-};
-
-// Represents a browser extension in the content process.
-class BrowserExtensionContent extends EventEmitter {
-  constructor(data) {
-    super();
-
-    this.id = data.id;
-    this.uuid = data.uuid;
-    this.data = data;
-    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();
-
-    let uri = Services.io.newURI(data.resourceURL);
-
-    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-      // Extension.jsm takes care of this in the parent.
-      ExtensionManagement.startupExtension(this.uuid, uri, this);
-    }
-
-    /* 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 */
-  }
-
-  shutdown() {
-    Services.cpmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
-
-    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-      ExtensionManagement.shutdownExtension(this.uuid);
-    }
-  }
-
-  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);
-});
-
-ExtensionManager = {
-  // Map[extensionId, BrowserExtensionContent]
-  extensions: new Map(),
-
-  init() {
-    ExtensionChild.initOnce();
-
-    Services.cpmm.addMessageListener("Extension:Startup", this);
-    Services.cpmm.addMessageListener("Extension:Shutdown", this);
-    Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
-
-    if (Services.cpmm.initialProcessData && "Extension:Extensions" in Services.cpmm.initialProcessData) {
-      let extensions = Services.cpmm.initialProcessData["Extension:Extensions"];
-      for (let data of extensions) {
-        this.extensions.set(data.id, new BrowserExtensionContent(data));
-        DocumentManager.startupExtension(data.id);
-      }
-    }
-  },
-
-  get(extensionId) {
-    return this.extensions.get(extensionId);
-  },
-
-  receiveMessage({name, data}) {
-    let extension;
-    switch (name) {
-      case "Extension:Startup": {
-        extension = new BrowserExtensionContent(data);
-
-        this.extensions.set(data.id, extension);
-
-        DocumentManager.startupExtension(data.id);
-
-        Services.cpmm.sendAsyncMessage("Extension:StartupComplete");
-        break;
-      }
-
-      case "Extension:Shutdown": {
-        extension = this.extensions.get(data.id);
-        extension.shutdown();
-
-        DocumentManager.shutdownExtension(data.id);
-
-        this.extensions.delete(data.id);
-        break;
-      }
-
-      case "Extension:FlushJarCache": {
-        flushJarCache(data.path);
-        Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete");
-        break;
-      }
-    }
-  },
-};
-
 class ExtensionGlobal {
   constructor(global) {
     this.global = global;
 
     MessageChannel.addListener(global, "Extension:Capture", this);
     MessageChannel.addListener(global, "Extension:DetectLanguage", this);
     MessageChannel.addListener(global, "Extension:Execute", this);
     MessageChannel.addListener(global, "WebNavigation:GetFrame", this);
     MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this);
 
-    this.windowId = getWinUtils(global.content).outerWindowID;
+    this.windowId = global.content.QueryInterface(Ci.nsIInterfaceRequestor)
+                          .getInterface(Ci.nsIDOMWindowUtils)
+                          .outerWindowID;
 
     global.sendAsyncMessage("Extension:TopWindowID", {windowId: this.windowId});
   }
 
   uninit() {
     this.global.sendAsyncMessage("Extension:RemoveTopWindowID", {windowId: this.windowId});
   }
 
@@ -1168,125 +203,409 @@ class ExtensionGlobal {
     return {
       innerWindowID: getInnerWindowID(this.global.content),
     };
   }
 
   receiveMessage({target, messageName, recipient, data}) {
     switch (messageName) {
       case "Extension:Capture":
-        return this.handleExtensionCapture(data.width, data.height, data.options);
+        return ExtensionContent.handleExtensionCapture(this.global, data.width, data.height, data.options);
       case "Extension:DetectLanguage":
-        return this.handleDetectLanguage(target);
+        return ExtensionContent.handleDetectLanguage(this.global, target);
       case "Extension:Execute":
-        return this.handleExtensionExecute(target, recipient.extensionId, data.options);
-      case "WebNavigation:GetFrame":
-        return this.handleWebNavigationGetFrame(data.options);
-      case "WebNavigation:GetAllFrames":
-        return this.handleWebNavigationGetAllFrames();
-    }
-  }
-
-  handleExtensionCapture(width, height, options) {
-    let win = this.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;
-
-    let ctx = canvas.getContext("2d");
-
-    // We need to scale the image to the visible size of the browser,
-    // in order for the result to appear as the user sees it when
-    // settings like full zoom come into play.
-    ctx.scale(canvas.width / win.innerWidth, canvas.height / win.innerHeight);
-
-    ctx.drawWindow(win, win.scrollX, win.scrollY, win.innerWidth, win.innerHeight, "#fff");
-
-    return canvas.toDataURL(`image/${options.format}`, options.quality / 100);
-  }
-
-  handleDetectLanguage(target) {
-    let doc = target.content.document;
-
-    return promiseDocumentReady(doc).then(() => {
-      let elem = doc.documentElement;
-
-      let language = (elem.getAttribute("xml:lang") || elem.getAttribute("lang") ||
-                      doc.contentLanguage || null);
+        let extension = ExtensionManager.get(recipient.extensionId);
+        let script = new ScriptMatcher(extension, data.options);
 
-      // We only want the last element of the TLD here.
-      // Only country codes have any effect on the results, but other
-      // values cause no harm.
-      let tld = doc.location.hostname.match(/[a-z]*$/)[0];
-
-      // The CLD2 library used by the language detector is capable of
-      // analyzing raw HTML. Unfortunately, that takes much more memory,
-      // and since it's hosted by emscripten, and therefore can't shrink
-      // its heap after it's grown, it has a performance cost.
-      // So we send plain text instead.
-      let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"].createInstance(Ci.nsIDocumentEncoder);
-      encoder.init(doc, "text/plain", encoder.SkipInvisibleContent);
-      let text = encoder.encodeToStringWithMaxLength(60 * 1024);
-
-      let encoding = doc.characterSet;
-
-      return LanguageDetector.detectLanguage({language, tld, text, encoding})
-        .then(result => result.language === "un" ? "und" : result.language);
-    });
-  }
-
-  // Used to executeScript, insertCSS and removeCSS.
-  handleExtensionExecute(target, extensionId, options) {
-    return DocumentManager.executeScript(target, extensionId, options).then(result => {
-      try {
-        // Make sure we can structured-clone the result value before
-        // we try to send it back over the message manager.
-        Cu.cloneInto(result, target);
-      } catch (e) {
-        const {js} = options;
-        const fileName = js.length ? js[js.length - 1] : "<anonymous code>";
-        const message = `Script '${fileName}' result is non-structured-clonable data`;
-        return Promise.reject({message, fileName});
-      }
-      return result;
-    });
-  }
-
-  handleWebNavigationGetFrame({frameId}) {
-    return WebNavigationFrames.getFrame(this.global.docShell, frameId);
-  }
-
-  handleWebNavigationGetAllFrames() {
-    return WebNavigationFrames.getAllFrames(this.global.docShell);
+        return ExtensionContent.handleExtensionExecute(this.global, target, data.options, script);
+      case "WebNavigation:GetFrame":
+        return ExtensionContent.handleWebNavigationGetFrame(this.global, data.options);
+      case "WebNavigation:GetAllFrames":
+        return ExtensionContent.handleWebNavigationGetAllFrames(this.global);
+    }
   }
 }
 
-this.ExtensionContent = {
+// Responsible for creating ExtensionContexts and injecting content
+// scripts into them when new documents are created.
+DocumentManager = {
   globals: new Map(),
 
-  init(global) {
+  // Initialize listeners that we need regardless of whether extensions are
+  // enabled.
+  earlyInit() {
+    Services.obs.addObserver(this, "tab-content-frameloader-created", false); // eslint-disable-line mozilla/balanced-listeners
+  },
+
+  // Initialize listeners that we need when any extension is enabled.
+  init() {
+    Services.obs.addObserver(this, "document-element-inserted", false);
+  },
+  uninit() {
+    Services.obs.removeObserver(this, "document-element-inserted");
+  },
+
+  // Initialize listeners that we need when any extension content script is
+  // enabled.
+  initMatchers() {
+    if (isContentProcess) {
+      Services.obs.addObserver(this, "http-on-opening-request", false);
+    }
+  },
+  uninitMatchers() {
+    if (isContentProcess) {
+      Services.obs.removeObserver(this, "http-on-opening-request");
+    }
+  },
+
+  // Initialize listeners that we need when any about:blank content script is
+  // enabled.
+  //
+  // Loads of about:blank are special, and do not trigger "document-element-inserted"
+  // observers. So if we have any scripts that match about:blank, we also need
+  // 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");
+  },
+
+  // 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);
     }
   },
-
-  uninit(global) {
+  uninitGlobal(global) {
     if (ExtensionManagement.isExtensionProcess) {
       ExtensionChild.uninit(global);
     }
     this.globals.get(global).uninit();
     this.globals.delete(global);
   },
 
-  // 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.
-  getContentScriptGlobalsForWindow(window) {
-    return DocumentManager.getContentScriptGlobalsForWindow(window);
+  initExtension(extension) {
+    if (this.extensionCount === 0) {
+      this.init();
+    }
+    this.extensionCount++;
+
+    for (let script of extension.scripts) {
+      this.addContentScript(script);
+    }
+
+    this.injectExtensionScripts(extension);
+  },
+  uninitExtension(extension) {
+    for (let script of extension.scripts) {
+      this.removeContentScript(script);
+    }
+
+    this.extensionCount--;
+    if (this.extensionCount === 0) {
+      this.uninit();
+    }
+  },
+
+
+  extensionCount: 0,
+  matchAboutBlankCount: 0,
+
+  contentScripts: new Set(),
+
+  addContentScript(script) {
+    if (this.contentScripts.size == 0) {
+      this.initMatchers();
+    }
+
+    if (script.matchAboutBlank) {
+      if (this.matchAboutBlankCount == 0) {
+        this.initAboutBlankMatchers();
+      }
+      this.matchAboutBlankCount++;
+    }
+
+    this.contentScripts.add(script);
+  },
+  removeContentScript(script) {
+    this.contentScripts.delete(script);
+
+    if (this.contentScripts.size == 0) {
+      this.uninitMatchers();
+    }
+
+    if (script.matchAboutBlank) {
+      this.matchAboutBlankCount--;
+      if (this.matchAboutBlankCount == 0) {
+        this.uninitAboutBlankMatchers();
+      }
+    }
+  },
+
+  // Listeners
+
+  observers: {
+    async "content-document-global-created"(window) {
+      // We only care about about:blank here, since it doesn't trigger
+      // "document-element-inserted".
+      if ((window.location && window.location.href !== "about:blank") ||
+          // Make sure we only load into frames that belong to tabs, or other
+          // special areas that we want to load content scripts into.
+          !this.globals.has(getMessageManager(window))) {
+        return;
+      }
+
+      // We can't tell for certain whether the final document will actually be
+      // about:blank at this point, though, so wait for the DOM to finish
+      // loading and check again before injecting scripts.
+      await new Promise(resolve => window.addEventListener(
+        "DOMContentLoaded", resolve, {once: true, capture: true}));
+
+      if (window.location.href === "about:blank") {
+        this.injectWindowScripts(window);
+      }
+    },
+
+    "document-element-inserted"(document) {
+      let window = document.defaultView;
+      if (!document.location || !window ||
+          // Make sure we only load into frames that belong to tabs, or other
+          // special areas that we want to load content scripts into.
+          !this.globals.has(getMessageManager(window))) {
+        return;
+      }
+
+      this.injectWindowScripts(window);
+      this.loadInto(window);
+    },
+
+    "http-on-opening-request"(subject, topic, data) {
+      // If this request is a docshell load, check whether any of our scripts
+      // are likely to be loaded into it, and begin preloading the ones that
+      // are.
+      let {loadInfo} = subject.QueryInterface(Ci.nsIChannel);
+      if (loadInfo) {
+        let {externalContentPolicyType: type} = loadInfo;
+        if (type === Ci.nsIContentPolicy.TYPE_DOCUMENT ||
+            type === Ci.nsIContentPolicy.TYPE_SUBDOCUMENT) {
+          this.preloadScripts(subject.URI, loadInfo);
+        }
+      }
+    },
+
+    "tab-content-frameloader-created"(global) {
+      this.initGlobal(global);
+    },
+  },
+
+  observe(subject, topic, data) {
+    this.observers[topic].call(this, subject, topic, data);
+  },
+
+  // Script loading
+
+  injectExtensionScripts(extension) {
+    for (let window of this.enumerateWindows()) {
+      for (let script of extension.scripts) {
+        if (script.matchesWindow(window)) {
+          script.injectInto(window);
+        }
+      }
+    }
+  },
+
+  injectWindowScripts(window) {
+    for (let script of this.contentScripts) {
+      if (script.matchesWindow(window)) {
+        script.injectInto(window);
+      }
+    }
+  },
+
+  preloadScripts(uri, loadInfo) {
+    for (let script of this.contentScripts) {
+      if (script.matchesLoadInfo(uri, loadInfo)) {
+        script.preload();
+      }
+    }
+  },
+
+  loadInto(window) {
+    let extensionId = ExtensionManagement.getAddonIdForWindow(window);
+    if (!extensionId) {
+      return;
+    }
+
+    let extension = ExtensionManager.get(extensionId);
+    if (!extension) {
+      throw new Error(`No registered extension for ID ${extensionId}`);
+    }
+
+    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);
+    } else {
+      throw new Error(`Unexpected window with extension ID ${extensionId}`);
+    }
+  },
+
+  // Helpers
+
+  * enumerateWindows(docShell) {
+    if (docShell) {
+      let enum_ = docShell.getDocShellEnumerator(docShell.typeContent,
+                                                 docShell.ENUMERATE_FORWARDS);
+
+      for (let docShell of XPCOMUtils.IterSimpleEnumerator(enum_, Ci.nsIInterfaceRequestor)) {
+        yield docShell.getInterface(Ci.nsIDOMWindow);
+      }
+    } else {
+      for (let global of this.globals.keys()) {
+        yield* this.enumerateWindows(global.docShell);
+      }
+    }
   },
 };
 
+/**
+ * This class is a minimal stub extension object which loads and instantiates a
+ * real extension object when non-basic functionality is needed.
+ */
+class StubExtension {
+  constructor(data) {
+    this.data = data;
+    this.id = data.id;
+    this.uuid = data.uuid;
+    this.instanceId = data.instanceId;
+    this.manifest = data.manifest;
+
+    this.scripts = data.content_scripts.map(scriptData => new ScriptMatcher(this, scriptData));
+
+    this._realExtension = null;
+
+    this.startup();
+  }
+
+  startup() {
+    // Extension.jsm takes care of this in the parent.
+    if (isContentProcess) {
+      let uri = Services.io.newURI(this.data.resourceURL);
+      ExtensionManagement.startupExtension(this.uuid, uri, this);
+    }
+  }
+
+  shutdown() {
+    if (isContentProcess) {
+      ExtensionManagement.shutdownExtension(this.uuid);
+    }
+    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);
+    }
+    return this._realExtension;
+  }
+
+  // Forward functions needed by ExtensionManagement.
+  hasPermission(...args) {
+    return this.realExtension.hasPermission(...args);
+  }
+  localize(...args) {
+    return this.realExtension.localize(...args);
+  }
+  get whiteListedHosts() {
+    return this.realExtension.whiteListedHosts;
+  }
+  get webAccessibleResources() {
+    return this.realExtension.webAccessibleResources;
+  }
+}
+
+ExtensionManager = {
+  // Map[extensionId -> StubExtension]
+  extensions: new Map(),
+
+  init() {
+    MessageChannel.setupMessageManagers([Services.cpmm]);
+
+    Services.cpmm.addMessageListener("Extension:Startup", this);
+    Services.cpmm.addMessageListener("Extension:Shutdown", this);
+    Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
+
+    let procData = Services.cpmm.initialProcessData || {};
+
+    for (let data of procData["Extension:Extensions"] || []) {
+      let extension = new StubExtension(data);
+      this.extensions.set(data.id, extension);
+      DocumentManager.initExtension(extension);
+    }
+
+    if (isContentProcess) {
+      // Make sure we handle new schema data until Schemas.jsm is loaded.
+      if (!procData["Extension:Schemas"]) {
+        procData["Extension:Schemas"] = new Map();
+      }
+      this.schemaJSON = procData["Extension:Schemas"];
+
+      Services.cpmm.addMessageListener("Schema:Add", this);
+    }
+  },
+
+  get(extensionId) {
+    return this.extensions.get(extensionId);
+  },
+
+  receiveMessage({name, data}) {
+    switch (name) {
+      case "Extension:Startup": {
+        let extension = new StubExtension(data);
+
+        this.extensions.set(data.id, extension);
+
+        DocumentManager.initExtension(extension);
+
+        Services.cpmm.sendAsyncMessage("Extension:StartupComplete");
+        break;
+      }
+
+      case "Extension:Shutdown": {
+        let extension = this.extensions.get(data.id);
+        this.extensions.delete(data.id);
+
+        extension.shutdown();
+
+        DocumentManager.uninitExtension(extension);
+        break;
+      }
+
+      case "Extension:FlushJarCache": {
+        ExtensionUtils.flushJarCache(data.path);
+        Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete");
+        break;
+      }
+
+      case "Schema:Add": {
+        this.schemaJSON.set(data.url, data.schema);
+        break;
+      }
+    }
+  },
+};
+
+DocumentManager.earlyInit();
 ExtensionManager.init();
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -1,14 +1,15 @@
 # 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/.
 
 toolkit.jar:
 % content extensions %content/extensions/
+    content/extensions/extension-process-script.js
     content/extensions/ext-alarms.js
     content/extensions/ext-backgroundPage.js
     content/extensions/ext-browser-content.js
     content/extensions/ext-contextualIdentities.js
     content/extensions/ext-cookies.js
     content/extensions/ext-downloads.js
     content/extensions/ext-extension.js
     content/extensions/ext-geolocation.js
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html
@@ -16,19 +16,24 @@
 add_task(function* test_contentscript_cache() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       content_scripts: [{
         "matches": ["http://example.com/"],
         "js": ["content_script.js"],
         "run_at": "document_start",
       }],
+
+      permissions: ["<all_urls>", "tabs"],
     },
 
-    background() {
+    async background() {
+      // Force our extension instance to be initialized for the current content process.
+      await browser.tabs.insertCSS({code: ""});
+
       browser.test.sendMessage("origin", location.origin);
     },
 
     files: {
       "content_script.js": function() {
         browser.test.sendMessage("content-script-loaded");
       },
     },
@@ -57,17 +62,19 @@ add_task(function* test_contentscript_ca
   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 ext = ExtensionManager.extensions.get(extensionId);
 
-        assert.equal(ext.staticScripts.size, 0, "Should have no cached scripts in the parent process");
+        if (ext) {
+          assert.equal(ext.staticScripts.size, 0, "Should have no cached scripts in the parent process");
+        }
 
         sendAsyncMessage("done");
       });
     });
     chromeScript.sendAsyncMessage("check-script-cache", extension.id);
     chromeScriptDone = chromeScript.promiseOneMessage("done");
   }
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html
@@ -45,17 +45,17 @@ add_task(function* test_contentscript_co
 
   let win = window.open("http://example.com/");
   yield extension.awaitMessage("content-script-ready");
   yield extension.awaitMessage("content-script-show");
 
   // Get the content script context and check that it points to the correct window.
 
   let {DocumentManager} = SpecialPowers.Cu.import("resource://gre/modules/ExtensionContent.jsm", {});
-  let context = DocumentManager.getContentScriptContext(extension, win);
+  let context = DocumentManager.getContext(extension.id, win);
   ok(context != null, "Got content script context");
 
   is(SpecialPowers.unwrap(context.contentWindow), win, "Context's contentWindow property is correct");
 
   // Navigate so that the content page is hidden in the bfcache.
 
   win.location = "http://example.org/";
   yield extension.awaitMessage("content-script-hide");
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html
@@ -55,27 +55,27 @@ add_task(function* test_contentscript_de
                                    .currentInnerWindowID;
 
   yield extension.awaitFinish("contentScript.executed");
 
   const {ExtensionContent} = SpecialPowers.Cu.import(
     "resource://gre/modules/ExtensionContent.jsm", {}
   );
 
-  let res = ExtensionContent.getContentScriptGlobalsForWindow(win);
+  let res = ExtensionContent.getContentScriptGlobals(win);
   is(res.length, 1, "Got the expected array of globals");
   let metadata = SpecialPowers.Cu.getSandboxMetadata(res[0]) || {};
 
   is(metadata.addonId, extension.id, "Got the expected addonId");
   is(metadata["inner-window-id"], innerWindowID, "Got the expected inner-window-id");
 
   yield extension.unload();
   info("extension unloaded");
 
-  res = ExtensionContent.getContentScriptGlobalsForWindow(win);
+  res = ExtensionContent.getContentScriptGlobals(win);
   is(res.length, 0, "No content scripts globals found once the extension is unloaded");
 
   win.close();
 });
 </script>
 
 </body>
 </html>
--- a/toolkit/modules/addons/WebNavigationFrames.jsm
+++ b/toolkit/modules/addons/WebNavigationFrames.jsm
@@ -69,29 +69,24 @@ function convertDocShellToFrameDetail(do
     url: window.location.href,
   };
 }
 
 /**
  * A generator function which iterates over a docShell tree, given a root docShell.
  *
  * @param   {nsIDocShell} docShell - the root docShell object
- * @returns {Iterator<DocShell>} the FrameDetail JSON object which represents the docShell.
  */
 function* iterateDocShellTree(docShell) {
   let docShellsEnum = docShell.getDocShellEnumerator(
-    Ci.nsIDocShellTreeItem.typeContent,
-    Ci.nsIDocShell.ENUMERATE_FORWARDS
-  );
+    docShell.typeContent, docShell.ENUMERATE_FORWARDS);
 
   while (docShellsEnum.hasMoreElements()) {
     yield docShellsEnum.getNext();
   }
-
-  return null;
 }
 
 /**
  * Returns the frame ID of the given window. If the window is the
  * top-level content window, its frame ID is 0. Otherwise, its frame ID
  * is its outer window ID.
  *
  * @param {Window} window - The window to retrieve the frame ID for.