Bug 1368102: Part 5 - Move static content script matching into C++. r=mixedpuppy,zombie
authorKris Maglione <maglione.k@gmail.com>
Sun, 04 Jun 2017 15:38:11 -0700
changeset 410377 32a3b7c392070cb1f9eb7b1ec1e177c6303d9d3f
parent 410376 417fd0cf65a8bddc78c59156cb317cc4156f9392
child 410378 350bf7ed1ad3ebe26e75360430cc0ee2d7477077
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy, zombie
bugs1368102
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1368102: Part 5 - Move static content script matching into C++. r=mixedpuppy,zombie MozReview-Commit-ID: Co04MoscqMx
browser/installer/package-manifest.in
mobile/android/installer/package-manifest.in
toolkit/components/extensions/ExtensionPolicyService.cpp
toolkit/components/extensions/ExtensionPolicyService.h
toolkit/components/extensions/WebExtensionContentScript.h
toolkit/components/extensions/extension-process-script.js
toolkit/components/extensions/extensions-toolkit.manifest
toolkit/components/extensions/jar.mn
toolkit/components/extensions/moz.build
toolkit/components/extensions/mozIExtensionProcessScript.idl
toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html
toolkit/mozapps/extensions/AddonManager.jsm
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -568,16 +568,17 @@
 
 #if defined(MOZ_DEBUG) || defined(NIGHTLY_BUILD)
 @RESPATH@/browser/components/testComponents.manifest
 @RESPATH@/browser/components/startupRecorder.js
 #endif
 
 ; [Extensions]
 @RESPATH@/components/extensions-toolkit.manifest
+@RESPATH@/components/extension-process-script.js
 @RESPATH@/browser/components/extensions-browser.manifest
 
 ; Modules
 @RESPATH@/browser/modules/*
 @RESPATH@/modules/*
 
 ; Safe Browsing
 @RESPATH@/components/nsURLClassifier.manifest
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -436,16 +436,17 @@
 
 ; [Browser Chrome Files]
 @BINPATH@/chrome/toolkit@JAREXT@
 @BINPATH@/chrome/toolkit.manifest
 
 ; [Extensions]
 @BINPATH@/components/extensions-toolkit.manifest
 @BINPATH@/components/extensions-mobile.manifest
+@BINPATH@/components/extension-process-script.js
 
 ; Features
 @BINPATH@/features/*
 
 ; DevTools
 #ifndef MOZ_GECKOVIEW_JAR
 @BINPATH@/chrome/devtools@JAREXT@
 @BINPATH@/chrome/devtools.manifest
--- a/toolkit/components/extensions/ExtensionPolicyService.cpp
+++ b/toolkit/components/extensions/ExtensionPolicyService.cpp
@@ -4,47 +4,81 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/ExtensionPolicyService.h"
 #include "mozilla/extensions/WebExtensionContentScript.h"
 #include "mozilla/extensions/WebExtensionPolicy.h"
 
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozIExtensionProcessScript.h"
 #include "nsEscape.h"
 #include "nsGkAtoms.h"
+#include "nsIChannel.h"
+#include "nsIContentPolicy.h"
+#include "nsIDocument.h"
+#include "nsILoadInfo.h"
+#include "nsNetUtil.h"
+#include "nsPIDOMWindow.h"
+#include "nsXULAppAPI.h"
 
 namespace mozilla {
 
 using namespace extensions;
 
 #define DEFAULT_BASE_CSP \
     "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; " \
     "object-src 'self' https://* moz-extension: blob: filesystem:;"
 
 #define DEFAULT_DEFAULT_CSP \
     "script-src 'self'; object-src 'self';"
 
 
+#define OBS_TOPIC_PRELOAD_SCRIPT "web-extension-preload-content-script"
+#define OBS_TOPIC_LOAD_SCRIPT "web-extension-load-content-script"
+
+
+static mozIExtensionProcessScript&
+ProcessScript()
+{
+  static nsCOMPtr<mozIExtensionProcessScript> sProcessScript;
+
+  if (MOZ_UNLIKELY(!sProcessScript)) {
+    sProcessScript = do_GetService("@mozilla.org/webextensions/extension-process-script;1");
+    MOZ_RELEASE_ASSERT(sProcessScript);
+    ClearOnShutdown(&sProcessScript);
+  }
+  return *sProcessScript;
+}
+
 /*****************************************************************************
  * ExtensionPolicyService
  *****************************************************************************/
 
 /* static */ ExtensionPolicyService&
 ExtensionPolicyService::GetSingleton()
 {
   static RefPtr<ExtensionPolicyService> sExtensionPolicyService;
 
   if (MOZ_UNLIKELY(!sExtensionPolicyService)) {
     sExtensionPolicyService = new ExtensionPolicyService();
     ClearOnShutdown(&sExtensionPolicyService);
   }
   return *sExtensionPolicyService.get();
 }
 
