Bug 1322235: Part 6 - Replace AddonPolicyService with a stub implementation in ExtensionPolicyService. r=mixedpuppy,zombie
authorKris Maglione <maglione.k@gmail.com>
Sat, 03 Jun 2017 17:12:14 -0700
changeset 362230 35e67c38d3cd93cc43f4665849847959f7d97656
parent 362229 02ce614daf1999405cd4e63d60ab2b6ebd4e4048
child 362231 e6001a0646bb46df0266896fe5bf2069b6cfadae
push id91027
push usermaglione.k@gmail.com
push dateMon, 05 Jun 2017 01:02:21 +0000
treeherdermozilla-inbound@1b3359583493 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy, zombie
bugs1322235
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 1322235: Part 6 - Replace AddonPolicyService with a stub implementation in ExtensionPolicyService. r=mixedpuppy,zombie This replaces the JS policy service stubs with a pure C++ version which directly makes policy decisions based on active WebExtensionPolicy objects. This is the first step in a larger refactoring, which will remove the ExtensionManagement module entirely, and replace the current add-on policy service with direct, non-virtual access to native WebExtensionPolicy objects. It will also be followed by related changes to migrate the content script and extension page matching to native code, based on the existing MatchPattern and WebExtensionPolicy bindings. MozReview-Commit-ID: 2MpbmXZGiPZ
caps/tests/mochitest/test_addonMayLoad.html
caps/tests/mochitest/test_extensionURL.html
devtools/shared/tests/unit/test_console_filtering.js
dom/tests/browser/browser_ConsoleAPI_originAttributes.js
js/xpconnect/tests/unit/test_nuke_webextension_wrappers.js
toolkit/components/build/nsToolkitCompsCID.h
toolkit/components/build/nsToolkitCompsModule.cpp
toolkit/components/extensions/.eslintrc.js
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionManagement.jsm
toolkit/components/extensions/ExtensionPolicyService.cpp
toolkit/components/extensions/ExtensionPolicyService.h
toolkit/components/extensions/extension-process-script.js
toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js
toolkit/components/extensions/test/xpcshell/test_locale_converter.js
toolkit/components/utils/simpleServices.js
toolkit/components/utils/utils.manifest
--- a/caps/tests/mochitest/test_addonMayLoad.html
+++ b/caps/tests/mochitest/test_addonMayLoad.html
@@ -10,26 +10,41 @@ https://bugzilla.mozilla.org/show_bug.cg
   <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
   <script type="application/javascript">
 
   /** Test for Bug 1180921 **/
   const Cc = Components.classes;
   const Ci = Components.interfaces;
   const Cu = Components.utils;
-  Cu.import("resource://gre/modules/Services.jsm");
+  let module = Cu.import("resource://gre/modules/Services.jsm", window);
   let ssm = Services.scriptSecurityManager;
-  let aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService).wrappedJSObject;
+
+  function StubPolicy(id, subdomain) {
+    /* globals MatchPatternSet */
+    return new module.WebExtensionPolicy({
+      id,
+      mozExtensionHostname: id,
+      baseURL: `file:///{id}`,
+
+      allowedOrigins: new MatchPatternSet([`*://${subdomain}.example.org/*`]),
+      localizeCallback(string) {},
+    });
+  }
+
+  /* globals WebExtensionPolicy */
+  let policyA = StubPolicy("addona", "test1");
+  let policyB = StubPolicy("addonb", "test2");
+  policyA.active = true;
+  policyB.active = true;
 
   SimpleTest.waitForExplicitFinish();
