merge mozilla-inbound to mozilla-central. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Mon, 05 Jun 2017 11:05:49 +0200
changeset 410389 275588f4d852d7dc183a9dcc70a311413dc7a063
parent 410354 7d5df2dead2c9edaa41688a82d9d9dcc86529ef2 (current diff)
parent 410388 da5a14ae4ce3084aa63a502e96af1a4c6ef76853 (diff)
child 410390 6d4b65abc362bd5f7b6406d3b98fa538a1f492eb
child 410407 81e0a353d2dc82c8ea71a5dc26956f292a867895
child 410435 43383407d7c14ca3f1ee006e2b57ec5fe3ff0d3e
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
275588f4d852 / 55.0a1 / 20170605100313 / files
nightly linux64
275588f4d852 / 55.0a1 / 20170605100313 / files
nightly mac
275588f4d852 / 55.0a1 / 20170605030204 / files
nightly win32
275588f4d852 / 55.0a1 / 20170605030204 / files
nightly win64
275588f4d852 / 55.0a1 / 20170605030204 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge mozilla-inbound to mozilla-central. r=merge a=merge MozReview-Commit-ID: G0dLvIdtcAi
toolkit/components/extensions/ExtensionManagement.jsm
toolkit/components/extensions/ExtensionParent.jsm
toolkit/components/extensions/test/xpcshell/xpcshell.ini
toolkit/mozapps/extensions/internal/XPIInstall.jsm
toolkit/mozapps/extensions/internal/XPIProvider.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)