+ExtensionPolicyService::ExtensionPolicyService()
+{
+  mObs = services::GetObserverService();
+  MOZ_RELEASE_ASSERT(mObs);
+
+  RegisterObservers();
+}
+
 
 WebExtensionPolicy*
 ExtensionPolicyService::GetByURL(const URLInfo& aURL)
 {
   if (aURL.Scheme() == nsGkAtoms::moz_extension) {
     return GetByHost(aURL.Host());
   }
   return nullptr;
@@ -110,16 +144,144 @@ ExtensionPolicyService::DefaultCSP(nsASt
   rv = Preferences::GetString("extensions.webextensions.default-content-security-policy", &aDefaultCSP);
   if (NS_FAILED(rv)) {
     aDefaultCSP.AssignLiteral(DEFAULT_DEFAULT_CSP);
   }
 }
 
 
 /*****************************************************************************
+ * Content script management
+ *****************************************************************************/
+
+void
+ExtensionPolicyService::RegisterObservers()
+{
+  mObs->AddObserver(this, "content-document-global-created", false);
+  mObs->AddObserver(this, "document-element-inserted", false);
+  if (XRE_IsContentProcess()) {
+    mObs->AddObserver(this, "http-on-opening-request", false);
+  }
+}
+
+void
+ExtensionPolicyService::UnregisterObservers()
+{
+  mObs->RemoveObserver(this, "content-document-global-created");
+  mObs->RemoveObserver(this, "document-element-inserted");
+  if (XRE_IsContentProcess()) {
+    mObs->RemoveObserver(this, "http-on-opening-request");
+  }
+}
+
+nsresult
+ExtensionPolicyService::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData)
+{
+  if (!strcmp(aTopic, "content-document-global-created")) {
+    nsCOMPtr<nsPIDOMWindowOuter> win = do_QueryInterface(aSubject);
+    if (win) {
+      CheckWindow(win);
+    }
+  } else if (!strcmp(aTopic, "document-element-inserted")) {
+    nsCOMPtr<nsIDocument> doc = do_QueryInterface(aSubject);
+    if (doc) {
+      CheckDocument(doc);
+    }
+  } else if (!strcmp(aTopic, "http-on-opening-request")) {
+    nsCOMPtr<nsIChannel> chan = do_QueryInterface(aSubject);
+    if (chan) {
+      CheckRequest(chan);
+    }
+  }
+  return NS_OK;
+}
+
+// Checks a request for matching content scripts, and begins pre-loading them
+// if necessary.
+void
+ExtensionPolicyService::CheckRequest(nsIChannel* aChannel)
+{
+  nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo();
+  if (!loadInfo) {
+    return;
+  }
+
+  auto loadType = loadInfo->GetExternalContentPolicyType();
+  if (loadType != nsIContentPolicy::TYPE_DOCUMENT &&
+      loadType != nsIContentPolicy::TYPE_SUBDOCUMENT) {
+    return;
+  }
+
+  nsCOMPtr<nsIURI> uri;
+  if (NS_FAILED(aChannel->GetURI(getter_AddRefs(uri)))) {
+    return;
+  }
+
+  CheckContentScripts({uri.get(), loadInfo}, true);
+}
+
+// Checks a document, just after the document element has been inserted, for
+// matching content scripts or extension principals, and loads them if
+// necessary.
+void
+ExtensionPolicyService::CheckDocument(nsIDocument* aDocument)
+{
+  nsCOMPtr<nsPIDOMWindowOuter> win = aDocument->GetWindow();
+  if (win) {
+    CheckContentScripts(win.get(), false);
+  }
+}
+
+// Checks for loads of about:blank into new window globals, and loads any
+// matching content scripts. about:blank loads do not trigger document element
+// inserted events, so they're the only load type that are special cased this
+// way.
+void
+ExtensionPolicyService::CheckWindow(nsPIDOMWindowOuter* aWindow)
+{
+  // We only care about non-initial document loads here. The initial
+  // about:blank document will usually be re-used to load another document.
+  nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc();
+  if (!doc || doc->IsInitialDocument()) {
+    return;
+  }
+
+  nsCOMPtr<nsIURI> aboutBlank;
+  NS_ENSURE_SUCCESS_VOID(NS_NewURI(getter_AddRefs(aboutBlank),
+                                   "about:blank"));
+
+  nsCOMPtr<nsIURI> uri = doc->GetDocumentURI();
+  bool equal;
+  if (NS_FAILED(uri->EqualsExceptRef(aboutBlank, &equal)) || !equal) {
+    return;
+  }
+
+  CheckContentScripts(aWindow, false);
+}
+
+void
+ExtensionPolicyService::CheckContentScripts(const DocInfo& aDocInfo, bool aIsPreload)
+{
+  for (auto iter = mExtensions.Iter(); !iter.Done(); iter.Next()) {
+    RefPtr<WebExtensionPolicy> policy = iter.Data();
+
+    for (auto& script : policy->ContentScripts()) {
+      if (script->Matches(aDocInfo)) {
+        if (aIsPreload) {
+          ProcessScript().PreloadContentScript(script);
+        } else {
+          ProcessScript().LoadContentScript(script, aDocInfo.GetWindow());
+        }
+      }
+    }
+  }
+}
+
+
+/*****************************************************************************
  * nsIAddonPolicyService
  *****************************************************************************/
 
 nsresult
 ExtensionPolicyService::GetBaseCSP(nsAString& aBaseCSP)
 {
   BaseCSP(aBaseCSP);
   return NS_OK;
@@ -208,15 +370,16 @@ ExtensionPolicyService::ExtensionURIToAd
   return NS_OK;
 }
 
 
 NS_IMPL_CYCLE_COLLECTION(ExtensionPolicyService, mExtensions, mExtensionHosts)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionPolicyService)
   NS_INTERFACE_MAP_ENTRY(nsIAddonPolicyService)
