merge mozilla-central to autoland. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Mon, 05 Jun 2017 11:06:54 +0200
changeset 410407 81e0a353d2dc82c8ea71a5dc26956f292a867895
parent 410406 8e78bd3e1e16594191f57bac69f9cd88460aa59d (current diff)
parent 410389 275588f4d852d7dc183a9dcc70a311413dc7a063 (diff)
child 410408 9d26051a28ea4bffdc654357855f50c9a685fbcc
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)
reviewersmerge, merge
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
merge mozilla-central to autoland. r=merge a=merge
toolkit/components/extensions/ExtensionManagement.jsm
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -1,14 +1,12 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-Cu.import("resource://gre/modules/ExtensionManagement.jsm");
-Cu.import("resource://gre/modules/MatchPattern.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 
 var {
   ExtensionError,
   IconDetails,
@@ -376,21 +374,21 @@ MenuItem.prototype = {
       if (createProperties[propName] === null) {
         // Omitted optional argument.
         continue;
       }
       this[propName] = createProperties[propName];
     }
 
     if (createProperties.documentUrlPatterns != null) {
-      this.documentUrlMatchPattern = new MatchPattern(this.documentUrlPatterns);
+      this.documentUrlMatchPattern = new MatchPatternSet(this.documentUrlPatterns);
     }
 
     if (createProperties.targetUrlPatterns != null) {
-      this.targetUrlMatchPattern = new MatchPattern(this.targetUrlPatterns);
+      this.targetUrlMatchPattern = new MatchPatternSet(this.targetUrlPatterns);
     }
 
     // If a child MenuItem does not specify any contexts, then it should
     // inherit the contexts specified from its parent.
     if (createProperties.parentId && !createProperties.contexts) {
       this.contexts = this.parent.contexts;
     }
   },
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -1,18 +1,16 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
                                    "@mozilla.org/browser/aboutnewtab-service;1",
                                    "nsIAboutNewTabService");
 
-XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
-                                  "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 let tabListener = {
@@ -473,17 +471,17 @@ this.tabs = class extends ExtensionAPI {
 
         async query(queryInfo) {
           if (queryInfo.url !== null) {
             if (!extension.hasPermission("tabs")) {
               return Promise.reject({message: 'The "tabs" permission is required to use the query API with the "url" parameter'});
             }
 
             queryInfo = Object.assign({}, queryInfo);
-            queryInfo.url = new MatchPattern(queryInfo.url);
+            queryInfo.url = new MatchPatternSet([].concat(queryInfo.url));
           }
 
           return Array.from(tabManager.query(queryInfo, context),
                             tab => tab.convert());
         },
 
         async captureVisibleTab(windowId, options) {
           let window = windowId == null ?
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus_urlPatterns.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_urlPatterns.js
@@ -97,60 +97,60 @@ add_task(async function() {
         title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextAll",
         documentUrlPatterns: ["*://*/*context.html"],
         targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
         contexts: ["all"],
       });
 
       browser.contextMenus.create({
         title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextAll",
-        documentUrlPatterns: ["*://does-not-match"],
+        documentUrlPatterns: ["*://*/does-not-match"],
         targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
         contexts: ["all"],
       });
 
       browser.contextMenus.create({
         title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextAll",
         documentUrlPatterns: ["*://*/*context.html"],
-        targetUrlPatterns: ["*://does-not-match"],
+        targetUrlPatterns: ["*://*/does-not-match"],
         contexts: ["all"],
       });
 
       browser.contextMenus.create({
         title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextAll",
-        documentUrlPatterns: ["*://does-not-match"],
-        targetUrlPatterns: ["*://does-not-match"],
+        documentUrlPatterns: ["*://*/does-not-match"],
+        targetUrlPatterns: ["*://*/does-not-match"],
         contexts: ["all"],
       });
 
       browser.contextMenus.create({
         title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextImage",
         documentUrlPatterns: ["*://*/*context.html"],
         targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
         contexts: ["image"],
       });
 
       browser.contextMenus.create({
         title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextImage",
-        documentUrlPatterns: ["*://does-not-match"],
+        documentUrlPatterns: ["*://*/does-not-match"],
         targetUrlPatterns: ["*://*/*ctxmenu-image.png"],
         contexts: ["image"],
       });
 
       browser.contextMenus.create({
         title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextImage",
         documentUrlPatterns: ["*://*/*context.html"],
-        targetUrlPatterns: ["*://does-not-match"],
+        targetUrlPatterns: ["*://*/does-not-match"],
         contexts: ["image"],
       });
 
       browser.contextMenus.create({
         title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextImage",
-        documentUrlPatterns: ["*://does-not-match"],
-        targetUrlPatterns: ["*://does-not-match"],
+        documentUrlPatterns: ["*://*/does-not-match/"],
+        targetUrlPatterns: ["*://*/does-not-match"],
         contexts: ["image"],
       });
 
       browser.test.notifyPass("contextmenus-urlPatterns");
     },
   });
 
   function confirmContextMenuItems(menu, expected) {
--- a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
@@ -1,14 +1,16 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+XPCOMUtils.defineLazyPreferenceGetter(this, "useRemoteWebExtensions",
+                                      "extensions.webextensions.remote", false);
+
 add_task(async function testExecuteScript() {
-  let {ExtensionManagement} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {});
   let {MessageChannel} = Cu.import("resource://gre/modules/MessageChannel.jsm", {});
 
   function countMM(messageManagerMap) {
     let count = 0;
     // List of permanent message managers in the main process. We should not
     // count them in the test because MessageChannel unsubscribes when the
     // message manager closes, which never happens to these, of course.
     let globalMMs = [
@@ -252,13 +254,13 @@ add_task(async function testExecuteScrip
 
   await extension.unload();
 
   await BrowserTestUtils.removeTab(tab);
 
   // Make sure that we're not holding on to references to closed message
   // managers.
   is(countMM(MessageChannel.messageManagers), messageManagersSize, "Message manager count");
-  if (!ExtensionManagement.useRemoteWebExtensions) {
+  if (!useRemoteWebExtensions) {
     is(countMM(MessageChannel.responseManagers), responseManagersSize, "Response manager count");
   }
   is(MessageChannel.pendingResponses.size, 0, "Pending response count");
 });
--- a/browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js
@@ -1,26 +1,13 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 add_task(async function webNavigation_getFrameId_of_existing_main_frame() {
-  // Whether the frame ID in the extension API is 0 is determined by a map that
-  // is maintained by |Frames| in ExtensionManagement.jsm. This map is filled
-  // using data from content processes. But if ExtensionManagement.jsm is not
-  // imported, then the "Extension:TopWindowID" message gets lost.
-  // As a result, if the state is not synchronized again, the webNavigation API
-  // will mistakenly report a non-zero frame ID for top-level frames.
-  //
-  // If you want to be absolutely sure that the frame ID is correct, don't open
-  // tabs before starting an extension, or explicitly load the module in the
-  // main process:
-  // Cu.import("resource://gre/modules/ExtensionManagement.jsm", {});
-  //
-  // Or simply run the test again.
   const BASE = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
   const DUMMY_URL = BASE + "file_dummy.html";
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, DUMMY_URL, true);
 
   async function background(DUMMY_URL) {
     let tabs = await browser.tabs.query({active: true, currentWindow: true});
     let frames = await browser.webNavigation.getAllFrames({tabId: tabs[0].id});
     browser.test.assertEq(1, frames.length, "The dummy page has one frame");
--- a/browser/components/extensions/test/xpcshell/head.js
+++ b/browser/components/extensions/test/xpcshell/head.js
@@ -7,18 +7,16 @@ const {classes: Cc, interfaces: Ci, util
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
                                   "resource://gre/modules/Extension.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
-                                  "resource://gre/modules/ExtensionManagement.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils",
                                   "resource://testing-common/ExtensionXPCShellUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
                                   "resource://testing-common/httpd.js");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.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/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/client/aboutdebugging/aboutdebugging.css
+++ b/devtools/client/aboutdebugging/aboutdebugging.css
@@ -202,16 +202,22 @@ button {
 .addons-web-ext-tip {
   display: inline-block;
   margin-inline-end: 1ch;
 }
 
 .addons-tip {
   display: flex;
   align-items: center;
+  margin-top: 1em;
+  margin-bottom: 1em;
+  background-image: url(chrome://devtools/skin/images/help.svg);
+  background-repeat: no-repeat;
+  padding-inline-start: 32px;
+  height: 24px;
 }
 
 .addons-tip-icon {
   margin-inline-end: 1ch;
 }
 
 .error-page {
   display: flex;
@@ -239,16 +245,17 @@ button {
   box-shadow: 0 1px 2px rgba(0, 0, 0, 0.24);
 }
 
 .addon-target-container .target {
   align-items: center;
   display: flex;
   margin: 0;
   padding: 16px 0;
+  font-size: 14px;
 }
 
 .addon-target-actions {
   border-top: 1px solid rgba(0, 0, 0, 0.2);
 }
 
 .addon-target-container .target-icon {
   margin-inline-end: 16px;
@@ -257,17 +264,16 @@ button {
 .addon-target-container .name {
   align-self: center;
   font-size: 16px;
   font-weight: 600;
 }
 
 .addon-target-info {
   display: grid;
-  font-size: 14px;
   grid-template-columns: 128px 1fr;
 }
 
 .addon-target-info-label {
   padding-inline-end: 8px;
   padding-bottom: 8px;
 }
 
--- a/devtools/client/aboutdebugging/components/addons/panel.js
+++ b/devtools/client/aboutdebugging/components/addons/panel.js
@@ -145,21 +145,16 @@ module.exports = createClass({
         name: temporaryName,
         targets: temporaryTargets,
         client,
         debugDisabled,
         targetClass,
         sort: true
       }),
       dom.div({ className: "addons-tip"},
-        dom.img({
-          className: "addons-tip-icon",
-          role: "presentation",
-          src: "chrome://devtools/skin/images/help.svg",
-        }),
         dom.span({
           className: "addons-web-ext-tip",
         }, Strings.GetStringFromName("webExtTip")),
         dom.a({ href: WEB_EXT_URL, target: "_blank" },
           Strings.GetStringFromName("webExtTip.learnMore")
         )
       )
     ),
--- a/devtools/client/aboutdebugging/components/addons/target.js
+++ b/devtools/client/aboutdebugging/components/addons/target.js
@@ -3,28 +3,31 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* eslint-env browser */
 
 "use strict";
 
 const { createClass, DOM: dom, PropTypes } =
   require("devtools/client/shared/vendor/react");
-const { debugAddon, uninstallAddon } = require("../../modules/addon");
+const { debugAddon, uninstallAddon, isTemporaryID } = require("../../modules/addon");
 const Services = require("Services");
 
 loader.lazyImporter(this, "BrowserToolboxProcess",
   "resource://devtools/client/framework/ToolboxProcess.jsm");
 
 loader.lazyRequireGetter(this, "DebuggerClient",
   "devtools/shared/client/main", true);
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
+const TEMP_ID_URL = "https://developer.mozilla.org/Add-ons" +
+                    "/WebExtensions/WebExtensions_and_the_Add-on_ID";
+
 function filePathForTarget(target) {
   // Only show file system paths, and only for temporarily installed add-ons.
   if (!target.temporarilyInstalled || !target.url || !target.url.startsWith("file://")) {
     return [];
   }
   let path = target.url.slice("file://".length);
   return [
     dom.dt(
@@ -62,16 +65,33 @@ function internalIDForTarget(target) {
           { href: target.manifestURL, target: "_blank", className: "manifest-url" },
           Strings.GetStringFromName("manifestURL"),
         ),
       )
     ),
   ];
 }
 
+function temporaryID(target) {
+  if (!isTemporaryID(target.addonID)) {
+    return [];
+  }
+
+  return [
+    dom.div({ className: "addons-tip" },
+      dom.span({ className: "addons-web-ext-tip" },
+        Strings.GetStringFromName("temporaryID")
+      ),
+      dom.a({ href: TEMP_ID_URL, className: "temporary-id-url", target: "_blank" },
+        Strings.GetStringFromName("temporaryID.learnMore")
+      )
+    )
+  ];
+}
+
 module.exports = createClass({
   displayName: "AddonTarget",
 
   propTypes: {
     client: PropTypes.instanceOf(DebuggerClient).isRequired,
     debugDisabled: PropTypes.bool,
     target: PropTypes.shape({
       addonActor: PropTypes.string.isRequired,
@@ -114,16 +134,17 @@ module.exports = createClass({
       dom.div({ className: "target" },
         dom.img({
           className: "target-icon",
           role: "presentation",
           src: target.icon
         }),
         dom.span({ className: "target-name", title: target.name }, target.name)
       ),
+      ...temporaryID(target),
       dom.dl(
         { className: "addon-target-info" },
         ...filePathForTarget(target),
         ...internalIDForTarget(target),
       ),
       dom.div({className: "addon-target-actions"},
         dom.button({
           className: "debug-button addon-target-button",
--- a/devtools/client/aboutdebugging/modules/addon.js
+++ b/devtools/client/aboutdebugging/modules/addon.js
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 loader.lazyImporter(this, "BrowserToolboxProcess",
   "resource://devtools/client/framework/ToolboxProcess.jsm");
 loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+loader.lazyImporter(this, "AddonManagerPrivate", "resource://gre/modules/AddonManager.jsm");
 
 let toolbox = null;
 
 exports.debugAddon = function (addonID) {
   if (toolbox) {
     toolbox.close();
   }
 
@@ -22,8 +23,12 @@ exports.debugAddon = function (addonID) 
     }
   });
 };
 
 exports.uninstallAddon = async function (addonID) {
   let addon = await AddonManager.getAddonByID(addonID);
   return addon && addon.uninstall();
 };
+
+exports.isTemporaryID = function (addonID) {
+  return AddonManagerPrivate.isTemporaryInstallID(addonID);
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension-noid/manifest.json
@@ -0,0 +1,5 @@
+{
+  "manifest_version": 2,
+  "name": "test-devtools-webextension-noid",
+  "version": "1.0"
+}
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -4,16 +4,17 @@ subsuite = devtools
 support-files =
   head.js
   addons/unpacked/bootstrap.js
   addons/unpacked/install.rdf
   addons/bad/manifest.json
   addons/bug1273184.xpi
   addons/test-devtools-webextension/*
   addons/test-devtools-webextension-nobg/*
+  addons/test-devtools-webextension-noid/*
   service-workers/delay-sw.html
   service-workers/delay-sw.js
   service-workers/empty-sw.html
   service-workers/empty-sw.js
   service-workers/fetch-sw.html
   service-workers/fetch-sw.js
   service-workers/push-sw.html
   service-workers/push-sw.js
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_info.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_info.js
@@ -51,8 +51,33 @@ add_task(function* testWebExtension() {
 
   let manifestURL = container.querySelector(".manifest-url");
   ok(manifestURL.href.startsWith("moz-extension://"), "href for manifestURL exists");
 
   yield uninstallAddon({document, id: addonId, name: addonName});
 
   yield closeAboutDebugging(tab);
 });
+
+add_task(function* testTemporaryWebExtension() {
+  let addonName = "test-devtools-webextension-noid";
+  let { tab, document } = yield openAboutDebugging("addons");
+
+  yield waitForInitialAddonList(document);
+  yield installAddon({
+    document,
+    path: "addons/test-devtools-webextension-noid/manifest.json",
+    name: addonName,
+    isWebExtension: true
+  });
+
+  let addons = document.querySelectorAll("#temporary-extensions .addon-target-container");
+  // Assuming that our temporary add-on is now at the top.
+  let container = addons[addons.length-1];
+  let addonId = container.dataset.addonId;
+
+  let temporaryID = container.querySelector(".temporary-id-url");
+  ok(temporaryID, "Temporary ID message does appear");
+
+  yield uninstallAddon({document, id: addonId, name: addonName});
+
+  yield closeAboutDebugging(tab);
+});
--- a/devtools/client/locales/en-US/aboutdebugging.properties
+++ b/devtools/client/locales/en-US/aboutdebugging.properties
@@ -83,16 +83,25 @@ manifestURL = Manifest URL
 webExtTip = You can use web-ext to load temporary WebExtensions from the command line.
 
 # LOCALIZATION NOTE (webExtTip.learnMore):
 # This string is displayed as a link next to webExtTip and leads the user to the MDN
 # documentation page for web-ext.
 # (https://developer.mozilla.org/Add-ons/WebExtensions/Getting_started_with_web-ext)
 webExtTip.learnMore = Learn more
 
+# LOCALIZATION NOTE (temporaryID):
+# This string is displayed as a message about the add-on having a temporaryID.
+temporaryID = This WebExtension has a temporary ID.
+
+# LOCALIZATION NOTE (temporaryID.learnMore):
+# This string is displayed as a link next to the temporaryID message and leads
+# the user to MDN.
+temporaryID.learnMore = Learn more
+
 # LOCALIZATION NOTE (selectAddonFromFile2):
 # This string is displayed as the title of the file picker that appears when
 # the user clicks the 'Load Temporary Add-on' button
 selectAddonFromFile2 = Select Manifest File or Package (.xpi)
 
 # LOCALIZATION NOTE (reload):
 # This string is displayed as a label of the button that reloads a given addon.
 reload = Reload
--- a/devtools/client/locales/en-US/layout.properties
+++ b/devtools/client/locales/en-US/layout.properties
@@ -37,8 +37,12 @@ layout.noGrids=No grids
 
 # LOCALIZATION NOTE (layout.overlayMultipleGrids): The header for the list of grid
 # container elements that can be highlighted in the CSS Grid pane.
 layout.overlayMultipleGrids=Overlay Multiple Grids
 
 # LOCALIZATION NOTE (layout.overlayGrid): Alternate header for the list of grid container
 # elements if only one item can be selected.
 layout.overlayGrid=Overlay Grid
+
+# LOCALIZATION NOTE (layout.rowColumnPositions): The row and column position of a grid
+# cell shown in the grid cell infobar when hovering over the CSS grid outline.
+layout.rowColumnPositions=Row %S \/ Column %S
--- a/devtools/client/webconsole/test/browser_console_dead_objects.js
+++ b/devtools/client/webconsole/test/browser_console_dead_objects.js
@@ -25,16 +25,18 @@ function test() {
     Services.prefs.clearUserPref("devtools.chrome.enabled");
   });
 
   Task.spawn(runner).then(finishTest);
 
   function* runner() {
     Services.prefs.setBoolPref("devtools.chrome.enabled", true);
     yield loadTab(TEST_URI);
+    let browser = gBrowser.selectedBrowser;
+    let winID = browser.outerWindowID;
 
     info("open the browser console");
 
     hud = yield HUDService.toggleBrowserConsole();
     ok(hud, "browser console opened");
 
     let jsterm = hud.jsterm;
 
@@ -45,16 +47,21 @@ function test() {
                   "Cu.import('resource://gre/modules/Services.jsm');" +
                   "chromeWindow = Services.wm.getMostRecentWindow('" +
                   "navigator:browser');" +
                   "foobarzTezt = chromeWindow.content.document;" +
                   "delete chromeWindow");
 
     gBrowser.removeCurrentTab();
 
+    yield TestUtils.topicObserved("outer-window-destroyed", (subject, data) => {
+      let id = subject.QueryInterface(Components.interfaces.nsISupportsPRUint64).data;
+      return id == winID;
+    });
+
     let msg = yield jsterm.execute("foobarzTezt");
 
     isnot(hud.outputNode.textContent.indexOf("[object DeadObject]"), -1,
           "dead object found");
 
     jsterm.setInputValue("foobarzTezt");
 
     for (let c of ".hello") {
--- a/devtools/server/actors/highlighters/css-grid.js
+++ b/devtools/server/actors/highlighters/css-grid.js
@@ -25,16 +25,20 @@ const {
   apply,
   translate,
   multiply,
   scale,
   getNodeTransformationMatrix,
   getNodeTransformOrigin
 } = require("devtools/shared/layout/dom-matrix-2d");
 const { stringifyGridFragments } = require("devtools/server/actors/utils/css-grid-utils");
+const { LocalizationHelper } = require("devtools/shared/l10n");
+
+const LAYOUT_STRINGS_URI = "devtools/client/locales/layout.properties";
+const LAYOUT_L10N = new LocalizationHelper(LAYOUT_STRINGS_URI);
 
 const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
 
 const DEFAULT_GRID_COLOR = "#4B0082";
 
 const COLUMNS = "cols";
 const ROWS = "rows";
 
@@ -824,17 +828,18 @@ CssGridHighlighter.prototype = extend(Au
   },
 
   _updateGridCellInfobar(rowNumber, columnNumber, x1, x2, y1, y2) {
     let width = x2 - x1;
     let height = y2 - y1;
     let dim = parseFloat(width.toPrecision(6)) +
               " \u00D7 " +
               parseFloat(height.toPrecision(6));
-    let position = `${rowNumber}\/${columnNumber}`;
+    let position = LAYOUT_L10N.getFormatStr("layout.rowColumnPositions",
+                   rowNumber, columnNumber);
 
     this.getElement("cell-infobar-position").setTextContent(position);
     this.getElement("cell-infobar-dimensions").setTextContent(dim);
 
     let container = this.getElement("cell-infobar-container");
     this._moveInfobar(container, x1, x2, y1, y2, {
       position: "top",
       hideIfOffscreen: true
--- a/devtools/server/actors/webextension-parent.js
+++ b/devtools/server/actors/webextension-parent.js
@@ -4,17 +4,16 @@
 
 "use strict";
 
 const {DebuggerServer} = require("devtools/server/main");
 const protocol = require("devtools/shared/protocol");
 const {webExtensionSpec} = require("devtools/shared/specs/webextension-parent");
 
 loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
-loader.lazyImporter(this, "ExtensionManagement", "resource://gre/modules/ExtensionManagement.jsm");
 loader.lazyImporter(this, "ExtensionParent", "resource://gre/modules/ExtensionParent.jsm");
 
 /**
  * Creates the actor that represents the addon in the parent process, which connects
  * itself to a WebExtensionChildActor counterpart which is created in the
  * extension process (or in the main process if the WebExtensions OOP mode is disabled).
  *
  * The WebExtensionParentActor subscribes itself as an AddonListener on the AddonManager
@@ -60,26 +59,27 @@ const WebExtensionParentActor = protocol
 
   reload() {
     return this.addon.reload().then(() => {
       return {};
     });
   },
 
   form() {
+    let policy = ExtensionParent.WebExtensionPolicy.getByID(this.id);
     return {
       actor: this.actorID,
       id: this.id,
       name: this.addon.name,
       url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined,
       iconURL: this.addon.iconURL,
       debuggable: this.addon.isDebuggable,
       temporarilyInstalled: this.addon.temporarilyInstalled,
       isWebExtension: true,
-      manifestURL: ExtensionManagement.getURLForExtension(this.id, "manifest.json"),
+      manifestURL: policy && policy.getURL("manifest.json"),
     };
   },
 
   connect() {
     if (this._childFormPormise) {
       return this._childFormPromise;
     }
 
--- 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/base/nsGkAtomList.h
+++ b/dom/base/nsGkAtomList.h
@@ -409,16 +409,17 @@ GK_ATOM(expr, "expr")
 GK_ATOM(extends, "extends")
 GK_ATOM(extensionElementPrefixes, "extension-element-prefixes")
 GK_ATOM(face, "face")
 GK_ATOM(fallback, "fallback")
 GK_ATOM(_false, "false")
 GK_ATOM(farthest, "farthest")
 GK_ATOM(field, "field")
 GK_ATOM(fieldset, "fieldset")
+GK_ATOM(file, "file")
 GK_ATOM(figcaption, "figcaption")
 GK_ATOM(figure, "figure")
 GK_ATOM(fixed, "fixed")
 GK_ATOM(flags, "flags")
 GK_ATOM(flex, "flex")
 GK_ATOM(flexgroup, "flexgroup")
 GK_ATOM(flip, "flip")
 GK_ATOM(floating, "floating")
@@ -2003,16 +2004,22 @@ GK_ATOM(onuserproximity, "onuserproximit
 GK_ATOM(ondevicelight, "ondevicelight")
 
 // MediaDevices device change event
 GK_ATOM(ondevicechange, "ondevicechange")
 
 // HTML element attributes that only exposed to XBL and chrome content
 GK_ATOM(mozinputrangeignorepreventdefault, "mozinputrangeignorepreventdefault")
 
+// WebExtensions
+GK_ATOM(moz_extension, "moz-extension")
+GK_ATOM(all_urlsPermission, "<all_urls>")
+GK_ATOM(http, "http")
+GK_ATOM(https, "https")
+
 //---------------------------------------------------------------------------
 // Special atoms
 //---------------------------------------------------------------------------
 
 // Node types
 GK_ATOM(cdataTagName, "#cdata-section")
 GK_ATOM(commentTagName, "#comment")
 GK_ATOM(documentNodeName, "#document")
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -567,16 +567,29 @@ DOMInterfaces = {
     'wrapperCache': False,
 },
 
 'LocalMediaStream': {
     'headerFile': 'DOMMediaStream.h',
     'nativeType': 'mozilla::DOMLocalMediaStream'
 },
 
+'MatchGlob': {
+    'nativeType': 'mozilla::extensions::MatchGlob',
+},
+
+'MatchPattern': {
+    'nativeType': 'mozilla::extensions::MatchPattern',
+},
+
+'MatchPatternSet': {
+    'headerFile': 'mozilla/extensions/MatchPattern.h',
+    'nativeType': 'mozilla::extensions::MatchPatternSet',
+},
+
 'MediaKeys' : {
     'implicitJSContext': [ 'createSession']
 },
 
 'MediaStream': {
     'headerFile': 'DOMMediaStream.h',
     'nativeType': 'mozilla::DOMMediaStream'
 },
@@ -1083,16 +1096,24 @@ DOMInterfaces = {
 'VTTRegion': {
   'nativeType': 'mozilla::dom::TextTrackRegion',
 },
 
 'WebAuthentication': {
     'implicitJSContext': 'makeCredential',
 },
 
+'WebExtensionContentScript': {
+    'nativeType': 'mozilla::extensions::WebExtensionContentScript',
+},
+
+'WebExtensionPolicy': {
+    'nativeType': 'mozilla::extensions::WebExtensionPolicy',
+},
+
 'WindowClient': {
     'nativeType': 'mozilla::dom::workers::ServiceWorkerWindowClient',
     'headerFile': 'mozilla/dom/workers/bindings/ServiceWorkerWindowClient.h',
 },
 
 'WebGLActiveInfo': {
     'nativeType': 'mozilla::WebGLActiveInfo',
     'headerFile': 'WebGLActiveInfo.h'
@@ -1684,21 +1705,25 @@ def addExternalIface(iface, nativeType=N
     if not nativeType is None:
         domInterface['nativeType'] = nativeType
     if not headerFile is None:
         domInterface['headerFile'] = headerFile
     domInterface['notflattened'] = notflattened
     DOMInterfaces[iface] = domInterface
 
 addExternalIface('ApplicationCache', nativeType='nsIDOMOfflineResourceList')
+addExternalIface('Cookie', nativeType='nsICookie2',
+                 headerFile='nsICookie2.h', notflattened=True)
 addExternalIface('Counter')
 addExternalIface('RTCDataChannel', nativeType='nsIDOMDataChannel')
 addExternalIface('HitRegionOptions', nativeType='nsISupports')
 addExternalIface('imgINotificationObserver', nativeType='imgINotificationObserver')
 addExternalIface('imgIRequest', nativeType='imgIRequest', notflattened=True)
+addExternalIface('LoadInfo', nativeType='nsILoadInfo',
+                 headerFile='nsILoadInfo.h', notflattened=True)
 addExternalIface('MenuBuilder', nativeType='nsIMenuBuilder', notflattened=True)
 addExternalIface('MozControllers', nativeType='nsIControllers')
 addExternalIface('MozFrameLoader', nativeType='nsIFrameLoader', notflattened=True)
 addExternalIface('MozObserver', nativeType='nsIObserver', notflattened=True)
 addExternalIface('MozRDFCompositeDataSource', nativeType='nsIRDFCompositeDataSource',
                  notflattened=True)
 addExternalIface('MozRDFResource', nativeType='nsIRDFResource', notflattened=True)
 addExternalIface('MozTreeView', nativeType='nsITreeView',
--- 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;
new file mode 100644
--- /dev/null
+++ b/dom/webidl/MatchGlob.webidl
@@ -0,0 +1,24 @@
+/* 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/. */
+
+/**
+ * Represents a simple glob pattern matcher. Any occurrence of "*" in the glob
+ * pattern matches any literal string of characters in the string being
+ * compared. Additionally, if created with `allowQuestion = true`, any
+ * occurrence of "?" in the glob matches any single literal character.
+ */
+[Constructor(DOMString glob, optional boolean allowQuestion = true),
+ ChromeOnly, Exposed=(Window,System)]
+interface MatchGlob {
+  /**
+   * Returns true if the string matches the glob.
+   */
+  boolean matches(DOMString string);
+
+  /**
+   * The glob string this MatchGlob represents.
+   */
+  [Constant]
+  readonly attribute DOMString glob;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/MatchPattern.webidl
@@ -0,0 +1,120 @@
+/* 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/. */
+
+interface Cookie;
+interface URI;
+
+/**
+ * A URL match pattern as used by the WebExtension and Chrome extension APIs.
+ *
+ * A match pattern is a string with one of the following formats:
+ *
+ *  - "<all_urls>"
+ *    The literal string "<all_urls>" matches any URL with a supported
+ *    protocol.
+ *
+ *  - <proto>://<host>/<path>
+ *    A URL pattern with the following placeholders:
+ *
+ *    - <proto>
+ *      The protocol to match, or "*" to match either "http" or "https".
+ *    - <host>
+ *      The hostname to match. May be either a complete, literal hostname to
+ *      match a specific host, the wildcard character "*", to match any host,
+ *      or a subdomain pattern, with "*." followed by a domain name, to match
+ *      that domain name or any subdomain thereof.
+ *    - <path>
+ *      A glob pattern for paths to match. A "*" may appear anywhere within
+ *      the path, and will match any string of characters. If no "*" appears,
+ *      the URL path must exactly match the pattern path.
+ */
+[Constructor(DOMString pattern, optional MatchPatternOptions options),
+ ChromeOnly, Exposed=(Window,System)]
+interface MatchPattern {
+  /**
+   * Returns true if the given URI matches the pattern.
+   *
+   * If explicit is true, only explicit domain matches, without wildcards, are
+   * considered.
+   */
+  boolean matches(URI uri, optional boolean explicit = false);
+
+  /**
+   * Returns true if a URL exists which a) would be able to access the given
+   * cookie, and b) would be matched by this match pattern.
+   */
+  boolean matchesCookie(Cookie cookie);
+
+  /**
+   * Returns true if this pattern will match any host which would be matched
+   * by the given pattern.
+   */
+  boolean subsumes(MatchPattern pattern);
+
+  /**
+   * Returns true if there is any host which would be matched by both this
+   * pattern and the given pattern.
+   */
+  boolean overlaps(MatchPattern pattern);
+
+  /**
+   * The match pattern string represented by this pattern.
+   */
+  [Constant]
+  readonly attribute DOMString pattern;
+};
+
+/**
+ * A set of MatchPattern objects, which implements the MatchPattern API and
+ * matches when any of its sub-patterns matches.
+ */
+[Constructor(sequence<(DOMString or MatchPattern)> patterns, optional MatchPatternOptions options),
+ ChromeOnly, Exposed=(Window,System)]
+interface MatchPatternSet {
+  /**
+   * Returns true if the given URI matches any sub-pattern.
+   *
+   * If explicit is true, only explicit domain matches, without wildcards, are
+   * considered.
+   */
+  boolean matches(URI uri, optional boolean explicit = false);
+
+  /**
+   * Returns true if any sub-pattern matches the given cookie.
+   */
+  boolean matchesCookie(Cookie cookie);
+
+  /**
+   * Returns true if any sub-pattern subsumes the given pattern.
+   */
+  boolean subsumes(MatchPattern pattern);
+
+  /**
+   * Returns true if any sub-pattern overlaps the given pattern.
+   */
+  boolean overlaps(MatchPattern pattern);
+
+  /**
+   * Returns true if any sub-pattern overlaps any sub-pattern the given
+   * pattern set.
+   */
+  boolean overlaps(MatchPatternSet patternSet);
+
+  /**
+   * Returns true if any sub-pattern overlaps *every* sub-pattern in the given
+   * pattern set.
+   */
+  boolean overlapsAll(MatchPatternSet patternSet);
+
+  [Cached, Constant, Frozen]
+  readonly attribute sequence<MatchPattern> patterns;
+};
+
+dictionary MatchPatternOptions {
+  /**
+   * If true, the path portion of the pattern is ignored, and replaced with a
+   * wildcard. The `pattern` property is updated to reflect this.
+   */
+  boolean ignorePath = false;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/WebExtensionContentScript.webidl
@@ -0,0 +1,161 @@
+/* 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/. */
+
+interface LoadInfo;
+interface URI;
+interface WindowProxy;
+
+/**
+ * Describes the earliest point in the load cycle at which a script should
+ * run.
+ */
+enum ContentScriptRunAt {
+  /**
+   * The point in the load cycle just after the document element has been
+   * inserted, before any page scripts have been allowed to run.
+   */
+  "document_start",
+  /**
+   * The point after which the page DOM has fully loaded, but before all page
+   * resources have necessarily been loaded. Corresponds approximately to the
+   * DOMContentLoaded event.
+   */
+  "document_end",
+  /**
+   * The first point after the page and all of its resources has fully loaded
+   * when the event loop is idle, and can run scripts without delaying a paint
+   * event.
+   */
+  "document_idle",
+};
+
+[Constructor(WebExtensionPolicy extension, WebExtensionContentScriptInit options), ChromeOnly, Exposed=System]
+interface WebExtensionContentScript {
+  /**
+   * Returns true if the script's match and exclude patterns match the given
+   * URI, without reference to attributes such as `allFrames`.
+   */
+  boolean matchesURI(URI uri);
+
+  /**
+   * Returns true if the script matches the given URI and LoadInfo objects.
+   * This should be used to determine whether to begin pre-loading a content
+   * script based on network events.
+   */
+  boolean matchesLoadInfo(URI uri, LoadInfo loadInfo);
+
+  /**
+   * Returns true if the script matches the given window. This should be used
+   * to determine whether to run a script in a window at load time.
+   */
+  boolean matchesWindow(WindowProxy window);
+
+  /**
+   * The policy object for the extension that this script belongs to.
+   */
+  [Constant]
+  readonly attribute WebExtensionPolicy extension;
+
+  /**
+   * If true, this script runs in all frames. If false, it only runs in
+   * top-level frames.
+   */
+  [Constant]
+  readonly attribute boolean allFrames;
+
+  /**
+   * If true, this (misleadingly-named, but inherited from Chrome) attribute
+   * causes the script to run in frames with URLs which inherit a principal
+   * that matches one of the match patterns, such as about:blank or
+   * about:srcdoc. If false, the script only runs in frames with an explicit
+   * matching URL.
+   */
+  [Constant]
+  readonly attribute boolean matchAboutBlank;
+
+  /**
+   * The earliest point in the load cycle at which this script should run. For
+   * static content scripts, in extensions which were present at browser
+   * startup, the browser makes every effort to make sure that the script runs
+   * no later than this point in the load cycle. For dynamic content scripts,
+   * and scripts from extensions installed during this session, the scripts
+   * may run at a later point.
+   */
+  [Constant]
+  readonly attribute ContentScriptRunAt runAt;
+
+  /**
+   * The outer window ID of the frame in which to run the script, or 0 if it
+   * should run in the top-level frame. Should only be used for
+   * dynamically-injected scripts.
+   */
+  [Constant]
+  readonly attribute unsigned long long? frameID;
+
+  /**
+   * The set of match patterns for URIs of pages in which this script should
+   * run. This attribute is mandatory, and is a prerequisite for all other
+   * match patterns.
+   */
+  [Constant]
+  readonly attribute MatchPatternSet matches;
+
+  /**
+   * A set of match patterns for URLs in which this script should not run,
+   * even if they match other include patterns or globs.
+   */
+  [Constant]
+  readonly attribute MatchPatternSet? excludeMatches;
+
+  /**
+   * A set of glob matchers for URLs in which this script should run. If this
+   * list is present, the script will only run in URLs which match the
+   * `matches` pattern as well as one of these globs.
+   */
+  [Cached, Constant, Frozen]
+  readonly attribute sequence<MatchGlob>? includeGlobs;
+
+  /**
+   * A set of glob matchers for URLs in which this script should not run, even
+   * if they match other include patterns or globs.
+   */
+  [Cached, Constant, Frozen]
+  readonly attribute sequence<MatchGlob>? excludeGlobs;
+
+  /**
+   * A set of paths, relative to the extension root, of CSS sheets to inject
+   * into matching pages.
+   */
+  [Cached, Constant, Frozen]
+  readonly attribute sequence<DOMString> cssPaths;
+
+  /**
+   * A set of paths, relative to the extension root, of JavaScript scripts to
+   * execute in matching pages.
+   */
+  [Cached, Constant, Frozen]
+  readonly attribute sequence<DOMString> jsPaths;
+};
+
+dictionary WebExtensionContentScriptInit {
+  boolean allFrames = false;
+
+  boolean matchAboutBlank = false;
+
+  ContentScriptRunAt runAt = "document_idle";
+
+  unsigned long long? frameID = null;
+
+  required MatchPatternSet matches;
+
+  MatchPatternSet? excludeMatches = null;
+
+  sequence<MatchGlob>? includeGlobs = null;
+
+  sequence<MatchGlob>? excludeGlobs = null;
+
+  sequence<DOMString> cssPaths = [];
+
+  sequence<DOMString> jsPaths = [];
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/WebExtensionPolicy.webidl
@@ -0,0 +1,158 @@
+/* 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/. */
+
+interface URI;
+interface WindowProxy;
+
+callback WebExtensionLocalizeCallback = DOMString (DOMString unlocalizedText);
+
+/**
+ * Defines the platform-level policies for a WebExtension, including its
+ * permissions and the characteristics of its moz-extension: URLs.
+ */
+[Constructor(WebExtensionInit options), ChromeOnly, Exposed=System]
+interface WebExtensionPolicy {
+  /**
+   * The add-on's internal ID, as specified in its manifest.json file or its
+   * XPI signature.
+   */
+  [Constant, StoreInSlot]
+  readonly attribute DOMString id;
+
+  /**
+   * The hostname part of the extension's moz-extension: URLs. This value is
+   * generated randomly at install time.
+   */
+  [Constant, StoreInSlot]
+  readonly attribute ByteString mozExtensionHostname;
+
+  /**
+   * The file: or jar: URL to use for the base of the extension's
+   * moz-extension: URL root.
+   */
+  [Constant]
+  readonly attribute ByteString baseURL;
+
+  /**
+   * The content security policy string to apply to all pages loaded from the
+   * extension's moz-extension: protocol.
+   */
+  [Constant]
+  readonly attribute DOMString contentSecurityPolicy;
+
+
+  /**
+   * The list of currently-active permissions for the extension, as specified
+   * in its manifest.json file. May be updated to reflect changes in the
+   * extension's optional permissions.
+   */
+  [Cached, Frozen, Pure]
+  attribute sequence<DOMString> permissions;
+
+  /**
+   * Match patterns for the set of web origins to which the extension is
+   * currently allowed access. May be updated to reflect changes in the
+   * extension's optional permissions.
+   */
+  [Pure]
+  attribute MatchPatternSet allowedOrigins;
+
+  /**
+   * The set of content scripts active for this extension.
+   */
+  [Cached, Constant, Frozen]
+  readonly attribute sequence<WebExtensionContentScript> contentScripts;
+
+  /**
+   * True if the extension is currently active, false otherwise. When active,
+   * the extension's moz-extension: protocol will point to the given baseURI,
+   * and the set of policies for this object will be active for its ID.
+   *
+   * Only one extension policy with a given ID or hostname may be active at a
+   * time. Attempting to activate a policy while a conflicting policy is
+   * active will raise an error.
+   */
+  [Affects=Everything, SetterThrows]
+  attribute boolean active;
+
+
+  static readonly attribute boolean isExtensionProcess;
+
+
+  /**
+   * Returns true if the extension has cross-origin access to the given URI.
+   */
+  boolean canAccessURI(URI uri, optional boolean explicit = false);
+
+  /**
+   * Returns true if the extension currently has the given permission.
+   */
+  boolean hasPermission(DOMString permission);
+
+  /**
+   * Returns true if the given path relative to the extension's moz-extension:
+   * URL root may be accessed by web content.
+   */
+  boolean isPathWebAccessible(DOMString pathname);
+
+  /**
+   * Replaces localization placeholders in the given string with localized
+   * text from the extension's currently active locale.
+   */
+  DOMString localize(DOMString unlocalizedText);
+
+  /**
+   * Returns the moz-extension: URL for the given path.
+   */
+  [Throws]
+  DOMString getURL(optional DOMString path = "");
+
+
+  /**
+   * Returns the list of currently active extension policies.
+   */
+  static sequence<WebExtensionPolicy> getActiveExtensions();
+
+  /**
+   * Returns the currently-active policy for the extension with the given ID,
+   * or null if no policy is active for that ID.
+   */
+  static WebExtensionPolicy? getByID(DOMString id);
+
+  /**
+   * Returns the currently-active policy for the extension with the given
+   * moz-extension: hostname, or null if no policy is active for that
+   * hostname.
+   */
+  static WebExtensionPolicy? getByHostname(ByteString hostname);
+
+  /**
+   * Returns the currently-active policy for the extension extension URI, or
+   * null if the URI is not an extension URI, or no policy is currently active
+   * for it.
+   */
+  static WebExtensionPolicy? getByURI(URI uri);
+};
+
+dictionary WebExtensionInit {
+  required DOMString id;
+
+  required ByteString mozExtensionHostname;
+
+  required DOMString baseURL;
+
+  required WebExtensionLocalizeCallback localizeCallback;
+
+  required MatchPatternSet allowedOrigins;
+
+  sequence<DOMString> permissions = [];
+
+  sequence<MatchGlob> webAccessibleResources = [];
+
+  sequence<WebExtensionContentScriptInit> contentScripts = [];
+
+  DOMString? contentSecurityPolicy = null;
+
+  sequence<DOMString>? backgroundScripts = null;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -658,16 +658,18 @@ WEBIDL_FILES = [
     'KeyframeAnimationOptions.webidl',
     'KeyframeEffect.webidl',
     'KeyIdsInitData.webidl',
     'LegacyQueryInterface.webidl',
     'LinkStyle.webidl',
     'ListBoxObject.webidl',
     'LocalMediaStream.webidl',
     'Location.webidl',
+    'MatchGlob.webidl',
+    'MatchPattern.webidl',
     'MediaDeviceInfo.webidl',
     'MediaDevices.webidl',
     'MediaElementAudioSourceNode.webidl',
     'MediaEncryptedEvent.webidl',
     'MediaError.webidl',
     'MediaKeyError.webidl',
     'MediaKeyMessageEvent.webidl',
     'MediaKeys.webidl',
@@ -937,16 +939,18 @@ WEBIDL_FILES = [
     'VRDisplay.webidl',
     'VRDisplayEvent.webidl',
     'VRServiceTest.webidl',
     'VTTCue.webidl',
     'VTTRegion.webidl',
     'WaveShaperNode.webidl',
     'WebAuthentication.webidl',
     'WebComponents.webidl',
+    'WebExtensionContentScript.webidl',
+    'WebExtensionPolicy.webidl',
     'WebGL2RenderingContext.webidl',
     'WebGLRenderingContext.webidl',
     'WebKitCSSMatrix.webidl',
     'WebSocket.webidl',
     'WheelEvent.webidl',
     'WidevineCDMManifest.webidl',
     'WindowOrWorkerGlobalScope.webidl',
     'WindowRoot.webidl',
--- a/js/public/ProfilingStack.h
+++ b/js/public/ProfilingStack.h
@@ -50,124 +50,118 @@ class ProfileEntry
     const char * volatile dynamicString_;
 
     // Stack pointer for non-JS entries, the script pointer otherwise.
     void * volatile spOrScript;
 
     // Line number for non-JS entries, the bytecode offset otherwise.
     int32_t volatile lineOrPcOffset;
 
-    // Flags are in the low bits. The category is in the high bits.
-    uint32_t volatile flagsAndCategory_;
+    // Bits 0..1 hold the Kind. Bits 2..3 are unused. Bits 4..12 hold the
+    // Category.
+    uint32_t volatile kindAndCategory_;
 
     static int32_t pcToOffset(JSScript* aScript, jsbytecode* aPc);
 
   public:
-    // These traits are bit masks. Make sure they're powers of 2.
-    enum Flags : uint32_t {
-        // Indicate whether a profile entry represents a CPP frame. If not set,
-        // a JS frame is assumed by default. You're not allowed to publicly
-        // change the frame type. Instead, initialize the ProfileEntry as either
-        // a JS or CPP frame with `initJsFrame` or `initCppFrame` respectively.
-        IS_CPP_ENTRY = 1u << 0,
+    enum class Kind : uint32_t {
+        // A normal C++ frame.
+        CPP_NORMAL = 0,
+
+        // A special C++ frame indicating the start of a run of JS pseudostack
+        // entries. CPP_MARKER_FOR_JS frames are ignored, except for the sp
+        // field.
+        CPP_MARKER_FOR_JS = 1,
 
-        // This ProfileEntry is a dummy entry indicating the start of a run
-        // of JS pseudostack entries.
-        BEGIN_PSEUDO_JS = 1u << 1,
+        // A normal JS frame.
+        JS_NORMAL = 2,
 
-        // This flag is used to indicate that an interpreter JS entry has OSR-ed
-        // into baseline.
-        OSR = 1u << 2,
+        // An interpreter JS frame that has OSR-ed into baseline. JS_NORMAL
+        // frames can be converted to JS_OSR and back. JS_OSR frames are
+        // ignored.
+        JS_OSR = 3,
 
-        // Union of all flags.
-        ALL = IS_CPP_ENTRY|BEGIN_PSEUDO_JS|OSR,
-
-        // Mask for removing all flags except the category information.
-        CATEGORY_MASK = ~ALL
+        KIND_MASK = 0x3,
     };
 
     // Keep these in sync with devtools/client/performance/modules/categories.js
     enum class Category : uint32_t {
         OTHER    = 1u << 4,
         CSS      = 1u << 5,
         JS       = 1u << 6,
         GC       = 1u << 7,
         CC       = 1u << 8,
         NETWORK  = 1u << 9,
         GRAPHICS = 1u << 10,
         STORAGE  = 1u << 11,
         EVENTS   = 1u << 12,
 
         FIRST    = OTHER,
-        LAST     = EVENTS
+        LAST     = EVENTS,
+
+        CATEGORY_MASK = ~uint32_t(Kind::KIND_MASK),
     };
 
-    static_assert((static_cast<int>(Category::FIRST) & Flags::ALL) == 0,
-                  "The category bitflags should not intersect with the other flags!");
+    static_assert((uint32_t(Category::FIRST) & uint32_t(Kind::KIND_MASK)) == 0,
+                  "Category overlaps with Kind");
 
     // All of these methods are marked with the 'volatile' keyword because the
     // Gecko Profiler's representation of the stack is stored such that all
     // ProfileEntry instances are volatile. These methods would not be
     // available unless they were marked as volatile as well.
 
-    bool isCpp() const volatile { return hasFlag(IS_CPP_ENTRY); }
-    bool isJs() const volatile { return !isCpp(); }
+    bool isCpp() const volatile
+    {
+        Kind k = kind();
+        return k == Kind::CPP_NORMAL || k == Kind::CPP_MARKER_FOR_JS;
+    }
+
+    bool isJs() const volatile
+    {
+        Kind k = kind();
+        return k == Kind::JS_NORMAL || k == Kind::JS_OSR;
+    }
 
     void setLabel(const char* aLabel) volatile { label_ = aLabel; }
     const char* label() const volatile { return label_; }
 
     const char* dynamicString() const volatile { return dynamicString_; }
 
     void initCppFrame(const char* aLabel, const char* aDynamicString, void* sp, uint32_t aLine,
-                      js::ProfileEntry::Flags aFlags, js::ProfileEntry::Category aCategory)
-                      volatile
+                      Kind aKind, Category aCategory) volatile
     {
         label_ = aLabel;
         dynamicString_ = aDynamicString;
         spOrScript = sp;
         lineOrPcOffset = static_cast<int32_t>(aLine);
-        flagsAndCategory_ = aFlags | js::ProfileEntry::IS_CPP_ENTRY | uint32_t(aCategory);
+        kindAndCategory_ = uint32_t(aKind) | uint32_t(aCategory);
+        MOZ_ASSERT(isCpp());
     }
 
     void initJsFrame(const char* aLabel, const char* aDynamicString, JSScript* aScript,
                      jsbytecode* aPc) volatile
     {
         label_ = aLabel;
         dynamicString_ = aDynamicString;
         spOrScript = aScript;
         lineOrPcOffset = pcToOffset(aScript, aPc);
-        flagsAndCategory_ = uint32_t(js::ProfileEntry::Category::JS);  // No flags needed.
-    }
-
-    void setFlag(uint32_t flag) volatile {
-        MOZ_ASSERT(flag != IS_CPP_ENTRY);
-        flagsAndCategory_ |= flag;
-    }
-    void unsetFlag(uint32_t flag) volatile {
-        MOZ_ASSERT(flag != IS_CPP_ENTRY);
-        flagsAndCategory_ &= ~flag;
-    }
-    bool hasFlag(uint32_t flag) const volatile {
-        return bool(flagsAndCategory_ & flag);
+        kindAndCategory_ = uint32_t(Kind::JS_NORMAL) | uint32_t(Category::JS);
+        MOZ_ASSERT(isJs());
     }
 
-    uint32_t category() const volatile {
-        return flagsAndCategory_ & CATEGORY_MASK;
+    void setKind(Kind aKind) volatile {
+        kindAndCategory_ = uint32_t(aKind) | uint32_t(category());
     }
 
-    void setOSR() volatile {
-        MOZ_ASSERT(isJs());
-        setFlag(OSR);
+    Kind kind() const volatile {
+        return Kind(kindAndCategory_ & uint32_t(Kind::KIND_MASK));
     }
-    void unsetOSR() volatile {
-        MOZ_ASSERT(isJs());
-        unsetFlag(OSR);
-    }
-    bool isOSR() const volatile {
-        return hasFlag(OSR);
+
+    Category category() const volatile {
+        return Category(kindAndCategory_ & uint32_t(Category::CATEGORY_MASK));
     }
 
     void* stackAddress() const volatile {
         MOZ_ASSERT(!isJs());
         return spOrScript;
     }
 
     JS_PUBLIC_API(JSScript*) script() const volatile;
@@ -219,20 +213,19 @@ class PseudoStack
     ~PseudoStack() {
         // The label macros keep a reference to the PseudoStack to avoid a TLS
         // access. If these are somehow not all cleared we will get a
         // use-after-free so better to crash now.
         MOZ_RELEASE_ASSERT(stackPointer == 0);
     }
 
     void pushCppFrame(const char* label, const char* dynamicString, void* sp, uint32_t line,
-                      js::ProfileEntry::Category category,
-                      js::ProfileEntry::Flags flags = js::ProfileEntry::Flags(0)) {
+                      js::ProfileEntry::Kind kind, js::ProfileEntry::Category category) {
         if (stackPointer < MaxEntries) {
-            entries[stackPointer].initCppFrame(label, dynamicString, sp, line, flags, category);
+            entries[stackPointer].initCppFrame(label, dynamicString, sp, line, kind, category);
         }
 
         // This must happen at the end! The compiler will not reorder this
         // update because stackPointer is Atomic.
         stackPointer++;
     }
 
     void pushJsFrame(const char* label, const char* dynamicString, JSScript* script,
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/auto-regress/bug1357330.js
@@ -0,0 +1,17 @@
+function byteValue(value) {
+  var isLittleEndian = new Uint8Array(new Uint16Array([1]).buffer)[0] !== 0;
+  var ui8 = new Uint8Array(new Float64Array([value]).buffer);
+
+  var hex = "0123456789ABCDEF";
+  var s = "";
+  for (var i = 0; i < 8; ++i) {
+    var v = ui8[isLittleEndian ? 7 - i : i];
+    s += hex[(v >> 4) & 0xf] + hex[v & 0xf];
+  }
+  return s;
+}
+
+var obj = {};
+Object.defineProperty(obj, "prop", {value: NaN});
+Object.defineProperty(obj, "prop", {value: -NaN});
+assertEq(byteValue(obj.prop), byteValue(NaN));
--- a/js/src/vm/GeckoProfiler.cpp
+++ b/js/src/vm/GeckoProfiler.cpp
@@ -370,19 +370,20 @@ GeckoProfilerEntryMarker::GeckoProfilerE
     MOZ_GUARD_OBJECT_NOTIFIER_INIT;
     if (!profiler->installed()) {
         profiler = nullptr;
         return;
     }
     spBefore_ = profiler->stackPointer();
 
     // We want to push a CPP frame so the profiler can correctly order JS and native stacks.
+    // Only the sp value is important.
     profiler->pseudoStack_->pushCppFrame(
-        "js::RunScript", /* dynamicString = */ nullptr, /* sp = */ this, /* line = */ 0,
-        js::ProfileEntry::Category::OTHER, js::ProfileEntry::BEGIN_PSEUDO_JS);
+        /* label = */ "", /* dynamicString = */ nullptr, /* sp = */ this, /* line = */ 0,
+        ProfileEntry::Kind::CPP_MARKER_FOR_JS, ProfileEntry::Category::OTHER);
 
     profiler->pseudoStack_->pushJsFrame(
         "js::RunScript", /* dynamicString = */ nullptr, script, script->code());
 }
 
 GeckoProfilerEntryMarker::~GeckoProfilerEntryMarker()
 {
     if (profiler == nullptr)
@@ -401,17 +402,18 @@ AutoGeckoProfilerEntry::AutoGeckoProfile
     MOZ_GUARD_OBJECT_NOTIFIER_INIT;
     if (!profiler_->installed()) {
         profiler_ = nullptr;
         return;
     }
     spBefore_ = profiler_->stackPointer();
 
     profiler_->pseudoStack_->pushCppFrame(
-        label, /* dynamicString = */ nullptr, /* sp = */ this, /* line = */ 0, category);
+        label, /* dynamicString = */ nullptr, /* sp = */ this, /* line = */ 0,
+        ProfileEntry::Kind::CPP_NORMAL, category);
 }
 
 AutoGeckoProfilerEntry::~AutoGeckoProfilerEntry()
 {
     if (!profiler_)
         return;
 
     profiler_->pseudoStack_->pop();
@@ -434,33 +436,33 @@ GeckoProfilerBaselineOSRMarker::GeckoPro
         return;
     }
 
     spBefore_ = sp;
     if (sp == 0)
         return;
 
     volatile ProfileEntry& entry = profiler->pseudoStack_->entries[sp - 1];
-    MOZ_ASSERT(entry.isJs());
-    entry.setOSR();
+    MOZ_ASSERT(entry.kind() == ProfileEntry::Kind::JS_NORMAL);
+    entry.setKind(ProfileEntry::Kind::JS_OSR);
 }
 
 GeckoProfilerBaselineOSRMarker::~GeckoProfilerBaselineOSRMarker()
 {
     if (profiler == nullptr)
         return;
 
     uint32_t sp = profiler->stackPointer();
     MOZ_ASSERT(spBefore_ == sp);
     if (sp == 0)
         return;
 
     volatile ProfileEntry& entry = profiler->stack()[sp - 1];
-    MOZ_ASSERT(entry.isJs());
-    entry.unsetOSR();
+    MOZ_ASSERT(entry.kind() == ProfileEntry::Kind::JS_OSR);
+    entry.setKind(ProfileEntry::Kind::JS_NORMAL);
 }
 
 JS_PUBLIC_API(JSScript*)
 ProfileEntry::script() const volatile
 {
     MOZ_ASSERT(isJs());
     auto script = reinterpret_cast<JSScript*>(spOrScript);
     if (!script)
--- a/js/src/vm/NativeObject.cpp
+++ b/js/src/vm/NativeObject.cpp
@@ -1403,35 +1403,34 @@ GetExistingPropertyValue(JSContext* cx, 
     MOZ_ASSERT(obj->contains(cx, prop.shape()));
 
     RootedValue receiver(cx, ObjectValue(*obj));
     RootedShape shape(cx, prop.shape());
     return GetExistingProperty<CanGC>(cx, receiver, obj, shape, vp);
 }
 
 /*
- * If ES6 draft rev 37 9.1.6.3 ValidateAndApplyPropertyDescriptor step 4 would
- * return early, because desc is redundant with an existing own property obj[id],
- * then set *redundant = true and return true.
+ * If desc is redundant with an existing own property obj[id], then set
+ * |*redundant = true| and return true.
  */
 static bool
 DefinePropertyIsRedundant(JSContext* cx, HandleNativeObject obj, HandleId id,
                           Handle<PropertyResult> prop, unsigned shapeAttrs,
                           Handle<PropertyDescriptor> desc, bool *redundant)
 {
     *redundant = false;
 
-    if (desc.hasConfigurable() && desc.configurable() != ((shapeAttrs & JSPROP_PERMANENT) == 0))
+    if (desc.hasConfigurable() && desc.configurable() != IsConfigurable(shapeAttrs))
         return true;
-    if (desc.hasEnumerable() && desc.enumerable() != ((shapeAttrs & JSPROP_ENUMERATE) != 0))
+    if (desc.hasEnumerable() && desc.enumerable() != IsEnumerable(shapeAttrs))
         return true;
     if (desc.isDataDescriptor()) {
-        if ((shapeAttrs & (JSPROP_GETTER | JSPROP_SETTER)) != 0)
+        if (IsAccessorDescriptor(shapeAttrs))
             return true;
-        if (desc.hasWritable() && desc.writable() != ((shapeAttrs & JSPROP_READONLY) == 0))
+        if (desc.hasWritable() && desc.writable() != IsWritable(shapeAttrs))
             return true;
         if (desc.hasValue()) {
             // Get the current value of the existing property.
             RootedValue currentValue(cx);
             if (!prop.isDenseOrTypedArrayElement() &&
                 prop.shape()->hasSlot() &&
                 prop.shape()->hasDefaultGetter())
             {
@@ -1439,18 +1438,18 @@ DefinePropertyIsRedundant(JSContext* cx,
                 // correctness assertion that's too strict for this particular
                 // call site. For details, see bug 1125624 comments 13-16.
                 currentValue.set(obj->getSlot(prop.shape()->slot()));
             } else {
                 if (!GetExistingPropertyValue(cx, obj, id, prop, &currentValue))
                     return false;
             }
 
-            // The specification calls for SameValue here, but it seems to be a
-            // bug. See <https://bugs.ecmascript.org/show_bug.cgi?id=3508>.
+            // Don't call SameValue here to ensure we properly update distinct
+            // NaN values.
             if (desc.value() != currentValue)
                 return true;
         }
 
         GetterOp existingGetterOp =
             prop.isDenseOrTypedArrayElement() ? nullptr : prop.shape()->getter();
         if (desc.getter() != existingGetterOp)
             return true;
@@ -1478,17 +1477,18 @@ DefinePropertyIsRedundant(JSContext* cx,
 
 bool
 js::NativeDefineProperty(JSContext* cx, HandleNativeObject obj, HandleId id,
                          Handle<PropertyDescriptor> desc_,
                          ObjectOpResult& result)
 {
     desc_.assertValid();
 
-    // Section numbers and step numbers below refer to ES2016.
+    // Section numbers and step numbers below refer to ES2018, draft rev
+    // 540b827fccf6122a984be99ab9af7be20e3b5562.
     //
     // This function aims to implement 9.1.6 [[DefineOwnProperty]] as well as
     // the [[DefineOwnProperty]] methods described in 9.4.2.1 (arrays), 9.4.4.2
     // (arguments), and 9.4.5.3 (typed array views).
 
     // Dispense with custom behavior of exotic native objects first.
     if (obj->is<ArrayObject>()) {
         // 9.4.2.1 step 2. Redefining an array's length is very special.
@@ -1515,18 +1515,18 @@ js::NativeDefineProperty(JSContext* cx, 
             MOZ_ASSERT(!cx->helperThread());
             return DefineTypedArrayElement(cx, obj, index, desc_, result);
         }
     } else if (obj->is<ArgumentsObject>()) {
         if (id == NameToId(cx->names().length)) {
             // Either we are resolving the .length property on this object, or
             // redefining it. In the latter case only, we must set a bit. To
             // distinguish the two cases, we note that when resolving, the
-            // property won't already exist; whereas the first time it is
-            // redefined, it will.
+            // JSPROP_RESOLVING mask is set; whereas the first time it is
+            // redefined, it isn't set.
             if ((desc_.attributes() & JSPROP_RESOLVING) == 0)
                 obj->as<ArgumentsObject>().markLengthOverridden();
         } else if (JSID_IS_SYMBOL(id) && JSID_TO_SYMBOL(id) == cx->wellKnownSymbols().iterator) {
             // Do same thing as .length for [@@iterator].
             if ((desc_.attributes() & JSPROP_RESOLVING) == 0)
                 obj->as<ArgumentsObject>().markIteratorOverridden();
         } else if (JSID_IS_INT(id)) {
             if ((desc_.attributes() & JSPROP_RESOLVING) == 0)
@@ -1564,17 +1564,17 @@ js::NativeDefineProperty(JSContext* cx, 
         // Fill in missing desc fields with defaults.
         CompletePropertyDescriptor(&desc);
 
         if (!AddOrChangeProperty<IsAddOrChange::Add>(cx, obj, id, desc))
             return false;
         return result.succeed();
     }
 
-    // Steps 3-4. (Step 3 is a special case of step 4.) We use shapeAttrs as a
+    // Step 3 and 7.a.i.3, 8.a.iii, 10 (partially). We use shapeAttrs as a
     // stand-in for shape in many places below, since shape might not be a
     // pointer to a real Shape (see IsImplicitDenseOrTypedArrayElement).
     unsigned shapeAttrs = GetPropertyAttributes(obj, prop);
     bool redundant;
     if (!DefinePropertyIsRedundant(cx, obj, id, prop, shapeAttrs, desc, &redundant))
         return false;
     if (redundant) {
         // In cases involving JSOP_NEWOBJECT and JSOP_INITPROP, obj can have a
@@ -1591,33 +1591,33 @@ js::NativeDefineProperty(JSContext* cx, 
     // global. The idea is that a DOM object can never have such a thing on
     // its proto chain directly on the web, so we should be OK optimizing
     // access to accessors found on such an object. Bug 1105518 contemplates
     // removing this hack.
     bool skipRedefineChecks = (desc.attributes() & JSPROP_REDEFINE_NONCONFIGURABLE) &&
                               obj->is<GlobalObject>() &&
                               !obj->getClass()->isDOMClass();
 
-    // Step 5.
+    // Step 4.
     if (!IsConfigurable(shapeAttrs) && !skipRedefineChecks) {
         if (desc.hasConfigurable() && desc.configurable())
             return result.fail(JSMSG_CANT_REDEFINE_PROP);
         if (desc.hasEnumerable() && desc.enumerable() != IsEnumerable(shapeAttrs))
             return result.fail(JSMSG_CANT_REDEFINE_PROP);
     }
 
     // Fill in desc.[[Configurable]] and desc.[[Enumerable]] if missing.
     if (!desc.hasConfigurable())
         desc.setConfigurable(IsConfigurable(shapeAttrs));
     if (!desc.hasEnumerable())
         desc.setEnumerable(IsEnumerable(shapeAttrs));
 
-    // Steps 6-9.
+    // Steps 5-8.
     if (desc.isGenericDescriptor()) {
-        // Step 6. No further validation is required.
+        // Step 5. No further validation is required.
 
         // Fill in desc. A generic descriptor has none of these fields, so copy
         // everything from shape.
         MOZ_ASSERT(!desc.hasValue());
         MOZ_ASSERT(!desc.hasWritable());
         MOZ_ASSERT(!desc.hasGetterObject());
         MOZ_ASSERT(!desc.hasSetterObject());
         if (IsDataDescriptor(shapeAttrs)) {
@@ -1626,32 +1626,34 @@ js::NativeDefineProperty(JSContext* cx, 
                 return false;
             desc.setValue(currentValue);
             desc.setWritable(IsWritable(shapeAttrs));
         } else {
             desc.setGetterObject(prop.shape()->getterObject());
             desc.setSetterObject(prop.shape()->setterObject());
         }
     } else if (desc.isDataDescriptor() != IsDataDescriptor(shapeAttrs)) {
-        // Step 7.
+        // Step 6.
         if (!IsConfigurable(shapeAttrs) && !skipRedefineChecks)
             return result.fail(JSMSG_CANT_REDEFINE_PROP);
 
         if (prop.isDenseOrTypedArrayElement()) {
             MOZ_ASSERT(!obj->is<TypedArrayObject>());
             if (!NativeObject::sparsifyDenseElement(cx, obj, JSID_TO_INT(id)))
                 return false;
             prop.setNativeProperty(obj->lookup(cx, id));
         }
 
-        // Fill in desc fields with default values (steps 7.b.i and 7.c.i).
+        // Fill in desc fields with default values (steps 6.b.i and 6.c.i).
         CompletePropertyDescriptor(&desc);
     } else if (desc.isDataDescriptor()) {
-        // Step 8.
+        // Step 7.
         bool frozen = !IsConfigurable(shapeAttrs) && !IsWritable(shapeAttrs);
+
+        // Step 7.a.i.1.
         if (frozen && desc.hasWritable() && desc.writable() && !skipRedefineChecks)
             return result.fail(JSMSG_CANT_REDEFINE_PROP);
 
         if (frozen || !desc.hasValue()) {
             if (prop.isDenseOrTypedArrayElement()) {
                 MOZ_ASSERT(!obj->is<TypedArrayObject>());
                 if (!NativeObject::sparsifyDenseElement(cx, obj, JSID_TO_INT(id)))
                     return false;
@@ -1661,62 +1663,72 @@ js::NativeDefineProperty(JSContext* cx, 
             RootedValue currentValue(cx);
             if (!GetExistingPropertyValue(cx, obj, id, prop, &currentValue))
                 return false;
 
             if (!desc.hasValue()) {
                 // Fill in desc.[[Value]].
                 desc.setValue(currentValue);
             } else {
-                // Step 8.a.ii.1.
+                // Step 7.a.i.2.
                 bool same;
                 MOZ_ASSERT(!cx->helperThread());
                 if (!SameValue(cx, desc.value(), currentValue, &same))
                     return false;
                 if (!same && !skipRedefineChecks)
                     return result.fail(JSMSG_CANT_REDEFINE_PROP);
             }
         }
 
+        // Step 7.a.i.3.
+        if (frozen && !skipRedefineChecks)
+            return result.succeed();
+
         if (!desc.hasWritable())
             desc.setWritable(IsWritable(shapeAttrs));
     } else {
-        // Step 9.
+        // Step 8.
         MOZ_ASSERT(prop.shape()->isAccessorDescriptor());
         MOZ_ASSERT(desc.isAccessorDescriptor());
 
         // The spec says to use SameValue, but since the values in
         // question are objects, we can just compare pointers.
         if (desc.hasSetterObject()) {
+            // Step 8.a.i.
             if (!IsConfigurable(shapeAttrs) &&
                 desc.setterObject() != prop.shape()->setterObject() &&
                 !skipRedefineChecks)
             {
                 return result.fail(JSMSG_CANT_REDEFINE_PROP);
             }
         } else {
             // Fill in desc.[[Set]] from shape.
             desc.setSetterObject(prop.shape()->setterObject());
         }
         if (desc.hasGetterObject()) {
+            // Step 8.a.ii.
             if (!IsConfigurable(shapeAttrs) &&
                 desc.getterObject() != prop.shape()->getterObject() &&
                 !skipRedefineChecks)
             {
                 return result.fail(JSMSG_CANT_REDEFINE_PROP);
             }
         } else {
             // Fill in desc.[[Get]] from shape.
             desc.setGetterObject(prop.shape()->getterObject());
         }
+
+        // Step 8.a.iii (Omitted).
     }
 
+    // Step 9.
+    if (!AddOrChangeProperty<IsAddOrChange::AddOrChange>(cx, obj, id, desc))
+        return false;
+
     // Step 10.
-    if (!AddOrChangeProperty<IsAddOrChange::AddOrChange>(cx, obj, id, desc))
-        return false;
     return result.succeed();
 }
 
 bool
 js::NativeDefineProperty(JSContext* cx, HandleNativeObject obj, HandleId id,
                          HandleValue value, GetterOp getter, SetterOp setter, unsigned attrs,
                          ObjectOpResult& result)
 {
--- a/js/xpconnect/tests/chrome/test_windowProxyDeadWrapper.html
+++ b/js/xpconnect/tests/chrome/test_windowProxyDeadWrapper.html
@@ -7,16 +7,17 @@ https://bugzilla.mozilla.org/show_bug.cg
   <meta charset="utf-8">
   <title>Test for Bug 1223372</title>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <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 1223372 **/
+Components.utils.import("resource://testing-common/TestUtils.jsm");
 
 function go() {
     SimpleTest.waitForExplicitFinish();
 
     var frame = $('subframe');
     frame.onload = null;
 
     var w = frame.contentWindow;
@@ -35,21 +36,28 @@ function go() {
 
     function tryWindow() {
         if (w.document.title != 'empty test page') {
             info("Document not loaded yet - retrying");
             SimpleTest.executeSoon(tryWindow);
             return;
         }
 
+        let winID = w.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+                     .getInterface(Components.interfaces.nsIDOMWindowUtils)
+                     .outerWindowID;
         // Remove the frame. This will nuke the WindowProxy wrapper from our chrome
-        // document's global, so evaluating 'this' in it will return a dead wrapper.
+        // document's global, so evaluating 'this' in it will return a dead wrapper
+        // once the window is destroyed.
         frame.remove();
 
-        SimpleTest.executeSoon(function() {
+        TestUtils.topicObserved("outer-window-destroyed", (subject, data) => {
+            let id = subject.QueryInterface(SpecialPowers.Ci.nsISupportsPRUint64).data;
+            return id == winID;
+        }).then(() => {
             ok(checkDead(), "Expected a dead object wrapper");
 
             // Wrapping the Window should throw now.
             var exc;
             try {
                 Components.utils.getGlobalForObject(getObject());
             } catch(e) {
                 exc = e;
--- 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/layout/base/FrameProperties.h
+++ b/layout/base/FrameProperties.h
@@ -301,18 +301,16 @@ public:
     // As for the PropertyValue entries: we don't need to measure the mProperty
     // field of because it always points to static memory, and we can't measure
     // mValue because the type is opaque.
     // XXX Can we do better, e.g. with a method on the descriptor?
     return mProperties.ShallowSizeOfExcludingThis(aMallocSizeOf);
   }
 
 private:
-  friend class ::nsIFrame;
-
   // Prevent copying of FrameProperties; we should always return/pass around
   // references to it, not copies!
   FrameProperties(const FrameProperties&) = delete;
   FrameProperties& operator=(const FrameProperties&) = delete;
 
   inline void
   SetInternal(UntypedDescriptor aProperty, void* aValue,
               const nsIFrame* aFrame);
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -1648,17 +1648,18 @@ nsCSSFrameConstructor::NotifyDestroyingF
   NS_PRECONDITION(mUpdateCount != 0,
                   "Should be in an update while destroying frames");
 
   if (aFrame->GetStateBits() & NS_FRAME_GENERATED_CONTENT) {
     if (mQuoteList.DestroyNodesFor(aFrame))
       QuotesDirty();
   }
 
-  if (mCounterManager.DestroyNodesFor(aFrame)) {
+  if (aFrame->HasAnyStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE) &&
+      mCounterManager.DestroyNodesFor(aFrame)) {
     // Technically we don't need to update anything if we destroyed only
     // USE nodes.  However, this is unlikely to happen in the real world
     // since USE nodes generally go along with INCREMENT nodes.
     CountersDirty();
   }
 
   RestyleManager()->NotifyDestroyingFrame(aFrame);
 
--- a/layout/base/nsCounterManager.cpp
+++ b/layout/base/nsCounterManager.cpp
@@ -13,22 +13,24 @@
 #include "nsContentUtils.h"
 #include "nsIContent.h"
 #include "nsTArray.h"
 
 using namespace mozilla;
 
 bool
 nsCounterUseNode::InitTextFrame(nsGenConList* aList,
-        nsIFrame* aPseudoFrame, nsIFrame* aTextFrame)
+                                nsIFrame* aPseudoFrame,
+                                nsIFrame* aTextFrame)
 {
   nsCounterNode::InitTextFrame(aList, aPseudoFrame, aTextFrame);
 
-  nsCounterList *counterList = static_cast<nsCounterList*>(aList);
+  nsCounterList* counterList = static_cast<nsCounterList*>(aList);
   counterList->Insert(this);
+  aPseudoFrame->AddStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE);
   bool dirty = counterList->IsDirty();
   if (!dirty) {
     if (counterList->IsLast(this)) {
       Calc(counterList);
       nsAutoString contentString;
       GetText(contentString);
       aTextFrame->GetContent()->SetText(contentString, false);
     } else {
@@ -40,185 +42,188 @@ nsCounterUseNode::InitTextFrame(nsGenCon
     }
   }
 
   return false;
 }
 
 // assign the correct |mValueAfter| value to a node that has been inserted
 // Should be called immediately after calling |Insert|.
-void nsCounterUseNode::Calc(nsCounterList *aList)
+void
+nsCounterUseNode::Calc(nsCounterList* aList)
 {
-    NS_ASSERTION(!aList->IsDirty(),
-                 "Why are we calculating with a dirty list?");
-    mValueAfter = aList->ValueBefore(this);
+  NS_ASSERTION(!aList->IsDirty(),
+               "Why are we calculating with a dirty list?");
+  mValueAfter = aList->ValueBefore(this);
 }
 
 // assign the correct |mValueAfter| value to a node that has been inserted
 // Should be called immediately after calling |Insert|.
-void nsCounterChangeNode::Calc(nsCounterList *aList)
+void
+nsCounterChangeNode::Calc(nsCounterList* aList)
 {
-    NS_ASSERTION(!aList->IsDirty(),
-                 "Why are we calculating with a dirty list?");
-    if (mType == RESET) {
-        mValueAfter = mChangeValue;
-    } else {
-        NS_ASSERTION(mType == INCREMENT, "invalid type");
-        mValueAfter = nsCounterManager::IncrementCounter(aList->ValueBefore(this),
-                                                         mChangeValue);
-    }
+  NS_ASSERTION(!aList->IsDirty(), "Why are we calculating with a dirty list?");
+  if (mType == RESET) {
+    mValueAfter = mChangeValue;
+  } else {
+    NS_ASSERTION(mType == INCREMENT, "invalid type");
+    mValueAfter = nsCounterManager::IncrementCounter(aList->ValueBefore(this),
+                                                     mChangeValue);
+  }
 }
 
 // The text that should be displayed for this counter.
 void
 nsCounterUseNode::GetText(nsString& aResult)
 {
-    aResult.Truncate();
+  aResult.Truncate();
+
+  AutoTArray<nsCounterNode*, 8> stack;
+  stack.AppendElement(static_cast<nsCounterNode*>(this));
 
-    AutoTArray<nsCounterNode*, 8> stack;
-    stack.AppendElement(static_cast<nsCounterNode*>(this));
-
-    if (mAllCounters && mScopeStart)
-        for (nsCounterNode *n = mScopeStart; n->mScopePrev; n = n->mScopeStart)
-            stack.AppendElement(n->mScopePrev);
+  if (mAllCounters && mScopeStart) {
+    for (nsCounterNode* n = mScopeStart; n->mScopePrev; n = n->mScopeStart) {
+      stack.AppendElement(n->mScopePrev);
+    }
+  }
 
-    WritingMode wm = mPseudoFrame ?
-        mPseudoFrame->GetWritingMode() : WritingMode();
-    for (uint32_t i = stack.Length() - 1;; --i) {
-        nsCounterNode *n = stack[i];
-        nsAutoString text;
-        bool isTextRTL;
-        mCounterStyle->GetCounterText(n->mValueAfter, wm, text, isTextRTL);
-        aResult.Append(text);
-        if (i == 0)
-            break;
-        aResult.Append(mSeparator);
+  WritingMode wm = mPseudoFrame ?
+    mPseudoFrame->GetWritingMode() : WritingMode();
+  for (uint32_t i = stack.Length() - 1;; --i) {
+    nsCounterNode* n = stack[i];
+    nsAutoString text;
+    bool isTextRTL;
+    mCounterStyle->GetCounterText(n->mValueAfter, wm, text, isTextRTL);
+    aResult.Append(text);
+    if (i == 0) {
+      break;
     }
+    aResult.Append(mSeparator);
+  }
 }
 
 void
-nsCounterList::SetScope(nsCounterNode *aNode)
+nsCounterList::SetScope(nsCounterNode* aNode)
 {
-    // This function is responsible for setting |mScopeStart| and
-    // |mScopePrev| (whose purpose is described in nsCounterManager.h).
-    // We do this by starting from the node immediately preceding
-    // |aNode| in content tree order, which is reasonably likely to be
-    // the previous element in our scope (or, for a reset, the previous
-    // element in the containing scope, which is what we want).  If
-    // we're not in the same scope that it is, then it's too deep in the
-    // frame tree, so we walk up parent scopes until we find something
-    // appropriate.
+  // This function is responsible for setting |mScopeStart| and
+  // |mScopePrev| (whose purpose is described in nsCounterManager.h).
+  // We do this by starting from the node immediately preceding
+  // |aNode| in content tree order, which is reasonably likely to be
+  // the previous element in our scope (or, for a reset, the previous
+  // element in the containing scope, which is what we want).  If
+  // we're not in the same scope that it is, then it's too deep in the
+  // frame tree, so we walk up parent scopes until we find something
+  // appropriate.
 
-    if (aNode == First()) {
-        aNode->mScopeStart = nullptr;
-        aNode->mScopePrev = nullptr;
-        return;
-    }
+  if (aNode == First()) {
+    aNode->mScopeStart = nullptr;
+    aNode->mScopePrev = nullptr;
+    return;
+  }
 
-    // Get the content node for aNode's rendering object's *parent*,
-    // since scope includes siblings, so we want a descendant check on
-    // parents.
-    nsIContent *nodeContent = aNode->mPseudoFrame->GetContent()->GetParent();
+  // Get the content node for aNode's rendering object's *parent*,
+  // since scope includes siblings, so we want a descendant check on
+  // parents.
+  nsIContent* nodeContent = aNode->mPseudoFrame->GetContent()->GetParent();
 
-    for (nsCounterNode *prev = Prev(aNode), *start;
-         prev; prev = start->mScopePrev) {
-        // If |prev| starts a scope (because it's a real or implied
-        // reset), we want it as the scope start rather than the start
-        // of its enclosing scope.  Otherwise, there's no enclosing
-        // scope, so the next thing in prev's scope shares its scope
-        // start.
-        start = (prev->mType == nsCounterNode::RESET || !prev->mScopeStart)
-                  ? prev : prev->mScopeStart;
+  for (nsCounterNode* prev = Prev(aNode), *start;
+       prev; prev = start->mScopePrev) {
+    // If |prev| starts a scope (because it's a real or implied
+    // reset), we want it as the scope start rather than the start
+    // of its enclosing scope.  Otherwise, there's no enclosing
+    // scope, so the next thing in prev's scope shares its scope
+    // start.
+    start = (prev->mType == nsCounterNode::RESET || !prev->mScopeStart)
+      ? prev : prev->mScopeStart;
 
-        // |startContent| is analogous to |nodeContent| (see above).
-        nsIContent *startContent = start->mPseudoFrame->GetContent()->GetParent();
-        NS_ASSERTION(nodeContent || !startContent,
-                     "null check on startContent should be sufficient to "
-                     "null check nodeContent as well, since if nodeContent "
-                     "is for the root, startContent (which is before it) "
-                     "must be too");
+    // |startContent| is analogous to |nodeContent| (see above).
+    nsIContent* startContent = start->mPseudoFrame->GetContent()->GetParent();
+    NS_ASSERTION(nodeContent || !startContent,
+                 "null check on startContent should be sufficient to "
+                 "null check nodeContent as well, since if nodeContent "
+                 "is for the root, startContent (which is before it) "
+                 "must be too");
 
-             // A reset's outer scope can't be a scope created by a sibling.
-        if (!(aNode->mType == nsCounterNode::RESET &&
-              nodeContent == startContent) &&
-              // everything is inside the root (except the case above,
-              // a second reset on the root)
-            (!startContent ||
-             nsContentUtils::ContentIsDescendantOf(nodeContent,
-                                                   startContent))) {
-            aNode->mScopeStart = start;
-            aNode->mScopePrev  = prev;
-            return;
-        }
+    // A reset's outer scope can't be a scope created by a sibling.
+    if (!(aNode->mType == nsCounterNode::RESET &&
+          nodeContent == startContent) &&
+        // everything is inside the root (except the case above,
+        // a second reset on the root)
+        (!startContent ||
+         nsContentUtils::ContentIsDescendantOf(nodeContent,
+                                               startContent))) {
+      aNode->mScopeStart = start;
+      aNode->mScopePrev  = prev;
+      return;
     }
+  }
 
-    aNode->mScopeStart = nullptr;
-    aNode->mScopePrev  = nullptr;
+  aNode->mScopeStart = nullptr;
+  aNode->mScopePrev  = nullptr;
 }
 
 void
 nsCounterList::RecalcAll()
 {
   mDirty = false;
 
   for (nsCounterNode* node = First(); node; node = Next(node)) {
     SetScope(node);
     node->Calc(this);
 
     if (node->mType == nsCounterNode::USE) {
-      nsCounterUseNode *useNode = node->UseNode();
+      nsCounterUseNode* useNode = node->UseNode();
       // Null-check mText, since if the frame constructor isn't
       // batching, we could end up here while the node is being
       // constructed.
       if (useNode->mText) {
         nsAutoString text;
         useNode->GetText(text);
         useNode->mText->SetData(text);
       }
     }
   }
 }
 
-nsCounterManager::nsCounterManager()
-    : mNames()
+bool
+nsCounterManager::AddCounterResetsAndIncrements(nsIFrame* aFrame)
 {
-}
+  const nsStyleContent* styleContent = aFrame->StyleContent();
+  if (!styleContent->CounterIncrementCount() &&
+      !styleContent->CounterResetCount()) {
+    MOZ_ASSERT(!aFrame->HasAnyStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE));
+    return false;
+  }
 
-bool
-nsCounterManager::AddCounterResetsAndIncrements(nsIFrame *aFrame)
-{
-    const nsStyleContent *styleContent = aFrame->StyleContent();
-    if (!styleContent->CounterIncrementCount() &&
-        !styleContent->CounterResetCount())
-        return false;
+  aFrame->AddStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE);
 
-    // Add in order, resets first, so all the comparisons will be optimized
-    // for addition at the end of the list.
-    int32_t i, i_end;
-    bool dirty = false;
-    for (i = 0, i_end = styleContent->CounterResetCount(); i != i_end; ++i)
-      dirty |= AddResetOrIncrement(aFrame, i, styleContent->CounterResetAt(i),
-                                   nsCounterChangeNode::RESET);
-    for (i = 0, i_end = styleContent->CounterIncrementCount(); i != i_end; ++i)
-      dirty |=
-        AddResetOrIncrement(aFrame, i, styleContent->CounterIncrementAt(i),
-                            nsCounterChangeNode::INCREMENT);
-    return dirty;
+  // Add in order, resets first, so all the comparisons will be optimized
+  // for addition at the end of the list.
+  int32_t i, i_end;
+  bool dirty = false;
+  for (i = 0, i_end = styleContent->CounterResetCount(); i != i_end; ++i) {
+    dirty |= AddResetOrIncrement(aFrame, i, styleContent->CounterResetAt(i),
+                                 nsCounterChangeNode::RESET);
+  }
+  for (i = 0, i_end = styleContent->CounterIncrementCount(); i != i_end; ++i) {
+    dirty |= AddResetOrIncrement(aFrame, i, styleContent->CounterIncrementAt(i),
+                                 nsCounterChangeNode::INCREMENT);
+  }
+  return dirty;
 }
 
 bool
 nsCounterManager::AddResetOrIncrement(nsIFrame* aFrame, int32_t aIndex,
                                       const nsStyleCounterData& aCounterData,
                                       nsCounterNode::Type aType)
 {
   nsCounterChangeNode* node =
     new nsCounterChangeNode(aFrame, aType, aCounterData.mValue, aIndex);
 
   nsCounterList* counterList = CounterListFor(aCounterData.mCounter);
-
   counterList->Insert(node);
   if (!counterList->IsLast(node)) {
     // Tell the caller it's responsible for recalculating the entire
     // list.
     counterList->SetDirty();
     return true;
   }
 
@@ -228,48 +233,45 @@ nsCounterManager::AddResetOrIncrement(ns
     node->Calc(counterList);
   }
   return false;
 }
 
 nsCounterList*
 nsCounterManager::CounterListFor(const nsSubstring& aCounterName)
 {
-    // XXX Why doesn't nsTHashtable provide an API that allows us to use
-    // get/put in one hashtable lookup?
-    nsCounterList *counterList;
-    if (!mNames.Get(aCounterName, &counterList)) {
-        counterList = new nsCounterList();
-        mNames.Put(aCounterName, counterList);
-    }
-    return counterList;
+  return mNames.LookupForAdd(aCounterName).OrInsert([]() {
+    return new nsCounterList();
+  });
 }
 
 void
 nsCounterManager::RecalcAll()
 {
-    for (auto iter = mNames.Iter(); !iter.Done(); iter.Next()) {
-        nsCounterList* list = iter.UserData();
-        if (list->IsDirty()) {
-            list->RecalcAll();
-        }
+  for (auto iter = mNames.Iter(); !iter.Done(); iter.Next()) {
+    nsCounterList* list = iter.UserData();
+    if (list->IsDirty()) {
+      list->RecalcAll();
     }
+  }
 }
 
 void
 nsCounterManager::SetAllDirty()
 {
   for (auto iter = mNames.Iter(); !iter.Done(); iter.Next()) {
     iter.UserData()->SetDirty();
   }
 }
 
 bool
-nsCounterManager::DestroyNodesFor(nsIFrame *aFrame)
+nsCounterManager::DestroyNodesFor(nsIFrame* aFrame)
 {
+  MOZ_ASSERT(aFrame->HasAnyStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE),
+             "why call me?");
   bool destroyedAny = false;
   for (auto iter = mNames.Iter(); !iter.Done(); iter.Next()) {
     nsCounterList* list = iter.UserData();
     if (list->DestroyNodesFor(aFrame)) {
       destroyedAny = true;
       list->SetDirty();
     }
   }
--- a/layout/base/nsCounterManager.h
+++ b/layout/base/nsCounterManager.h
@@ -202,17 +202,16 @@ private:
 };
 
 /**
  * The counter manager maintains an |nsCounterList| for each named
  * counter to keep track of all scopes with that name.
  */
 class nsCounterManager {
 public:
-    nsCounterManager();
     // Returns true if dirty
     bool AddCounterResetsAndIncrements(nsIFrame *aFrame);
 
     // Gets the appropriate counter list, creating it if necessary.
     // Guaranteed to return non-null. (Uses an infallible hashtable API.)
     nsCounterList* CounterListFor(const nsSubstring& aCounterName);
 
     // Clean up data in any dirty counter lists.
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -2860,20 +2860,20 @@ nsLayoutUtils::TransformRect(nsIFrame* a
       Rect(-std::numeric_limits<Float>::max() * 0.5f,
            -std::numeric_limits<Float>::max() * 0.5f,
            std::numeric_limits<Float>::max(),
            std::numeric_limits<Float>::max())),
     Rect(-std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame * 0.5f,
          -std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame * 0.5f,
          std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame,
          std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame));
-  aRect.x = toDevPixels.x / devPixelsPerAppUnitToFrame;
-  aRect.y = toDevPixels.y / devPixelsPerAppUnitToFrame;
-  aRect.width = toDevPixels.width / devPixelsPerAppUnitToFrame;
-  aRect.height = toDevPixels.height / devPixelsPerAppUnitToFrame;
+  aRect.x = NSToCoordRound(toDevPixels.x / devPixelsPerAppUnitToFrame);
+  aRect.y = NSToCoordRound(toDevPixels.y / devPixelsPerAppUnitToFrame);
+  aRect.width = NSToCoordRound(toDevPixels.width / devPixelsPerAppUnitToFrame);
+  aRect.height = NSToCoordRound(toDevPixels.height / devPixelsPerAppUnitToFrame);
   return TRANSFORM_SUCCEEDED;
 }
 
 nsRect
 nsLayoutUtils::GetRectRelativeToFrame(Element* aElement, nsIFrame* aFrame)
 {
   if (!aElement || !aFrame) {
     return nsRect();
--- a/layout/generic/nsFrameStateBits.h
+++ b/layout/generic/nsFrameStateBits.h
@@ -265,18 +265,18 @@ FRAME_STATE_BIT(Generic, 53, NS_FRAME_IS
 
 // Frame has a LayerActivityProperty property
 FRAME_STATE_BIT(Generic, 54, NS_FRAME_HAS_LAYER_ACTIVITY_PROPERTY)
 
 // Frame owns anonymous boxes whose style contexts it will need to update during
 // a stylo tree traversal.
 FRAME_STATE_BIT(Generic, 55, NS_FRAME_OWNS_ANON_BOXES)
 
-// ** currently unused Generic bit **
-// FRAME_STATE_BIT(Generic, 56, ...)
+// Frame maybe has a counter-reset/increment style
+FRAME_STATE_BIT(Generic, 56, NS_FRAME_HAS_CSS_COUNTER_STYLE)
 
 // The display list of the frame can be handled by the shortcut for
 // COMMON CASE.
 FRAME_STATE_BIT(Generic, 57, NS_FRAME_SIMPLE_DISPLAYLIST)
 
 // Set for all descendants of MathML sub/supscript elements (other than the
 // base frame) to indicate that the SSTY font feature should be used.
 FRAME_STATE_BIT(Generic, 58, NS_FRAME_MATHML_SCRIPT_DESCENDANT)
--- 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/netwerk/protocol/res/ExtensionProtocolHandler.cpp
+++ b/netwerk/protocol/res/ExtensionProtocolHandler.cpp
@@ -1,47 +1,55 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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 "ExtensionProtocolHandler.h"
 
-#include "nsIAddonPolicyService.h"
+#include "mozilla/ExtensionPolicyService.h"
 #include "nsServiceManagerUtils.h"
 #include "nsIURL.h"
 #include "nsIChannel.h"
 #include "nsIStreamListener.h"
 #include "nsIInputStream.h"
 #include "nsIOutputStream.h"
 #include "nsIStreamConverterService.h"
 #include "nsNetUtil.h"
 #include "LoadInfo.h"
 #include "SimpleChannel.h"
 
 namespace mozilla {
 namespace net {
 
+using extensions::URLInfo;
+
 NS_IMPL_QUERY_INTERFACE(ExtensionProtocolHandler, nsISubstitutingProtocolHandler,
                         nsIProtocolHandler, nsIProtocolHandlerWithDynamicFlags,
                         nsISupportsWeakReference)
 NS_IMPL_ADDREF_INHERITED(ExtensionProtocolHandler, SubstitutingProtocolHandler)
 NS_IMPL_RELEASE_INHERITED(ExtensionProtocolHandler, SubstitutingProtocolHandler)
 
+static inline ExtensionPolicyService&
+EPS()
+{
+  return ExtensionPolicyService::GetSingleton();
+}
+
 nsresult
 ExtensionProtocolHandler::GetFlagsForURI(nsIURI* aURI, uint32_t* aFlags)
 {
   // In general a moz-extension URI is only loadable by chrome, but a whitelisted
   // subset are web-accessible (and cross-origin fetchable). Check that whitelist.
-  nsCOMPtr<nsIAddonPolicyService> aps = do_GetService("@mozilla.org/addons/policy-service;1");
   bool loadableByAnyone = false;
-  if (aps) {
-    nsresult rv = aps->ExtensionURILoadableByAnyone(aURI, &loadableByAnyone);
-    NS_ENSURE_SUCCESS(rv, rv);
+
+  URLInfo url(aURI);
+  if (auto* policy = EPS().GetByURL(url)) {
+    loadableByAnyone = policy->IsPathWebAccessible(url.FilePath());
   }
 
   *aFlags = URI_STD | URI_IS_LOCAL_RESOURCE | (loadableByAnyone ? (URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE) : URI_DANGEROUS_TO_LOAD);
   return NS_OK;
 }
 
 bool
 ExtensionProtocolHandler::ResolveSpecialCases(const nsACString& aHost,
@@ -50,32 +58,25 @@ ExtensionProtocolHandler::ResolveSpecial
                                               nsACString& aResult)
 {
   // Create special moz-extension:-pages such as moz-extension://foo/_blank.html
   // for all registered extensions. We can't just do this as a substitution
   // because substitutions can only match on host.
   if (!SubstitutingProtocolHandler::HasSubstitution(aHost)) {
     return false;
   }
+
   if (aPathname.EqualsLiteral("/_blank.html")) {
     aResult.AssignLiteral("about:blank");
     return true;
   }
+
   if (aPathname.EqualsLiteral("/_generated_background_page.html")) {
-    nsCOMPtr<nsIAddonPolicyService> aps =
-      do_GetService("@mozilla.org/addons/policy-service;1");
-    if (!aps) {
-      return false;
-    }
-    nsresult rv = aps->GetGeneratedBackgroundPageUrl(aHost, aResult);
-    NS_ENSURE_SUCCESS(rv, false);
-    if (!aResult.IsEmpty()) {
-      MOZ_RELEASE_ASSERT(Substring(aResult, 0, 5).Equals("data:"));
-      return true;
-    }
+    Unused << EPS().GetGeneratedBackgroundPageUrl(aHost, aResult);
+    return !aResult.IsEmpty();
   }
 
   return false;
 }
 
 static inline Result<Ok, nsresult>
 WrapNSResult(nsresult aRv)
 {
--- 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
@@ -4,16 +4,23 @@ module.exports = {
 
   "globals": {
     "Cc": true,
     "Ci": true,
     "Cr": true,
     "Cu": true,
     "TextDecoder": false,
     "TextEncoder": false,
+
+    "MatchGlob": false,
+    "MatchPattern": true,
+    "MatchPatternSet": false,
+    "WebExtensionContentScript": 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
@@ -57,20 +57,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage",
                                   "resource://gre/modules/ExtensionStorage.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestCommon",
                                   "resource://testing-common/ExtensionTestCommon.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Locale",
                                   "resource://gre/modules/Locale.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Log",
                                   "resource://gre/modules/Log.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
-                                  "resource://gre/modules/MatchPattern.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
-                                  "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
@@ -78,24 +74,31 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "require",
                                   "resource://devtools/shared/Loader.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
                                   "resource://gre/modules/TelemetryStopwatch.jsm");
 
-Cu.import("resource://gre/modules/ExtensionManagement.jsm");
+XPCOMUtils.defineLazyGetter(
+  this, "processScript",
+  () => Cc["@mozilla.org/webextensions/extension-process-script;1"]
+          .getService().wrappedJSObject);
+
 Cu.import("resource://gre/modules/ExtensionParent.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
+XPCOMUtils.defineLazyPreferenceGetter(this, "useRemoteWebExtensions",
+                                      "extensions.webextensions.remote", false);
+
 var {
   GlobalManager,
   ParentAPIManager,
   apiManager: Management,
 } = ExtensionParent;
 
 const {
   classifyPermission,
@@ -422,17 +425,17 @@ this.ExtensionData = class {
 
   // This method should return a structured representation of any
   // capabilities this extension has access to, as derived from the
   // manifest.  The current implementation just returns the contents
   // of the permissions attribute, if we add things like url_overrides,
   // they should also be added here.
   get userPermissions() {
     let result = {
-      origins: this.whiteListedHosts.pat,
+      origins: this.whiteListedHosts.patterns.map(matcher => matcher.pattern),
       apis: [...this.apiNames],
     };
 
     if (Array.isArray(this.manifest.content_scripts)) {
       for (let entry of this.manifest.content_scripts) {
         result.origins.push(...entry.matches);
       }
     }
@@ -530,25 +533,29 @@ this.ExtensionData = class {
       if (perm === "geckoProfiler") {
         const acceptedExtensions = Preferences.get("extensions.geckoProfiler.acceptedExtensionIds");
         if (!acceptedExtensions.split(",").includes(this.id)) {
           this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler.");
           continue;
         }
       }
 
-      this.permissions.add(perm);
       let type = classifyPermission(perm);
       if (type.origin) {
-        whitelist.push(perm);
+        let matcher = new MatchPattern(perm, {ignorePath: true});
+
+        whitelist.push(matcher);
+        perm = matcher.pattern;
       } else if (type.api) {
         this.apiNames.add(type.api);
       }
+
+      this.permissions.add(perm);
     }
-    this.whiteListedHosts = new MatchPattern(whitelist);
+    this.whiteListedHosts = new MatchPatternSet(whitelist);
 
     for (let api of this.apiNames) {
       this.dependencies.add(`${api}@experiments.addons.mozilla.org`);
     }
 
     return this.manifest;
   }
 
@@ -699,17 +706,17 @@ this.Extension = class extends Extension
 
     this.addonData = addonData;
     this.startupReason = startupReason;
 
     if (["ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(startupReason)) {
       StartupCache.clearAddonData(addonData.id);
     }
 
-    this.remote = ExtensionManagement.useRemoteWebExtensions;
+    this.remote = useRemoteWebExtensions;
 
     if (this.remote && processCount !== 1) {
       throw new Error("Out-of-process WebExtensions are not supported with multiple child processes");
     }
     if (this.remote && !Services.prefs.getBoolPref("layers.popups.compositing.enabled", false)) {
       Cu.reportError(new Error("Remote extensions should not be enabled without also setting " +
                                "the layers.popups.compositing.enabled preference to true"));
     }
@@ -737,28 +744,40 @@ this.Extension = class extends Extension
 
     /* eslint-disable mozilla/balanced-listeners */
     this.on("add-permissions", (ignoreEvent, permissions) => {
       for (let perm of permissions.permissions) {
         this.permissions.add(perm);
       }
 
       if (permissions.origins.length > 0) {
-        this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...permissions.origins));
+        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);
       }
 
-      for (let origin of permissions.origins) {
-        this.whiteListedHosts.removeOne(origin);
-      }
+      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);
   }
 
@@ -855,18 +874,18 @@ this.Extension = class extends Extension
     return {
       id: this.id,
       uuid: this.uuid,
       instanceId: this.instanceId,
       manifest: this.manifest,
       resourceURL: this.addonData.resourceURI.spec,
       baseURL: this.baseURI.spec,
       content_scripts: this.manifest.content_scripts || [],  // eslint-disable-line camelcase
-      webAccessibleResources: this.webAccessibleResources.serialize(),
-      whiteListedHosts: this.whiteListedHosts.serialize(),
+      webAccessibleResources: this.webAccessibleResources.map(res => res.glob),
+      whiteListedHosts: this.whiteListedHosts.patterns.map(pat => pat.pattern),
       localeData: this.localeData.serialize(),
       permissions: this.permissions,
       principal: this.principal,
       optionalPermissions: this.manifest.optional_permissions,
     };
   }
 
   broadcast(msg, data) {
@@ -899,24 +918,16 @@ this.Extension = class extends Extension
       Services.obs.addObserver(observer, "message-manager-close");
       Services.obs.addObserver(observer, "message-manager-disconnect");
 
       ppmm.broadcastAsyncMessage(msg, data);
     });
   }
 
   runManifest(manifest) {
-    // Strip leading slashes from web_accessible_resources.
-    let strippedWebAccessibleResources = [];
-    if (manifest.web_accessible_resources) {
-      strippedWebAccessibleResources = manifest.web_accessible_resources.map(path => path.replace(/^\/+/, ""));
-    }
-
-    this.webAccessibleResources = new MatchGlobs(strippedWebAccessibleResources);
-
     let promises = [];
     for (let directive in manifest) {
       if (manifest[directive] !== null) {
         promises.push(Management.emit(`manifest_${directive}`, directive, this, manifest));
 
         promises.push(Management.asyncEmitManifestEntry(this, directive));
       }
     }
@@ -966,25 +977,37 @@ this.Extension = class extends Extension
   }
 
   startup() {
     this.startupPromise = this._startup();
     return this.startupPromise;
   }
 
   async _startup() {
+    // Create a temporary policy object for the devtools and add-on
+    // manager callers that depend on it being available early.
+    this.policy = new WebExtensionPolicy({
+      id: this.id,
+      mozExtensionHostname: this.uuid,
+      baseURL: this.baseURI.spec,
+      allowedOrigins: new MatchPatternSet([]),
+      localizeCallback() {},
+    });
+    if (!WebExtensionPolicy.getByID(this.id)) {
+      // The add-on manager doesn't handle async startup and shutdown,
+      // so during upgrades and add-on restarts, startup() gets called
+      // before the last shutdown has completed, and this fails when
+      // there's another active add-on with the same ID.
+      this.policy.active = true;
+    }
+
     TelemetryStopwatch.start("WEBEXT_EXTENSION_STARTUP_MS", this);
-    this.started = false;
-
     try {
       let [, perms] = await Promise.all([this.loadManifest(), ExtensionPermissions.get(this)]);
 
-      ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this);
-      this.started = true;
-
       if (!this.hasShutdown) {
         await this.initLocale();
       }
 
       if (this.errors.length) {
         return Promise.reject({errors: this.errors});
       }
 
@@ -994,38 +1017,50 @@ this.Extension = class extends Extension
 
       GlobalManager.init(this);
 
       // Apply optional permissions
       for (let perm of perms.permissions) {
         this.permissions.add(perm);
       }
       if (perms.origins.length > 0) {
-        this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...perms.origins));
+        let patterns = this.whiteListedHosts.patterns.map(host => host.pattern);
+
+        this.whiteListedHosts = new MatchPatternSet([...patterns, ...perms.origins],
+                                                    {ignorePath: true});
       }
 
+      // Normalize all patterns to contain a single leading /
+      let resources = (this.manifest.web_accessible_resources || [])
+          .map(path => path.replace(/^\/*/, "/"));
+
+      this.webAccessibleResources = resources.map(res => new MatchGlob(res));
+
+
+      this.policy.active = false;
+      this.policy = processScript.initExtension(this.serialize(), this);
+
       // The "startup" Management event sent on the extension instance itself
       // is emitted just before the Management "startup" event,
       // and it is used to run code that needs to be executed before
       // any of the "startup" listeners.
       this.emit("startup", this);
       Management.emit("startup", this);
 
       await this.runManifest(this.manifest);
 
       Management.emit("ready", this);
       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);
+      if (this.policy) {
+        this.policy.active = false;
       }
 
       this.cleanupGeneratedFile();
 
       throw e;
     }
 
     this.startupPromise = null;
@@ -1057,35 +1092,34 @@ this.Extension = class extends Extension
       }
     } catch (e) {
       Cu.reportError(e);
     }
 
     this.shutdownReason = reason;
     this.hasShutdown = true;
 
-    if (!this.started) {
+    if (!this.policy) {
       return;
     }
 
     if (this.cleanupFile ||
         ["ADDON_INSTALL", "ADDON_UNINSTALL", "ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(reason)) {
       StartupCache.clearAddonData(this.id);
     }
 
     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);
+      this.policy.active = false;
 
-      this.cleanupGeneratedFile();
-      return;
+      return this.cleanupGeneratedFile();
     }
 
     GlobalManager.uninit(this);
 
     for (let obj of this.onShutdown) {
       obj.close();
     }
 
@@ -1093,21 +1127,21 @@ this.Extension = class extends Extension
       api.destroy();
     }
 
     ParentAPIManager.shutdownExtension(this.id);
 
     Management.emit("shutdown", this);
     this.emit("shutdown");
 
-    Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
+    await this.broadcast("Extension:Shutdown", {id: this.id});
 
     MessageChannel.abortResponses({extensionId: this.id});
 
-    ExtensionManagement.shutdownExtension(this.uuid);
+    this.policy.active = false;
 
     return this.cleanupGeneratedFile();
   }
 
   observe(subject, topic, data) {
     if (topic === "xpcom-shutdown") {
       this.cleanupGeneratedFile();
     }
@@ -1132,13 +1166,13 @@ this.Extension = class extends Extension
 
   get name() {
     return this.manifest.name;
   }
 
   get optionalOrigins() {
     if (this._optionalOrigins == null) {
       let origins = this.manifest.optional_permissions.filter(perm => classifyPermission(perm).origin);
-      this._optionalOrigins = new MatchPattern(origins);
+      this._optionalOrigins = new MatchPatternSet(origins, {ignorePath: true});
     }
     return this._optionalOrigins;
   }
 };
--- a/toolkit/components/extensions/ExtensionAPI.jsm
+++ b/toolkit/components/extensions/ExtensionAPI.jsm
@@ -5,17 +5,16 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["ExtensionAPI", "ExtensionAPIs"];
 
 /* exported ExtensionAPIs */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
-Cu.import("resource://gre/modules/ExtensionManagement.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI",
                                   "resource://gre/modules/Console.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
                                   "resource://gre/modules/EventEmitter.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
@@ -42,17 +41,17 @@ class ExtensionAPI {
   }
 
   getAPI(context) {
     throw new Error("Not Implemented");
   }
 }
 
 var ExtensionAPIs = {
-  apis: ExtensionManagement.APIs.apis,
+  apis: new Map(),
 
   load(apiName) {
     let api = this.apis.get(apiName);
 
     if (api.loadPromise) {
       return api.loadPromise;
     }
 
@@ -90,9 +89,25 @@ var ExtensionAPIs = {
     let {schema} = api;
 
     Schemas.unload(schema);
     Cu.nukeSandbox(api.sandbox);
 
     api.sandbox = null;
     api.loadPromise = null;
   },
+
+  register(namespace, schema, script) {
+    if (this.apis.has(namespace)) {
+      throw new Error(`API namespace already exists: ${namespace}`);
+    }
+
+    this.apis.set(namespace, {schema, script});
+  },
+
+  unregister(namespace) {
+    if (!this.apis.has(namespace)) {
+      throw new Error(`API namespace does not exist: ${namespace}`);
+    }
+
+    this.apis.delete(namespace);
+  },
 };
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -21,20 +21,16 @@ 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, "ExtensionContent",
                                   "resource://gre/modules/ExtensionContent.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
-                                  "resource://gre/modules/MatchPattern.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
-                                  "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
                                   "resource://gre/modules/NativeMessaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
 
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
@@ -478,18 +474,18 @@ class BrowserExtensionContent extends Ev
 
     this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
     Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
 
     defineLazyGetter(this, "scripts", () => {
       return data.content_scripts.map(scriptData => new ExtensionContent.Script(this, scriptData));
     });
 
-    this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
-    this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
+    this.webAccessibleResources = data.webAccessibleResources.map(res => new MatchGlob(res));
+    this.whiteListedHosts = new MatchPatternSet(data.whiteListedHosts, {ignorePath: true});
     this.permissions = data.permissions;
     this.optionalPermissions = data.optionalPermissions;
     this.principal = data.principal;
 
     this.localeData = new LocaleData(data.localeData);
 
     this.manifest = data.manifest;
     this.baseURI = Services.io.newURI(data.baseURL);
@@ -504,31 +500,47 @@ class BrowserExtensionContent extends Ev
     this.on("add-permissions", (ignoreEvent, permissions) => {
       if (permissions.permissions.length > 0) {
         for (let perm of permissions.permissions) {
           this.permissions.add(perm);
         }
       }
 
       if (permissions.origins.length > 0) {
-        this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...permissions.origins));
+        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);
         }
       }
 
       if (permissions.origins.length > 0) {
-        for (let origin of permissions.origins) {
-          this.whiteListedHosts.removeOne(origin);
-        }
+        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() {
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -1062,17 +1062,17 @@ class SchemaAPIManager extends EventEmit
    * @returns {object} A sandbox that is used as the global by `loadScript`.
    */
   _createExtGlobal() {
     let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), {
       wantXrays: false,
       sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`,
     });
 
-    Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, ChromeWorker, extensions: this});
+    Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, ChromeWorker, MatchPattern, MatchPatternSet, extensions: this});
 
     Cu.import("resource://gre/modules/AppConstants.jsm", global);
     Cu.import("resource://gre/modules/ExtensionAPI.jsm", global);
 
     XPCOMUtils.defineLazyGetter(global, "console", getConsole);
 
     XPCOMUtils.defineLazyModuleGetter(global, "ExtensionUtils",
                                       "resource://gre/modules/ExtensionUtils.jsm");
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -191,63 +191,72 @@ defineLazyGetter(BrowserExtensionContent
 });
 
 defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", () => {
   return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET);
 });
 
 // Represents a content script.
 class Script {
-  constructor(extension, options) {
+  constructor(extension, matcher) {
     this.extension = extension;
-    this.options = options;
+    this.matcher = matcher;
 
-    this.runAt = this.options.run_at;
-    this.js = this.options.js || [];
-    this.css = this.options.css || [];
-    this.remove_css = this.options.remove_css;
-    this.css_origin = this.options.css_origin;
+    this.runAt = this.matcher.runAt;
+    this.js = this.matcher.jsPaths;
+    this.css = this.matcher.cssPaths;
+    this.removeCSS = this.matcher.removeCSS;
+    this.cssOrigin = this.matcher.cssOrigin;
 
-    this.cssCache = extension[this.css_origin === "user" ? "userCSS"
-                                                         : "authorCSS"];
-    this.scriptCache = extension[options.wantReturnValue ? "dynamicScripts"
+    this.cssCache = extension[this.cssOrigin === "user" ? "userCSS"
+                                                        : "authorCSS"];
+    this.scriptCache = extension[matcher.wantReturnValue ? "dynamicScripts"
                                                          : "staticScripts"];
 
-    if (options.wantReturnValue) {
+    if (matcher.wantReturnValue) {
       this.compileScripts();
       this.loadCSS();
     }
 
-    this.requiresCleanup = !this.remove_css && (this.css.length > 0 || options.cssCode);
+    this.requiresCleanup = !this.removeCss && (this.css.length > 0 || matcher.cssCode);
   }
 
   compileScripts() {
     return this.js.map(url => this.scriptCache.get(url));
   }
 
   loadCSS() {
     return this.cssURLs.map(url => this.cssCache.get(url));
   }
 
+  preload() {
+    this.loadCSS();
+    this.compileScripts();
+  }
+
   cleanup(window) {
-    if (!this.remove_css && this.cssURLs.length) {
+    if (!this.removeCss && this.cssURLs.length) {
       let winUtils = getWinUtils(window);
 
-      let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
+      let type = this.cssOrigin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
       for (let url of this.cssURLs) {
         this.cssCache.deleteDocument(url, window.document);
         runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
       }
 
       // Clear any sheets that were kept alive past their timeout as
       // a result of living in this document.
       this.cssCache.clear(CSS_EXPIRY_TIMEOUT_MS);
     }
   }
 
+  matchesWindow(window) {
+    return this.matcher.matchesWindow(window);
+  }
+
   async injectInto(window) {
     let context = this.extension.getContext(window);
 
     if (this.runAt === "document_end") {
       await promiseDocumentReady(window.document);
     } else if (this.runAt === "document_idle") {
       await promiseDocumentLoaded(window.document);
     }
@@ -271,19 +280,19 @@ class Script {
       context.addScript(this);
     }
 
     let cssPromise;
     if (this.cssURLs.length) {
       let window = context.contentWindow;
       let winUtils = getWinUtils(window);
 
-      let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
+      let type = this.cssOrigin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET;
 
-      if (this.remove_css) {
+      if (this.removeCSS) {
         for (let url of this.cssURLs) {
           this.cssCache.deleteDocument(url, window.document);
 
           runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type);
         }
       } else {
         cssPromise = Promise.all(this.loadCSS()).then(sheets => {
           let window = context.contentWindow;
@@ -320,31 +329,31 @@ class Script {
     }
 
     // The evaluations below may throw, in which case the promise will be
     // automatically rejected.
     for (let script of scripts) {
       result = script.executeInGlobal(context.cloneScope);
     }
 
-    if (this.options.jsCode) {
-      result = Cu.evalInSandbox(this.options.jsCode, context.cloneScope, "latest");
+    if (this.matcher.jsCode) {
+      result = Cu.evalInSandbox(this.matcher.jsCode, context.cloneScope, "latest");
     }
 
     await cssPromise;
     return result;
   }
 }
 
 defineLazyGetter(Script.prototype, "cssURLs", function() {
   // We can handle CSS urls (css) and CSS code (cssCode).
   let urls = this.css.slice();
 
-  if (this.options.cssCode) {
-    urls.push("data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode));
+  if (this.matcher.cssCode) {
+    urls.push("data:text/css;charset=utf-8," + encodeURIComponent(this.matcher.cssCode));
   }
 
   return urls;
 });
 
 /**
  * An execution context for semi-privileged extension content scripts.
  *
deleted file mode 100644
--- a/toolkit/components/extensions/ExtensionManagement.jsm
+++ /dev/null
@@ -1,212 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["ExtensionManagement"];
-
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-const Cr = Components.results;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "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(),
-
-  register(namespace, schema, script) {
-    if (this.apis.has(namespace)) {
-      throw new Error(`API namespace already exists: ${namespace}`);
-    }
-
-    this.apis.set(namespace, {schema, script});
-  },
-
-  unregister(namespace) {
-    if (!this.apis.has(namespace)) {
-      throw new Error(`API namespace does not exist: ${namespace}`);
-    }
-
-    this.apis.delete(namespace);
-  },
-};
-
-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;
-    if (path.length > 0 && path[0] == "/") {
-      path = path.substr(1);
-    }
-    return extension.webAccessibleResources.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 = {
-  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),
-
-  registerAPI: APIs.register.bind(APIs),
-  unregisterAPI: APIs.unregister.bind(APIs),
-
-  getURLForExtension,
-
-  APIs,
-};
-
-XPCOMUtils.defineLazyPreferenceGetter(ExtensionManagement, "useRemoteWebExtensions",
-                                      "extensions.webextensions.remote", false);
--- a/toolkit/components/extensions/ExtensionPageChild.jsm
+++ b/toolkit/components/extensions/ExtensionPageChild.jsm
@@ -18,18 +18,16 @@ 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, "ExtensionChildDevToolsUtils",
                                   "resource://gre/modules/ExtensionChildDevToolsUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
-                                  "resource://gre/modules/ExtensionManagement.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
                                   "resource://gre/modules/WebNavigationFrames.jsm");
 
 const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
 const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
 
@@ -401,17 +399,17 @@ ExtensionPageChild = {
       return;
     }
     this.initialized = true;
 
     Services.obs.addObserver(this, "inner-window-destroyed"); // eslint-ignore-line mozilla/balanced-listeners
   },
 
   init(global) {
-    if (!ExtensionManagement.isExtensionProcess) {
+    if (!WebExtensionPolicy.isExtensionProcess) {
       throw new Error("Cannot init extension page global in current process");
     }
 
     if (!this.contentGlobals.has(global)) {
       this.contentGlobals.set(global, new ContentGlobal(global));
     }
   },
 
@@ -435,17 +433,17 @@ ExtensionPageChild = {
    *
    * @param {BrowserExtensionContent} extension
    *     The extension for which the context should be created.
    * @param {nsIDOMWindow} contentWindow The global of the page.
    */
   initExtensionContext(extension, contentWindow) {
     this._init();
 
-    if (!ExtensionManagement.isExtensionProcess) {
+    if (!WebExtensionPolicy.isExtensionProcess) {
       throw new Error("Cannot create an extension page context in current process");
     }
 
     let windowId = getInnerWindowID(contentWindow);
     let context = this.extensionContexts.get(windowId);
     if (context) {
       if (context.extension !== extension) {
         throw new Error("A different extension context already exists for this frame");
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -1070,16 +1070,17 @@ function extensionNameFromURI(uri) {
   return GlobalManager.getExtension(id).name;
 }
 
 const ExtensionParent = {
   extensionNameFromURI,
   GlobalManager,
   HiddenExtensionPage,
   ParentAPIManager,
+  WebExtensionPolicy,
   apiManager,
   get baseManifestProperties() {
     if (gBaseManifestProperties) {
       return gBaseManifestProperties;
     }
 
     let types = Schemas.schemaJSON.get(BASE_SCHEMA)[0].types;
     let manifest = types.find(type => type.id === "WebExtensionManifest");
--- a/toolkit/components/extensions/ExtensionPermissions.jsm
+++ b/toolkit/components/extensions/ExtensionPermissions.jsm
@@ -52,17 +52,19 @@ this.ExtensionPermissions = {
     let added = emptyPermissions();
 
     for (let perm of perms.permissions) {
       if (!permissions.includes(perm)) {
         added.permissions.push(perm);
         permissions.push(perm);
       }
     }
+
     for (let origin of perms.origins) {
+      origin = new MatchPattern(origin, {ignorePath: true}).pattern;
       if (!origins.includes(origin)) {
         added.origins.push(origin);
         origins.push(origin);
       }
     }
 
     if (added.permissions.length > 0 || added.origins.length > 0) {
       prefs.saveSoon();
@@ -84,17 +86,20 @@ this.ExtensionPermissions = {
 
     for (let perm of perms.permissions) {
       let i = permissions.indexOf(perm);
       if (i >= 0) {
         removed.permissions.push(perm);
         permissions.splice(i, 1);
       }
     }
+
     for (let origin of perms.origins) {
+      origin = new MatchPattern(origin, {ignorePath: true}).pattern;
+
       let i = origins.indexOf(origin);
       if (i >= 0) {
         removed.origins.push(origin);
         origins.splice(i, 1);
       }
     }
 
     if (removed.permissions.length > 0 || removed.origins.length > 0) {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionPolicyService.cpp
@@ -0,0 +1,414 @@
+/* -*-  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/WebExtensionContentScript.h"
+#include "mozilla/extensions/WebExtensionPolicy.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozIExtensionProcessScript.h"
+#include "nsEscape.h"
+#include "nsGkAtoms.h"
+#include "nsIChannel.h"
+#include "nsIContentPolicy.h"
+#include "nsIDOMDocument.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 */ bool ExtensionPolicyService::sRemoteExtensions;
+
+/* 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);
+
+  Preferences::AddBoolVarCache(&sRemoteExtensions, "extensions.webextensions.remote", false);
+
+  RegisterObservers();
+}
+
+
+bool
+ExtensionPolicyService::IsExtensionProcess() const
+{
+  if (sRemoteExtensions && XRE_IsContentProcess()) {
+    auto& remoteType = dom::ContentChild::GetSingleton()->GetRemoteType();
+    return remoteType.EqualsLiteral(EXTENSION_REMOTE_TYPE);
+  }
+  return XRE_IsParentProcess();
+}
+
+
+WebExtensionPolicy*
+ExtensionPolicyService::GetByURL(const URLInfo& aURL)
+{
+  if (aURL.Scheme() == nsGkAtoms::moz_extension) {
+    return GetByHost(aURL.Host());
+  }
+  return nullptr;
+}
+
+void
+ExtensionPolicyService::GetAll(nsTArray<RefPtr<WebExtensionPolicy>>& aResult)
+{
+  for (auto iter = mExtensions.Iter(); !iter.Done(); iter.Next()) {
+    aResult.AppendElement(iter.Data());
+  }
+}
+
+bool
+ExtensionPolicyService::RegisterExtension(WebExtensionPolicy& aPolicy)
+{
+  bool ok = (!GetByID(aPolicy.Id()) &&
+             !GetByHost(aPolicy.MozExtensionHostname()));
+  MOZ_ASSERT(ok);
+
+  if (!ok) {
+    return false;
+  }
+
+  mExtensions.Put(aPolicy.Id(), &aPolicy);
+  mExtensionHosts.Put(aPolicy.MozExtensionHostname(), &aPolicy);
+  return true;
+}
+
+bool
+ExtensionPolicyService::UnregisterExtension(WebExtensionPolicy& aPolicy)
+{
+  bool ok = (GetByID(aPolicy.Id()) == &aPolicy &&
+             GetByHost(aPolicy.MozExtensionHostname()) == &aPolicy);
+  MOZ_ASSERT(ok);
+
+  if (!ok) {
+    return false;
+  }
+
+  mExtensions.Remove(aPolicy.Id());
+  mExtensionHosts.Remove(aPolicy.MozExtensionHostname());
+  return true;
+}
+
+
+void
+ExtensionPolicyService::BaseCSP(nsAString& aBaseCSP) const
+{
+  nsresult rv;
+
+  rv = Preferences::GetString("extensions.webextensions.base-content-security-policy", &aBaseCSP);
+  if (NS_FAILED(rv)) {
+    aBaseCSP.AssignLiteral(DEFAULT_BASE_CSP);
+  }
+}
+
+void
+ExtensionPolicyService::DefaultCSP(nsAString& aDefaultCSP) const
+{
+  nsresult rv;
+
+  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);
+
+    nsIPrincipal* principal = aDocument->NodePrincipal();
+
+    nsAutoString addonId;
+    Unused << principal->GetAddonId(addonId);
+
+    RefPtr<WebExtensionPolicy> policy = GetByID(addonId);
+    if (policy) {
+      nsCOMPtr<nsIDOMDocument> doc = do_QueryInterface(aDocument);
+      ProcessScript().InitExtensionDocument(policy, doc);
+    }
+  }
+}
+
+// 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;
+}
+
+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(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
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionPolicyService.h
@@ -0,0 +1,105 @@
+/* -*-  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/. */
+
+#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 "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_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());
+  }
+
+  WebExtensionPolicy*
+  GetByID(const nsIAtom* aAddonId)
+  {
+    return mExtensions.GetWeak(aAddonId);
+  }
+
+  WebExtensionPolicy* GetByID(const nsAString& aAddonId)
+  {
+    nsCOMPtr<nsIAtom> atom = NS_AtomizeMainThread(aAddonId);
+    return GetByID(atom);
+  }
+
+  WebExtensionPolicy* GetByURL(const extensions::URLInfo& aURL);
+
+  WebExtensionPolicy* GetByHost(const nsACString& aHost) const
+  {
+    return mExtensionHosts.GetWeak(aHost);
+  }
+
+  void GetAll(nsTArray<RefPtr<WebExtensionPolicy>>& aResult);
+
+  bool RegisterExtension(WebExtensionPolicy& aPolicy);
+  bool UnregisterExtension(WebExtensionPolicy& aPolicy);
+
+  void BaseCSP(nsAString& aDefaultCSP) const;
+  void DefaultCSP(nsAString& aDefaultCSP) const;
+
+  bool IsExtensionProcess() const;
+
+protected:
+  virtual ~ExtensionPolicyService() = default;
+
+private:
+  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;
+
+  static bool sRemoteExtensions;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ExtensionPolicyService_h
--- a/toolkit/components/extensions/ExtensionTabs.jsm
+++ b/toolkit/components/extensions/ExtensionTabs.jsm
@@ -543,19 +543,19 @@ class TabBase {
 
     if (details.frameId !== null && details.allFrames) {
       return Promise.reject({message: `'frameId' and 'allFrames' are mutually exclusive`});
     }
 
     if (this.hasActiveTabPermission) {
       // If we have the "activeTab" permission for this tab, ignore
       // the host whitelist.
-      options.matchesHost = ["<all_urls>"];
+      options.matches = ["<all_urls>"];
     } else {
-      options.matchesHost = this.extension.whiteListedHosts.serialize();
+      options.matches = this.extension.whiteListedHosts.patterns.map(host => host.pattern);
     }
 
     if (details.code !== null) {
       options[`${kind}Code`] = details.code;
     }
     if (details.file !== null) {
       let url = context.uri.resolve(details.file);
       if (!this.extension.isExtensionURL(url)) {
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -17,18 +17,16 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI",
                                   "resource://gre/modules/Console.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
-                                  "resource://gre/modules/ExtensionManagement.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "IndexedDB",
                                   "resource://gre/modules/IndexedDB.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
@@ -57,18 +55,16 @@ let StartupCache = {
   DB_NAME: "ExtensionStartupCache",
 
   SCHEMA_VERSION: 2,
 
   STORE_NAMES: Object.freeze(["locales", "manifests", "schemas"]),
 
   dbPromise: null,
 
-  cacheInvalidated: 0,
-
   initDB(db) {
     for (let name of StartupCache.STORE_NAMES) {
       try {
         db.deleteObjectStore(name);
       } catch (e) {
         // Don't worry if the store doesn't already exist.
       }
       db.createObjectStore(name, {keyPath: "key"});
@@ -79,42 +75,39 @@ let StartupCache = {
     let range = IDBKeyRange.bound([id], [id, "\uFFFF"]);
 
     return Promise.all([
       this.locales.delete(range),
       this.manifests.delete(range),
     ]).catch(e => {
       // Ignore the error. It happens when we try to flush the add-on
       // data after the AddonManager has flushed the entire startup cache.
+      this.dbPromise = this.reallyOpen(true).catch(e => {});
     });
   },
 
   async reallyOpen(invalidate = false) {
     if (this.dbPromise) {
       let db = await this.dbPromise;
       db.close();
     }
 
     if (invalidate) {
-      this.cacheInvalidated = ExtensionManagement.cacheInvalidated;
-
       if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) {
         IndexedDB.deleteDatabase(this.DB_NAME, {storage: "persistent"});
       }
     }
 
     return IndexedDB.open(this.DB_NAME,
                           {storage: "persistent", version: this.SCHEMA_VERSION},
                           db => this.initDB(db));
   },
 
   async open() {
-    if (ExtensionManagement.cacheInvalidated > this.cacheInvalidated) {
-      this.dbPromise = this.reallyOpen(true);
-    } else if (!this.dbPromise) {
+    if (!this.dbPromise) {
       this.dbPromise = this.reallyOpen();
     }
 
     return this.dbPromise;
   },
 
   observe(subject, topic, data) {
     if (topic === "startupcache-invalidate") {
@@ -143,18 +136,22 @@ class CacheStore {
 
       return createFunc(key);
     }
 
     if (result === undefined) {
       let value = await createFunc(key);
       result = {key, value};
 
-      db.objectStore(this.storeName, "readwrite")
-        .put(result);
+      try {
+        db.objectStore(this.storeName, "readwrite")
+          .put(result);
+      } catch (e) {
+        Cu.reportError(e);
+      }
     }
 
     return result && result.value;
   }
 
   async getAll() {
     let result = new Map();
     try {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/MatchGlob.h
@@ -0,0 +1,98 @@
+/* -*-  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/. */
+
+#ifndef mozilla_extensions_MatchGlob_h
+#define mozilla_extensions_MatchGlob_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/MatchGlobBinding.h"
+
+#include "jspubtd.h"
+#include "js/RootingAPI.h"
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+namespace extensions {
+
+class MatchPattern;
+
+class MatchGlob final : public nsISupports
+                      , public nsWrapperCache
+{
+public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MatchGlob)
+
+  static already_AddRefed<MatchGlob>
+  Constructor(dom::GlobalObject& aGlobal, const nsAString& aGlob, bool aAllowQuestion,
+              ErrorResult& aRv);
+
+  bool Matches(const nsAString& aString) const;
+
+  bool IsWildcard() const
+  {
+    return mIsPrefix && mPathLiteral.IsEmpty();
+  }
+
+  void GetGlob(nsAString& aGlob) const
+  {
+    aGlob = mGlob;
+  }
+
+  nsISupports* GetParentObject() const { return mParent; }
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override;
+
+protected:
+  virtual ~MatchGlob();
+
+private:
+  friend class MatchPattern;
+
+  explicit MatchGlob(nsISupports* aParent) : mParent(aParent) {}
+
+  void Init(JSContext* aCx, const nsAString& aGlob, bool aAllowQuestion, ErrorResult& aRv);
+
+  nsCOMPtr<nsISupports> mParent;
+
+  // The original glob string that this glob object represents.
+  nsString mGlob;
+
+  // The literal path string to match against. If this contains a non-void
+  // value, the glob matches against this exact literal string, rather than
+  // performng a pattern match. If mIsPrefix is true, the literal must appear
+  // at the start of the matched string. If it is false, the the literal must
+  // be exactly equal to the matched string.
+  nsString mPathLiteral;
+  bool mIsPrefix = false;
+
+  // The regular expression object which is equivalent to this glob pattern.
+  // Used for matching if, and only if, mPathLiteral is non-void.
+  JS::Heap<JSObject*> mRegExp;
+};
+
+class MatchGlobSet final : public nsTArray<RefPtr<MatchGlob>>
+{
+public:
+  // Note: We can't use the nsTArray constructors directly, since the static
+  // analyzer doesn't handle their MOZ_IMPLICIT annotations correctly.
+  MatchGlobSet() {}
+  explicit MatchGlobSet(size_type aCapacity) : nsTArray(aCapacity) {}
+  explicit MatchGlobSet(const nsTArray& aOther) : nsTArray(aOther) {}
+  MOZ_IMPLICIT MatchGlobSet(nsTArray&& aOther) : nsTArray(mozilla::Move(aOther)) {}
+  MOZ_IMPLICIT MatchGlobSet(std::initializer_list<RefPtr<MatchGlob>> aIL) : nsTArray(aIL) {}
+
+  bool Matches(const nsAString& aValue) const;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_MatchGlob_h
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/MatchPattern.cpp
@@ -0,0 +1,758 @@
+/* -*-  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/extensions/MatchPattern.h"
+#include "mozilla/extensions/MatchGlob.h"
+
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/Unused.h"
+
+#include "nsGkAtoms.h"
+#include "nsIProtocolHandler.h"
+#include "nsIURL.h"
+#include "nsNetUtil.h"
+
+namespace mozilla {
+namespace extensions {
+
+using namespace mozilla::dom;
+
+
+/*****************************************************************************
+ * AtomSet
+ *****************************************************************************/
+
+AtomSet::AtomSet(const nsTArray<nsString>& aElems)
+{
+  mElems.SetCapacity(aElems.Length());
+
+  for (const auto& elem : aElems) {
+    mElems.AppendElement(NS_AtomizeMainThread(elem));
+  }
+
+  SortAndUniquify();
+}
+
+AtomSet::AtomSet(const char** aElems)
+{
+  for (const char** elemp = aElems; *elemp; elemp++) {
+    mElems.AppendElement(NS_Atomize(*elemp));
+  }
+
+  SortAndUniquify();
+}
+
+AtomSet::AtomSet(std::initializer_list<nsIAtom*> aIL)
+{
+  mElems.SetCapacity(aIL.size());
+
+  for (const auto& elem : aIL) {
+    mElems.AppendElement(elem);
+  }
+
+  SortAndUniquify();
+}
+
+void
+AtomSet::SortAndUniquify()
+{
+  mElems.Sort();
+
+  nsIAtom* prev = nullptr;
+  mElems.RemoveElementsBy([&prev] (const RefPtr<nsIAtom>& aAtom) {
+    bool remove = aAtom == prev;
+    prev = aAtom;
+    return remove;
+  });
+
+  mElems.Compact();
+}
+
+bool
+AtomSet::Intersects(const AtomSet& aOther) const
+{
+  for (const auto& atom : *this) {
+    if (aOther.Contains(atom)) {
+      return true;
+    }
+  }
+  for (const auto& atom : aOther) {
+    if (Contains(atom)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+void
+AtomSet::Add(nsIAtom* aAtom)
+{
+  auto index = mElems.IndexOfFirstElementGt(aAtom);
+  if (index == 0 || mElems[index - 1] != aAtom) {
+    mElems.InsertElementAt(index, aAtom);
+  }
+}
+
+void
+AtomSet::Remove(nsIAtom* aAtom)
+{
+  auto index = mElems.BinaryIndexOf(aAtom);
+  if (index != mElems.NoIndex) {
+    mElems.RemoveElementAt(index);
+  }
+}
+
+
+/*****************************************************************************
+ * URLInfo
+ *****************************************************************************/
+
+nsIAtom*
+URLInfo::Scheme() const
+{
+  if (!mScheme) {
+    nsCString scheme;
+    if (NS_SUCCEEDED(mURI->GetScheme(scheme))) {
+      mScheme = NS_AtomizeMainThread(NS_ConvertASCIItoUTF16(scheme));
+    }
+  }
+  return mScheme;
+}
+
+const nsCString&
+URLInfo::Host() const
+{
+  if (mHost.IsVoid()) {
+    Unused << mURI->GetHost(mHost);
+  }
+  return mHost;
+}
+
+const nsString&
+URLInfo::FilePath() const
+{
+  if (mFilePath.IsEmpty()) {
+    nsCString path;
+    nsCOMPtr<nsIURL> url = do_QueryInterface(mURI);
+    if (url && NS_SUCCEEDED(url->GetFilePath(path))) {
+      AppendUTF8toUTF16(path, mFilePath);
+    } else {
+      mFilePath = Path();
+    }
+  }
+  return mFilePath;
+}
+
+const nsString&
+URLInfo::Path() const
+{
+  if (mPath.IsEmpty()) {
+    nsCString path;
+    if (NS_SUCCEEDED(URINoRef()->GetPath(path))) {
+      AppendUTF8toUTF16(path, mPath);
+    }
+  }
+  return mPath;
+}
+
+const nsString&
+URLInfo::Spec() const
+{
+  if (mSpec.IsEmpty()) {
+    nsCString spec;
+    if (NS_SUCCEEDED(URINoRef()->GetSpec(spec))) {
+      AppendUTF8toUTF16(spec, mSpec);
+    }
+  }
+  return mSpec;
+}
+
+nsIURI*
+URLInfo::URINoRef() const
+{
+  if (!mURINoRef) {
+    if (NS_FAILED(mURI->CloneIgnoringRef(getter_AddRefs(mURINoRef)))) {
+      mURINoRef = mURI;
+    }
+  }
+  return mURINoRef;
+}
+
+bool
+URLInfo::InheritsPrincipal() const
+{
+  if (!mInheritsPrincipal.isSome()) {
+    // For our purposes, about:blank and about:srcdoc are treated as URIs that
+    // inherit principals.
+    bool inherits = Spec().EqualsLiteral("about:blank") || Spec().EqualsLiteral("about:srcdoc");
+
+    if (!inherits) {
+      nsresult rv = NS_URIChainHasFlags(mURI, nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT,
+                                        &inherits);
+      Unused << NS_WARN_IF(NS_FAILED(rv));
+    }
+
+    mInheritsPrincipal.emplace(inherits);
+  }
+  return mInheritsPrincipal.ref();
+}
+
+
+/*****************************************************************************
+ * CookieInfo
+ *****************************************************************************/
+
+bool
+CookieInfo::IsDomain() const
+{
+  if (mIsDomain.isNothing()) {
+    mIsDomain.emplace(false);
+    MOZ_ALWAYS_SUCCEEDS(mCookie->GetIsDomain(mIsDomain.ptr()));
+  }
+  return mIsDomain.ref();
+}
+
+bool
+CookieInfo::IsSecure() const
+{
+  if (mIsSecure.isNothing()) {
+    mIsSecure.emplace(false);
+    MOZ_ALWAYS_SUCCEEDS(mCookie->GetIsSecure(mIsSecure.ptr()));
+  }
+  return mIsSecure.ref();
+}
+
+const nsCString&
+CookieInfo::Host() const
+{
+  if (mHost.IsEmpty()) {
+    MOZ_ALWAYS_SUCCEEDS(mCookie->GetHost(mHost));
+  }
+  return mHost;
+}
+
+const nsCString&
+CookieInfo::RawHost() const
+{
+  if (mRawHost.IsEmpty()) {
+    MOZ_ALWAYS_SUCCEEDS(mCookie->GetRawHost(mRawHost));
+  }
+  return mRawHost;
+}
+
+
+/*****************************************************************************
+ * MatchPattern
+ *****************************************************************************/
+
+const char* PERMITTED_SCHEMES[] = {"http", "https", "file", "ftp", "data", nullptr};
+
+const char* WILDCARD_SCHEMES[] = {"http", "https", nullptr};
+
+/* static */ already_AddRefed<MatchPattern>
+MatchPattern::Constructor(dom::GlobalObject& aGlobal,
+                          const nsAString& aPattern,
+                          const MatchPatternOptions& aOptions,
+                          ErrorResult& aRv)
+{
+  RefPtr<MatchPattern> pattern = new MatchPattern(aGlobal.GetAsSupports());
+  pattern->Init(aGlobal.Context(), aPattern, aOptions.mIgnorePath, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+  return pattern.forget();
+}
+
+void
+MatchPattern::Init(JSContext* aCx, const nsAString& aPattern, bool aIgnorePath, ErrorResult& aRv)
+{
+  RefPtr<AtomSet> permittedSchemes = AtomSet::Get<PERMITTED_SCHEMES>();
+
+  mPattern = aPattern;
+
+  if (aPattern.EqualsLiteral("<all_urls>")) {
+    mSchemes = permittedSchemes;
+    mMatchSubdomain = true;
+    return;
+  }
+
+  // The portion of the URL we're currently examining.
+  uint32_t offset = 0;
+  auto tail = Substring(aPattern, offset);
+
+  /***************************************************************************
+   * Scheme
+   ***************************************************************************/
+  int32_t index = aPattern.FindChar(':');
+  if (index <= 0) {
+    aRv.Throw(NS_ERROR_INVALID_ARG);
+    return;
+  }
+
+  nsCOMPtr<nsIAtom> scheme = NS_AtomizeMainThread(StringHead(aPattern, index));
+  if (scheme == nsGkAtoms::_asterisk) {
+    mSchemes = AtomSet::Get<WILDCARD_SCHEMES>();
+  } else if (permittedSchemes->Contains(scheme)) {
+    mSchemes = new AtomSet({scheme});
+  } else {
+    aRv.Throw(NS_ERROR_INVALID_ARG);
+    return;
+  }
+
+  /***************************************************************************
+   * Host
+   ***************************************************************************/
+  offset = index + 1;
+  tail.Rebind(aPattern, offset);
+
+  if (!StringHead(tail, 2).EqualsLiteral("//")) {
+    aRv.Throw(NS_ERROR_INVALID_ARG);
+    return;
+  }
+
+  offset += 2;
+  tail.Rebind(aPattern, offset);
+  index = tail.FindChar('/');
+  if (index < 0) {
+    index = tail.Length();
+  }
+
+  auto host = StringHead(tail, index);
+  if (host.IsEmpty() && scheme != nsGkAtoms::file) {
+    aRv.Throw(NS_ERROR_INVALID_ARG);
+    return;
+  }
+
+  offset += index;
+  tail.Rebind(aPattern, offset);
+
+  if (host.EqualsLiteral("*")) {
+    mMatchSubdomain = true;
+  } else if (StringHead(host, 2).EqualsLiteral("*.")) {
+    mDomain = NS_ConvertUTF16toUTF8(Substring(host, 2));
+    mMatchSubdomain = true;
+  } else {
+    mDomain = NS_ConvertUTF16toUTF8(host);
+  }
+
+  /***************************************************************************
+   * Path
+   ***************************************************************************/
+  if (aIgnorePath) {
+    mPattern.Truncate(offset);
+    mPattern.AppendLiteral("/*");
+    return;
+  }
+
+  auto path = tail;
+  if (path.IsEmpty()) {
+    aRv.Throw(NS_ERROR_INVALID_ARG);
+    return;
+  }
+
+  mPath = new MatchGlob(this);
+  mPath->Init(aCx, path, false, aRv);
+}
+
+
+bool
+MatchPattern::MatchesDomain(const nsACString& aDomain) const
+{
+  if (DomainIsWildcard() || mDomain == aDomain) {
+    return true;
+  }
+
+  if (mMatchSubdomain) {
+    int64_t offset = (int64_t)aDomain.Length() - mDomain.Length();
+    if (offset > 0 && aDomain[offset - 1] == '.' &&
+        Substring(aDomain, offset) == mDomain) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+bool
+MatchPattern::Matches(const URLInfo& aURL, bool aExplicit) const
+{
+  if (aExplicit && mMatchSubdomain) {
+    return false;
+  }
+
+  if (!mSchemes->Contains(aURL.Scheme())) {
+    return false;
+  }
+
+  if (!DomainIsWildcard() && !MatchesDomain(aURL.Host())) {
+    return false;
+  }
+
+  if (mPath && !mPath->IsWildcard() && !mPath->Matches(aURL.Path())) {
+    return false;
+  }
+
+  return true;
+}
+
+bool
+MatchPattern::MatchesCookie(const CookieInfo& aCookie) const
+{
+  if (!mSchemes->Contains(nsGkAtoms::https) &&
+      (aCookie.IsSecure() || !mSchemes->Contains(nsGkAtoms::http))) {
+    return false;
+  }
+
+  if (MatchesDomain(aCookie.RawHost())) {
+    return true;
+  }
+
+  if (!aCookie.IsDomain()) {
+    return false;
+  }
+
+  // Things get tricker for domain cookies. The extension needs to be able
+  // to read any cookies that could be read by any host it has permissions
+  // for. This means that our normal host matching checks won't work,
+  // since the pattern "*://*.foo.example.com/" doesn't match ".example.com",
+  // but it does match "bar.foo.example.com", which can read cookies
+  // with the domain ".example.com".
+  //
+  // So, instead, we need to manually check our filters, and accept any
+  // with hosts that end with our cookie's host.
+
+  auto& host = aCookie.Host();
+  return StringTail(mDomain, host.Length()) == host;
+}
+
+bool
+MatchPattern::SubsumesDomain(const MatchPattern& aPattern) const
+{
+  if (!mMatchSubdomain && aPattern.mMatchSubdomain && aPattern.mDomain == mDomain) {
+    return false;
+  }
+
+  return MatchesDomain(aPattern.mDomain);
+}
+
+bool
+MatchPattern::Subsumes(const MatchPattern& aPattern) const
+{
+  for (auto& scheme : *aPattern.mSchemes) {
+    if (!mSchemes->Contains(scheme)) {
+      return false;
+    }
+  }
+
+  return SubsumesDomain(aPattern);
+}
+
+bool
+MatchPattern::Overlaps(const MatchPattern& aPattern) const
+{
+  if (!mSchemes->Intersects(*aPattern.mSchemes)) {
+    return false;
+  }
+
+  return SubsumesDomain(aPattern) || aPattern.SubsumesDomain(*this);
+}
+
+
+JSObject*
+MatchPattern::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto)
+{
+  return MatchPatternBinding::Wrap(aCx, this, aGivenProto);
+}
+
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MatchPattern, mPath, mParent)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MatchPattern)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MatchPattern)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MatchPattern)
+
+
+/*****************************************************************************
+ * MatchPatternSet
+ *****************************************************************************/
+
+/* static */ already_AddRefed<MatchPatternSet>
+MatchPatternSet::Constructor(dom::GlobalObject& aGlobal,
+                             const nsTArray<dom::OwningStringOrMatchPattern>& aPatterns,
+                             const MatchPatternOptions& aOptions,
+                             ErrorResult& aRv)
+{
+  ArrayType patterns;
+
+  for (auto& elem : aPatterns) {
+    if (elem.IsMatchPattern()) {
+      patterns.AppendElement(elem.GetAsMatchPattern());
+    } else {
+      RefPtr<MatchPattern> pattern = MatchPattern::Constructor(
+        aGlobal, elem.GetAsString(), aOptions, aRv);
+
+      if (!pattern) {
+        return nullptr;
+      }
+      patterns.AppendElement(Move(pattern));
+    }
+  }
+
+  RefPtr<MatchPatternSet> patternSet = new MatchPatternSet(aGlobal.GetAsSupports(),
+                                                           Move(patterns));
+  return patternSet.forget();
+}
+
+
+bool
+MatchPatternSet::Matches(const URLInfo& aURL, bool aExplicit) const
+{
+  for (const auto& pattern : mPatterns) {
+    if (pattern->Matches(aURL, aExplicit)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+bool
+MatchPatternSet::MatchesCookie(const CookieInfo& aCookie) const
+{
+  for (const auto& pattern : mPatterns) {
+    if (pattern->MatchesCookie(aCookie)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+bool
+MatchPatternSet::Subsumes(const MatchPattern& aPattern) const
+{
+  for (const auto& pattern : mPatterns) {
+    if (pattern->Subsumes(aPattern)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+bool
+MatchPatternSet::Overlaps(const MatchPatternSet& aPatternSet) const
+{
+  for (const auto& pattern : aPatternSet.mPatterns) {
+    if (Overlaps(*pattern)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+bool
+MatchPatternSet::Overlaps(const MatchPattern& aPattern) const
+{
+  for (const auto& pattern : mPatterns) {
+    if (pattern->Overlaps(aPattern)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+
+bool
+MatchPatternSet::OverlapsAll(const MatchPatternSet& aPatternSet) const
+{
+  for (const auto& pattern : aPatternSet.mPatterns) {
+    if (!Overlaps(*pattern)) {
+      return false;
+    }
+  }
+  return aPatternSet.mPatterns.Length() > 0;
+}
+
+
+JSObject*
+MatchPatternSet::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto)
+{
+  return MatchPatternSetBinding::Wrap(aCx, this, aGivenProto);
+}
+
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MatchPatternSet, mPatterns, mParent)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MatchPatternSet)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MatchPatternSet)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MatchPatternSet)
+
+
+/*****************************************************************************
+ * MatchGlob
+ *****************************************************************************/
+
+MatchGlob::~MatchGlob()
+{
+  mozilla::DropJSObjects(this);
+}
+
+/* static */ already_AddRefed<MatchGlob>
+MatchGlob::Constructor(dom::GlobalObject& aGlobal,
+                       const nsAString& aGlob,
+                       bool aAllowQuestion,
+                       ErrorResult& aRv)
+{
+  RefPtr<MatchGlob> glob = new MatchGlob(aGlobal.GetAsSupports());
+  glob->Init(aGlobal.Context(), aGlob, aAllowQuestion, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+  return glob.forget();
+}
+
+void
+MatchGlob::Init(JSContext* aCx, const nsAString& aGlob, bool aAllowQuestion, ErrorResult& aRv)
+{
+  mGlob = aGlob;
+
+  // Check for a literal match with no glob metacharacters.
+  auto index = mGlob.FindCharInSet(aAllowQuestion ? "*?" : "*");
+  if (index < 0) {
+    mPathLiteral = mGlob;
+    return;
+  }
+
+  // Check for a prefix match, where the only glob metacharacter is a "*"
+  // at the end of the string.
+  if (index == (int32_t)mGlob.Length() - 1 && mGlob[index] == '*') {
+    mPathLiteral = StringHead(mGlob, index);
+    mIsPrefix = true;
+    return;
+  }
+
+  // Fall back to the regexp slow path.
+  NS_NAMED_LITERAL_CSTRING(metaChars, ".+*?^${}()|[]\\");
+
+  nsAutoString escaped;
+  escaped.Append('^');
+
+  for (uint32_t i = 0; i < mGlob.Length(); i++) {
+    auto c = mGlob[i];
+    if (c == '*') {
+      escaped.AppendLiteral(".*");
+    } else if (c == '?' && aAllowQuestion) {
+      escaped.Append('.');
+    } else {
+      if (metaChars.Contains(c)) {
+        escaped.Append('\\');
+      }
+      escaped.Append(c);
+    }
+  }
+
+  escaped.Append('$');
+
+  // TODO: Switch to the Rust regexp crate, when Rust integration is easier.
+  // It uses a much more efficient, linear time matching algorithm, and
+  // doesn't require special casing for the literal and prefix cases.
+  mRegExp = JS_NewUCRegExpObject(aCx, escaped.get(), escaped.Length(), 0);
+  if (mRegExp) {
+    mozilla::HoldJSObjects(this);
+  } else {
+    aRv.NoteJSContextException(aCx);
+  }
+}
+
+bool
+MatchGlob::Matches(const nsAString& aString) const
+{
+  if (mRegExp) {
+    AutoJSAPI jsapi;
+    jsapi.Init();
+    JSContext* cx = jsapi.cx();
+
+    JSAutoCompartment ac(cx, mRegExp);
+
+    JS::RootedObject regexp(cx, mRegExp);
+    JS::RootedValue result(cx);
+
+    nsString input(aString);
+
+    size_t index = 0;
+    if (!JS_ExecuteRegExpNoStatics(cx, regexp, input.BeginWriting(), aString.Length(),
+                                   &index, true, &result)) {
+      return false;
+    }
+
+    return result.isBoolean() && result.toBoolean();
+  }
+
+  if (mIsPrefix) {
+    return mPathLiteral == StringHead(aString, mPathLiteral.Length());
+  }
+
+  return mPathLiteral == aString;
+}
+
+
+JSObject*
+MatchGlob::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto)
+{
+  return MatchGlobBinding::Wrap(aCx, this, aGivenProto);
+}
+
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MatchGlob)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MatchGlob)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
+  tmp->mRegExp = nullptr;
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MatchGlob)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(MatchGlob)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mRegExp)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MatchGlob)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MatchGlob)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MatchGlob)
+
+
+/*****************************************************************************
+ * MatchGlobSet
+ *****************************************************************************/
+
+bool
+MatchGlobSet::Matches(const nsAString& aValue) const
+{
+  for (auto& glob : *this) {
+    if (glob->Matches(aValue)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+} // namespace extensions
+} // namespace mozilla
+
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/MatchPattern.h
@@ -0,0 +1,326 @@
+/* -*-  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/. */
+
+#ifndef mozilla_extensions_MatchPattern_h
+#define mozilla_extensions_MatchPattern_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/MatchPatternBinding.h"
+#include "mozilla/extensions/MatchGlob.h"
+
+#include "jspubtd.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Likely.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefCounted.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTArray.h"
+#include "nsIAtom.h"
+#include "nsICookie2.h"
+#include "nsISupports.h"
+#include "nsIURI.h"
+#include "nsWrapperCache.h"
+
+
+namespace mozilla {
+namespace extensions {
+
+using dom::MatchPatternOptions;
+
+
+// A sorted, binary-search-backed set of atoms, optimized for frequent lookups
+// and infrequent updates.
+class AtomSet final : public RefCounted<AtomSet>
+{
+  using ArrayType = AutoTArray<RefPtr<nsIAtom>, 1>;
+
+public:
+  MOZ_DECLARE_REFCOUNTED_TYPENAME(AtomSet)
+
+  explicit AtomSet(const nsTArray<nsString>& aElems);
+
+  explicit AtomSet(const char** aElems);
+
+  MOZ_IMPLICIT AtomSet(std::initializer_list<nsIAtom*> aIL);
+
+  bool Contains(const nsAString& elem) const
+  {
+    nsCOMPtr<nsIAtom> atom = NS_AtomizeMainThread(elem);
+    return Contains(atom);
+  }
+
+  bool Contains(const nsACString& aElem) const
+  {
+    nsCOMPtr<nsIAtom> atom = NS_Atomize(aElem);
+    return Contains(atom);
+  }
+
+  bool Contains(const nsIAtom* aAtom) const
+  {
+    return mElems.BinaryIndexOf(aAtom) != mElems.NoIndex;
+  }
+
+  bool Intersects(const AtomSet& aOther) const;
+
+
+  void Add(nsIAtom* aElem);
+  void Remove(nsIAtom* aElem);
+
+  void Add(const nsAString& aElem)
+  {
+    nsCOMPtr<nsIAtom> atom = NS_AtomizeMainThread(aElem);
+    return Add(atom);
+  }
+
+  void Remove(const nsAString& aElem)
+  {
+    nsCOMPtr<nsIAtom> atom = NS_AtomizeMainThread(aElem);
+    return Remove(atom);
+  }
+
+  // Returns a cached, statically-allocated matcher for the given set of
+  // literal strings.
+  template <const char** schemes>
+  static already_AddRefed<AtomSet>
+  Get()
+  {
+    static RefPtr<AtomSet> sMatcher;
+
+    if (MOZ_UNLIKELY(!sMatcher)) {
+      sMatcher = new AtomSet(schemes);
+      ClearOnShutdown(&sMatcher);
+    }
+
+    return do_AddRef(sMatcher);
+  }
+
+  void
+  Get(nsTArray<nsString>& aResult) const
+  {
+    aResult.SetCapacity(mElems.Length());
+
+    for (const auto& atom : mElems) {
+      aResult.AppendElement(nsDependentAtomString(atom));
+    }
+  }
+
+  auto begin() const
+    -> decltype(DeclVal<const ArrayType>().begin())
+  {
+    return mElems.begin();
+  }
+
+  auto end() const
+    -> decltype(DeclVal<const ArrayType>().end())
+  {
+    return mElems.end();
+  }
+
+private:
+  ArrayType mElems;
+
+  void SortAndUniquify();
+};
+
+
+// A helper class to lazily retrieve, transcode, and atomize certain URI
+// properties the first time they're used, and cache the results, so that they
+// can be used across multiple match operations.
+class MOZ_STACK_CLASS URLInfo final
+{
+public:
+  MOZ_IMPLICIT URLInfo(nsIURI* aURI)
+    : mURI(aURI)
+  {
+    mHost.SetIsVoid(true);
+  }
+
+  URLInfo(const URLInfo& aOther)
+    : URLInfo(aOther.mURI.get())
+  {}
+
+  nsIURI* URI() const { return mURI; }
+
+  nsIAtom* Scheme() const;
+  const nsCString& Host() const;
+  const nsString& Path() const;
+  const nsString& FilePath() const;
+  const nsString& Spec() const;
+
+  bool InheritsPrincipal() const;
+
+private:
+  nsIURI* URINoRef() const;
+
+  nsCOMPtr<nsIURI> mURI;
+  mutable nsCOMPtr<nsIURI> mURINoRef;
+
+  mutable nsCOMPtr<nsIAtom> mScheme;
+  mutable nsCString mHost;
+
+  mutable nsAutoString mPath;
+  mutable nsAutoString mFilePath;
+  mutable nsAutoString mSpec;
+
+  mutable Maybe<bool> mInheritsPrincipal;
+};
+
+
+// Similar to URLInfo, but for cookies.
+class MOZ_STACK_CLASS CookieInfo final
+{
+public:
+  MOZ_IMPLICIT CookieInfo(nsICookie2* aCookie)
+    : mCookie(aCookie)
+  {}
+
+  bool IsSecure() const;
+  bool IsDomain() const;
+
+  const nsCString& Host() const;
+  const nsCString& RawHost() const;
+
+private:
+  nsCOMPtr<nsICookie2> mCookie;
+
+  mutable Maybe<bool> mIsSecure;
+  mutable Maybe<bool> mIsDomain;
+
+  mutable nsCString mHost;
+  mutable nsCString mRawHost;
+};
+
+
+class MatchPattern final : public nsISupports
+                         , public nsWrapperCache
+{
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MatchPattern)
+
+  static already_AddRefed<MatchPattern>
+  Constructor(dom::GlobalObject& aGlobal,
+              const nsAString& aPattern,
+              const MatchPatternOptions& aOptions,
+              ErrorResult& aRv);
+
+  bool Matches(const URLInfo& aURL, bool aExplicit = false) const;
+
+  bool MatchesCookie(const CookieInfo& aCookie) const;
+
+  bool MatchesDomain(const nsACString& aDomain) const;
+
+  bool Subsumes(const MatchPattern& aPattern) const;
+
+  bool Overlaps(const MatchPattern& aPattern) const;
+
+  bool DomainIsWildcard() const
+  {
+    return mMatchSubdomain && mDomain.IsEmpty();
+  }
+
+  void GetPattern(nsAString& aPattern) const
+  {
+    aPattern = mPattern;
+  }
+
+  nsISupports* GetParentObject() const { return mParent; }
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override;
+
+protected:
+  virtual ~MatchPattern() = default;
+
+private:
+  explicit MatchPattern(nsISupports* aParent) : mParent(aParent) {}
+
+  void Init(JSContext* aCx, const nsAString& aPattern, bool aIgnorePath, ErrorResult& aRv);
+
+  bool SubsumesDomain(const MatchPattern& aPattern) const;
+
+
+  nsCOMPtr<nsISupports> mParent;
+
+  // The normalized match pattern string that this object represents.
+  nsString mPattern;
+
+  // The set of atomized URI schemes that this pattern matches.
+  RefPtr<AtomSet> mSchemes;
+
+  // The domain that this matcher matches. If mMatchSubdomain is false, only
+  // matches the exact domain. If it's true, matches the domain or any
+  // subdomain.
+  //
+  // For instance, "*.foo.com" gives mDomain = "foo.com" and mMatchSubdomain = true,
+  // and matches "foo.com" or "bar.foo.com" but not "barfoo.com".
+  //
+  // While "foo.com" gives mDomain = "foo.com" and mMatchSubdomain = false,
+  // and matches "foo.com" but not "bar.foo.com".
+  nsCString mDomain;
+  bool mMatchSubdomain = false;
+
+  // The glob against which the URL path must match. If null, the path is
+  // ignored entirely. If non-null, the path must match this glob.
+  RefPtr<MatchGlob> mPath;
+};
+
+
+class MatchPatternSet final : public nsISupports
+                            , public nsWrapperCache
+{
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MatchPatternSet)
+
+  using ArrayType = nsTArray<RefPtr<MatchPattern>>;
+
+
+  static already_AddRefed<MatchPatternSet>
+  Constructor(dom::GlobalObject& aGlobal,
+              const nsTArray<dom::OwningStringOrMatchPattern>& aPatterns,
+              const MatchPatternOptions& aOptions,
+              ErrorResult& aRv);
+
+
+  bool Matches(const URLInfo& aURL, bool aExplicit = false) const;
+
+  bool MatchesCookie(const CookieInfo& aCookie) const;
+
+  bool Subsumes(const MatchPattern& aPattern) const;
+
+  bool Overlaps(const MatchPattern& aPattern) const;
+
+  bool Overlaps(const MatchPatternSet& aPatternSet) const;
+
+  bool OverlapsAll(const MatchPatternSet& aPatternSet) const;
+
+  void GetPatterns(ArrayType& aPatterns)
+  {
+    aPatterns.AppendElements(mPatterns);
+  }
+
+
+  nsISupports* GetParentObject() const { return mParent; }
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override;
+
+protected:
+  virtual ~MatchPatternSet() = default;
+
+private:
+  explicit MatchPatternSet(nsISupports* aParent, ArrayType&& aPatterns)
+    : mParent(aParent)
+    , mPatterns(Forward<ArrayType>(aPatterns))
+  {}
+
+  nsCOMPtr<nsISupports> mParent;
+
+  ArrayType mPatterns;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_MatchPattern_h
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/WebExtensionContentScript.h
@@ -0,0 +1,186 @@
+/* -*-  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/. */
+
+#ifndef mozilla_extensions_WebExtensionContentScript_h
+#define mozilla_extensions_WebExtensionContentScript_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/WebExtensionContentScriptBinding.h"
+
+#include "jspubtd.h"
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Variant.h"
+#include "mozilla/extensions/MatchGlob.h"
+#include "mozilla/extensions/MatchPattern.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+class nsILoadInfo;
+class nsPIDOMWindowOuter;
+
+namespace mozilla {
+namespace extensions {
+
+using dom::Nullable;
+using ContentScriptInit = dom::WebExtensionContentScriptInit;
+
+class WebExtensionPolicy;
+
+class MOZ_STACK_CLASS DocInfo final
+{
+public:
+  DocInfo(const URLInfo& aURL, nsILoadInfo* aLoadInfo);
+
+  MOZ_IMPLICIT DocInfo(nsPIDOMWindowOuter* aWindow);
+
+  const URLInfo& URL() const { return mURL; }
+
+  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;
+  mutable Maybe<uint64_t> mFrameID;
+
+  using Window = nsPIDOMWindowOuter*;
+  using LoadInfo = nsILoadInfo*;
+
+  const Variant<LoadInfo, Window> mObj;
+};
+
+
+class WebExtensionContentScript final : public nsISupports
+                                      , public nsWrapperCache
+{
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WebExtensionContentScript)
+
+
+  using MatchGlobArray = nsTArray<RefPtr<MatchGlob>>;
+  using RunAtEnum = dom::ContentScriptRunAt;
+
+  static already_AddRefed<WebExtensionContentScript>
+  Constructor(dom::GlobalObject& aGlobal,
+              WebExtensionPolicy& aExtension,
+              const ContentScriptInit& aInit,
+              ErrorResult& aRv);
+
+
+  bool Matches(const DocInfo& aDoc) const;
+  bool MatchesURI(const URLInfo& aURL) const;
+
+  bool MatchesLoadInfo(const URLInfo& aURL, nsILoadInfo* aLoadInfo) const
+  {
+    return Matches({aURL, aLoadInfo});
+  }
+  bool MatchesWindow(nsPIDOMWindowOuter* aWindow) const
+  {
+    return Matches(aWindow);
+  }
+
+
+  WebExtensionPolicy* Extension() { return mExtension; }
+  const WebExtensionPolicy* Extension() const { return mExtension; }
+
+  bool AllFrames() const { return mAllFrames; }
+  bool MatchAboutBlank() const { return mMatchAboutBlank; }
+  RunAtEnum RunAt() const { return mRunAt; }
+
+  Nullable<uint64_t> GetFrameID() const { return mFrameID; }
+
+  MatchPatternSet* Matches() { return mMatches; }
+  const MatchPatternSet* GetMatches() const { return mMatches; }
+
+  MatchPatternSet* GetExcludeMatches() { return mExcludeMatches; }
+  const MatchPatternSet* GetExcludeMatches() const { return mExcludeMatches; }
+
+  void GetIncludeGlobs(Nullable<MatchGlobArray>& aGlobs)
+  {
+    ToNullable(mExcludeGlobs, aGlobs);
+  }
+  void GetExcludeGlobs(Nullable<MatchGlobArray>& aGlobs)
+  {
+    ToNullable(mExcludeGlobs, aGlobs);
+  }
+
+  void GetCssPaths(nsTArray<nsString>& aPaths) const
+  {
+    aPaths.AppendElements(mCssPaths);
+  }
+  void GetJsPaths(nsTArray<nsString>& aPaths) const
+  {
+    aPaths.AppendElements(mJsPaths);
+  }
+
+
+  WebExtensionPolicy* GetParentObject() const { return mExtension; }
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override;
+
+protected:
+  friend class WebExtensionPolicy;
+
+  virtual ~WebExtensionContentScript() = default;
+
+  WebExtensionContentScript(WebExtensionPolicy& aExtension,
+                            const ContentScriptInit& aInit,
+                            ErrorResult& aRv);
+
+private:
+  RefPtr<WebExtensionPolicy> mExtension;
+
+  RefPtr<MatchPatternSet> mMatches;
+  RefPtr<MatchPatternSet> mExcludeMatches;
+
+  Nullable<MatchGlobSet> mIncludeGlobs;
+  Nullable<MatchGlobSet> mExcludeGlobs;
+
+  nsTArray<nsString> mCssPaths;
+  nsTArray<nsString> mJsPaths;
+
+  RunAtEnum mRunAt;
+
+  bool mAllFrames;
+  Nullable<uint64_t> mFrameID;
+  bool mMatchAboutBlank;
+
+  template <typename T, typename U>
+  void
+  ToNullable(const Nullable<T>& aInput, Nullable<U>& aOutput)
+  {
+    if (aInput.IsNull()) {
+      aOutput.SetNull();
+    } else {
+      aOutput.SetValue(aInput.Value());
+    }
+  }
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_WebExtensionContentScript_h
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/WebExtensionPolicy.cpp
@@ -0,0 +1,507 @@
+/* -*-  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/WebExtensionContentScript.h"
+#include "mozilla/extensions/WebExtensionPolicy.h"
+
+#include "mozilla/AddonManagerWebAPI.h"
+#include "nsEscape.h"
+#include "nsISubstitutingProtocolHandler.h"
+#include "nsNetUtil.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla {
+namespace extensions {
+
+using namespace dom;
+
+static inline Result<Ok, nsresult>
+WrapNSResult(PRStatus aRv)
+{
+  if (aRv != PR_SUCCESS) {
+    return Err(NS_ERROR_FAILURE);
+  }
+  return Ok();
+}
+
+static inline Result<Ok, nsresult>
+WrapNSResult(nsresult aRv)
+{
+  if (NS_FAILED(aRv)) {
+    return Err(aRv);
+  }
+  return Ok();
+}
+
+#define NS_TRY(expr) MOZ_TRY(WrapNSResult(expr))
+
+static const char kProto[] = "moz-extension";
+
+static const char kBackgroundPageHTMLStart[] = "<!DOCTYPE html>\n\
+<html>\n\
+  <head><meta charset=\"utf-8\"></head>\n\
+  <body>";
+
+static const char kBackgroundPageHTMLScript[] = "\n\
+    <script type=\"text/javascript\" src=\"%s\"></script>";
+
+static const char kBackgroundPageHTMLEnd[] = "\n\
+  <body>\n\
+</html>";
+
+class EscapeHTML final : public nsAdoptingCString
+{
+public:
+  explicit EscapeHTML(const nsACString& str)
+    : nsAdoptingCString(nsEscapeHTML(str.BeginReading()))
+  {}
+};
+
+
+static inline ExtensionPolicyService&
+EPS()
+{
+  return ExtensionPolicyService::GetSingleton();
+}
+
+static nsISubstitutingProtocolHandler*
+Proto()
+{
+  static nsCOMPtr<nsISubstitutingProtocolHandler> sHandler;
+
+  if (MOZ_UNLIKELY(!sHandler)) {
+    nsCOMPtr<nsIIOService> ios = do_GetIOService();
+    MOZ_RELEASE_ASSERT(ios);
+
+    nsCOMPtr<nsIProtocolHandler> handler;
+    ios->GetProtocolHandler(kProto, getter_AddRefs(handler));
+
+    sHandler = do_QueryInterface(handler);
+    MOZ_RELEASE_ASSERT(sHandler);
+
+    ClearOnShutdown(&sHandler);
+  }
+
+  return sHandler;
+}
+
+
+/*****************************************************************************
+ * WebExtensionPolicy
+ *****************************************************************************/
+
+WebExtensionPolicy::WebExtensionPolicy(GlobalObject& aGlobal,
+                                       const WebExtensionInit& aInit,
+                                       ErrorResult& aRv)
+  : mId(NS_AtomizeMainThread(aInit.mId))
+  , mHostname(aInit.mMozExtensionHostname)
+  , mContentSecurityPolicy(aInit.mContentSecurityPolicy)
+  , mLocalizeCallback(aInit.mLocalizeCallback)
+  , mPermissions(new AtomSet(aInit.mPermissions))
+  , mHostPermissions(aInit.mAllowedOrigins)
+{
+  mWebAccessiblePaths.AppendElements(aInit.mWebAccessibleResources);
+
+  if (!aInit.mBackgroundScripts.IsNull()) {
+    mBackgroundScripts.SetValue().AppendElements(aInit.mBackgroundScripts.Value());
+  }
+
+  if (mContentSecurityPolicy.IsVoid()) {
+    EPS().DefaultCSP(mContentSecurityPolicy);
+  }
+
+  mContentScripts.SetCapacity(aInit.mContentScripts.Length());
+  for (const auto& scriptInit : aInit.mContentScripts) {
+    RefPtr<WebExtensionContentScript> contentScript =
+      new WebExtensionContentScript(*this, scriptInit, aRv);
+    if (aRv.Failed()) {
+      return;
+    }
+    mContentScripts.AppendElement(Move(contentScript));
+  }
+
+  nsresult rv = NS_NewURI(getter_AddRefs(mBaseURI), aInit.mBaseURL);
+  if (NS_FAILED(rv)) {
+    aRv.Throw(rv);
+  }
+}
+
+already_AddRefed<WebExtensionPolicy>
+WebExtensionPolicy::Constructor(GlobalObject& aGlobal,
+                                const WebExtensionInit& aInit,
+                                ErrorResult& aRv)
+{
+  RefPtr<WebExtensionPolicy> policy = new WebExtensionPolicy(aGlobal, aInit, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+  return policy.forget();
+}
+
+
+/* static */ void
+WebExtensionPolicy::GetActiveExtensions(dom::GlobalObject& aGlobal,
+                                        nsTArray<RefPtr<WebExtensionPolicy>>& aResults)
+{
+  EPS().GetAll(aResults);
+}
+
+/* static */ already_AddRefed<WebExtensionPolicy>
+WebExtensionPolicy::GetByID(dom::GlobalObject& aGlobal, const nsAString& aID)
+{
+  return do_AddRef(EPS().GetByID(aID));
+}
+
+/* static */ already_AddRefed<WebExtensionPolicy>
+WebExtensionPolicy::GetByHostname(dom::GlobalObject& aGlobal, const nsACString& aHostname)
+{
+  return do_AddRef(EPS().GetByHost(aHostname));
+}
+
+/* static */ already_AddRefed<WebExtensionPolicy>
+WebExtensionPolicy::GetByURI(dom::GlobalObject& aGlobal, nsIURI* aURI)
+{
+  return do_AddRef(EPS().GetByURL(aURI));
+}
+
+
+void
+WebExtensionPolicy::SetActive(bool aActive, ErrorResult& aRv)
+{
+  if (aActive == mActive) {
+    return;
+  }
+
+  bool ok = aActive ? Enable() : Disable();
+
+  if (!ok) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+  }
+}
+
+bool
+WebExtensionPolicy::Enable()
+{
+  MOZ_ASSERT(!mActive);
+
+  if (!EPS().RegisterExtension(*this)) {
+    return false;
+  }
+
+  Unused << Proto()->SetSubstitution(MozExtensionHostname(), mBaseURI);
+
+  mActive = true;
+  return true;
+}
+
+bool
+WebExtensionPolicy::Disable()
+{
+  MOZ_ASSERT(mActive);
+  MOZ_ASSERT(EPS().GetByID(Id()) == this);
+
+  if (!EPS().UnregisterExtension(*this)) {
+    return false;
+  }
+
+  Unused << Proto()->SetSubstitution(MozExtensionHostname(), nullptr);
+
+  mActive = false;
+  return true;
+}
+
+void
+WebExtensionPolicy::GetURL(const nsAString& aPath,
+                           nsAString& aResult,
+                           ErrorResult& aRv) const
+{
+  auto result = GetURL(aPath);
+  if (result.isOk()) {
+    aResult = result.unwrap();
+  } else {
+    aRv.Throw(result.unwrapErr());
+  }
+}
+
+Result<nsString, nsresult>
+WebExtensionPolicy::GetURL(const nsAString& aPath) const
+{
+  nsPrintfCString spec("%s://%s/", kProto, mHostname.get());
+
+  nsCOMPtr<nsIURI> uri;
+  NS_TRY(NS_NewURI(getter_AddRefs(uri), spec));
+
+  NS_TRY(uri->Resolve(NS_ConvertUTF16toUTF8(aPath), spec));
+
+  return NS_ConvertUTF8toUTF16(spec);
+}
+
+/* static */ bool
+WebExtensionPolicy::IsExtensionProcess(GlobalObject& aGlobal)
+{
+  return EPS().IsExtensionProcess();
+}
+
+nsCString
+WebExtensionPolicy::BackgroundPageHTML() const
+{
+  nsAutoCString result;
+
+  if (mBackgroundScripts.IsNull()) {
+    result.SetIsVoid(true);
+    return result;
+  }
+
+  result.AppendLiteral(kBackgroundPageHTMLStart);
+
+  for (auto& script : mBackgroundScripts.Value()) {
+    EscapeHTML escaped{NS_ConvertUTF16toUTF8(script)};
+
+    result.AppendPrintf(kBackgroundPageHTMLScript, escaped.get());
+  }
+
+  result.AppendLiteral(kBackgroundPageHTMLEnd);
+  return result;
+}
+
+void
+WebExtensionPolicy::Localize(const nsAString& aInput, nsString& aOutput) const
+{
+  mLocalizeCallback->Call(aInput, aOutput);
+}
+
+
+JSObject*
+WebExtensionPolicy::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto)
+{
+  return WebExtensionPolicyBinding::Wrap(aCx, this, aGivenProto);
+}
+
+void
+WebExtensionPolicy::GetContentScripts(nsTArray<RefPtr<WebExtensionContentScript>>& aScripts) const
+{
+  aScripts.AppendElements(mContentScripts);
+}
+
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WebExtensionPolicy, mParent,
+                                      mLocalizeCallback,
+                                      mHostPermissions,
+                                      mWebAccessiblePaths,
+                                      mContentScripts)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebExtensionPolicy)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(WebExtensionPolicy)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(WebExtensionPolicy)
+
+
+/*****************************************************************************
+ * WebExtensionContentScript
+ *****************************************************************************/
+
+/* static */ already_AddRefed<WebExtensionContentScript>
+WebExtensionContentScript::Constructor(GlobalObject& aGlobal,
+                                       WebExtensionPolicy& aExtension,
+                                       const ContentScriptInit& aInit,
+                                       ErrorResult& aRv)
+{
+  RefPtr<WebExtensionContentScript> script = new WebExtensionContentScript(aExtension, aInit, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+  return script.forget();
+}
+
+WebExtensionContentScript::WebExtensionContentScript(WebExtensionPolicy& aExtension,
+                                                     const ContentScriptInit& aInit,
+                                                     ErrorResult& aRv)
+  : mExtension(&aExtension)
+  , mMatches(aInit.mMatches)
+  , mExcludeMatches(aInit.mExcludeMatches)
+  , mCssPaths(aInit.mCssPaths)
+  , mJsPaths(aInit.mJsPaths)
+  , mRunAt(aInit.mRunAt)
+  , mAllFrames(aInit.mAllFrames)
+  , mFrameID(aInit.mFrameID)
+  , mMatchAboutBlank(aInit.mMatchAboutBlank)
+{
+  if (!aInit.mIncludeGlobs.IsNull()) {
+    mIncludeGlobs.SetValue().AppendElements(aInit.mIncludeGlobs.Value());
+  }
+
+  if (!aInit.mExcludeGlobs.IsNull()) {
+    mExcludeGlobs.SetValue().AppendElements(aInit.mExcludeGlobs.Value());
+  }
+}
+
+
+bool
+WebExtensionContentScript::Matches(const DocInfo& aDoc) const
+{
+  if (!mFrameID.IsNull()) {
+    if (aDoc.FrameID() != mFrameID.Value()) {
+      return false;
+    }
+  } else {
+    if (!mAllFrames && !aDoc.IsTopLevel()) {
+      return false;
+    }
+  }
+
+  if (!mMatchAboutBlank && aDoc.URL().InheritsPrincipal()) {
+    return false;
+  }
+
+  // Top-level about:blank is a special case. We treat it as a match if
+  // matchAboutBlank is true and it has the null principal. In all other
+  // cases, we test the URL of the principal that it inherits.
+  if (mMatchAboutBlank && aDoc.IsTopLevel() &&
+      aDoc.URL().Spec().EqualsLiteral("about:blank") &&
+      aDoc.Principal()->GetIsNullPrincipal()) {
+    return true;
+  }
+
+  return MatchesURI(aDoc.PrincipalURL());
+}
+
+bool
+WebExtensionContentScript::MatchesURI(const URLInfo& aURL) const
+{
+  if (!mMatches->Matches(aURL)) {
+    return false;
+  }
+
+  if (mExcludeMatches && mExcludeMatches->Matches(aURL)) {
+    return false;
+  }
+
+  if (!mIncludeGlobs.IsNull() && !mIncludeGlobs.Value().Matches(aURL.Spec())) {
+    return false;
+  }
+
+  if (!mExcludeGlobs.IsNull() && mExcludeGlobs.Value().Matches(aURL.Spec())) {
+    return false;
+  }
+
+  if (AddonManagerWebAPI::IsValidSite(aURL.URI())) {
+    return false;
+  }
+
+  return true;
+}
+
+
+JSObject*
+WebExtensionContentScript::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto)
+{
+  return WebExtensionContentScriptBinding::Wrap(aCx, this, aGivenProto);
+}
+
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WebExtensionContentScript,
+                                      mMatches, mExcludeMatches,
+                                      mIncludeGlobs, mExcludeGlobs,
+                                      mExtension)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebExtensionContentScript)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(WebExtensionContentScript)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(WebExtensionContentScript)
+
+
+/*****************************************************************************
+ * DocInfo
+ *****************************************************************************/
+
+DocInfo::DocInfo(const URLInfo& aURL, nsILoadInfo* aLoadInfo)
+  : mURL(aURL)
+  , mObj(AsVariant(aLoadInfo))
+{}
+
+DocInfo::DocInfo(nsPIDOMWindowOuter* aWindow)
+  : mURL(aWindow->GetDocumentURI())
+  , mObj(AsVariant(aWindow))
+{}
+
+bool
+DocInfo::IsTopLevel() const
+{
+  if (mIsTopLevel.isNothing()) {
+    struct Matcher
+    {
+      bool match(Window aWin) { return aWin->IsTopLevelWindow(); }
+      bool match(LoadInfo aLoadInfo) { return aLoadInfo->GetIsTopLevelLoad(); }
+    };
+    mIsTopLevel.emplace(mObj.match(Matcher()));
+  }
+  return mIsTopLevel.ref();
+}
+
+uint64_t
+DocInfo::FrameID() const
+{
+  if (mFrameID.isNothing()) {
+    if (IsTopLevel()) {
+      mFrameID.emplace(0);
+    } else {
+      struct Matcher
+      {
+        uint64_t match(Window aWin) { return aWin->WindowID(); }
+        uint64_t match(LoadInfo aLoadInfo) { return aLoadInfo->GetOuterWindowID(); }
+      };
+      mFrameID.emplace(mObj.match(Matcher()));
+    }
+  }
+  return mFrameID.ref();
+}
+
+nsIPrincipal*
+DocInfo::Principal() const
+{
+  if (mPrincipal.isNothing()) {
+    struct Matcher
+    {
+      nsIPrincipal* match(Window aWin)
+      {
+        nsCOMPtr<nsIDocument> doc = aWin->GetDoc();
+        return doc->NodePrincipal();
+      }
+      nsIPrincipal* match(LoadInfo aLoadInfo) { return aLoadInfo->PrincipalToInherit(); }
+    };
+    mPrincipal.emplace(mObj.match(Matcher()));
+  }
+  return mPrincipal.ref();
+}
+
+const URLInfo&
+DocInfo::PrincipalURL() const
+{
+  if (!URL().InheritsPrincipal()) {
+    return URL();
+  }
+
+  if (mPrincipalURL.isNothing()) {
+    nsIPrincipal* prin = Principal();
+    nsCOMPtr<nsIURI> uri;
+    if (prin && NS_SUCCEEDED(prin->GetURI(getter_AddRefs(uri)))) {
+      mPrincipalURL.emplace(uri);
+    } else {
+      mPrincipalURL.emplace(URL());
+    }
+  }
+
+  return mPrincipalURL.ref();
+}
+
+} // namespace extensions
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/WebExtensionPolicy.h
@@ -0,0 +1,174 @@
+/* -*-  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/. */
+
+#ifndef mozilla_extensions_WebExtensionPolicy_h
+#define mozilla_extensions_WebExtensionPolicy_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/WebExtensionPolicyBinding.h"
+#include "mozilla/extensions/MatchPattern.h"
+
+#include "jspubtd.h"
+
+#include "mozilla/Result.h"
+#include "mozilla/WeakPtr.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+namespace extensions {
+
+using dom::WebExtensionInit;
+using dom::WebExtensionLocalizeCallback;
+
+class WebExtensionContentScript;
+
+class WebExtensionPolicy final : public nsISupports
+                               , public nsWrapperCache
+                               , public SupportsWeakPtr<WebExtensionPolicy>
+{
+public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WebExtensionPolicy)
+  MOZ_DECLARE_WEAKREFERENCE_TYPENAME(WebExtensionPolicy)
+
+  using ScriptArray = nsTArray<RefPtr<WebExtensionContentScript>>;
+
+  static already_AddRefed<WebExtensionPolicy>
+  Constructor(dom::GlobalObject& aGlobal, const WebExtensionInit& aInit, ErrorResult& aRv);
+
+  nsIAtom* Id() const { return mId; }
+  void GetId(nsAString& aId) const { aId = nsDependentAtomString(mId); };
+
+  const nsCString& MozExtensionHostname() const { return mHostname; }
+  void GetMozExtensionHostname(nsACString& aHostname) const
+  {
+    aHostname = MozExtensionHostname();
+  }
+
+  void GetBaseURL(nsACString& aBaseURL) const
+  {
+    MOZ_ALWAYS_SUCCEEDS(mBaseURI->GetSpec(aBaseURL));
+  }
+
+  void GetURL(const nsAString& aPath, nsAString& aURL, ErrorResult& aRv) const;
+
+  Result<nsString, nsresult> GetURL(const nsAString& aPath) const;
+
+  bool CanAccessURI(nsIURI* aURI, bool aExplicit = false) const
+  {
+    return mHostPermissions && mHostPermissions->Matches(aURI, aExplicit);
+  }
+
+  bool IsPathWebAccessible(const nsAString& aPath) const
+  {
+    return mWebAccessiblePaths.Matches(aPath);
+  }
+
+  bool HasPermission(const nsIAtom* aPermission) const
+  {
+    return mPermissions->Contains(aPermission);
+  }
+  bool HasPermission(const nsAString& aPermission) const
+  {
+    return mPermissions->Contains(aPermission);
+  }
+
+  nsCString BackgroundPageHTML() const;
+
+  void Localize(const nsAString& aInput, nsString& aResult) const;
+
+  const nsString& ContentSecurityPolicy() const
+  {
+    return mContentSecurityPolicy;
+  }
+  void GetContentSecurityPolicy(nsAString& aCSP) const
+  {
+    aCSP = mContentSecurityPolicy;
+  }
+
+
+  already_AddRefed<MatchPatternSet> AllowedOrigins()
+  {
+    return do_AddRef(mHostPermissions);
+  }
+  void SetAllowedOrigins(MatchPatternSet& aAllowedOrigins)
+  {
+    mHostPermissions = &aAllowedOrigins;
+  }
+
+  void GetPermissions(nsTArray<nsString>& aResult) const
+  {
+    mPermissions->Get(aResult);
+  }
+  void SetPermissions(const nsTArray<nsString>& aPermissions)
+  {
+    mPermissions = new AtomSet(aPermissions);
+  }
+
+  void GetContentScripts(ScriptArray& aScripts) const;
+  const ScriptArray& ContentScripts() const { return mContentScripts; }
+
+
+  bool Active() const { return mActive; }
+  void SetActive(bool aActive, ErrorResult& aRv);
+
+
+  static void
+  GetActiveExtensions(dom::GlobalObject& aGlobal, nsTArray<RefPtr<WebExtensionPolicy>>& aResults);
+
+  static already_AddRefed<WebExtensionPolicy>
+  GetByID(dom::GlobalObject& aGlobal, const nsAString& aID);
+
+  static already_AddRefed<WebExtensionPolicy>
+  GetByHostname(dom::GlobalObject& aGlobal, const nsACString& aHostname);
+
+  static already_AddRefed<WebExtensionPolicy>
+  GetByURI(dom::GlobalObject& aGlobal, nsIURI* aURI);
+
+
+  static bool IsExtensionProcess(dom::GlobalObject& aGlobal);
+
+
+  nsISupports* GetParentObject() const { return mParent; }
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override;
+
+protected:
+  virtual ~WebExtensionPolicy() = default;
+
+private:
+  WebExtensionPolicy(dom::GlobalObject& aGlobal, const WebExtensionInit& aInit, ErrorResult& aRv);
+
+  bool Enable();
+  bool Disable();
+
+  nsCOMPtr<nsISupports> mParent;
+
+  nsCOMPtr<nsIAtom> mId;
+  nsCString mHostname;
+  nsCOMPtr<nsIURI> mBaseURI;
+
+  nsString mContentSecurityPolicy;
+
+  bool mActive = false;
+
+  RefPtr<WebExtensionLocalizeCallback> mLocalizeCallback;
+
+  RefPtr<AtomSet> mPermissions;
+  RefPtr<MatchPatternSet> mHostPermissions;
+  MatchGlobSet mWebAccessiblePaths;
+
+  Nullable<nsTArray<nsString>> mBackgroundScripts;
+
+  nsTArray<RefPtr<WebExtensionContentScript>> mContentScripts;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_WebExtensionPolicy_h
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -64,17 +64,17 @@ function checkSetCookiePermissions(exten
   //
   // See source/netwerk/cookie/nsCookieService.cpp, in particular
   // CheckDomain() and SetCookieInternal().
 
   if (uri.scheme != "http" && uri.scheme != "https") {
     return false;
   }
 
-  if (!extension.whiteListedHosts.matchesIgnoringPath(uri)) {
+  if (!extension.whiteListedHosts.matches(uri)) {
     return false;
   }
 
   if (!cookie.host) {
     // If no explicit host is specified, this becomes a host-only cookie.
     cookie.host = uri.host;
     return true;
   }
--- a/toolkit/components/extensions/ext-management.js
+++ b/toolkit/components/extensions/ext-management.js
@@ -51,20 +51,24 @@ function getExtensionInfoForAddon(extens
     enabled: addon.isActive,
     optionsUrl: addon.optionsURL || "",
     installType: installType(addon),
     type: addon.type,
   };
 
   if (extension) {
     let m = extension.manifest;
+
+    let hostPerms = extension.whiteListedHosts.patterns.map(matcher => matcher.pattern);
+
     extInfo.permissions = Array.from(extension.permissions).filter(perm => {
-      return !extension.whiteListedHosts.pat.includes(perm);
+      return !hostPerms.includes(perm);
     });
-    extInfo.hostPermissions = extension.whiteListedHosts.pat;
+    extInfo.hostPermissions = hostPerms;
+
     extInfo.shortName = m.short_name || "";
     if (m.icons) {
       extInfo.icons = Object.keys(m.icons).map(key => {
         return {size: Number(key), url: m.icons[key]};
       });
     }
   }
 
--- a/toolkit/components/extensions/ext-permissions.js
+++ b/toolkit/components/extensions/ext-permissions.js
@@ -25,17 +25,17 @@ this.permissions = class extends Extensi
           for (let perm of permissions) {
             if (!manifestPermissions.includes(perm)) {
               throw new ExtensionError(`Cannot request permission ${perm} since it was not declared in optional_permissions`);
             }
           }
 
           let optionalOrigins = context.extension.optionalOrigins;
           for (let origin of origins) {
-            if (!optionalOrigins.subsumes(origin)) {
+            if (!optionalOrigins.subsumes(new MatchPattern(origin))) {
               throw new ExtensionError(`Cannot request origin permission for ${origin} since it was not declared in optional_permissions`);
             }
           }
 
           if (promptsEnabled) {
             let allow = await new Promise(resolve => {
               let subject = {
                 wrappedJSObject: {
@@ -66,17 +66,17 @@ this.permissions = class extends Extensi
         async contains(permissions) {
           for (let perm of permissions.permissions) {
             if (!context.extension.hasPermission(perm)) {
               return false;
             }
           }
 
           for (let origin of permissions.origins) {
-            if (!context.extension.whiteListedHosts.subsumes(origin)) {
+            if (!context.extension.whiteListedHosts.subsumes(new MatchPattern(origin))) {
               return false;
             }
           }
 
           return true;
         },
 
         async remove(permissions) {
--- a/toolkit/components/extensions/ext-runtime.js
+++ b/toolkit/components/extensions/ext-runtime.js
@@ -1,18 +1,16 @@
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
-                                  "resource://gre/modules/ExtensionManagement.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 
 this.runtime = class extends ExtensionAPI {
   getAPI(context) {
     let {extension} = context;
     return {
       runtime: {
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -1,14 +1,10 @@
 "use strict";
 
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
-                                  "resource://gre/modules/ExtensionManagement.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
-                                  "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebRequest",
                                   "resource://gre/modules/WebRequest.jsm");
 
 // EventManager-like class specifically for WebRequest. Inherits from
 // SingletonEventManager. Takes care of converting |details| parameter
 // when invoking listeners.
 function WebRequestEventManager(context, eventName) {
   let name = `webRequest.${eventName}`;
@@ -17,23 +13,23 @@ function WebRequestEventManager(context,
       // Prevent listening in on requests originating from system principal to
       // prevent tinkering with OCSP, app and addon updates, etc.
       if (data.isSystemPrincipal) {
         return;
       }
 
       // Check hosts permissions for both the resource being requested,
       const hosts = context.extension.whiteListedHosts;
-      if (!hosts.matchesIgnoringPath(Services.io.newURI(data.url))) {
+      if (!hosts.matches(Services.io.newURI(data.url))) {
         return;
       }
       // and the origin that is loading the resource.
       const origin = data.documentUrl;
       const own = origin && origin.startsWith(context.extension.getURL());
-      if (origin && !own && !hosts.matchesIgnoringPath(Services.io.newURI(origin))) {
+      if (origin && !own && !hosts.matches(Services.io.newURI(origin))) {
         return;
       }
 
       let browserData = {tabId: -1, windowId: -1};
       if (data.browser) {
         browserData = tabTracker.getBrowserData(data.browser);
       }
       if (filter.tabId != null && browserData.tabId != filter.tabId) {
@@ -73,18 +69,22 @@ function WebRequestEventManager(context,
         }
       }
 
       return fire.sync(data2);
     };
 
     let filter2 = {};
     if (filter.urls) {
-      filter2.urls = new MatchPattern(filter.urls);
-      if (!filter2.urls.overlapsPermissions(context.extension.whiteListedHosts, context.extension.optionalOrigins)) {
+      let perms = new MatchPatternSet([...context.extension.whiteListedHosts.patterns,
+                                       ...context.extension.optionalOrigins.patterns]);
+
+      filter2.urls = new MatchPatternSet(filter.urls);
+
+      if (!perms.overlapsAll(filter2.urls)) {
         Cu.reportError("The webRequest.addListener filter doesn't overlap with host permissions.");
       }
     }
     if (filter.types) {
       filter2.types = filter.types;
     }
     if (filter.tabId) {
       filter2.tabId = filter.tabId;
--- a/toolkit/components/extensions/extension-process-script.js
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -10,168 +10,67 @@
  * after startup, in *every* browser process live outside of this file.
  */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
-                                  "resource://gre/modules/ExtensionManagement.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs",
-                                  "resource://gre/modules/MatchPattern.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
-                                  "resource://gre/modules/MatchPattern.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
-                                  "resource://gre/modules/WebNavigationFrames.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild",
                                   "resource://gre/modules/ExtensionChild.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContent",
                                   "resource://gre/modules/ExtensionContent.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPageChild",
                                   "resource://gre/modules/ExtensionPageChild.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
-                                  "resource://gre/modules/ExtensionUtils.jsm");
+
+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;
 
-
-class ScriptMatcher {
-  constructor(extension, options) {
-    this.extension = extension;
-    this.options = options;
-
-    this._script = null;
-
-    this.allFrames = options.all_frames;
-    this.matchAboutBlank = options.match_about_blank;
-    this.frameId = options.frame_id;
-    this.runAt = options.run_at;
-
-    this.matches = new MatchPattern(options.matches);
-    this.excludeMatches = new MatchPattern(options.exclude_matches || null);
-    // TODO: MatchPattern should pre-mangle host-only patterns so that we
-    // don't need to call a separate match function.
-    this.matchesHost = new MatchPattern(options.matchesHost || null);
-    this.includeGlobs = options.include_globs && new MatchGlobs(options.include_globs);
-    this.excludeGlobs = new MatchGlobs(options.exclude_globs);
-  }
-
-  toString() {
-    return `[Script {js: [${this.options.js}], matchAboutBlank: ${this.matchAboutBlank}, runAt: ${this.runAt}, matches: ${this.options.matches}}]`;
-  }
+function parseScriptOptions(options) {
+  return {
+    allFrames: options.all_frames,
+    matchAboutBlank: options.match_about_blank,
+    frameID: options.frame_id,
+    runAt: options.run_at,
 
-  get script() {
-    if (!this._script) {
-      this._script = new ExtensionContent.Script(this.extension.realExtension,
-                                                 this.options);
-    }
-    return this._script;
-  }
-
-  preload() {
-    let {script} = this;
-
-    script.loadCSS();
-    script.compileScripts();
-  }
-
-  matchesLoadInfo(uri, loadInfo) {
-    if (!this.matchesURI(uri)) {
-      return false;
-    }
-
-    if (!this.allFrames && !loadInfo.isTopLevelLoad) {
-      return false;
-    }
-
-    return true;
-  }
-
-  matchesURI(uri) {
-    if (!(this.matches.matches(uri) || this.matchesHost.matchesIgnoringPath(uri))) {
-      return false;
-    }
-
-    if (this.excludeMatches.matches(uri)) {
-      return false;
-    }
+    matches: new MatchPatternSet(options.matches),
+    excludeMatches: new MatchPatternSet(options.exclude_matches || []),
+    includeGlobs: options.include_globs && options.include_globs.map(glob => new MatchGlob(glob)),
+    excludeGlobs: options.exclude_globs && options.exclude_globs.map(glob => new MatchGlob(glob)),
 
-    if (this.includeGlobs != null && !this.includeGlobs.matches(uri.spec)) {
-      return false;
-    }
-
-    if (this.excludeGlobs.matches(uri.spec)) {
-      return false;
-    }
-
-    return true;
-  }
-
-  matchesWindow(window) {
-    if (!this.allFrames && this.frameId == null && window.parent !== window) {
-      return false;
-    }
-
-    let uri = window.document.documentURIObject;
-    let principal = window.document.nodePrincipal;
-
-    if (this.matchAboutBlank) {
-      // When matching top-level about:blank documents,
-      // allow loading into any with a NullPrincipal.
-      if (uri.spec === "about:blank" && window === window.parent && principal.isNullPrincipal) {
-        return true;
-      }
+    jsPaths: options.js || [],
+    cssPaths: options.css || [],
+  };
+}
 
-      // When matching about:blank/srcdoc iframes, the checks below
-      // need to be performed against the "owner" document's URI.
-      if (["about:blank", "about:srcdoc"].includes(uri.spec)) {
-        uri = principal.URI;
-      }
-    }
-
-    // Documents from data: URIs also inherit the principal.
-    if (Services.netUtils.URIChainHasFlags(uri, Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT)) {
-      if (!this.matchAboutBlank) {
-        return false;
-      }
-      uri = principal.URI;
-    }
+var extensions = new DefaultWeakMap(policy => {
+  let extension = new ExtensionChild.BrowserExtensionContent(policy.initData);
+  extension.policy = policy;
+  return extension;
+});
 
-    if (!this.matchesURI(uri)) {
-      return false;
-    }
-
-    if (this.frameId != null && WebNavigationFrames.getFrameId(window) !== this.frameId) {
-      return false;
-    }
-
-    // If mozAddonManager is present on this page, don't allow
-    // content scripts.
-    if (window.navigator.mozAddonManager !== undefined) {
-      return false;
-    }
-
-    return true;
-  }
-
-  injectInto(window) {
-    return this.script.injectInto(window);
-  }
-}
+var contentScripts = new DefaultWeakMap(matcher => {
+  return new ExtensionContent.Script(extensions.get(matcher.extension),
+                                     matcher);
+});
 
 function getMessageManager(window) {
   let docShell = window.document.docShell.QueryInterface(Ci.nsIInterfaceRequestor);
   try {
     return docShell.getInterface(Ci.nsIContentFrameMessageManager);
   } catch (e) {
     // Some windows don't support this interface (hidden window).
     return null;
@@ -187,34 +86,42 @@ class ExtensionGlobal {
 
     MessageChannel.addListener(global, "Extension:Capture", this);
     MessageChannel.addListener(global, "Extension:DetectLanguage", this);
     MessageChannel.addListener(global, "Extension:Execute", this);
     MessageChannel.addListener(global, "WebNavigation:GetFrame", this);
     MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this);
   }
 
-  uninit() {
-  }
-
   get messageFilterStrict() {
     return {
       innerWindowID: getInnerWindowID(this.global.content),
     };
   }
 
   receiveMessage({target, messageName, recipient, data}) {
     switch (messageName) {
       case "Extension:Capture":
         return ExtensionContent.handleExtensionCapture(this.global, data.width, data.height, data.options);
       case "Extension:DetectLanguage":
         return ExtensionContent.handleDetectLanguage(this.global, target);
       case "Extension:Execute":
-        let extension = ExtensionManager.get(recipient.extensionId);
-        let script = new ScriptMatcher(extension, data.options);
+        let policy = WebExtensionPolicy.getByID(recipient.extensionId);
+
+        let matcher = new WebExtensionContentScript(policy, parseScriptOptions(data.options));
+
+        Object.assign(matcher, {
+          wantReturnValue: data.options.wantReturnValue,
+          removeCSS: data.options.remove_css,
+          cssOrigin: data.options.css_origin,
+          cssCode: data.options.cssCode,
+          jsCode: data.options.jsCode,
+        });
+
+        let script = contentScripts.get(matcher);
 
         return ExtensionContent.handleExtensionExecute(this.global, target, data.options, script);
       case "WebNavigation:GetFrame":
         return ExtensionContent.handleWebNavigationGetFrame(this.global, data.options);
       case "WebNavigation:GetAllFrames":
         return ExtensionContent.handleWebNavigationGetAllFrames(this.global);
     }
   }
@@ -226,53 +133,19 @@ DocumentManager = {
   globals: new Map(),
 
   // Initialize listeners that we need regardless of whether extensions are
   // enabled.
   earlyInit() {
     Services.obs.addObserver(this, "tab-content-frameloader-created"); // eslint-disable-line mozilla/balanced-listeners
   },
 
-  // 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) {
+    if (this.extensionProcessInitialized || !WebExtensionPolicy.isExtensionProcess) {
       return;
     }
     this.extensionProcessInitialized = true;
 
     for (let global of this.globals.keys()) {
       ExtensionPageChild.init(global);
     }
   },
@@ -281,176 +154,53 @@ DocumentManager = {
   // into.
   initGlobal(global) {
     // Note: {once: true} does not work as expected here.
     global.addEventListener("unload", event => { // eslint-disable-line mozilla/balanced-listeners
       this.uninitGlobal(global);
     });
 
     this.globals.set(global, new ExtensionGlobal(global));
-    this.initExtensionProcess();
-    if (this.extensionProcessInitialized && ExtensionManagement.isExtensionProcess) {
+    if (this.extensionProcessInitialized && WebExtensionPolicy.isExtensionProcess) {
       ExtensionPageChild.init(global);
     }
   },
   uninitGlobal(global) {
     if (this.extensionProcessInitialized) {
       ExtensionPageChild.uninit(global);
     }
-    this.globals.get(global).uninit();
     this.globals.delete(global);
   },
 
   initExtension(extension) {
-    if (this.extensionCount === 0) {
-      this.init();
-      this.initExtensionProcess();
-    }
-    this.extensionCount++;
-
-    for (let script of extension.scripts) {
-      this.addContentScript(script);
-    }
+    this.initExtensionProcess();
 
     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);
+    if (topic == "tab-content-frameloader-created") {
+      this.initGlobal(subject);
+    }
   },
 
   // Script loading
 
   injectExtensionScripts(extension) {
     for (let window of this.enumerateWindows()) {
-      for (let script of extension.scripts) {
+      for (let script of extension.contentScripts) {
         if (script.matchesWindow(window)) {
-          script.injectInto(window);
+          contentScripts.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
@@ -483,35 +233,26 @@ DocumentManager = {
       if (principal.addonId !== addonId) {
         return false;
       }
     }
 
     return true;
   },
 
-  loadInto(window) {
-    let {addonId} = Cu.getObjectPrincipal(window);
-    if (!addonId) {
-      return;
-    }
-
-    let extension = ExtensionManager.get(addonId);
-    if (!extension) {
-      throw new Error(`No registered extension for ID ${addonId}`);
-    }
-
-    if (this.checkParentFrames(window, addonId) && ExtensionManagement.isExtensionProcess) {
+  loadInto(policy, window) {
+    let extension = extensions.get(policy);
+    if (WebExtensionPolicy.isExtensionProcess && this.checkParentFrames(window, policy.id)) {
       // We're in a top-level extension frame, or a sub-frame thereof,
       // in the extension process. Inject the full extension page API.
-      ExtensionPageChild.initExtensionContext(extension.realExtension, window);
+      ExtensionPageChild.initExtensionContext(extension, window);
     } else {
       // We're in a content sub-frame or not in the extension process.
       // Only inject a minimal content script API.
-      ExtensionContent.initExtensionContext(extension.realExtension, window);
+      ExtensionContent.initExtensionContext(extension, window);
     }
   },
 
   // Helpers
 
   * enumerateWindows(docShell) {
     if (docShell) {
       let enum_ = docShell.getDocShellEnumerator(docShell.typeContent,
@@ -523,131 +264,99 @@ DocumentManager = {
     } else {
       for (let global of this.globals.keys()) {
         yield* this.enumerateWindows(global.docShell);
       }
     }
   },
 };
 
-/**
- * This class is a minimal stub extension object which loads and instantiates a
- * real extension object when non-basic functionality is needed.
- */
-class StubExtension {
-  constructor(data) {
-    this.data = data;
-    this.id = data.id;
-    this.uuid = data.uuid;
-    this.instanceId = data.instanceId;
-    this.manifest = data.manifest;
-
-    this.scripts = data.content_scripts.map(scriptData => new ScriptMatcher(this, scriptData));
-
-    this._realExtension = null;
-
-    this.startup();
-  }
-
-  startup() {
-    // Extension.jsm takes care of this in the parent.
-    if (isContentProcess) {
-      let uri = Services.io.newURI(this.data.resourceURL);
-      ExtensionManagement.startupExtension(this.uuid, uri, this);
-    }
-  }
-
-  shutdown() {
-    if (isContentProcess) {
-      ExtensionManagement.shutdownExtension(this.uuid);
-    }
-    if (this._realExtension) {
-      this._realExtension.shutdown();
-    }
-  }
-
-  // Lazily create the real extension object when needed.
-  get realExtension() {
-    if (!this._realExtension) {
-      this._realExtension = new ExtensionChild.BrowserExtensionContent(this.data);
-    }
-    return this._realExtension;
-  }
-
-  // Forward functions needed by ExtensionManagement.
-  hasPermission(...args) {
-    return this.realExtension.hasPermission(...args);
-  }
-  localize(...args) {
-    return this.realExtension.localize(...args);
-  }
-  get whiteListedHosts() {
-    return this.realExtension.whiteListedHosts;
-  }
-  get webAccessibleResources() {
-    return this.realExtension.webAccessibleResources;
-  }
-}
-
 ExtensionManager = {
-  // Map[extensionId -> StubExtension]
-  extensions: new Map(),
-
   init() {
     MessageChannel.setupMessageManagers([Services.cpmm]);
 
     Services.cpmm.addMessageListener("Extension:Startup", this);
     Services.cpmm.addMessageListener("Extension:Shutdown", this);
     Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
 
     let procData = Services.cpmm.initialProcessData || {};
 
     for (let data of procData["Extension:Extensions"] || []) {
-      let extension = new StubExtension(data);
-      this.extensions.set(data.id, extension);
-      DocumentManager.initExtension(extension);
+      this.initExtension(data);
     }
 
     if (isContentProcess) {
       // Make sure we handle new schema data until Schemas.jsm is loaded.
       if (!procData["Extension:Schemas"]) {
         procData["Extension:Schemas"] = new Map();
       }
       this.schemaJSON = procData["Extension:Schemas"];
 
       Services.cpmm.addMessageListener("Schema:Add", this);
     }
   },
 
-  get(extensionId) {
-    return this.extensions.get(extensionId);
+  initExtensionPolicy(data, extension) {
+    let policy = WebExtensionPolicy.getByID(data.id);
+    if (!policy) {
+      let localizeCallback = (
+        extension ? extension.localize.bind(extension)
+                  : str => extensions.get(policy).localize(str));
+
+      policy = new WebExtensionPolicy({
+        id: data.id,
+        mozExtensionHostname: data.uuid,
+        baseURL: data.resourceURL,
+
+        permissions: Array.from(data.permissions),
+        allowedOrigins: new MatchPatternSet(data.whiteListedHosts),
+        webAccessibleResources: data.webAccessibleResources.map(host => new MatchGlob(host)),
+
+        contentSecurityPolicy: data.manifest.content_security_policy,
+
+        localizeCallback,
+
+        backgroundScripts: (data.manifest.background &&
+                            data.manifest.background.scripts),
+
+        contentScripts: (data.manifest.content_scripts || []).map(parseScriptOptions),
+      });
+
+      policy.active = true;
+      policy.initData = data;
+    }
+    return policy;
+  },
+
+  initExtension(data) {
+    let policy = this.initExtensionPolicy(data);
+
+    DocumentManager.initExtension(policy);
   },
 
   receiveMessage({name, data}) {
     switch (name) {
       case "Extension:Startup": {
-        let extension = new StubExtension(data);
-
-        this.extensions.set(data.id, extension);
-
-        DocumentManager.initExtension(extension);
+        this.initExtension(data);
 
         Services.cpmm.sendAsyncMessage("Extension:StartupComplete");
         break;
       }
 
       case "Extension:Shutdown": {
-        let extension = this.extensions.get(data.id);
-        this.extensions.delete(data.id);
+        let policy = WebExtensionPolicy.getByID(data.id);
 
-        if (extension) {
-          extension.shutdown();
+        if (extensions.has(policy)) {
+          extensions.get(policy).shutdown();
+        }
 
-          DocumentManager.uninitExtension(extension);
+        if (isContentProcess) {
+          policy.active = false;
         }
+        Services.cpmm.sendAsyncMessage("Extension:ShutdownComplete");
         break;
       }
 
       case "Extension:FlushJarCache": {
         ExtensionUtils.flushJarCache(data.path);
         Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete");
         break;
       }
@@ -655,10 +364,48 @@ 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]),
+
+  get wrappedJSObject() { return this; },
+
+  initExtension(data, extension) {
+    return ExtensionManager.initExtensionPolicy(data, extension);
+  },
+
+  initExtensionDocument(policy, doc) {
+    if (DocumentManager.globals.has(getMessageManager(doc.defaultView))) {
+      DocumentManager.loadInto(policy, doc.defaultView);
+    }
+  },
+
+  preloadContentScript(contentScript) {
+    contentScripts.get(contentScript).preload();
+  },
+
+  loadContentScript(contentScript, window) {
+    if (DocumentManager.globals.has(getMessageManager(window))) {
+      contentScripts.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
@@ -9,17 +9,16 @@ with Files('**'):
 
 EXTRA_JS_MODULES += [
     'Extension.jsm',
     'ExtensionAPI.jsm',
     'ExtensionChild.jsm',
     'ExtensionChildDevToolsUtils.jsm',
     'ExtensionCommon.jsm',
     'ExtensionContent.jsm',
-    'ExtensionManagement.jsm',
     'ExtensionPageChild.jsm',
     'ExtensionParent.jsm',
     'ExtensionPermissions.jsm',
     'ExtensionPreferencesManager.jsm',
     'ExtensionSettingsStore.jsm',
     'ExtensionStorage.jsm',
     'ExtensionStorageSync.jsm',
     'ExtensionTabs.jsm',
@@ -27,37 +26,66 @@ 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',
+    'WebExtensionPolicy.h',
+]
+
+UNIFIED_SOURCES += [
+    'ExtensionPolicyService.cpp',
+    'MatchPattern.cpp',
+    'WebExtensionPolicy.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
+
+
 JAR_MANIFESTS += ['jar.mn']
 
 BROWSER_CHROME_MANIFESTS += [
     'test/browser/browser.ini',
 ]
 
 MOCHITEST_MANIFESTS += [
     'test/mochitest/mochitest-remote.ini',
     'test/mochitest/mochitest.ini'
 ]
 MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
 XPCSHELL_TESTS_MANIFESTS += [
     'test/xpcshell/native_messaging.ini',
     'test/xpcshell/xpcshell-remote.ini',
     'test/xpcshell/xpcshell.ini',
 ]
+
+include('/ipc/chromium/chromium-config.mozbuild')
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/mozIExtensionProcessScript.idl
@@ -0,0 +1,18 @@
+/* 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;
+interface nsIDOMDocument;
+
+[scriptable,uuid(6b09dc51-6caa-4ca7-9d6d-30c87258a630)]
+interface mozIExtensionProcessScript : nsISupports
+{
+  void preloadContentScript(in nsISupports contentScript);
+
+  void loadContentScript(in nsISupports contentScript, in mozIDOMWindowProxy window);
+
+  void initExtensionDocument(in nsISupports extension, in nsIDOMDocument doc);
+};
--- 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");
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/data/file_iframe.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>Iframe document</title>
+</head>
+<body>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/data/file_toplevel.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>Top-level frame document</title>
+</head>
+<body>
+  <iframe src="file_iframe.html"></iframe>
+  <iframe src="about:blank"></iframe>
+  <iframe srcdoc="Iframe srcdoc"></iframe>
+</body>
+</html>
--- a/toolkit/components/extensions/test/xpcshell/head.js
+++ b/toolkit/components/extensions/test/xpcshell/head.js
@@ -10,18 +10,16 @@ Components.utils.import("resource://gre/
 Components.utils.import("resource://testing-common/AddonTestUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentTask",
                                   "resource://testing-common/ContentTask.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Extension",
                                   "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
                                   "resource://gre/modules/Extension.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
-                                  "resource://gre/modules/ExtensionManagement.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils",
                                   "resource://testing-common/ExtensionXPCShellUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
                                   "resource://testing-common/httpd.js");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
copy from toolkit/modules/tests/xpcshell/test_MatchPattern.js
copy to toolkit/components/extensions/test/xpcshell/test_MatchPattern.js
--- a/toolkit/modules/tests/xpcshell/test_MatchPattern.js
+++ b/toolkit/components/extensions/test/xpcshell/test_MatchPattern.js
@@ -1,51 +1,74 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-Components.utils.import("resource://gre/modules/MatchPattern.jsm");
-Components.utils.import("resource://gre/modules/Services.jsm");
+add_task(async function test_MatchPattern_matches() {
+  function test(url, pattern, normalized = pattern) {
+    let uri = Services.io.newURI(url);
+
+    pattern = Array.concat(pattern);
+    normalized = Array.concat(normalized);
+
+    let patterns = pattern.map(pat => new MatchPattern(pat));
+
+    let set = new MatchPatternSet(pattern);
+    let set2 = new MatchPatternSet(patterns);
 
-function test_matches() {
-  function test(url, pattern) {
-    let uri = Services.io.newURI(url);
-    let m = new MatchPattern(pattern);
-    return m.matches(uri);
+    deepEqual(set2.patterns, patterns, "Patterns in set should equal the input patterns");
+
+    equal(set.matches(uri), set2.matches(uri), "Single pattern and pattern set should return the same match");
+
+    for (let [i, pat] of patterns.entries()) {
+      equal(pat.pattern, normalized[i], "Pattern property should contain correct normalized pattern value");
+    }
+
+    if (patterns.length == 1) {
+      equal(patterns[0].matches(uri), set.matches(uri), "Single pattern and string set should return the same match");
+    }
+
+    return set.matches(uri);
   }
 
-  function pass({url, pattern}) {
-    do_check_true(test(url, pattern), `Expected match: ${JSON.stringify(pattern)}, ${url}`);
+  function pass({url, pattern, normalized}) {
+    ok(test(url, pattern, normalized), `Expected match: ${JSON.stringify(pattern)}, ${url}`);
   }
 
-  function fail({url, pattern}) {
-    do_check_false(test(url, pattern), `Expected no match: ${JSON.stringify(pattern)}, ${url}`);
+  function fail({url, pattern, normalized}) {
+    ok(!test(url, pattern, normalized), `Expected no match: ${JSON.stringify(pattern)}, ${url}`);
+  }
+
+  function invalid({pattern}) {
+    Assert.throws(() => new MatchPattern(pattern), /.*/,
+                  `Invalid pattern '${pattern}' should throw`);
+    Assert.throws(() => new MatchPatternSet([pattern]), /.*/,
+                  `Invalid pattern '${pattern}' should throw`);
   }
 
   // Invalid pattern.
-  fail({url: "http://mozilla.org", pattern: ""});
+  invalid({pattern: ""});
 
   // Pattern must include trailing slash.
-  fail({url: "http://mozilla.org", pattern: "http://mozilla.org"});
+  invalid({pattern: "http://mozilla.org"});
 
   // Protocol not allowed.
-  fail({url: "http://mozilla.org", pattern: "gopher://wuarchive.wustl.edu/"});
+  invalid({pattern: "gopher://wuarchive.wustl.edu/"});
 
   pass({url: "http://mozilla.org", pattern: "http://mozilla.org/"});
   pass({url: "http://mozilla.org/", pattern: "http://mozilla.org/"});
 
   pass({url: "http://mozilla.org/", pattern: "*://mozilla.org/"});
   pass({url: "https://mozilla.org/", pattern: "*://mozilla.org/"});
   fail({url: "file://mozilla.org/", pattern: "*://mozilla.org/"});
   fail({url: "ftp://mozilla.org/", pattern: "*://mozilla.org/"});
 
   fail({url: "http://mozilla.com", pattern: "http://*mozilla.com*/"});
   fail({url: "http://mozilla.com", pattern: "http://mozilla.*/"});
-  fail({url: "http://mozilla.com", pattern: "http:/mozilla.com/"});
+  invalid({pattern: "http:/mozilla.com/"});
 
   pass({url: "http://google.com", pattern: "http://*.google.com/"});
   pass({url: "http://docs.google.com", pattern: "http://*.google.com/"});
 
   pass({url: "http://mozilla.org:8080", pattern: "http://mozilla.org/"});
   pass({url: "http://mozilla.org:8080", pattern: "*://mozilla.org/"});
   fail({url: "http://mozilla.org:8080", pattern: "http://mozilla.org:8080/"});
 
@@ -65,17 +88,17 @@ function test_matches() {
   fail({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/"});
   pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*"});
   pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/a*f"});
   pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/a*"});
   pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*f"});
   fail({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*e"});
   fail({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*c"});
 
-  fail({url: "http:///a.html", pattern: "http:///a.html"});
+  invalid({pattern: "http:///a.html"});
   pass({url: "file:///foo", pattern: "file:///foo*"});
   pass({url: "file:///foo/bar.html", pattern: "file:///foo*"});
 
   pass({url: "http://mozilla.org/a", pattern: "<all_urls>"});
   pass({url: "https://mozilla.org/a", pattern: "<all_urls>"});
   pass({url: "ftp://mozilla.org/a", pattern: "<all_urls>"});
   pass({url: "file:///a", pattern: "<all_urls>"});
   fail({url: "gopher://wuarchive.wustl.edu/a", pattern: "<all_urls>"});
@@ -83,22 +106,27 @@ function test_matches() {
   // Multiple patterns.
   pass({url: "http://mozilla.org", pattern: ["http://mozilla.org/"]});
   pass({url: "http://mozilla.org", pattern: ["http://mozilla.org/", "http://mozilla.com/"]});
   pass({url: "http://mozilla.com", pattern: ["http://mozilla.org/", "http://mozilla.com/"]});
   fail({url: "http://mozilla.biz", pattern: ["http://mozilla.org/", "http://mozilla.com/"]});
 
   // Match url with fragments.
   pass({url: "http://mozilla.org/base#some-fragment", pattern: "http://mozilla.org/base"});
-}
+});
 
-function test_overlaps() {
+add_task(async function test_MatchPattern_overlaps() {
   function test(filter, hosts, optional) {
-    const f = new MatchPattern(filter);
-    return f.overlapsPermissions(new MatchPattern(hosts), new MatchPattern(optional));
+    filter = Array.concat(filter);
+    hosts = Array.concat(hosts);
+    optional = Array.concat(optional);
+
+    const set = new MatchPatternSet([...hosts, ...optional]);
+    const pat = new MatchPatternSet(filter);
+    return set.overlapsAll(pat);
   }
 
   function pass({filter = [], hosts = [], optional = []}) {
     ok(test(filter, hosts, optional), `Expected overlap: ${filter}, ${hosts} (${optional})`);
   }
 
   function fail({filter = [], hosts = [], optional = []}) {
     ok(!test(filter, hosts, optional), `Expected no overlap: ${filter}, ${hosts} (${optional})`);
@@ -127,17 +155,16 @@ function test_overlaps() {
   fail({hosts: "http://*.ab.cd/", filter: "http://*.fake-ab.cd/"});
 
   // Wildcard domain.
   pass({hosts: "http://*/", filter: "http://ab.cd/"});
   fail({hosts: "http://*/", filter: "https://ab.cd/"});
 
   // Wildcard wildcards.
   pass({hosts: "<all_urls>", filter: "ftp://ab.cd/"});
-  fail({hosts: "<all_urls>", filter: ""});
   fail({hosts: "<all_urls>"});
 
   // Multiple hosts.
   pass({hosts: ["http://ab.cd/"], filter: ["http://ab.cd/"]});
   pass({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: "http://ab.cd/"});
   pass({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: "http://ab.xy/"});
   fail({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: "http://ab.zz/"});
 
@@ -145,14 +172,57 @@ function test_overlaps() {
   pass({hosts: ["http://*.ab.cd/"], filter: ["http://ab.cd/", "http://www.ab.cd/"]});
   pass({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: ["http://ab.cd/", "http://ab.xy/"]});
   fail({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: ["http://ab.cd/", "http://ab.zz/"]});
 
   // Optional.
   pass({hosts: [], optional: "http://ab.cd/", filter: "http://ab.cd/"});
   pass({hosts: "http://ab.cd/", optional: "http://ab.xy/", filter: ["http://ab.cd/", "http://ab.xy/"]});
   fail({hosts: "http://ab.cd/", optional: "https://ab.xy/", filter: "http://ab.xy/"});
-}
+});
+
+add_task(async function test_MatchGlob() {
+  function test(url, pattern) {
+    let m = new MatchGlob(pattern[0]);
+    return m.matches(Services.io.newURI(url).spec);
+  }
+
+  function pass({url, pattern}) {
+    ok(test(url, pattern), `Expected match: ${JSON.stringify(pattern)}, ${url}`);
+  }
+
+  function fail({url, pattern}) {
+    ok(!test(url, pattern), `Expected no match: ${JSON.stringify(pattern)}, ${url}`);
+  }
+
+  let moz = "http://mozilla.org";
+
+  pass({url: moz, pattern: ["*"]});
+  pass({url: moz, pattern: ["http://*"]});
+  pass({url: moz, pattern: ["*mozilla*"]});
+  // pass({url: moz, pattern: ["*example*", "*mozilla*"]});
 
-function run_test() {
-  test_matches();
-  test_overlaps();
-}
+  pass({url: moz, pattern: ["*://*"]});
+  pass({url: "https://mozilla.org", pattern: ["*://*"]});
+
+  // Documentation example
+  pass({url: "http://www.example.com/foo/bar", pattern: ["http://???.example.com/foo/*"]});
+  pass({url: "http://the.example.com/foo/", pattern: ["http://???.example.com/foo/*"]});
+  fail({url: "http://my.example.com/foo/bar", pattern: ["http://???.example.com/foo/*"]});
+  fail({url: "http://example.com/foo/", pattern: ["http://???.example.com/foo/*"]});
+  fail({url: "http://www.example.com/foo", pattern: ["http://???.example.com/foo/*"]});
+
+  // Matches path
+  let path = moz + "/abc/def";
+  pass({url: path, pattern: ["*def"]});
+  pass({url: path, pattern: ["*c/d*"]});
+  pass({url: path, pattern: ["*org/abc*"]});
+  fail({url: path + "/", pattern: ["*def"]});
+
+  // Trailing slash
+  pass({url: moz, pattern: ["*.org/"]});
+  fail({url: moz, pattern: ["*.org"]});
+
+  // Wrong TLD
+  fail({url: moz, pattern: ["*oz*.com/"]});
+  // Case sensitive
+  fail({url: moz, pattern: ["*.ORG/"]});
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_WebExtensionContentScript.js
@@ -0,0 +1,176 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const {newURI} = Services.io;
+
+const server = createHttpServer();
+server.registerDirectory("/data/", do_get_file("data"));
+
+let policy = new WebExtensionPolicy({
+  id: "foo@bar.baz",
+  mozExtensionHostname: "88fb51cd-159f-4859-83db-7065485bc9b2",
+  baseURL: "file:///foo",
+
+  allowedOrigins: new MatchPatternSet([]),
+  localizeCallback() {},
+});
+
+add_task(async function test_WebExtensinonContentScript_url_matching() {
+  let contentScript = new WebExtensionContentScript(policy, {
+    matches: new MatchPatternSet(["http://foo.com/bar", "*://bar.com/baz/*"]),
+
+    excludeMatches: new MatchPatternSet(["*://bar.com/baz/quux"]),
+
+    includeGlobs: ["*flerg*", "*.com/bar", "*/quux"].map(glob => new MatchGlob(glob)),
+
+    excludeGlobs: ["*glorg*"].map(glob => new MatchGlob(glob)),
+  });
+
+  ok(contentScript.matchesURI(newURI("http://foo.com/bar")),
+     "Simple matches include should match");
+
+  ok(contentScript.matchesURI(newURI("https://bar.com/baz/xflergx")),
+     "Simple matches include should match");
+
+  ok(!contentScript.matchesURI(newURI("https://bar.com/baz/xx")),
+     "Failed includeGlobs match pattern should not match");
+
+  ok(!contentScript.matchesURI(newURI("https://bar.com/baz/quux")),
+     "Excluded match pattern should not match");
+
+  ok(!contentScript.matchesURI(newURI("https://bar.com/baz/xflergxglorgx")),
+     "Excluded match glob should not match");
+});
+
+async function loadURL(url, {frameCount}) {
+  let windows = new Map();
+  let requests = new Map();
+
+  let resolveLoad;
+  let loadPromise = new Promise(resolve => { resolveLoad = resolve; });
+
+  function requestObserver(request) {
+    request.QueryInterface(Ci.nsIChannel);
+    if (request.isDocument) {
+      requests.set(request.name, request);
+    }
+  }
+  function loadObserver(window) {
+    windows.set(window.location.href, window);
+    if (windows.size == frameCount) {
+      resolveLoad();
+    }
+  }
+
+  Services.obs.addObserver(requestObserver, "http-on-examine-response");
+  Services.obs.addObserver(loadObserver, "content-document-global-created");
+
+  let webNav = Services.appShell.createWindowlessBrowser(false);
+  webNav.loadURI(url, 0, null, null, null);
+
+  await loadPromise;
+
+  Services.obs.removeObserver(requestObserver, "http-on-examine-response");
+  Services.obs.removeObserver(loadObserver, "content-document-global-created");
+
+  return {webNav, windows, requests};
+}
+
+add_task(async function test_WebExtensinonContentScript_frame_matching() {
+  if (AppConstants.platform == "linux") {
+    // The windowless browser currently does not load correctly on Linux on
+    // infra.
+    return;
+  }
+
+  let baseURL = `http://localhost:${server.identity.primaryPort}/data`;
+  let urls = {
+    topLevel: `${baseURL}/file_toplevel.html`,
+    iframe: `${baseURL}/file_iframe.html`,
+    srcdoc: "about:srcdoc",
+    aboutBlank: "about:blank",
+  };
+
+  let {webNav, windows, requests} = await loadURL(urls.topLevel, {frameCount: 4});
+
+  let tests = [
+    {
+      contentScript: {
+        matches: new MatchPatternSet(["http://localhost/data/*"]),
+      },
+      topLevel: true,
+      iframe: false,
+      aboutBlank: false,
+      srcdoc: false,
+    },
+
+    {
+      contentScript: {
+        matches: new MatchPatternSet(["http://localhost/data/*"]),
+        frameID: 0,
+      },
+      topLevel: true,
+      iframe: false,
+      aboutBlank: false,
+      srcdoc: false,
+    },
+
+    {
+      contentScript: {
+        matches: new MatchPatternSet(["http://localhost/data/*"]),
+        allFrames: true,
+      },
+      topLevel: true,
+      iframe: true,
+      aboutBlank: false,
+      srcdoc: false,
+    },
+
+    {
+      contentScript: {
+        matches: new MatchPatternSet(["http://localhost/data/*"]),
+        allFrames: true,
+        matchAboutBlank: true,
+      },
+      topLevel: true,
+      iframe: true,
+      aboutBlank: true,
+      srcdoc: true,
+    },
+
+    {
+      contentScript: {
+        matches: new MatchPatternSet(["http://foo.com/data/*"]),
+        allFrames: true,
+        matchAboutBlank: true,
+      },
+      topLevel: false,
+      iframe: false,
+      aboutBlank: false,
+      srcdoc: false,
+    },
+  ];
+
+  for (let [i, test] of tests.entries()) {
+    let contentScript = new WebExtensionContentScript(policy, test.contentScript);
+
+    for (let [frame, url] of Object.entries(urls)) {
+      let should = test[frame] ? "should" : "should not";
+
+      equal(contentScript.matchesWindow(windows.get(url)),
+            test[frame],
+            `Script ${i} ${should} match the ${frame} frame`);
+
+      if (url.startsWith("http")) {
+        let request = requests.get(url);
+
+        equal(contentScript.matchesLoadInfo(request.URI, request.loadInfo),
+              test[frame],
+              `Script ${i} ${should} match the request LoadInfo for ${frame} frame`);
+      }
+    }
+  }
+
+  webNav.close();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js
@@ -0,0 +1,140 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const {newURI} = Services.io;
+
+add_task(async function test_WebExtensinonPolicy() {
+  const id = "foo@bar.baz";
+  const uuid = "ca9d3f23-125c-4b24-abfc-1ca2692b0610";
+
+  const baseURL = "file:///foo/";
+  const mozExtURL = `moz-extension://${uuid}/`;
+  const mozExtURI = newURI(mozExtURL);
+
+  let policy = new WebExtensionPolicy({
+    id,
+    mozExtensionHostname: uuid,
+    baseURL,
+
+    localizeCallback(str) {
+      return `<${str}>`;
+    },
+
+    allowedOrigins: new MatchPatternSet(["http://foo.bar/", "*://*.baz/"], {ignorePath: true}),
+    permissions: ["<all_urls>"],
+    webAccessibleResources: ["/foo/*", "/bar.baz"].map(glob => new MatchGlob(glob)),
+  });
+
+  equal(policy.active, false, "Active attribute should initially be false");
+
+  // GetURL
+
+  equal(policy.getURL(), mozExtURL, "getURL() should return the correct root URL");
+  equal(policy.getURL("path/foo.html"), `${mozExtURL}path/foo.html`, "getURL(path) should return the correct URL");
+
+
+  // Permissions
+
+  deepEqual(policy.permissions, ["<all_urls>"], "Initial permissions should be correct");
+
+  ok(policy.hasPermission("<all_urls>"), "hasPermission should match existing permission");
+  ok(!policy.hasPermission("history"), "hasPermission should not match nonexistent permission");
+
+  Assert.throws(() => { policy.permissions[0] = "foo"; },
+                TypeError,
+                "Permissions array should be frozen");
+
+  policy.permissions = ["history"];
+  deepEqual(policy.permissions, ["history"], "Permissions should be updateable as a set");
+
+  ok(policy.hasPermission("history"), "hasPermission should match existing permission");
+  ok(!policy.hasPermission("<all_urls>"), "hasPermission should not match nonexistent permission");
+
+
+  // Origins
+
+  ok(policy.canAccessURI(newURI("http://foo.bar/quux")), "Should be able to access whitelisted URI");
+  ok(policy.canAccessURI(newURI("https://x.baz/foo")), "Should be able to access whitelisted URI");
+
+  ok(!policy.canAccessURI(newURI("https://foo.bar/quux")), "Should not be able to access non-whitelisted URI");
+
+  policy.allowedOrigins = new MatchPatternSet(["https://foo.bar/"], {ignorePath: true});
+
+  ok(policy.canAccessURI(newURI("https://foo.bar/quux")), "Should be able to access updated whitelisted URI");
+  ok(!policy.canAccessURI(newURI("https://x.baz/foo")), "Should not be able to access removed whitelisted URI");
+
+
+  // Web-accessible resources
+
+  ok(policy.isPathWebAccessible("/foo/bar"), "Web-accessible glob should be web-accessible");
+  ok(policy.isPathWebAccessible("/bar.baz"), "Web-accessible path should be web-accessible");
+  ok(!policy.isPathWebAccessible("/bar.baz/quux"), "Non-web-accessible path should not be web-accessible");
+
+
+  // Localization
+
+  equal(policy.localize("foo"), "<foo>", "Localization callback should work as expected");
+
+
+  // Protocol and lookups.
+
+  let proto = Services.io.getProtocolHandler("moz-extension", uuid).QueryInterface(Ci.nsISubstitutingProtocolHandler);
+
+  deepEqual(WebExtensionPolicy.getActiveExtensions(), [], "Should have no active extensions");
+  equal(WebExtensionPolicy.getByID(id), null, "ID lookup should not return extension when not active");
+  equal(WebExtensionPolicy.getByHostname(uuid), null, "Hostname lookup should not return extension when not active");
+  Assert.throws(() => proto.resolveURI(mozExtURI), /NS_ERROR_NOT_AVAILABLE/,
+                "URL should not resolve when not active");
+
+  policy.active = true;
+  equal(policy.active, true, "Active attribute should be updated");
+
+  let exts = WebExtensionPolicy.getActiveExtensions();
+  equal(exts.length, 1, "Should have one active extension");
+  equal(exts[0], policy, "Should have the correct active extension");
+
+  equal(WebExtensionPolicy.getByID(id), policy, "ID lookup should return extension when active");
+  equal(WebExtensionPolicy.getByHostname(uuid), policy, "Hostname lookup should return extension when active");
+
+  equal(proto.resolveURI(mozExtURI), baseURL, "URL should resolve correctly while active");
+
+  policy.active = false;
+  equal(policy.active, false, "Active attribute should be updated");
+
+  deepEqual(WebExtensionPolicy.getActiveExtensions(), [], "Should have no active extensions");
+  equal(WebExtensionPolicy.getByID(id), null, "ID lookup should not return extension when not active");
+  equal(WebExtensionPolicy.getByHostname(uuid), null, "Hostname lookup should not return extension when not active");
+  Assert.throws(() => proto.resolveURI(mozExtURI), /NS_ERROR_NOT_AVAILABLE/,
+                "URL should not resolve when not active");
+
+
+  // Conflicting policies.
+
+  // This asserts in debug builds, so only test in non-debug builds.
+  if (!AppConstants.DEBUG) {
+    policy.active = true;
+
+    let attrs = [{id, uuid},
+                 {id, uuid: "d916886c-cfdf-482e-b7b1-d7f5b0facfa5"},
+                 {id: "foo@quux", uuid}];
+
+    // eslint-disable-next-line no-shadow
+    for (let {id, uuid} of attrs) {
+      let policy2 = new WebExtensionPolicy({
+        id,
+        mozExtensionHostname: uuid,
+        baseURL: "file://bar/",
+
+        localizeCallback() {},
+
+        allowedOrigins: new MatchPatternSet([]),
+      });
+
+      Assert.throws(() => { policy2.active = true; }, /NS_ERROR_UNEXPECTED/,
+                    `Should not be able to activate conflicting policy: ${id} ${uuid}`);
+    }
+
+    policy.active = false;
+  }
+});
--- 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_ext_experiments.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js
@@ -137,18 +137,18 @@ add_task(async function test_experiments
     Services.obs.removeObserver(observer, "webext-api-loaded");
     Services.obs.removeObserver(observer, "webext-api-hello");
   });
 
 
   // Install API add-on.
   let apiAddon = await AddonManager.installTemporaryAddon(apiAddonFile);
 
-  let {APIs} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {});
-  ok(APIs.apis.has("meh"), "Should have meh API.");
+  let {ExtensionAPIs} = Cu.import("resource://gre/modules/ExtensionAPI.jsm", {});
+  ok(ExtensionAPIs.apis.has("meh"), "Should have meh API.");
 
 
   // Install boring WebExtension add-on.
   let boringAddon = await AddonManager.installTemporaryAddon(boringAddonFile);
   await promiseAddonStartup();
 
 
   // Install interesting WebExtension add-on.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
@@ -24,18 +24,21 @@ function findWinUtils(extension) {
   notEqual(bgwin, null, "Found background window for the test extension");
   return bgwin.QueryInterface(Ci.nsIInterfaceRequestor)
               .getInterface(Ci.nsIDOMWindowUtils);
 }
 
 add_task(async function test_permissions() {
   const REQUIRED_PERMISSIONS = ["downloads"];
   const REQUIRED_ORIGINS = ["*://site.com/", "*://*.domain.com/"];
+  const REQUIRED_ORIGINS_NORMALIZED = ["*://site.com/*", "*://*.domain.com/*"];
+
   const OPTIONAL_PERMISSIONS = ["idle", "clipboardWrite"];
   const OPTIONAL_ORIGINS = ["http://optionalsite.com/", "https://*.optionaldomain.com/"];
+  const OPTIONAL_ORIGINS_NORMALIZED = ["http://optionalsite.com/*", "https://*.optionaldomain.com/*"];
 
   let acceptPrompt = false;
   const observer = {
     observe(subject, topic, data) {
       if (topic == "webextension-optional-permission-prompt") {
         let {resolve} = subject.wrappedJSObject;
         resolve(acceptPrompt);
       }
@@ -87,17 +90,17 @@ add_task(async function test_permissions
 
   function call(method, arg) {
     extension.sendMessage(method, arg);
     return extension.awaitMessage(`${method}.result`);
   }
 
   let result = await call("getAll");
   deepEqual(result.permissions, REQUIRED_PERMISSIONS);
-  deepEqual(result.origins, REQUIRED_ORIGINS);
+  deepEqual(result.origins, REQUIRED_ORIGINS_NORMALIZED);
 
   for (let perm of REQUIRED_PERMISSIONS) {
     result = await call("contains", {permissions: [perm]});
     equal(result, true, `contains() returns true for fixed permission ${perm}`);
   }
   for (let origin of REQUIRED_ORIGINS) {
     result = await call("contains", {origins: [origin]});
     equal(result, true, `contains() returns true for fixed origin ${origin}`);
@@ -150,17 +153,17 @@ add_task(async function test_permissions
   };
   result = await call("request", allOptional);
   equal(result.status, "success", "request() returned cleanly");
   equal(result.result, true, "request() returned true for accepted permissions");
   userInputHandle.destruct();
 
   let allPermissions = {
     permissions: [...REQUIRED_PERMISSIONS, ...OPTIONAL_PERMISSIONS],
-    origins: [...REQUIRED_ORIGINS, ...OPTIONAL_ORIGINS],
+    origins: [...REQUIRED_ORIGINS_NORMALIZED, ...OPTIONAL_ORIGINS_NORMALIZED],
   };
 
   result = await call("getAll");
   deepEqual(result, allPermissions, "getAll() returns required and runtime requested permissions");
 
   result = await call("contains", allPermissions);
   equal(result, true, "contains() returns true for runtime requested permissions");
 
@@ -172,25 +175,25 @@ add_task(async function test_permissions
   deepEqual(result, allPermissions, "Runtime requested permissions are still present after restart");
 
   // Check remove()
   result = await call("remove", {permissions: OPTIONAL_PERMISSIONS});
   equal(result, true, "remove() succeeded");
 
   let perms = {
     permissions: REQUIRED_PERMISSIONS,
-    origins: [...REQUIRED_ORIGINS, ...OPTIONAL_ORIGINS],
+    origins: [...REQUIRED_ORIGINS_NORMALIZED, ...OPTIONAL_ORIGINS_NORMALIZED],
   };
   result = await call("getAll");
   deepEqual(result, perms, "Expected permissions remain after removing some");
 
   result = await call("remove", {origins: OPTIONAL_ORIGINS});
   equal(result, true, "remove() succeeded");
 
-  perms.origins = REQUIRED_ORIGINS;
+  perms.origins = REQUIRED_ORIGINS_NORMALIZED;
   result = await call("getAll");
   deepEqual(result, perms, "Back to default permissions after removing more");
 
   await extension.unload();
 });
 
 add_task(async function test_startup() {
   async function background() {
@@ -202,17 +205,17 @@ add_task(async function test_startup() {
     let all = await browser.permissions.getAll();
     browser.test.sendMessage("perms", all);
   }
 
   const PERMS1 = {
     permissions: ["clipboardRead", "tabs"],
   };
   const PERMS2 = {
-    origins: ["https://site2.com/"],
+    origins: ["https://site2.com/*"],
   };
 
   let extension1 = ExtensionTestUtils.loadExtension({
     background,
     manifest: {optional_permissions: PERMS1.permissions},
     useAddonManager: "permanent",
   });
   let extension2 = ExtensionTestUtils.loadExtension({
--- 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/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -4,16 +4,20 @@ firefox-appdir = browser
 skip-if = appname == "thunderbird"
 dupe-manifest =
 support-files =
   data/**
   head_sync.js
   xpcshell-content.ini
 tags = webextensions
 
+[test_MatchPattern.js]
+[test_WebExtensionContentScript.js]
+[test_WebExtensionPolicy.js]
+
 [test_csp_custom_policies.js]
 [test_csp_validator.js]
 [test_ext_alarms.js]
 [test_ext_alarms_does_not_fire.js]
 [test_ext_alarms_periodic.js]
 [test_ext_alarms_replaces.js]
 [test_ext_api_permissions.js]
 [test_ext_background_generated_load_events.js]
--- 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}
--- a/toolkit/modules/addons/MatchPattern.jsm
+++ b/toolkit/modules/addons/MatchPattern.jsm
@@ -106,16 +106,18 @@ SingleMatchPattern.prototype = {
     );
   },
 
   // Tests if this can possibly overlap with the |other| SingleMatchPattern.
   overlapsIgnoringPath(other) {
     return this.schemes.some(scheme => other.schemes.includes(scheme)) &&
            (this.hostMatch(other) || other.hostMatch(this));
   },
+
+  get pattern() { return this.pat; },
 };
 
 this.MatchPattern = function(pat) {
   this.pat = pat;
   if (!pat) {
     this.matchers = [];
   } else if (pat instanceof String || typeof(pat) == "string") {
     this.matchers = [new SingleMatchPattern(pat)];
@@ -131,16 +133,18 @@ this.MatchPattern = function(pat) {
 };
 
 MatchPattern.prototype = {
   // |uri| should be an nsIURI.
   matches(uri) {
     return this.matchers.some(matcher => matcher.matches(uri));
   },
 
+  get patterns() { return this.matchers; },
+
   matchesIgnoringPath(uri, explicit = false) {
     if (explicit) {
       return this.explicitMatchers.some(matcher => matcher.matches(uri, true));
     }
     return this.matchers.some(matcher => matcher.matches(uri, true));
   },
 
   // Checks that this match pattern grants access to read the given
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -304,17 +304,17 @@ var ContentPolicyManager = {
 
   addListener(callback, opts) {
     // Clone opts, since we're going to modify them for IPC.
     opts = Object.assign({}, opts);
     let id = this.nextId++;
     opts.id = id;
     if (opts.filter.urls) {
       opts.filter = Object.assign({}, opts.filter);
-      opts.filter.urls = opts.filter.urls.serialize();
+      opts.filter.urls = opts.filter.urls.patterns.map(url => url.pattern);
     }
     Services.ppmm.broadcastAsyncMessage("WebRequest:AddContentPolicy", opts);
 
     this.policyData.set(id, opts);
 
     this.policies.set(id, callback);
     this.idMap.set(callback, id);
   },
--- 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
--- a/toolkit/mozapps/extensions/AddonManagerWebAPI.h
+++ b/toolkit/mozapps/extensions/AddonManagerWebAPI.h
@@ -10,17 +10,16 @@
 #include "nsPIDOMWindow.h"
 
 namespace mozilla {
 
 class AddonManagerWebAPI {
 public:
   static bool IsAPIEnabled(JSContext* cx, JSObject* obj);
 
-private:
   static bool IsValidSite(nsIURI* uri);
 };
 
 namespace dom {
 
 class AddonManagerPermissions {
 public:
   static bool IsHostPermitted(const GlobalObject&, const nsAString& host);
--- a/toolkit/mozapps/extensions/internal/APIExtensionBootstrap.js
+++ b/toolkit/mozapps/extensions/internal/APIExtensionBootstrap.js
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-/* exported startup, shutdown, install, uninstall */
+/* exported startup, shutdown, install, uninstall, ExtensionAPIs */
 
-Components.utils.import("resource://gre/modules/ExtensionManagement.jsm");
+Components.utils.import("resource://gre/modules/ExtensionAPI.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 var namespace;
 var resource;
 var resProto;
 
 function install(data, reason) {
 }
@@ -20,22 +20,22 @@ function startup(data, reason) {
   namespace = data.id.replace(/@.*/, "");
   resource = `extension-${namespace}-api`;
 
   resProto = Services.io.getProtocolHandler("resource")
                      .QueryInterface(Components.interfaces.nsIResProtocolHandler);
 
   resProto.setSubstitution(resource, data.resourceURI);
 
-  ExtensionManagement.registerAPI(
+  ExtensionAPIs.register(
     namespace,
     `resource://${resource}/schema.json`,
     `resource://${resource}/api.js`);
 }
 
 function shutdown(data, reason) {
   resProto.setSubstitution(resource, null);
 
-  ExtensionManagement.unregisterAPI(namespace);
+  ExtensionAPIs.unregister(namespace);
 }
 
 function uninstall(data, reason) {
 }
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -31,16 +31,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyGetter(this, "CertUtils",
                             () => Cu.import("resource://gre/modules/CertUtils.jsm", {}));
 XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser",
                                   "resource://gre/modules/ChromeManifestParser.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData",
                                   "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Locale",
+                                  "resource://gre/modules/Locale.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyGetter(this, "IconDetails", () => {
   return Cu.import("resource://gre/modules/ExtensionUtils.jsm", {}).ExtensionUtils.IconDetails;
 });
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -6,33 +6,33 @@
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 this.EXPORTED_SYMBOLS = ["XPIProvider", "XPIInternal"];
 
+/* globals WebExtensionPolicy */
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
                                   "resource://gre/modules/addons/AddonRepository.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonSettings",
                                   "resource://gre/modules/addons/AddonSettings.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser",
                                   "resource://gre/modules/ChromeManifestParser.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
-                                  "resource://gre/modules/ExtensionManagement.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Locale",
                                   "resource://gre/modules/Locale.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils",
                                   "resource://gre/modules/ZipUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
@@ -5213,20 +5213,21 @@ AddonWrapper.prototype = {
 
     let addon = addonFor(this);
     if (addon.optionsURL) {
       if (this.isWebExtension || this.hasEmbeddedWebExtension) {
         // The internal object's optionsURL property comes from the addons
         // DB and should be a relative URL.  However, extensions with
         // options pages installed before bug 1293721 was fixed got absolute
         // URLs in the addons db.  This code handles both cases.
-        let base = ExtensionManagement.getURLForExtension(addon.id);
-        if (!base) {
+        let policy = WebExtensionPolicy.getByID(addon.id);
+        if (!policy) {
           return null;
         }
+        let base = policy.getURL();
         return new URL(addon.optionsURL, base).href;
       }
       return addon.optionsURL;
     }
 
     if (this.hasResource("options.xul"))
       return this.getResourceURI("options.xul").spec;
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js
@@ -14,17 +14,17 @@ function backgroundGetSelf() {
     browser.test.notifyFail(`getSelf rejected with error: ${error}`);
   });
 }
 /* eslint-enable no-undef */
 
 add_task(async function test_management_get_self_complete() {
   const id = "get_self_test_complete@tests.mozilla.com";
   const permissions = ["management", "cookies"];
-  const hostPermissions = ["*://example.org/", "https://foo.example.org/"];
+  const hostPermissions = ["*://example.org/*", "https://foo.example.org/*"];
 
   let manifest = {
     applications: {
       gecko: {
         id,
         update_url: "https://updates.mozilla.com/",
       },
     },
--- a/tools/profiler/core/platform.cpp
+++ b/tools/profiler/core/platform.cpp
@@ -722,21 +722,18 @@ AddDynamicCodeLocationTag(ProfileBuffer*
   }
 }
 
 static void
 AddPseudoEntry(PSLockRef aLock, ProfileBuffer* aBuffer,
                volatile js::ProfileEntry& entry,
                NotNull<RacyThreadInfo*> aRacyInfo)
 {
-  // Pseudo-frames with the BEGIN_PSEUDO_JS flag are just annotations and
-  // should not be recorded in the profile.
-  if (entry.hasFlag(js::ProfileEntry::BEGIN_PSEUDO_JS)) {
-    return;
-  }
+  MOZ_ASSERT(entry.kind() == js::ProfileEntry::Kind::CPP_NORMAL ||
+             entry.kind() == js::ProfileEntry::Kind::JS_NORMAL);
 
   int lineno = -1;
 
   // First entry has kind CodeLocation.
   const char* label = entry.label();
   bool includeDynamicString = !ActivePS::FeaturePrivacy(aLock);
   const char* dynamicString =
     includeDynamicString ? entry.dynamicString() : nullptr;
@@ -792,22 +789,17 @@ AddPseudoEntry(PSLockRef aLock, ProfileB
       lineno = entry.line();
     }
   }
 
   if (lineno != -1) {
     aBuffer->addTag(ProfileBufferEntry::LineNumber(lineno));
   }
 
-  uint32_t category = entry.category();
-  MOZ_ASSERT(!(category & js::ProfileEntry::IS_CPP_ENTRY));
-
-  if (category) {
-    aBuffer->addTag(ProfileBufferEntry::Category((int)category));
-  }
+  aBuffer->addTag(ProfileBufferEntry::Category(uint32_t(entry.category())));
 }
 
 struct NativeStack
 {
   void** pc_array;
   void** sp_array;
   size_t size;
   size_t count;
@@ -911,23 +903,22 @@ MergeStacksIntoProfile(PSLockRef aLock, 
 
     if (pseudoIndex != pseudoCount) {
       volatile js::ProfileEntry& pseudoEntry = pseudoEntries[pseudoIndex];
 
       if (pseudoEntry.isCpp()) {
         lastPseudoCppStackAddr = (uint8_t*) pseudoEntry.stackAddress();
       }
 
-      // Skip any pseudo-stack JS frames which are marked isOSR. Pseudostack
-      // frames are marked isOSR when the JS interpreter enters a jit frame on
-      // a loop edge (via on-stack-replacement, or OSR). To avoid both the
-      // pseudoframe and jit frame being recorded (and showing up twice), the
-      // interpreter marks the interpreter pseudostack entry with the OSR flag
-      // to ensure that it doesn't get counted.
-      if (pseudoEntry.isJs() && pseudoEntry.isOSR()) {
+      // Skip any JS_OSR frames. Such frames are used when the JS interpreter
+      // enters a jit frame on a loop edge (via on-stack-replacement, or OSR).
+      // To avoid both the pseudoframe and jit frame being recorded (and
+      // showing up twice), the interpreter marks the interpreter pseudostack
+      // frame as JS_OSR to ensure that it doesn't get counted.
+      if (pseudoEntry.kind() == js::ProfileEntry::Kind::JS_OSR) {
           pseudoIndex++;
           continue;
       }
 
       MOZ_ASSERT(lastPseudoCppStackAddr);
       pseudoStackAddr = lastPseudoCppStackAddr;
     }
 
@@ -957,17 +948,22 @@ MergeStacksIntoProfile(PSLockRef aLock, 
                                jsStackAddr != nativeStackAddr);
     MOZ_ASSERT_IF(nativeStackAddr, nativeStackAddr != pseudoStackAddr &&
                                    nativeStackAddr != jsStackAddr);
 
     // Check to see if pseudoStack frame is top-most.
     if (pseudoStackAddr > jsStackAddr && pseudoStackAddr > nativeStackAddr) {
       MOZ_ASSERT(pseudoIndex < pseudoCount);
       volatile js::ProfileEntry& pseudoEntry = pseudoEntries[pseudoIndex];
-      AddPseudoEntry(aLock, aBuffer, pseudoEntry, racyInfo);
+
+      // Pseudo-frames with the CPP_MARKER_FOR_JS kind are just annotations and
+      // should not be recorded in the profile.
+      if (pseudoEntry.kind() != js::ProfileEntry::Kind::CPP_MARKER_FOR_JS) {
+        AddPseudoEntry(aLock, aBuffer, pseudoEntry, racyInfo);
+      }
       pseudoIndex++;
       continue;
     }
 
     // Check to see if JS jit stack frame is top-most
     if (jsStackAddr > nativeStackAddr) {
       MOZ_ASSERT(jsIndex >= 0);
       const JS::ProfilingFrameIterator::Frame& jsFrame = jsFrames[jsIndex];
--- a/tools/profiler/public/GeckoProfiler.h
+++ b/tools/profiler/public/GeckoProfiler.h
@@ -424,17 +424,18 @@ profiler_call_enter(const char* aLabel, 
                     const char* aDynamicString = nullptr)
 {
   // This function runs both on and off the main thread.
 
   PseudoStack* pseudoStack = sPseudoStack.get();
   if (!pseudoStack) {
     return pseudoStack;
   }
-  pseudoStack->pushCppFrame(aLabel, aDynamicString, aFrameAddress, aLine, aCategory);
+  pseudoStack->pushCppFrame(aLabel, aDynamicString, aFrameAddress, aLine,
+                            js::ProfileEntry::Kind::CPP_NORMAL, aCategory);
 
   // The handle is meant to support future changes but for now it is simply
   // used to avoid having to call TLSInfo::RacyInfo() in profiler_call_exit().
   return pseudoStack;
 }
 
 inline void
 profiler_call_exit(void* aHandle)