-  let oldAddonIdCallback = aps.setExtensionURIToAddonIdCallback(uri => uri.host);
   SimpleTest.registerCleanupFunction(function() {
-    aps.setAddonLoadURICallback("addona", null);
-    aps.setAddonLoadURICallback("addonb", null);
-    aps.setExtensionURIToAddonIdCallback(oldAddonIdCallback);
+    policyA.active = false;
+    policyB.active = false;
   });
 
   function tryLoad(sb, uri) {
     let p = new Promise(function(resolve, reject) {
       Cu.exportFunction(resolve, sb, { defineAs: "finish" });
       Cu.exportFunction(reject, sb, { defineAs: "error" });
       sb.eval("try { (function () { " +
               "  var xhr = new XMLHttpRequest();" +
@@ -43,21 +58,19 @@ https://bugzilla.mozilla.org/show_bug.cg
 
   let addonA = new Cu.Sandbox(ssm.createCodebasePrincipal(Services.io.newURI("moz-extension://addonA/"), {}),
                               {wantGlobalProperties: ["XMLHttpRequest"]});
   let addonB = new Cu.Sandbox(ssm.createCodebasePrincipal(Services.io.newURI("moz-extension://addonB/"), {}),
                               {wantGlobalProperties: ["XMLHttpRequest"]});
 
   function uriForDomain(d) { return d + "/tests/caps/tests/mochitest/file_data.txt" }
 
-  tryLoad(addonA, uriForDomain("http://test1.example.org"))
+  tryLoad(addonA, uriForDomain("http://test4.example.org"))
   .then(function(success) {
     ok(!success, "cross-origin load should fail for addon A");
-    aps.setAddonLoadURICallback("addona", function(uri) { return /test1/.test(uri.host); });
-    aps.setAddonLoadURICallback("addonb", function(uri) { return /test2/.test(uri.host); });
     return tryLoad(addonA, uriForDomain("http://test1.example.org"));
   }).then(function(success) {
     ok(success, "whitelisted cross-origin load of test1 should succeed for addon A");
     return tryLoad(addonB, uriForDomain("http://test1.example.org"));
   }).then(function(success) {
     ok(!success, "non-whitelisted cross-origin load of test1 should fail for addon B");
     return tryLoad(addonB, uriForDomain("http://test2.example.org"));
   }).then(function(success) {
--- a/caps/tests/mochitest/test_extensionURL.html
+++ b/caps/tests/mochitest/test_extensionURL.html
@@ -8,46 +8,58 @@ https://bugzilla.mozilla.org/show_bug.cg
   <title>Test for Bug 1161831</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <script type="application/javascript">
 
   /** Test for Bug 1161831 **/
   SimpleTest.waitForExplicitFinish();
 
-  var aps = SpecialPowers.Cc["@mozilla.org/addons/policy-service;1"]
-                         .getService(SpecialPowers.Ci.nsIAddonPolicyService).wrappedJSObject;
-  var oldLoadCallback = aps.setExtensionURILoadCallback(null);
-  var oldMapCallback = aps.setExtensionURIToAddonIdCallback(null);
+  let module = SpecialPowers.Cu.import("resource://gre/modules/Services.jsm", {});
+  var {MatchGlob, MatchPatternSet, WebExtensionPolicy} = module;
+
+  var policy1, policy2;
+
   var resourceHandler = SpecialPowers.Services.io.getProtocolHandler("resource")
                                      .QueryInterface(SpecialPowers.Ci.nsISubstitutingProtocolHandler);
   var extensionHandler = SpecialPowers.Services.io.getProtocolHandler("moz-extension")
                                      .QueryInterface(SpecialPowers.Ci.nsISubstitutingProtocolHandler);
 
   SimpleTest.registerCleanupFunction(function() {
-      extensionHandler.setSubstitution("cherise", null);
-      extensionHandler.setSubstitution("liebchen", null);
-      aps.setExtensionURILoadCallback(oldLoadCallback);
-      aps.setExtensionURIToAddonIdCallback(oldMapCallback);
+      policy1.active = false;
+      policy2.active = false;
   });
 
   addLoadEvent(function() {
 
     // First, get a file:// URI to something - open to suggestions on how to do
     //  this more easily.
     var resURI = SpecialPowers.Services.io.newURI("resource://testing-common/resource_test_file.html");
     var filePath = resourceHandler.resolveURI(resURI);
     ok(filePath.startsWith("file://"), "resource:// URI resolves where we expect: " + filePath);
     var fileURI = SpecialPowers.Services.io.newURI(filePath);
 
-    // Register a moz-extension:// URI.
-    extensionHandler.setSubstitution("cherise", fileURI);
+    function StubPolicy(id, accessible) {
+      let policy = new WebExtensionPolicy(SpecialPowers.Cu.cloneInto({
+        id: `imaginaryaddon-${id[0]}`,
+        mozExtensionHostname: id,
+        baseURL: fileURI.spec,
 
-    // Alias the above.
-    extensionHandler.setSubstitution("liebchen", SpecialPowers.Services.io.newURI("moz-extension://cherise"));
+        allowedOrigins: SpecialPowers.unwrap(new MatchPatternSet([])),
+        webAccessibleResources: accessible ? [SpecialPowers.unwrap(new MatchGlob("*"))] : [],
+        localizeCallback(string) {},
+      }, module, {cloneFunctions: true, wrapReflectors: true}));
+
+      policy.active = true;
+      return policy;
+    }
+
+    // Register a moz-extension:// URI.
+    policy1 = StubPolicy("cherise", false);
+    policy2 = StubPolicy("liebchen", false);
 
     //
     // Make sure that non-file:// URIs don't work.
     //
 
     // resource://
     try {
       extensionHandler.setSubstitution("interdit", resURI);
@@ -71,23 +83,24 @@ https://bugzilla.mozilla.org/show_bug.cg
     function navigateFromChromeWithWebNav(ifr, url) {
       SpecialPowers.wrap(ifr).contentWindow
                    .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
                    .getInterface(SpecialPowers.Ci.nsIWebNavigation)
                    .loadURI(url, 0, null, null, null);
     }
 
 
-    function setWhitelistCallback(rgxp) {
-      var cb = SpecialPowers.wrapCallback(function(uri) { return rgxp.test(uri.spec); });
-      aps.setExtensionURILoadCallback(cb);
+    function setWhitelistCallback(paths) {
+      policy1.active = false;
+      policy2.active = false;
+
+      policy1 = StubPolicy("cherise", paths.includes("cherise"));
+      policy2 = StubPolicy("liebchen", paths.includes("liebchen"));
     }
 
-    aps.setExtensionURIToAddonIdCallback(SpecialPowers.wrapCallback(function(uri) { return "imaginaryaddon-" + uri.host[0]; }));
-
     function testLoad(url, navigate, shouldThrow) {
       var ifr = document.createElement("iframe");
       var p = new Promise(function(resolve, reject) {
         ifr.onload = function() {
           ok(true, "Loaded " + url);
           var prin = SpecialPowers.wrap(ifr.contentWindow).document.nodePrincipal;
           function stripTrailingSlash(s) { return s.replace(/\/$/, ""); }
           is(stripTrailingSlash(prin.URI.spec), url, "Principal uri is correct: " + url);
@@ -133,22 +146,22 @@ https://bugzilla.mozilla.org/show_bug.cg
 
     //
     // Perform some loads and make sure they work correctly.
     //
     testLoad.bind(null, "moz-extension://cherise", navigateFromChromeWithLocation)()
     .then(testLoad.bind(null, "moz-extension://cherise", navigateFromChromeWithWebNav))
     .then(testLoad.bind(null, "moz-extension://cherise", navigateWithLocation, /* shouldThrow = */ true))
     .then(testXHR.bind(null, "moz-extension://cherise", /* shouldError = */ true))
-    .then(setWhitelistCallback.bind(null, /cherise/))
+    .then(setWhitelistCallback.bind(null, ["cherise"]))
     .then(testLoad.bind(null, "moz-extension://cherise", navigateWithLocation))
     .then(testXHR.bind(null, "moz-extension://cherise"))
     .then(testLoad.bind(null, "moz-extension://liebchen", navigateWithLocation, /* shouldThrow = */ true))
     .then(testXHR.bind(null, "moz-extension://liebchen", /* shouldError = */ true))
-    .then(setWhitelistCallback.bind(null, /cherise|liebchen/))
+    .then(setWhitelistCallback.bind(null, ["cherise", "liebchen"]))
     .then(testLoad.bind(null, "moz-extension://liebchen", navigateWithLocation))
     .then(testLoad.bind(null, "moz-extension://liebchen", navigateWithSrc))
     .then(testLoad.bind(null, "moz-extension://cherise", navigateWithSrc))
     .then(testLoad.bind(null, "moz-extension://cherise/_blank.html", navigateWithSrc))
     .then(SimpleTest.finish.bind(SimpleTest),
           function(e) { ok(false, "rejected promise: " + e); SimpleTest.finish() }
     );
   });
--- a/devtools/shared/tests/unit/test_console_filtering.js
+++ b/devtools/shared/tests/unit/test_console_filtering.js
@@ -2,19 +2,16 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const { console, ConsoleAPI } = require("resource://gre/modules/Console.jsm");
 const { ConsoleAPIListener } = require("devtools/server/actors/utils/webconsole-listeners");
 const Services = require("Services");
 
-// FIXME: This test shouldn't need to rely on low-level internals.
-const {Service} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {});
-
 var seenMessages = 0;
 var seenTypes = 0;
 
 var callback = {
   onConsoleAPICall: function (message) {
     if (message.consoleID && message.consoleID == "addon/foo") {
       do_check_eq(message.level, "warn");
       do_check_eq(message.arguments[0], "Warning from foo");
@@ -27,42 +24,55 @@ var callback = {
       do_check_eq(message.level, "log");
       do_check_eq(message.arguments[0], "Hello from default console");
       seenTypes |= 4;
     }
     seenMessages++;
   }
 };
 
+let policy;
+do_register_cleanup(() => {
+  policy.active = false;
+});
+
 function createFakeAddonWindow({addonId} = {}) {
   const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
   const uuid = uuidGen.generateUUID().number.slice(1, -1);
 
-  const url = `moz-extension://${uuid}/`;
-  Service.uuidMap.set(uuid, {id: addonId});
+  if (policy) {
+    policy.active = false;
+  }
+  /* globals MatchPatternSet, WebExtensionPolicy */
+  policy = new WebExtensionPolicy({
+    id: addonId,
+    mozExtensionHostname: uuid,
+    baseURL: "file:///",
+    allowedOrigins: new MatchPatternSet([]),
+    localizeCallback() {},
+  });
+  policy.active = true;
 
-  let baseURI = Services.io.newURI(url);
+  let baseURI = Services.io.newURI(`moz-extension://${uuid}/`);
   let principal = Services.scriptSecurityManager
         .createCodebasePrincipal(baseURI, {});
   let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
   let docShell = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor)
                              .getInterface(Ci.nsIDocShell);
   docShell.createAboutBlankContentViewer(principal);
   let addonWindow = docShell.contentViewer.DOMDocument.defaultView;
 
   return {addonWindow, chromeWebNav};
 }
 
 /**
  * Tests that the consoleID property of the ConsoleAPI options gets passed
  * through to console messages.
  */
 function run_test() {
-  Service.init();
-
   // console1 Test Console.jsm messages tagged by the Addon SDK
   // are still filtered correctly.
   let console1 = new ConsoleAPI({
     consoleID: "addon/foo"
   });
 
   // console2 - WebExtension page's console messages tagged
   // by 'originAttributes.addonId' are filtered correctly.
--- a/dom/tests/browser/browser_ConsoleAPI_originAttributes.js
+++ b/dom/tests/browser/browser_ConsoleAPI_originAttributes.js
@@ -1,16 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"]
       .getService(Ci.nsIConsoleAPIStorage);
 
-// FIXME: This test shouldn't need to rely on low-level internals.
-const {Service} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {});
+const {WebExtensionPolicy} = Cu.import("resource://gre/modules/Services.jsm", {});
 
 const FAKE_ADDON_ID = "test-webext-addon@mozilla.org";
 const EXPECTED_CONSOLE_ID = `addon/${FAKE_ADDON_ID}`;
 const EXPECTED_CONSOLE_MESSAGE_CONTENT = "fake-webext-addon-test-log-message";
 const ConsoleObserver = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
   init() {
@@ -41,40 +40,48 @@ const ConsoleObserver = {
 
       finish();
     }
   }
 };
 
 function test()
 {
-  Service.init();
   ConsoleObserver.init();
 
   waitForExplicitFinish();
 
   let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
   let uuid = uuidGenerator.generateUUID().number;
   uuid = uuid.slice(1, -1); // Strip { and } off the UUID.
 
   const url = `moz-extension://${uuid}/`;
-  Service.uuidMap.set(uuid, {id: FAKE_ADDON_ID});
+  /* globals MatchPatternSet, WebExtensionPolicy */
+  let policy = new WebExtensionPolicy({
+    id: FAKE_ADDON_ID,
+    mozExtensionHostname: uuid,
+    baseURL: "file:///",
+    allowedOrigins: new MatchPatternSet([]),
+    localizeCallback() {},
+  });
+  policy.active = true;
 
   let baseURI = Services.io.newURI(url);
   let principal = Services.scriptSecurityManager
         .createCodebasePrincipal(baseURI, {});
 
   let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
   let interfaceRequestor = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor);
   let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
   docShell.createAboutBlankContentViewer(principal);
 
   info("fake webextension docShell created");
 
   registerCleanupFunction(function() {
+    policy.active = false;
     if (chromeWebNav) {
       chromeWebNav.close();
       chromeWebNav = null;
     }
     ConsoleObserver.uninit();
   });
 
   let window = docShell.contentViewer.DOMDocument.defaultView;
--- a/js/xpconnect/tests/unit/test_nuke_webextension_wrappers.js
+++ b/js/xpconnect/tests/unit/test_nuke_webextension_wrappers.js
@@ -2,44 +2,47 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://testing-common/TestUtils.jsm");
 
-let aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService).wrappedJSObject;
-
-let oldAddonIdCallback = aps.setExtensionURIToAddonIdCallback(uri => uri.host);
-do_register_cleanup(() => {
-  aps.setExtensionURIToAddonIdCallback(oldAddonIdCallback);
-});
-
 function getWindowlessBrowser(url) {
   let ssm = Services.scriptSecurityManager;
 
   let uri = NetUtil.newURI(url);
-  // TODO: Remove when addonId origin attribute is removed.
-  let attrs = {addonId: uri.host};
 
-  let principal = ssm.createCodebasePrincipal(uri, attrs);
+  let principal = ssm.createCodebasePrincipal(uri, {});
 
   let webnav = Services.appShell.createWindowlessBrowser(false);
 
   let docShell = webnav.QueryInterface(Ci.nsIInterfaceRequestor)
                        .getInterface(Ci.nsIDocShell);
 
   docShell.createAboutBlankContentViewer(principal);
 
   return webnav;
 }
 
+function StubPolicy(id) {
+  return new WebExtensionPolicy({
+    id,
+    mozExtensionHostname: id,
+    baseURL: `file:///{id}`,
+
+    allowedOrigins: new MatchPatternSet([]),
+    localizeCallback(string) {},
+  });
+}
+
 add_task(function*() {
-  let ssm = Services.scriptSecurityManager;
+  let policy = StubPolicy("foo");
+  policy.active = true;
 
   let webnavA = getWindowlessBrowser("moz-extension://foo/a.html");
   let webnavB = getWindowlessBrowser("moz-extension://foo/b.html");
 
   let winA = Cu.waiveXrays(webnavA.document.defaultView);
   let winB = Cu.waiveXrays(webnavB.document.defaultView);
 
   winB.winA = winA;
@@ -62,9 +65,11 @@ add_task(function*() {
   yield TestUtils.topicObserved("inner-window-destroyed");
 
   // Check that it can't be accessed after he window has been closed.
   let result = getThing();
   ok(/dead object/.test(result),
      `Result should show a dead wrapper error: ${result}`);
 
   webnavA.close();
+
+  policy.active = false;
 });
--- a/toolkit/components/build/nsToolkitCompsCID.h
+++ b/toolkit/components/build/nsToolkitCompsCID.h
@@ -181,8 +181,14 @@
 #define NS_APPLICATION_REPUTATION_SERVICE_CID \
 { 0x8576c950, 0xf4a2, 0x11e2, { 0xb7, 0x78, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66 } }
 
 #define NS_ADDONCONTENTPOLICY_CID \
 { 0xc26a8241, 0xecf4, 0x4aed, { 0x9f, 0x3c, 0xf1, 0xf5, 0xc7, 0x13, 0xb9, 0xa5 } }
 
 #define NS_ADDON_PATH_SERVICE_CID \
 { 0xa39f39d0, 0xdfb6, 0x11e3, { 0x8b, 0x68, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66 } }
+
+#define NS_ADDON_POLICY_SERVICE_CID \
+{ 0x562de129, 0x8338, 0x482c, { 0xbb, 0x96, 0xa1, 0xff, 0x09, 0xee, 0x53, 0xcc } }
+
+#define NS_ADDON_POLICY_SERVICE_CONTRACTID \
+  "@mozilla.org/addons/policy-service;1"
--- a/toolkit/components/build/nsToolkitCompsModule.cpp
+++ b/toolkit/components/build/nsToolkitCompsModule.cpp
@@ -32,16 +32,17 @@
 #include "nsUrlClassifierPrefixSet.h"
 
 #include "nsBrowserStatusFilter.h"
 #include "mozilla/FinalizationWitnessService.h"
 #include "mozilla/NativeOSFileInternals.h"
 #include "mozilla/AddonContentPolicy.h"
 #include "mozilla/AddonManagerStartup.h"
 #include "mozilla/AddonPathService.h"
+#include "mozilla/ExtensionPolicyService.h"
 
 #if defined(XP_WIN)
 #include "NativeFileWatcherWin.h"
 #else
 #include "NativeFileWatcherNotSupported.h"
 #endif // (XP_WIN)
 
 #include "nsWebRequestListener.h"
@@ -121,16 +122,17 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsUpdateP
 #endif
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(FinalizationWitnessService, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR(NativeOSFileInternalsService)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(NativeFileWatcherService, Init)
 
 NS_GENERIC_FACTORY_CONSTRUCTOR(AddonContentPolicy)
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(AddonPathService, AddonPathService::GetInstance)
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(AddonManagerStartup, AddonManagerStartup::GetInstance)
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(ExtensionPolicyService, ExtensionPolicyService::GetInstance)
 
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsWebRequestListener)
 
 NS_DEFINE_NAMED_CID(NS_TOOLKIT_APPSTARTUP_CID);
 #if defined(MOZ_HAS_PERFSTATS)
 NS_DEFINE_NAMED_CID(NS_TOOLKIT_PERFORMANCESTATSSERVICE_CID);
 #endif // defined (MOZ_HAS_PERFSTATS)
 
@@ -156,16 +158,17 @@ NS_DEFINE_NAMED_CID(NS_BROWSERSTATUSFILT
 #if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID)
 NS_DEFINE_NAMED_CID(NS_UPDATEPROCESSOR_CID);
 #endif
 NS_DEFINE_NAMED_CID(FINALIZATIONWITNESSSERVICE_CID);
 NS_DEFINE_NAMED_CID(NATIVE_OSFILE_INTERNALS_SERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_ADDONCONTENTPOLICY_CID);
 NS_DEFINE_NAMED_CID(NS_ADDON_PATH_SERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_ADDON_MANAGER_STARTUP_CID);
+NS_DEFINE_NAMED_CID(NS_ADDON_POLICY_SERVICE_CID);
 NS_DEFINE_NAMED_CID(NATIVE_FILEWATCHER_SERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_WEBREQUESTLISTENER_CID);
 
 static const Module::CIDEntry kToolkitCIDs[] = {
   { &kNS_TOOLKIT_APPSTARTUP_CID, false, nullptr, nsAppStartupConstructor },
 #if defined(MOZ_HAS_TERMINATOR)
   { &kNS_TOOLKIT_TERMINATOR_CID, false, nullptr, nsTerminatorConstructor },
 #endif
@@ -191,16 +194,17 @@ static const Module::CIDEntry kToolkitCI
 #if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID)
   { &kNS_UPDATEPROCESSOR_CID, false, nullptr, nsUpdateProcessorConstructor },
 #endif
   { &kFINALIZATIONWITNESSSERVICE_CID, false, nullptr, FinalizationWitnessServiceConstructor },
   { &kNATIVE_OSFILE_INTERNALS_SERVICE_CID, false, nullptr, NativeOSFileInternalsServiceConstructor },
   { &kNS_ADDONCONTENTPOLICY_CID, false, nullptr, AddonContentPolicyConstructor },
   { &kNS_ADDON_PATH_SERVICE_CID, false, nullptr, AddonPathServiceConstructor },
   { &kNS_ADDON_MANAGER_STARTUP_CID, false, nullptr, AddonManagerStartupConstructor },
+  { &kNS_ADDON_POLICY_SERVICE_CID, false, nullptr, ExtensionPolicyServiceConstructor },
   { &kNATIVE_FILEWATCHER_SERVICE_CID, false, nullptr, NativeFileWatcherServiceConstructor },
   { &kNS_WEBREQUESTLISTENER_CID, false, nullptr, nsWebRequestListenerConstructor },
   { nullptr }
 };
 
 static const Module::ContractIDEntry kToolkitContracts[] = {
   { NS_APPSTARTUP_CONTRACTID, &kNS_TOOLKIT_APPSTARTUP_CID },
 #if defined(MOZ_HAS_TERMINATOR)
@@ -229,16 +233,17 @@ static const Module::ContractIDEntry kTo
 #if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID)
   { NS_UPDATEPROCESSOR_CONTRACTID, &kNS_UPDATEPROCESSOR_CID },
 #endif
   { FINALIZATIONWITNESSSERVICE_CONTRACTID, &kFINALIZATIONWITNESSSERVICE_CID },
   { NATIVE_OSFILE_INTERNALS_SERVICE_CONTRACTID, &kNATIVE_OSFILE_INTERNALS_SERVICE_CID },
   { NS_ADDONCONTENTPOLICY_CONTRACTID, &kNS_ADDONCONTENTPOLICY_CID },
   { NS_ADDONPATHSERVICE_CONTRACTID, &kNS_ADDON_PATH_SERVICE_CID },
   { NS_ADDONMANAGERSTARTUP_CONTRACTID, &kNS_ADDON_MANAGER_STARTUP_CID },
+  { NS_ADDON_POLICY_SERVICE_CONTRACTID, &kNS_ADDON_POLICY_SERVICE_CID },
   { NATIVE_FILEWATCHER_SERVICE_CONTRACTID, &kNATIVE_FILEWATCHER_SERVICE_CID },
   { NS_WEBREQUESTLISTENER_CONTRACTID, &kNS_WEBREQUESTLISTENER_CID },
   { nullptr }
 };
 
 static const mozilla::Module::CategoryEntry kToolkitCategories[] = {
   { "content-policy", NS_ADDONCONTENTPOLICY_CONTRACTID, NS_ADDONCONTENTPOLICY_CONTRACTID },
   { nullptr }
--- a/toolkit/components/extensions/.eslintrc.js
+++ b/toolkit/components/extensions/.eslintrc.js
@@ -8,16 +8,17 @@ module.exports = {
     "Cr": true,
     "Cu": true,
     "TextDecoder": false,
     "TextEncoder": false,
 
     "MatchGlob": false,
     "MatchPattern": true,
     "MatchPatternSet": false,
+    "WebExtensionPolicy": false,
     // Specific to WebExtensions:
     "AppConstants": true,
     "Extension": true,
     "ExtensionAPI": true,
     "ExtensionManagement": true,
     "ExtensionUtils": true,
     "extensions": true,
     "getContainerForCookieStoreId": true,
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -742,29 +742,35 @@ this.Extension = class extends Extension
       }
 
       if (permissions.origins.length > 0) {
         let patterns = this.whiteListedHosts.patterns.map(host => host.pattern);
 
         this.whiteListedHosts = new MatchPatternSet([...patterns, ...permissions.origins],
                                                     {ignorePath: true});
       }
+
+      this.policy.permissions = Array.from(this.permissions);
+      this.policy.allowedOrigins = this.whiteListedHosts;
     });
 
     this.on("remove-permissions", (ignoreEvent, permissions) => {
       for (let perm of permissions.permissions) {
         this.permissions.delete(perm);
       }
 
       let origins = permissions.origins.map(
         origin => new MatchPattern(origin, {ignorePath: true}).pattern);
 
       this.whiteListedHosts = new MatchPatternSet(
         this.whiteListedHosts.patterns
             .filter(host => !origins.includes(host.pattern)));
+
+      this.policy.permissions = Array.from(this.permissions);
+      this.policy.allowedOrigins = this.whiteListedHosts;
     });
     /* eslint-enable mozilla/balanced-listeners */
   }
 
   static generateXPI(data) {
     return ExtensionTestCommon.generateXPI(data);
   }
 
@@ -1023,17 +1029,17 @@ this.Extension = class extends Extension
       this.emit("ready");
       TelemetryStopwatch.finish("WEBEXT_EXTENSION_STARTUP_MS", this);
     } catch (e) {
       dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`);
       Cu.reportError(e);
 
       if (this.started) {
         this.started = false;
-        ExtensionManagement.shutdownExtension(this.uuid);
+        ExtensionManagement.shutdownExtension(this);
       }
 
       this.cleanupGeneratedFile();
 
       throw e;
     }
 
     this.startupPromise = null;
@@ -1080,17 +1086,17 @@ this.Extension = class extends Extension
     }
 
     let data = Services.ppmm.initialProcessData;
     data["Extension:Extensions"] = data["Extension:Extensions"].filter(e => e.id !== this.id);
 
     Services.ppmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
 
     if (!this.manifest) {
-      ExtensionManagement.shutdownExtension(this.uuid);
+      ExtensionManagement.shutdownExtension(this);
 
       this.cleanupGeneratedFile();
       return;
     }
 
     GlobalManager.uninit(this);
 
     for (let obj of this.onShutdown) {
@@ -1105,17 +1111,17 @@ this.Extension = class extends Extension
 
     Management.emit("shutdown", this);
     this.emit("shutdown");
 
     Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
 
     MessageChannel.abortResponses({extensionId: this.id});
 
-    ExtensionManagement.shutdownExtension(this.uuid);
+    ExtensionManagement.shutdownExtension(this);
 
     return this.cleanupGeneratedFile();
   }
 
   observe(subject, topic, data) {
     if (topic === "xpcom-shutdown") {
       this.cleanupGeneratedFile();
     }
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -505,16 +505,21 @@ class BrowserExtensionContent extends Ev
       }
 
       if (permissions.origins.length > 0) {
         let patterns = this.whiteListedHosts.patterns.map(host => host.pattern);
 
         this.whiteListedHosts = new MatchPatternSet([...patterns, ...permissions.origins],
                                                     {ignorePath: true});
       }
+
+      if (this.policy) {
+        this.policy.permissions = Array.from(this.permissions);
+        this.policy.allowedOrigins = this.whiteListedHosts;
+      }
     });
 
     this.on("remove-permissions", (ignoreEvent, permissions) => {
       if (permissions.permissions.length > 0) {
         for (let perm of permissions.permissions) {
           this.permissions.delete(perm);
         }
       }
@@ -522,16 +527,21 @@ class BrowserExtensionContent extends Ev
       if (permissions.origins.length > 0) {
         let origins = permissions.origins.map(
           origin => new MatchPattern(origin, {ignorePath: true}).pattern);
 
         this.whiteListedHosts = new MatchPatternSet(
           this.whiteListedHosts.patterns
               .filter(host => !origins.includes(host.pattern)));
       }
+
+      if (this.policy) {
+        this.policy.permissions = Array.from(this.permissions);
+        this.policy.allowedOrigins = this.whiteListedHosts;
+      }
     });
     /* eslint-enable mozilla/balanced-listeners */
 
     ExtensionManager.extensions.set(this.id, this);
   }
 
   shutdown() {
     ExtensionManager.extensions.delete(this.id);
--- a/toolkit/components/extensions/ExtensionManagement.jsm
+++ b/toolkit/components/extensions/ExtensionManagement.jsm
@@ -11,31 +11,25 @@ const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "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;
 
-var ExtensionManagement;
-
 /*
  * This file should be kept short and simple since it's loaded even
  * when no extensions are running.
  */
 
 var APIs = {
   apis: new Map(),
 
@@ -60,147 +54,61 @@ function getURLForExtension(id, path = "
   let uuid = UUIDMap.get(id, false);
   if (!uuid) {
     Cu.reportError(`Called getURLForExtension on unmapped extension ${id}`);
     return null;
   }
   return `moz-extension://${uuid}/${path}`;
 }
 
-// This object manages various platform-level issues related to
-// moz-extension:// URIs. It lives here so that it can be used in both
-// the parent and child processes.
-//
-// moz-extension URIs have the form moz-extension://uuid/path. Each
-// extension has its own UUID, unique to the machine it's installed
-// on. This is easier and more secure than using the extension ID,
-// since it makes it slightly harder to fingerprint for extensions if
-// each user uses different URIs for the extension.
-var Service = {
-  initialized: false,
-
-  // Map[uuid -> extension].
-  // extension can be an Extension (parent process) or BrowserExtensionContent (child process).
-  uuidMap: new Map(),
-
-  init() {
-    let aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService);
-    aps = aps.wrappedJSObject;
-    this.aps = aps;
-    aps.setExtensionURILoadCallback(this.extensionURILoadableByAnyone.bind(this));
-    aps.setExtensionURIToAddonIdCallback(this.extensionURIToAddonID.bind(this));
-  },
-
-  // Called when a new extension is loaded.
-  startupExtension(uuid, uri, extension) {
-    if (!this.initialized) {
-      this.initialized = true;
-      this.init();
-    }
-
-    // Create the moz-extension://uuid mapping.
-    let handler = Services.io.getProtocolHandler("moz-extension");
-    handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
-    handler.setSubstitution(uuid, uri);
-
-    this.uuidMap.set(uuid, extension);
-    this.aps.setAddonHasPermissionCallback(extension.id, extension.hasPermission.bind(extension));
-    this.aps.setAddonLoadURICallback(extension.id, this.checkAddonMayLoad.bind(this, extension));
-    this.aps.setAddonLocalizeCallback(extension.id, extension.localize.bind(extension));
-    this.aps.setAddonCSP(extension.id, extension.manifest.content_security_policy);
-    this.aps.setBackgroundPageUrlCallback(uuid, this.generateBackgroundPageUrl.bind(this, extension));
-  },
-
-  // Called when an extension is unloaded.
-  shutdownExtension(uuid) {
-    let extension = this.uuidMap.get(uuid);
-    if (!extension) {
-      return;
-    }
-    this.uuidMap.delete(uuid);
-    this.aps.setAddonHasPermissionCallback(extension.id, null);
-    this.aps.setAddonLoadURICallback(extension.id, null);
-    this.aps.setAddonLocalizeCallback(extension.id, null);
-    this.aps.setAddonCSP(extension.id, null);
-    this.aps.setBackgroundPageUrlCallback(uuid, null);
-
-    let handler = Services.io.getProtocolHandler("moz-extension");
-    handler.QueryInterface(Ci.nsISubstitutingProtocolHandler);
-    handler.setSubstitution(uuid, null);
-  },
-
-  // Return true if the given URI can be loaded from arbitrary web
-  // content. The manifest.json |web_accessible_resources| directive
-  // determines this.
-  extensionURILoadableByAnyone(uri) {
-    let uuid = uri.host;
-    let extension = this.uuidMap.get(uuid);
-    if (!extension || !extension.webAccessibleResources) {
-      return false;
-    }
-
-    let path = uri.QueryInterface(Ci.nsIURL).filePath;
-    return extension.webAccessibleResources.some(res => res.matches(path));
-  },
-
-  // Checks whether a given extension can load this URI (typically via
-  // an XML HTTP request). The manifest.json |permissions| directive
-  // determines this.
-  checkAddonMayLoad(extension, uri, explicit = false) {
-    return extension.whiteListedHosts.matchesIgnoringPath(uri, explicit);
-  },
-
-  generateBackgroundPageUrl(extension) {
-    let background_scripts = (extension.manifest.background &&
-                              extension.manifest.background.scripts);
-
-    if (!background_scripts) {
-      return;
-    }
-
-    let html = "<!DOCTYPE html>\n<html>\n<body>\n";
-    for (let script of background_scripts) {
-      script = script.replace(/"/g, "&quot;");
-      html += `<script src="${script}"></script>\n`;
-    }
-    html += "</body>\n</html>\n";
-
-    return "data:text/html;charset=utf-8," + encodeURIComponent(html);
-  },
-
-  // Finds the add-on ID associated with a given moz-extension:// URI.
-  // This is used to set the addonId on the for the nsIPrincipal
-  // attached to the URI.
-  extensionURIToAddonID(uri) {
-    let uuid = uri.host;
-    let extension = this.uuidMap.get(uuid);
-    return extension ? extension.id : undefined;
-  },
-};
-
 let cacheInvalidated = 0;
 function onCacheInvalidate() {
   cacheInvalidated++;
 }
 Services.obs.addObserver(onCacheInvalidate, "startupcache-invalidate");
 
-ExtensionManagement = {
+var ExtensionManagement = {
   get cacheInvalidated() {
     return cacheInvalidated;
   },
 
   get isExtensionProcess() {
     if (this.useRemoteWebExtensions) {
       return appinfo.remoteType === E10SUtils.EXTENSION_REMOTE_TYPE;
     }
     return isParentProcess;
   },
 
-  startupExtension: Service.startupExtension.bind(Service),
-  shutdownExtension: Service.shutdownExtension.bind(Service),
+  // Called when a new extension is loaded.
+  startupExtension(uuid, uri, extension) {
+    let policy = new WebExtensionPolicy({
+      id: extension.id,
+      mozExtensionHostname: uuid,
+      baseURL: uri.spec,
+
+      permissions: Array.from(extension.permissions),
+      allowedOrigins: extension.whiteListedHosts,
+      webAccessibleResources: extension.webAccessibleResources || [],
+
+      contentSecurityPolicy: extension.manifest.content_security_policy,
+
+      localizeCallback: extension.localize.bind(extension),
+
+      backgroundScripts: (extension.manifest.background &&
+                          extension.manifest.background.scripts),
+    });
+
+    extension.policy = policy;
+    policy.active = true;
+  },
+
+  // Called when an extension is unloaded.
+  shutdownExtension(extension) {
+    extension.policy.active = false;
+  },
 
   registerAPI: APIs.register.bind(APIs),
   unregisterAPI: APIs.unregister.bind(APIs),
 
   getURLForExtension,
 
   APIs,
 };
--- a/toolkit/components/extensions/ExtensionPolicyService.cpp
+++ b/toolkit/components/extensions/ExtensionPolicyService.cpp
@@ -1,17 +1,19 @@
 /* -*-  Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
 /* 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 "mozilla/ExtensionPolicyService.h"
 #include "mozilla/extensions/WebExtensionPolicy.h"
+
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/Preferences.h"
+#include "nsEscape.h"
 #include "nsGkAtoms.h"
 
 namespace mozilla {
 
 using namespace extensions;
 
 #define DEFAULT_BASE_CSP \
     "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; " \
@@ -105,18 +107,115 @@ ExtensionPolicyService::DefaultCSP(nsASt
   nsresult rv;
 
   rv = Preferences::GetString("extensions.webextensions.default-content-security-policy", &aDefaultCSP);
   if (NS_FAILED(rv)) {
     aDefaultCSP.AssignLiteral(DEFAULT_DEFAULT_CSP);
   }
 }
 
+
+/*****************************************************************************
+ * nsIAddonPolicyService
+ *****************************************************************************/
+
+nsresult
+ExtensionPolicyService::GetBaseCSP(nsAString& aBaseCSP)
+{
+  BaseCSP(aBaseCSP);
+  return NS_OK;
+}
+
+nsresult
+ExtensionPolicyService::GetDefaultCSP(nsAString& aDefaultCSP)
+{
+  DefaultCSP(aDefaultCSP);
+  return NS_OK;
+}
+
+nsresult
+ExtensionPolicyService::GetAddonCSP(const nsAString& aAddonId,
+                                    nsAString& aResult)
+{
+  if (WebExtensionPolicy* policy = GetByID(aAddonId)) {
+    policy->GetContentSecurityPolicy(aResult);
+    return NS_OK;
+  }
+  return NS_ERROR_INVALID_ARG;
+}
+
+nsresult
+ExtensionPolicyService::GetGeneratedBackgroundPageUrl(const nsACString& aHostname,
+                                                      nsACString& aResult)
+{
+  if (WebExtensionPolicy* policy = GetByHost(aHostname)) {
+    nsAutoCString url("data:text/html,");
+
+    nsCString html = policy->BackgroundPageHTML();
+    nsAutoCString escaped;
+
+    url.Append(NS_EscapeURL(html, esc_Minimal, escaped));
+
+    aResult = url;
+    return NS_OK;
+  }
+  return NS_ERROR_INVALID_ARG;
+}
+
+nsresult
+ExtensionPolicyService::AddonHasPermission(const nsAString& aAddonId,
+                                           const nsAString& aPerm,
+                                           bool* aResult)
+{
+  if (WebExtensionPolicy* policy = GetByID(aAddonId)) {
+    *aResult = policy->HasPermission(aPerm);
+    return NS_OK;
+  }
+  return NS_ERROR_INVALID_ARG;
+}
+
+nsresult
+ExtensionPolicyService::AddonMayLoadURI(const nsAString& aAddonId,
+                                        nsIURI* aURI,
+                                        bool aExplicit,
+                                        bool* aResult)
+{
+  if (WebExtensionPolicy* policy = GetByID(aAddonId)) {
+    *aResult = policy->CanAccessURI(aURI, aExplicit);
+    return NS_OK;
+  }
+  return NS_ERROR_INVALID_ARG;
+}
+
+nsresult
+ExtensionPolicyService::ExtensionURILoadableByAnyone(nsIURI* aURI, bool* aResult)
+{
+  URLInfo url(aURI);
+  if (WebExtensionPolicy* policy = GetByURL(url)) {
+    *aResult = policy->IsPathWebAccessible(url.FilePath());
+    return NS_OK;
+  }
+  return NS_ERROR_INVALID_ARG;
+}
+
+nsresult
+ExtensionPolicyService::ExtensionURIToAddonId(nsIURI* aURI, nsAString& aResult)
+{
+  if (WebExtensionPolicy* policy = GetByURL(aURI)) {
+    policy->GetId(aResult);
+  } else {
+    aResult.SetIsVoid(true);
+  }
+  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_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
@@ -5,33 +5,40 @@
 
 #ifndef mozilla_ExtensionPolicyService_h
 #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 "nsISupports.h"
 #include "nsPointerHashKeys.h"
 #include "nsRefPtrHashtable.h"
 
 namespace mozilla {
 
 using extensions::WebExtensionPolicy;
 
-class ExtensionPolicyService final : public nsISupports
+class ExtensionPolicyService final : public nsIAddonPolicyService
 {
 public:
   NS_DECL_CYCLE_COLLECTION_CLASS(ExtensionPolicyService)
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_NSIADDONPOLICYSERVICE
 
   static ExtensionPolicyService& GetSingleton();
 
+  static already_AddRefed<ExtensionPolicyService> GetInstance()
+  {
+    return do_AddRef(&GetSingleton());
+  }
+
   WebExtensionPolicy*
   GetByID(const nsIAtom* aAddonId)
   {
     return mExtensions.GetWeak(aAddonId);
   }
 
   WebExtensionPolicy* GetByID(const nsAString& aAddonId)
   {
--- a/toolkit/components/extensions/extension-process-script.js
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -534,16 +534,19 @@ DocumentManager = {
  */
 class StubExtension {
   constructor(data) {
     this.data = data;
     this.id = data.id;
     this.uuid = data.uuid;
     this.instanceId = data.instanceId;
     this.manifest = data.manifest;
+    this.permissions = data.permissions;
+    this.whiteListedHosts = new MatchPatternSet(data.whiteListedHosts);
+    this.webAccessibleResources = data.webAccessibleResources.map(path => new MatchGlob(path));
 
     this.scripts = data.content_scripts.map(scriptData => new ScriptMatcher(this, scriptData));
 
     this._realExtension = null;
 
     this.startup();
   }
 
@@ -552,44 +555,39 @@ class StubExtension {
     if (isContentProcess) {
       let uri = Services.io.newURI(this.data.resourceURL);
       ExtensionManagement.startupExtension(this.uuid, uri, this);
     }
   }
 
   shutdown() {
     if (isContentProcess) {
-      ExtensionManagement.shutdownExtension(this.uuid);
+      ExtensionManagement.shutdownExtension(this);
     }
     if (this._realExtension) {
       this._realExtension.shutdown();
     }
   }
 
   // Lazily create the real extension object when needed.
   get realExtension() {
     if (!this._realExtension) {
       this._realExtension = new ExtensionChild.BrowserExtensionContent(this.data);
+      this._realExtension.policy = this.policy;
     }
     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]);
--- a/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js
+++ b/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js
@@ -2,37 +2,55 @@
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 Cu.import("resource://gre/modules/Preferences.jsm");
 
 const ADDON_ID = "test@web.extension";
 
 const aps = Cc["@mozilla.org/addons/policy-service;1"]
-  .getService(Ci.nsIAddonPolicyService).wrappedJSObject;
+  .getService(Ci.nsIAddonPolicyService);
+
+let policy = null;
+
+function setAddonCSP(csp) {
+  if (policy) {
+    policy.active = false;
+  }
+
+  policy = new WebExtensionPolicy({
+    id: ADDON_ID,
+    mozExtensionHostname: ADDON_ID,
+    baseURL: "file:///",
+
+    allowedOrigins: new MatchPatternSet([]),
+    localizeCallback() {},
+
+    contentSecurityPolicy: csp,
+  });
+
+  policy.active = true;
+}
 
 do_register_cleanup(() => {
-  aps.setAddonCSP(ADDON_ID, null);
+  policy.active = false;
 });
 
 add_task(function* test_addon_csp() {
   equal(aps.baseCSP, Preferences.get("extensions.webextensions.base-content-security-policy"),
         "Expected base CSP value");
 
   equal(aps.defaultCSP, Preferences.get("extensions.webextensions.default-content-security-policy"),
         "Expected default CSP value");
 
-  equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP,
-        "CSP for unknown add-on ID should be the default CSP");
-
 
   const CUSTOM_POLICY = "script-src: 'self' https://xpcshell.test.custom.csp; object-src: 'none'";
 
-  aps.setAddonCSP(ADDON_ID, CUSTOM_POLICY);
+  setAddonCSP(CUSTOM_POLICY);
 
   equal(aps.getAddonCSP(ADDON_ID), CUSTOM_POLICY, "CSP should point to add-on's custom policy");
 
 
-  aps.setAddonCSP(ADDON_ID, null);
+  setAddonCSP(null);
 
   equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP,
         "CSP should revert to default when set to null");
 });
--- a/toolkit/components/extensions/test/xpcshell/test_locale_converter.js
+++ b/toolkit/components/extensions/test/xpcshell/test_locale_converter.js
@@ -18,32 +18,32 @@ function StringStream(string) {
   stream.data = string;
   return stream;
 }
 
 
 // Initialize the policy service with a stub localizer for our
 // add-on ID.
 add_task(async function init() {
-  const aps = Cc["@mozilla.org/addons/policy-service;1"]
-    .getService(Ci.nsIAddonPolicyService).wrappedJSObject;
+  let policy = new WebExtensionPolicy({
+    id: ADDON_ID,
+    mozExtensionHostname: UUID,
+    baseURL: "file:///",
 
-  let oldCallback = aps.setExtensionURIToAddonIdCallback(uri => {
-    if (uri.host == UUID) {
-      return ADDON_ID;
-    }
+    allowedOrigins: new MatchPatternSet([]),
+
+    localizeCallback(string) {
+      return string.replace(/__MSG_(.*?)__/g, "<localized-$1>");
+    },
   });
 
-  aps.setAddonLocalizeCallback(ADDON_ID, string => {
-    return string.replace(/__MSG_(.*?)__/g, "<localized-$1>");
-  });
+  policy.active = true;
 
   do_register_cleanup(() => {
-    aps.setExtensionURIToAddonIdCallback(oldCallback);
-    aps.setAddonLocalizeCallback(ADDON_ID, null);
+    policy.active = false;
   });
 });
 
 
 // Test that the synchronous converter works as expected with a
 // simple string.
 add_task(async function testSynchronousConvert() {
   let stream = StringStream("Foo __MSG_xxx__ bar __MSG_yyy__ baz");
--- a/toolkit/components/utils/simpleServices.js
+++ b/toolkit/components/utils/simpleServices.js
@@ -12,224 +12,33 @@
 
 "use strict";
 
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
+/* globals WebExtensionPolicy */
+
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
-function AddonPolicyService() {
-  this.wrappedJSObject = this;
-  this.cspStrings = new Map();
-  this.backgroundPageUrlCallbacks = new Map();
-  this.checkHasPermissionCallbacks = new Map();
-  this.mayLoadURICallbacks = new Map();
-  this.localizeCallbacks = new Map();
-
-  XPCOMUtils.defineLazyPreferenceGetter(
-    this, "baseCSP", "extensions.webextensions.base-content-security-policy",
-    "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; " +
-    "object-src 'self' https://* moz-extension: blob: filesystem:;");
-
-  XPCOMUtils.defineLazyPreferenceGetter(
-    this, "defaultCSP", "extensions.webextensions.default-content-security-policy",
-    "script-src 'self'; object-src 'self';");
-}
-
-AddonPolicyService.prototype = {
-  classID: Components.ID("{89560ed3-72e3-498d-a0e8-ffe50334d7c5}"),
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonPolicyService]),
-
-  /**
-   * Returns the content security policy which applies to documents belonging
-   * to the extension with the given ID. This may be either a custom policy,
-   * if one was supplied, or the default policy if one was not.
-   */
-  getAddonCSP(aAddonId) {
-    let csp = this.cspStrings.get(aAddonId);
-    return csp || this.defaultCSP;
-  },
-
-  /**
-   * Returns the generated background page as a data-URI, if any. If the addon
-   * does not have an auto-generated background page, an empty string is
-   * returned.
-   */
-  getGeneratedBackgroundPageUrl(aAddonId) {
-    let cb = this.backgroundPageUrlCallbacks.get(aAddonId);
-    return cb && cb(aAddonId) || "";
-  },
-
-  /*
-   * Invokes a callback (if any) associated with the addon to determine whether
-   * the addon is granted the |aPerm| API permission.
-   *
-   * @see nsIAddonPolicyService.addonHasPermission
-   */
-  addonHasPermission(aAddonId, aPerm) {
-    let cb = this.checkHasPermissionCallbacks.get(aAddonId);
-    return cb ? cb(aPerm) : false;
-  },
-
-  /*
-   * Invokes a callback (if any) associated with the addon to determine whether
-   * unprivileged code running within the addon is allowed to perform loads from
-   * the given URI.
-   *
-   * @see nsIAddonPolicyService.addonMayLoadURI
-   */
-  addonMayLoadURI(aAddonId, aURI, aExplicit = false) {
-    let cb = this.mayLoadURICallbacks.get(aAddonId);
-    return cb ? cb(aURI, aExplicit) : false;
-  },
-
-  /*
-   * Invokes a callback (if any) associated with the addon to loclaize a
-   * resource belonging to that add-on.
-   */
-  localizeAddonString(aAddonId, aString) {
-    let cb = this.localizeCallbacks.get(aAddonId);
-    return cb ? cb(aString) : aString;
-  },
-
-  /*
-   * Invokes a callback (if any) to determine if an extension URI should be
-   * web-accessible.
-   *
-   * @see nsIAddonPolicyService.extensionURILoadableByAnyone
-   */
-  extensionURILoadableByAnyone(aURI) {
-    if (aURI.scheme != "moz-extension") {
-      throw new TypeError("non-extension URI passed");
-    }
-
-    let cb = this.extensionURILoadCallback;
-    return cb ? cb(aURI) : false;
-  },
-
-  /*
-   * Maps an extension URI to an addon ID.
-   *
-   * @see nsIAddonPolicyService.extensionURIToAddonId
-   */
-  extensionURIToAddonId(aURI) {
-    if (aURI.scheme != "moz-extension") {
-      throw new TypeError("non-extension URI passed");
-    }
-
-    let cb = this.extensionURIToAddonIdCallback;
-    if (!cb) {
-      throw new Error("no callback set to map extension URIs to addon Ids");
-    }
-    return cb(aURI);
-  },
-
-  /*
-   * Sets the callbacks used in addonHasPermission above. Not accessible over
-   * XPCOM - callers should use .wrappedJSObject on the service to call it
-   * directly.
-   */
-  setAddonHasPermissionCallback(aAddonId, aCallback) {
-    if (aCallback) {
-      this.checkHasPermissionCallbacks.set(aAddonId, aCallback);
-    } else {
-      this.checkHasPermissionCallbacks.delete(aAddonId);
-    }
-  },
-
-  /*
-   * Sets the callbacks used in addonMayLoadURI above. Not accessible over
-   * XPCOM - callers should use .wrappedJSObject on the service to call it
-   * directly.
-   */
-  setAddonLoadURICallback(aAddonId, aCallback) {
-    if (aCallback) {
-      this.mayLoadURICallbacks.set(aAddonId, aCallback);
-    } else {
-      this.mayLoadURICallbacks.delete(aAddonId);
-    }
-  },
-
-  /*
-   * Sets the custom CSP string to be used for the add-on. Not accessible over
-   * XPCOM - callers should use .wrappedJSObject on the service to call it
-   * directly.
-   */
-  setAddonCSP(aAddonId, aCSPString) {
-    if (aCSPString) {
-      this.cspStrings.set(aAddonId, aCSPString);
-    } else {
-      this.cspStrings.delete(aAddonId);
-    }
-  },
-
-  /**
-   * Set the callback that generates a data-URL for the background page.
-   */
-  setBackgroundPageUrlCallback(aAddonId, aCallback) {
-    if (aCallback) {
-      this.backgroundPageUrlCallbacks.set(aAddonId, aCallback);
-    } else {
-      this.backgroundPageUrlCallbacks.delete(aAddonId);
-    }
-  },
-
-  /*
-   * Sets the callbacks used by the stream converter service to localize
-   * add-on resources.
-   */
-  setAddonLocalizeCallback(aAddonId, aCallback) {
-    if (aCallback) {
-      this.localizeCallbacks.set(aAddonId, aCallback);
-    } else {
-      this.localizeCallbacks.delete(aAddonId);
-    }
-  },
-
-  /*
-   * Sets the callback used in extensionURILoadableByAnyone above. Not
-   * accessible over XPCOM - callers should use .wrappedJSObject on the
-   * service to call it directly.
-   */
-  setExtensionURILoadCallback(aCallback) {
-    var old = this.extensionURILoadCallback;
-    this.extensionURILoadCallback = aCallback;
-    return old;
-  },
-
-  /*
-   * Sets the callback used in extensionURIToAddonId above. Not accessible over
-   * XPCOM - callers should use .wrappedJSObject on the service to call it
-   * directly.
-   */
-  setExtensionURIToAddonIdCallback(aCallback) {
-    var old = this.extensionURIToAddonIdCallback;
-    this.extensionURIToAddonIdCallback = aCallback;
-    return old;
-  }
-};
-
 /*
  * This class provides a stream filter for locale messages in CSS files served
  * by the moz-extension: protocol handler.
  *
  * See SubstituteChannel in netwerk/protocol/res/ExtensionProtocolHandler.cpp
  * for usage.
  */
 function AddonLocalizationConverter() {
-  this.aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService)
-    .wrappedJSObject;
 }
 
 AddonLocalizationConverter.prototype = {
   classID: Components.ID("{ded150e3-c92e-4077-a396-0dba9953e39f}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamConverter]),
 
   FROM_TYPE: "application/vnd.mozilla.webext.unlocalized",
   TO_TYPE: "text/css",
@@ -241,71 +50,70 @@ AddonLocalizationConverter.prototype = {
     }
     if (aToType != this.TO_TYPE) {
       throw Components.Exception("Invalid aToType value", Cr.NS_ERROR_INVALID_ARG,
                                  Components.stack.caller.caller);
     }
   },
 
   // aContext must be a nsIURI object for a valid moz-extension: URL.
-  getAddonId(aContext) {
+  getAddon(aContext) {
     // In this case, we want the add-on ID even if the URL is web accessible,
     // so check the root rather than the exact path.
     let uri = Services.io.newURI("/", null, aContext);
 
-    let id = this.aps.extensionURIToAddonId(uri);
-    if (id == undefined) {
+    let addon = WebExtensionPolicy.getByURI(uri);
+    if (!addon) {
       throw new Components.Exception("Invalid context", Cr.NS_ERROR_INVALID_ARG);
     }
-    return id;
+    return addon;
   },
 
-  convertToStream(aAddonId, aString) {
+  convertToStream(aAddon, aString) {
     let stream = Cc["@mozilla.org/io/string-input-stream;1"]
       .createInstance(Ci.nsIStringInputStream);
 
-    stream.data = this.aps.localizeAddonString(aAddonId, aString);
+    stream.data = aAddon.localize(aString);
     return stream;
   },
 
   convert(aStream, aFromType, aToType, aContext) {
     this.checkTypes(aFromType, aToType);
-    let addonId = this.getAddonId(aContext);
+    let addon = this.getAddon(aContext);
 
     let string = (
       aStream.available() ?
       NetUtil.readInputStreamToString(aStream, aStream.available()) : ""
     );
-    return this.convertToStream(addonId, string);
+    return this.convertToStream(addon, string);
   },
 
   asyncConvertData(aFromType, aToType, aListener, aContext) {
     this.checkTypes(aFromType, aToType);
-    this.addonId = this.getAddonId(aContext);
+    this.addon = this.getAddon(aContext);
     this.listener = aListener;
   },
 
   onStartRequest(aRequest, aContext) {
     this.parts = [];
   },
 
   onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount) {
     this.parts.push(NetUtil.readInputStreamToString(aInputStream, aCount));
   },
 
   onStopRequest(aRequest, aContext, aStatusCode) {
     try {
       this.listener.onStartRequest(aRequest, null);
       if (Components.isSuccessCode(aStatusCode)) {
         let string = this.parts.join("");
-        let stream = this.convertToStream(this.addonId, string);
+        let stream = this.convertToStream(this.addon, string);
 
         this.listener.onDataAvailable(aRequest, null, stream, 0, stream.data.length);
       }
     } catch (e) {
       aStatusCode = e.result || Cr.NS_ERROR_FAILURE;
     }
     this.listener.onStopRequest(aRequest, null, aStatusCode);
   },
 };
 
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AddonPolicyService,
-                                                     AddonLocalizationConverter]);
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AddonLocalizationConverter]);
--- a/toolkit/components/utils/utils.manifest
+++ b/toolkit/components/utils/utils.manifest
@@ -1,6 +1,4 @@
 component {dfd07380-6083-11e4-9803-0800200c9a66} simpleServices.js
 contract @mozilla.org/addons/remote-tag-service;1 {dfd07380-6083-11e4-9803-0800200c9a66}
-component {89560ed3-72e3-498d-a0e8-ffe50334d7c5} simpleServices.js
-contract @mozilla.org/addons/policy-service;1 {89560ed3-72e3-498d-a0e8-ffe50334d7c5}
 component {ded150e3-c92e-4077-a396-0dba9953e39f} simpleServices.js
 contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.webext.unlocalized&to=text/css {ded150e3-c92e-4077-a396-0dba9953e39f}