-  NS_INTERFACE_MAP_ENTRY(nsISupports)
+  NS_INTERFACE_MAP_ENTRY(nsIObserver)
+  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAddonPolicyService)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionPolicyService)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionPolicyService)
 
 } // namespace mozilla
--- a/toolkit/components/extensions/ExtensionPolicyService.h
+++ b/toolkit/components/extensions/ExtensionPolicyService.h
@@ -7,30 +7,44 @@
 #define mozilla_ExtensionPolicyService_h
 
 #include "mozilla/extensions/WebExtensionPolicy.h"
 #include "nsCOMPtr.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsHashKeys.h"
 #include "nsIAddonPolicyService.h"
 #include "nsIAtom.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
 #include "nsISupports.h"
 #include "nsPointerHashKeys.h"
 #include "nsRefPtrHashtable.h"
 
+class nsIChannel;
+class nsIObserverService;
+class nsIDocument;
+class nsIPIDOMWindowOuter;
+
 namespace mozilla {
+namespace extensions {
+  class DocInfo;
+}
 
+using extensions::DocInfo;
 using extensions::WebExtensionPolicy;
 
 class ExtensionPolicyService final : public nsIAddonPolicyService
+                                   , public nsIObserver
 {
 public:
-  NS_DECL_CYCLE_COLLECTION_CLASS(ExtensionPolicyService)
+  NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(ExtensionPolicyService,
+                                           nsIAddonPolicyService)
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_NSIADDONPOLICYSERVICE
+  NS_DECL_NSIOBSERVER
 
   static ExtensionPolicyService& GetSingleton();
 
   static already_AddRefed<ExtensionPolicyService> GetInstance()
   {
     return do_AddRef(&GetSingleton());
   }
 
@@ -60,17 +74,28 @@ public:
 
   void BaseCSP(nsAString& aDefaultCSP) const;
   void DefaultCSP(nsAString& aDefaultCSP) const;
 
 protected:
   virtual ~ExtensionPolicyService() = default;
 
 private:
-  ExtensionPolicyService() = default;
+  ExtensionPolicyService();
+
+  void RegisterObservers();
+  void UnregisterObservers();
+
+  void CheckRequest(nsIChannel* aChannel);
+  void CheckDocument(nsIDocument* aDocument);
+  void CheckWindow(nsPIDOMWindowOuter* aWindow);
+
+  void CheckContentScripts(const DocInfo& aDocInfo, bool aIsPreload);
 
   nsRefPtrHashtable<nsPtrHashKey<const nsIAtom>, WebExtensionPolicy> mExtensions;
   nsRefPtrHashtable<nsCStringHashKey, WebExtensionPolicy> mExtensionHosts;
+
+  nsCOMPtr<nsIObserverService> mObs;
 };
 
 } // namespace mozilla
 
 #endif // mozilla_ExtensionPolicyService_h
