Bug 1368102: Part 5 - Move static content script matching into C++. r?mixedpuppy,zombie draft
authorKris Maglione <maglione.k@gmail.com>
Thu, 25 May 2017 21:13:04 -0700
changeset 585214 760921ea68741aec84f97eb6c573d9507ac8aafc
parent 585213 e9a2a61c3aa96da5f547f976f6ba8879091e4084
child 585215 123aafcd5e345ae1086494dcfb3ab99f8b744dbd
push id61052
push usermaglione.k@gmail.com
push dateFri, 26 May 2017 17:14:32 +0000
reviewersmixedpuppy, zombie
bugs1368102
milestone55.0a1
Bug 1368102: Part 5 - Move static content script matching into C++. r?mixedpuppy,zombie MozReview-Commit-ID: Co04MoscqMx
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/mozapps/extensions/AddonManager.jsm
--- a/toolkit/components/extensions/ExtensionPolicyService.cpp
+++ b/toolkit/components/extensions/ExtensionPolicyService.cpp
@@ -3,47 +3,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;
@@ -109,16 +143,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;
@@ -207,15 +369,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()
   {
     RefPtr<ExtensionPolicyService> service = &GetSingleton();
     return service.forget();
   }
@@ -61,17 +75,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
@@ -26,29 +26,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/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -96,17 +96,17 @@ 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);
+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