--- a/toolkit/components/extensions/WebExtensionContentScript.h
+++ b/toolkit/components/extensions/WebExtensionContentScript.h
@@ -43,16 +43,24 @@ public:
   nsIPrincipal* Principal() const;
 
   const URLInfo& PrincipalURL() const;
 
   bool IsTopLevel() const;
 
   uint64_t FrameID() const;
 
+  nsPIDOMWindowOuter* GetWindow() const
+  {
+    if (mObj.is<Window>()) {
+      return mObj.as<Window>();
+    }
+    return nullptr;
+  }
+
 private:
   void SetURL(const URLInfo& aURL);
 
   const URLInfo mURL;
   mutable Maybe<const URLInfo> mPrincipalURL;
 
   mutable Maybe<bool> mIsTopLevel;
   mutable Maybe<nsCOMPtr<nsIPrincipal>> mPrincipal;
--- a/toolkit/components/extensions/extension-process-script.js
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -21,21 +21,25 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/MessageChannel.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
                                   "resource://gre/modules/ExtensionChild.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContent",
                                   "resource://gre/modules/ExtensionContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPageChild",
                                   "resource://gre/modules/ExtensionPageChild.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
-                                  "resource://gre/modules/ExtensionUtils.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole());
-XPCOMUtils.defineLazyGetter(this, "getInnerWindowID", () => ExtensionUtils.getInnerWindowID);
+
+const {
+  DefaultWeakMap,
+  getInnerWindowID,
+} = ExtensionUtils;
 
 // We need to avoid touching Services.appinfo here in order to prevent
 // the wrong version from being cached during xpcshell test startup.
 const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
 const isContentProcess = appinfo.processType == appinfo.PROCESS_TYPE_CONTENT;
 
 function parseScriptOptions(options) {
   return {
@@ -57,39 +61,31 @@ function parseScriptOptions(options) {
 class ScriptMatcher {
   constructor(extension, matcher) {
     this.extension = extension;
     this.matcher = matcher;
 
     this._script = null;
   }
 
-  get matchAboutBlank() {
-    return this.matcher.matchAboutBlank;
-  }
-
   get script() {
     if (!this._script) {
       this._script = new ExtensionContent.Script(this.extension.realExtension,
                                                  this.matcher);
     }
     return this._script;
   }
 
   preload() {
     let {script} = this;
 
     script.loadCSS();
     script.compileScripts();
   }
 
-  matchesLoadInfo(uri, loadInfo) {
-    return this.matcher.matchesLoadInfo(uri, loadInfo);
-  }
-
   matchesWindow(window) {
     return this.matcher.matchesWindow(window);
   }
 
   injectInto(window) {
     return this.script.injectInto(window);
   }
 }
@@ -152,16 +148,20 @@ class ExtensionGlobal {
       case "WebNavigation:GetFrame":
         return ExtensionContent.handleWebNavigationGetFrame(this.global, data.options);
       case "WebNavigation:GetAllFrames":
         return ExtensionContent.handleWebNavigationGetAllFrames(this.global);
     }
   }
 }
 
+let stubExtensions = new WeakMap();
+let scriptMatchers = new DefaultWeakMap(matcher => new ScriptMatcher(stubExtensions.get(matcher.extension),
+                                                                     matcher));
+
 // Responsible for creating ExtensionContexts and injecting content
 // scripts into them when new documents are created.
 DocumentManager = {
   globals: new Map(),
 
   // Initialize listeners that we need regardless of whether extensions are
   // enabled.
   earlyInit() {
@@ -171,42 +171,16 @@ DocumentManager = {
   // Initialize listeners that we need when any extension is enabled.
   init() {
     Services.obs.addObserver(this, "document-element-inserted");
   },
   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");
-    }
-  },
-  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");
-  },
-  uninitAboutBlankMatchers() {
-    Services.obs.removeObserver(this, "content-document-global-created");
-  },
-
   extensionProcessInitialized: false,
   initExtensionProcess() {
     if (this.extensionProcessInitialized || !ExtensionManagement.isExtensionProcess) {
       return;
     }
     this.extensionProcessInitialized = true;
 
     for (let global of this.globals.keys()) {
@@ -238,156 +212,63 @@ DocumentManager = {
 
   initExtension(extension) {
     if (this.extensionCount === 0) {
       this.init();
       this.initExtensionProcess();
     }
     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) {
+      for (let script of extension.policy.contentScripts) {
         if (script.matchesWindow(window)) {
-          script.injectInto(window);
+          scriptMatchers.get(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();
-      }
-    }
-  },
-
   /**
    * Checks that all parent frames for the given withdow either have the
    * same add-on ID, or are special chrome-privileged documents such as
    * about:addons or developer tools panels.
    *
    * @param {Window} window
    *        The window to check.
    * @param {string} addonId
@@ -490,16 +371,18 @@ class StubExtension {
   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);
     } else {
       this.policy = WebExtensionPolicy.getByID(this.id);
     }
+
+    stubExtensions.set(this.policy, this);
   }
 
   shutdown() {
     if (isContentProcess) {
       ExtensionManagement.shutdownExtension(this);
     }
     if (this._realExtension) {
       this._realExtension.shutdown();
@@ -592,10 +475,36 @@ ExtensionManager = {
       case "Schema:Add": {
         this.schemaJSON.set(data.url, data.schema);
         break;
       }
     }
   },
 };
 
+function ExtensionProcessScript() {
+  if (!ExtensionProcessScript.singleton) {
+    ExtensionProcessScript.singleton = this;
+  }
+  return ExtensionProcessScript.singleton;
+}
+
+ExtensionProcessScript.singleton = null;
+
+ExtensionProcessScript.prototype = {
+  classID: Components.ID("{21f9819e-4cdf-49f9-85a0-850af91a5058}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.mozIExtensionProcessScript]),
+
+  preloadContentScript(contentScript) {
+    scriptMatchers.get(contentScript).preload();
+  },
+
+  loadContentScript(contentScript, window) {
+    if (DocumentManager.globals.has(getMessageManager(window))) {
+      scriptMatchers.get(contentScript).injectInto(window);
+    }
+  },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ExtensionProcessScript]);
+
 DocumentManager.earlyInit();
 ExtensionManager.init();
--- a/toolkit/components/extensions/extensions-toolkit.manifest
+++ b/toolkit/components/extensions/extensions-toolkit.manifest
@@ -2,8 +2,12 @@
 category webextension-scripts toolkit chrome://extensions/content/ext-toolkit.js
 category webextension-scripts-content toolkit chrome://extensions/content/ext-c-toolkit.js
 category webextension-scripts-devtools toolkit chrome://extensions/content/ext-c-toolkit.js
 category webextension-scripts-addon toolkit chrome://extensions/content/ext-c-toolkit.js
 
 category webextension-schemas events chrome://extensions/content/schemas/events.json
 category webextension-schemas native_host_manifest chrome://extensions/content/schemas/native_host_manifest.json
 category webextension-schemas types chrome://extensions/content/schemas/types.json
+
+
+component {21f9819e-4cdf-49f9-85a0-850af91a5058} extension-process-script.js
+contract @mozilla.org/webextensions/extension-process-script;1 {21f9819e-4cdf-49f9-85a0-850af91a5058}
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -1,15 +1,14 @@
 # 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/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -27,29 +27,36 @@ EXTRA_JS_MODULES += [
     'LegacyExtensionsUtils.jsm',
     'MessageChannel.jsm',
     'NativeMessaging.jsm',
     'ProxyScriptContext.jsm',
     'Schemas.jsm',
 ]
 
 EXTRA_COMPONENTS += [
+    'extension-process-script.js',
     'extensions-toolkit.manifest',
 ]
 
 TESTING_JS_MODULES += [
     'ExtensionTestCommon.jsm',
     'ExtensionXPCShellUtils.jsm',
 ]
 
 DIRS += [
     'schemas',
     'webrequest',
 ]
 
+XPIDL_SOURCES += [
+    'mozIExtensionProcessScript.idl',
+]
+
+XPIDL_MODULE = 'webextensions'
+
 EXPORTS.mozilla = [
     'ExtensionPolicyService.h',
 ]
 
 EXPORTS.mozilla.extensions = [
     'MatchGlob.h',
     'MatchPattern.h',
     'WebExtensionContentScript.h',
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/mozIExtensionProcessScript.idl
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface mozIDOMWindowProxy;
+
+[scriptable,uuid(6b09dc51-6caa-4ca7-9d6d-30c87258a630)]
+interface mozIExtensionProcessScript : nsISupports
+{
+  void preloadContentScript(in nsISupports contentScript);
+
+  void loadContentScript(in nsISupports contentScript, in mozIDOMWindowProxy window);
+
+};
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html
@@ -62,17 +62,17 @@ add_task(async function test_contentscri
   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/ExtensionChild.jsm", {});
         let ext = ExtensionManager.extensions.get(extensionId);
 
-        if (ext) {
+        if (ext && ext.staticScripts) {
           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/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -96,17 +96,22 @@ XPCOMUtils.defineLazyGetter(this, "CertU
   let certUtils = {};
   Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
   return certUtils;
 });
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS",
                                       PREF_WEBEXT_PERM_PROMPTS, false);
 
-Services.ppmm.loadProcessScript("chrome://extensions/content/extension-process-script.js", true);
+// Initialize the WebExtension process script service as early as possible,
+// since it needs to be able to track things like new frameLoader globals that
+// are created before other framework code has been initialized.
+Services.ppmm.loadProcessScript(
+  "data:,Components.classes['@mozilla.org/webextensions/extension-process-script;1'].getService()",
+  true);
 
 const INTEGER = /^[1-9]\d*$/;
 
 this.EXPORTED_SYMBOLS = [ "AddonManager", "AddonManagerPrivate" ];
 
 const CATEGORY_PROVIDER_MODULE = "addon-provider-module";
 
 // A list of providers to load by default