Merge mozilla-central to autoland. a=merge CLOSED TREE
authorMargareta Eliza Balazs <ebalazs@mozilla.com>
Tue, 24 Apr 2018 14:51:28 +0300
changeset 468824 e583796f1c09477e789945e22361fa7c5674e70a
parent 468823 52805bf2d0f867b148e7f8e90ac1a3b08dd1ad3d (current diff)
parent 468764 26e53729a10976f52e75efa44e17b5e054969fec (diff)
child 468825 77e5d42d7d55a5994ca54707b7abbf3d72167852
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to autoland. a=merge CLOSED TREE
browser/app/profile/extensions/moz.build
browser/app/profile/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/install.rdf.in
browser/app/profile/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/moz.build
browser/base/content/default-theme-icon.svg
browser/base/content/test/general/browser_bug592338.js
browser/locales/en-US/chrome/browser/browser.properties
testing/mozharness/configs/releases/checksums_devedition.py
testing/mozharness/configs/releases/checksums_fennec.py
testing/mozharness/configs/releases/checksums_firefox.py
testing/mozharness/configs/releases/dev_checksums_devedition.py
testing/mozharness/configs/releases/dev_checksums_fennec.py
testing/mozharness/configs/releases/dev_checksums_firefox.py
testing/web-platform/meta/css/css-align/gaps/gap-normal-computed-001.html.ini
testing/web-platform/meta/css/css-align/gaps/gap-parsing-001.html.ini
testing/web-platform/meta/css/css-align/gaps/grid-column-gap-parsing-001.html.ini
testing/web-platform/meta/css/css-align/gaps/grid-gap-parsing-001.html.ini
testing/web-platform/meta/css/css-align/gaps/grid-row-gap-parsing-001.html.ini
testing/web-platform/meta/css/css-align/gaps/row-gap-animation-001.html.ini
testing/web-platform/meta/css/css-align/gaps/row-gap-animation-002.html.ini
testing/web-platform/meta/css/css-align/gaps/row-gap-animation-003.html.ini
testing/web-platform/meta/css/css-align/gaps/row-gap-parsing-001.html.ini
testing/web-platform/meta/css/css-grid/alignment/grid-gutters-001.html.ini
testing/web-platform/meta/css/css-grid/alignment/grid-gutters-003.html.ini
testing/web-platform/meta/css/css-grid/alignment/grid-gutters-005.html.ini
testing/web-platform/meta/css/css-grid/alignment/grid-gutters-007.html.ini
testing/web-platform/meta/css/css-grid/alignment/grid-gutters-009.html.ini
testing/web-platform/meta/css/css-grid/alignment/grid-gutters-011.html.ini
toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
toolkit/mozapps/extensions/internal/XPIDatabase.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
toolkit/mozapps/extensions/jar.mn
toolkit/mozapps/extensions/test/xpinstall/theme.xpi
--- a/browser/app/moz.build
+++ b/browser/app/moz.build
@@ -19,26 +19,21 @@ with Files("macversion.py"):
 with Files("macbuild/**"):
     BUG_COMPONENT = ("Core", "Widget: Cocoa")
 
 with Files("moz.build"):
     BUG_COMPONENT = ("Firefox Build System", "General")
 with Files("Makefile.in"):
     BUG_COMPONENT = ("Firefox Build System", "General")
 
-with Files("profile/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/**"):
-    BUG_COMPONENT = ("Firefox", "Theme")
 with Files("profile/channel-prefs.js"):
     BUG_COMPONENT = ("Firefox", "Installer")
 with Files("profile/firefox.js"):
     BUG_COMPONENT = ("Firefox", "General")
 
-
-DIRS += ['profile/extensions']
-
 GeckoProgram(CONFIG['MOZ_APP_NAME'])
 
 SOURCES += [
     'nsBrowserApp.cpp',
 ]
 
 # Neither channel-prefs.js nor firefox.exe want to end up in dist/bin/browser.
 DIST_SUBDIR = ""
deleted file mode 100644
--- a/browser/app/profile/extensions/moz.build
+++ /dev/null
@@ -1,7 +0,0 @@
-# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
-# vim: set filetype=python:
-# 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/.
-
-DIRS += ['{972ce4c6-7e08-4474-a285-3208198ce6fd}']
deleted file mode 100644
--- a/browser/app/profile/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/install.rdf.in
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.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/. -->
-
-
-#filter substitution
-
-<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
-
-  <Description about="urn:mozilla:install-manifest">
-    <em:id>{972ce4c6-7e08-4474-a285-3208198ce6fd}</em:id>
-    <em:version>@FIREFOX_VERSION@</em:version>
-
-    <!-- Target Application this theme can install into,
-        with minimum and maximum supported versions. -->
-    <em:targetApplication>
-      <Description>
-        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
-        <em:minVersion>@FIREFOX_VERSION@</em:minVersion>
-        <em:maxVersion>@FIREFOX_VERSION@</em:maxVersion>
-      </Description>
-    </em:targetApplication>
-
-    <!-- Front End MetaData -->
-    <em:name>Default</em:name>
-    <em:description>The default theme.</em:description>
-
-    <!-- Front End Integration Hooks (used by Theme Manager)-->
-    <em:creator>Mozilla</em:creator>
-    <em:contributor>Mozilla Contributors</em:contributor>
-
-    <!-- Allow lightweight themes to apply to this theme -->
-    <em:skinnable>true</em:skinnable>
-
-    <em:internalName>classic/1.0</em:internalName>
-
-    <em:iconURL>chrome://browser/content/default-theme-icon.svg</em:iconURL>
-  </Description>
-
-</RDF>
deleted file mode 100644
--- a/browser/app/profile/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/moz.build
+++ /dev/null
@@ -1,11 +0,0 @@
-# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
-# vim: set filetype=python:
-# 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/.
-
-FINAL_TARGET = 'dist/bin/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}'
-
-FINAL_TARGET_PP_FILES += [
-    'install.rdf.in',
-]
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -76,17 +76,17 @@ pref("extensions.geckoProfiler.getSymbol
 pref("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:;");
 pref("extensions.webextensions.default-content-security-policy", "script-src 'self'; object-src 'self';");
 
 #if defined(XP_WIN) || defined(XP_MACOSX)
 pref("extensions.webextensions.remote", true);
 #endif
 
 // Extensions that should not be flagged as legacy in about:addons
-pref("extensions.legacy.exceptions", "{972ce4c6-7e08-4474-a285-3208198ce6fd},testpilot@cliqz.com,@testpilot-containers,jid1-NeEaf3sAHdKHPA@jetpack,@activity-streams,pulse@mozilla.com,@testpilot-addon,@min-vid,tabcentertest1@mozilla.com,snoozetabs@mozilla.com,speaktome@mozilla.com,hoverpad@mozilla.com");
+pref("extensions.legacy.exceptions", "testpilot@cliqz.com,@testpilot-containers,jid1-NeEaf3sAHdKHPA@jetpack,@activity-streams,pulse@mozilla.com,@testpilot-addon,@min-vid,tabcentertest1@mozilla.com,snoozetabs@mozilla.com,speaktome@mozilla.com,hoverpad@mozilla.com");
 
 // Require signed add-ons by default
 pref("extensions.langpacks.signatures.required", true);
 pref("xpinstall.signatures.required", true);
 pref("xpinstall.signatures.devInfoURL", "https://wiki.mozilla.org/Addons/Extension_Signing");
 
 // Dictionary download preference
 pref("browser.dictionaries.download.url", "https://addons.mozilla.org/%LOCALE%/firefox/dictionaries/");
@@ -185,19 +185,16 @@ pref("app.update.service.enabled", true)
 //  .. etc ..
 //
 pref("extensions.update.enabled", true);
 pref("extensions.update.url", "https://versioncheck.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=%APP_VERSION%&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%&currentAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
 pref("extensions.update.background.url", "https://versioncheck-bg.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=%APP_VERSION%&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%&currentAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
 pref("extensions.update.interval", 86400);  // Check for updates to Extensions and
                                             // Themes every day
 
-pref("extensions.{972ce4c6-7e08-4474-a285-3208198ce6fd}.name", "chrome://browser/locale/browser.properties");
-pref("extensions.{972ce4c6-7e08-4474-a285-3208198ce6fd}.description", "chrome://browser/locale/browser.properties");
-
 pref("extensions.webextensions.themes.enabled", true);
 pref("extensions.webextensions.themes.icons.buttons", "back,forward,reload,stop,bookmark_star,bookmark_menu,downloads,home,app_menu,cut,copy,paste,new_window,new_private_window,save_page,print,history,full_screen,find,options,addons,developer,synced_tabs,open_file,sidebars,share_page,subscribe,text_encoding,email_link,forget,pocket");
 
 pref("lightweightThemes.update.enabled", true);
 pref("lightweightThemes.getMoreURL", "https://addons.mozilla.org/%LOCALE%/firefox/themes");
 pref("lightweightThemes.recommendedThemes", "[{\"id\":\"recommended-1\",\"homepageURL\":\"https://addons.mozilla.org/firefox/addon/a-web-browser-renaissance/\",\"headerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/1.header.jpg\",\"textcolor\":\"#000000\",\"accentcolor\":\"#834d29\",\"iconURL\":\"resource:///chrome/browser/content/browser/defaultthemes/1.icon.jpg\",\"previewURL\":\"resource:///chrome/browser/content/browser/defaultthemes/1.preview.jpg\",\"author\":\"Sean.Martell\",\"version\":\"0\"},{\"id\":\"recommended-2\",\"homepageURL\":\"https://addons.mozilla.org/firefox/addon/space-fantasy/\",\"headerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/2.header.jpg\",\"textcolor\":\"#ffffff\",\"accentcolor\":\"#d9d9d9\",\"iconURL\":\"resource:///chrome/browser/content/browser/defaultthemes/2.icon.jpg\",\"previewURL\":\"resource:///chrome/browser/content/browser/defaultthemes/2.preview.jpg\",\"author\":\"fx5800p\",\"version\":\"1.0\"},{\"id\":\"recommended-4\",\"homepageURL\":\"https://addons.mozilla.org/firefox/addon/pastel-gradient/\",\"headerURL\":\"resource:///chrome/browser/content/browser/defaultthemes/4.header.png\",\"textcolor\":\"#000000\",\"accentcolor\":\"#000000\",\"iconURL\":\"resource:///chrome/browser/content/browser/defaultthemes/4.icon.png\",\"previewURL\":\"resource:///chrome/browser/content/browser/defaultthemes/4.preview.png\",\"author\":\"darrinhenein\",\"version\":\"1.0\"}]");
 
 #if defined(MOZ_WIDEVINE_EME)
@@ -1252,17 +1249,17 @@ pref("services.sync.prefs.sync.xpinstall
 // user's tabs and bookmarks. Note this pref is also synced.
 pref("services.sync.syncedTabs.showRemoteIcons", true);
 
 // Developer edition preferences
 #ifdef MOZ_DEV_EDITION
 pref("lightweightThemes.selectedThemeID", "firefox-compact-dark@mozilla.org",
      sticky);
 #else
-pref("lightweightThemes.selectedThemeID", "", sticky);
+pref("lightweightThemes.selectedThemeID", "default-theme@mozilla.org", sticky);
 #endif
 
 // Whether the character encoding menu is under the main Firefox button. This
 // preference is a string so that localizers can alter it.
 pref("browser.menu.showCharacterEncoding", "chrome://browser/locale/browser.properties");
 
 // Allow using tab-modal prompts when possible.
 pref("prompts.tab_modal.enabled", true);
--- a/browser/base/content/moz.build
+++ b/browser/base/content/moz.build
@@ -141,19 +141,16 @@ with Files("browser-sync.js"):
     BUG_COMPONENT = ("Firefox", "Sync")
 
 with Files("browser-tabPreviews.xml"):
     BUG_COMPONENT = ("Firefox", "Tabbed Browser")
 
 with Files("contentSearch*"):
     BUG_COMPONENT = ("Firefox", "Search")
 
-with Files("*.svg"):
-    BUG_COMPONENT = ("Firefox", "Theme")
-
 with Files("hiddenWindow.xul"):
     BUG_COMPONENT = ("Firefox", "Device Permissions")
 
 with Files("macWindow.inc.xul"):
     BUG_COMPONENT = ("Firefox", "Shell Integration")
 
 with Files("tabbrowser*"):
     BUG_COMPONENT = ("Firefox", "Tabbed Browser")
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -73,17 +73,16 @@ support-files =
   video.ogg
   web_video.html
   web_video1.ogv
   web_video1.ogv^headers^
   zoom_test.html
   !/image/test/mochitest/blue.png
   !/toolkit/content/tests/browser/common/mockTransfer.js
   !/toolkit/modules/tests/browser/metadata_*.html
-  !/toolkit/mozapps/extensions/test/xpinstall/theme.xpi
 
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_addKeywordSearch.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_alltabslistener.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_audioTabIcon.js]
 tags = audiochannel
@@ -206,18 +205,16 @@ skip-if = toolkit != "cocoa" # Because o
 [browser_bug581242.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug581253.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug585785.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug585830.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_bug592338.js]
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug594131.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug596687.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug597218.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug609700.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
deleted file mode 100644
--- a/browser/base/content/test/general/browser_bug592338.js
+++ /dev/null
@@ -1,117 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- */
-
-const TESTROOT = "http://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/";
-
-const {LightweightThemeManager} = ChromeUtils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
-
-/**
- * Wait for the given PopupNotification to display
- *
- * @param {string} name
- *        The name of the notification to wait for.
- *
- * @returns {Promise}
- *          Resolves with the notification window.
- */
-function promisePopupNotificationShown(name) {
-  return new Promise(resolve => {
-    function popupshown() {
-      let notification = PopupNotifications.getNotification(name);
-      if (!notification) { return; }
-
-      ok(notification, `${name} notification shown`);
-      ok(PopupNotifications.isPanelOpen, "notification panel open");
-
-      PopupNotifications.panel.removeEventListener("popupshown", popupshown);
-      resolve(PopupNotifications.panel.firstChild);
-    }
-
-    PopupNotifications.panel.addEventListener("popupshown", popupshown);
-  });
-}
-
-
-var TESTS = [
-function test_install_http() {
-  is(LightweightThemeManager.currentTheme, null, "Should be no lightweight theme selected");
-
-  var pm = Services.perms;
-  pm.add(makeURI("http://example.org/"), "install", pm.ALLOW_ACTION);
-
-  // NB: Not https so no installs allowed.
-  const URL = "http://example.org/browser/browser/base/content/test/general/bug592338.html";
-  BrowserTestUtils.openNewForegroundTab({ gBrowser, url: URL }).then(async function() {
-    let prompted = promisePopupNotificationShown("addon-webext-permissions");
-    BrowserTestUtils.synthesizeMouse("#theme-install", 2, 2, {}, gBrowser.selectedBrowser);
-    await prompted;
-
-    is(LightweightThemeManager.currentTheme, null, "Should not have installed the test theme");
-
-    gBrowser.removeTab(gBrowser.selectedTab);
-
-    pm.remove(makeURI("http://example.org/"), "install");
-
-    runNextTest();
-  });
-},
-
-function test_install_lwtheme() {
-  is(LightweightThemeManager.currentTheme, null, "Should be no lightweight theme selected");
-
-  var pm = Services.perms;
-  pm.add(makeURI("https://example.com/"), "install", pm.ALLOW_ACTION);
-
-  const URL = "https://example.com/browser/browser/base/content/test/general/bug592338.html";
-  BrowserTestUtils.openNewForegroundTab({ gBrowser, url: URL }).then(() => {
-    let promise = promisePopupNotificationShown("addon-installed");
-    BrowserTestUtils.synthesizeMouse("#theme-install", 2, 2, {}, gBrowser.selectedBrowser);
-    promise.then(() => {
-      is(LightweightThemeManager.currentTheme.id, "test", "Should have installed the test theme");
-
-      LightweightThemeManager.currentTheme = null;
-      gBrowser.removeTab(gBrowser.selectedTab);
-      Services.perms.remove(makeURI("http://example.com/"), "install");
-
-      runNextTest();
-    });
-  });
-}
-];
-
-async function runNextTest() {
-  let aInstalls = await AddonManager.getAllInstalls();
-  is(aInstalls.length, 0, "Should be no active installs");
-
-  if (TESTS.length == 0) {
-    let aAddon = await AddonManager.getAddonByID("theme-xpi@tests.mozilla.org");
-    aAddon.uninstall();
-
-    Services.prefs.setBoolPref("extensions.logging.enabled", false);
-
-    finish();
-    return;
-  }
-
-  info("Running " + TESTS[0].name);
-  TESTS.shift()();
-}
-
-async function test() {
-  waitForExplicitFinish();
-
-  Services.prefs.setBoolPref("extensions.logging.enabled", true);
-
-  let aInstall = await AddonManager.getInstallForURL(TESTROOT + "theme.xpi", "application/x-xpinstall");
-  aInstall.addListener({
-    async onInstallEnded() {
-      let aAddon = await AddonManager.getAddonByID("theme-xpi@tests.mozilla.org");
-      isnot(aAddon, null, "Should have installed the test theme.");
-
-      runNextTest();
-    }
-  });
-
-  aInstall.install();
-}
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -71,34 +71,33 @@ browser.jar:
         content/browser/browser-sync.js               (content/browser-sync.js)
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)
         content/browser/browser-tabsintitlebar.js       (content/browser-tabsintitlebar.js)
         content/browser/browser-thumbnails.js         (content/browser-thumbnails.js)
         content/browser/browser-trackingprotection.js (content/browser-trackingprotection.js)
         content/browser/browser-webrender.js          (content/browser-webrender.js)
         content/browser/tab-content.js                (content/tab-content.js)
         content/browser/content.js                    (content/content.js)
-        content/browser/default-theme-icon.svg        (content/default-theme-icon.svg)
         content/browser/defaultthemes/1.header.jpg    (content/defaultthemes/1.header.jpg)
         content/browser/defaultthemes/1.icon.jpg      (content/defaultthemes/1.icon.jpg)
         content/browser/defaultthemes/1.preview.jpg   (content/defaultthemes/1.preview.jpg)
         content/browser/defaultthemes/2.header.jpg    (content/defaultthemes/2.header.jpg)
         content/browser/defaultthemes/2.icon.jpg      (content/defaultthemes/2.icon.jpg)
         content/browser/defaultthemes/2.preview.jpg   (content/defaultthemes/2.preview.jpg)
         content/browser/defaultthemes/3.header.png    (content/defaultthemes/3.header.png)
         content/browser/defaultthemes/3.icon.png      (content/defaultthemes/3.icon.png)
         content/browser/defaultthemes/3.preview.png   (content/defaultthemes/3.preview.png)
         content/browser/defaultthemes/4.header.png    (content/defaultthemes/4.header.png)
         content/browser/defaultthemes/4.icon.png      (content/defaultthemes/4.icon.png)
         content/browser/defaultthemes/4.preview.png   (content/defaultthemes/4.preview.png)
         content/browser/defaultthemes/5.header.png    (content/defaultthemes/5.header.png)
         content/browser/defaultthemes/5.icon.jpg      (content/defaultthemes/5.icon.jpg)
         content/browser/defaultthemes/5.preview.jpg   (content/defaultthemes/5.preview.jpg)
-        content/browser/defaultthemes/dark.icon.svg  (content/defaultthemes/dark.icon.svg)
-        content/browser/defaultthemes/light.icon.svg (content/defaultthemes/light.icon.svg)
+        content/browser/defaultthemes/dark.icon.svg   (content/defaultthemes/dark.icon.svg)
+        content/browser/defaultthemes/light.icon.svg  (content/defaultthemes/light.icon.svg)
 *       content/browser/pageinfo/pageInfo.xul         (content/pageinfo/pageInfo.xul)
         content/browser/pageinfo/pageInfo.js          (content/pageinfo/pageInfo.js)
         content/browser/pageinfo/pageInfo.css         (content/pageinfo/pageInfo.css)
         content/browser/pageinfo/feeds.js             (content/pageinfo/feeds.js)
         content/browser/pageinfo/permissions.js       (content/pageinfo/permissions.js)
         content/browser/pageinfo/security.js          (content/pageinfo/security.js)
         content/browser/robot.ico                     (content/robot.ico)
         content/browser/static-robot.png              (content/static-robot.png)
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -2900,17 +2900,18 @@ var CustomizableUIInternal = {
       return false;
     }
 
     if (Services.prefs.prefHasUserValue(kPrefExtraDragSpace)) {
       log.debug(kPrefExtraDragSpace + " pref is non-default");
       return false;
     }
 
-    if (LightweightThemeManager.currentTheme) {
+    if (LightweightThemeManager.currentTheme &&
+        LightweightThemeManager.currentTheme.id != "default-theme@mozilla.org") {
       log.debug(LightweightThemeManager.currentTheme + " theme is non-default");
       return false;
     }
 
     return true;
   },
 
   setToolbarVisibility(aToolbarId, aIsVisible) {
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -1334,18 +1334,18 @@ CustomizeMode.prototype = {
   updateAutoTouchMode(checked) {
     Services.prefs.setBoolPref("browser.touchmode.auto", checked);
     // Re-render the menu items since the active mode might have
     // change because of this.
     this.onUIDensityMenuShowing();
     this._onUIChange();
   },
 
-  async onLWThemesMenuShowing(aEvent) {
-    const DEFAULT_THEME_ID = "{972ce4c6-7e08-4474-a285-3208198ce6fd}";
+  onLWThemesMenuShowing(aEvent) {
+    const DEFAULT_THEME_ID = "default-theme@mozilla.org";
     const LIGHT_THEME_ID = "firefox-compact-light@mozilla.org";
     const DARK_THEME_ID = "firefox-compact-dark@mozilla.org";
     const MAX_THEME_COUNT = 6;
 
     this._clearLWThemesMenu(aEvent.target);
 
     function previewTheme(aPreviewThemeEvent) {
       LightweightThemeManager.previewTheme(
@@ -1358,31 +1358,23 @@ CustomizeMode.prototype = {
     }
 
     let onThemeSelected = panel => {
       this._updateLWThemeButtonIcon();
       this._onUIChange();
       panel.hidePopup();
     };
 
-    let aDefaultTheme = await AddonManager.getAddonByID(DEFAULT_THEME_ID);
     let doc = this.window.document;
 
     function buildToolbarButton(aTheme) {
       let tbb = doc.createElement("toolbarbutton");
       tbb.theme = aTheme;
       tbb.setAttribute("label", aTheme.name);
-      if (aDefaultTheme == aTheme) {
-        // The actual icon is set up so it looks nice in about:addons, but
-        // we'd like the version that's correct for the OS we're on, so we set
-        // an attribute that our styling will then use to display the icon.
-        tbb.setAttribute("defaulttheme", "true");
-      } else {
-        tbb.setAttribute("image", aTheme.iconURL);
-      }
+      tbb.setAttribute("image", aTheme.iconURL);
       if (aTheme.description)
         tbb.setAttribute("tooltiptext", aTheme.description);
       tbb.setAttribute("tabindex", "0");
       tbb.classList.add("customization-lwtheme-menu-theme");
       let isActive = activeThemeID == aTheme.id;
       tbb.setAttribute("aria-checked", isActive);
       tbb.setAttribute("role", "menuitemradio");
       if (isActive) {
@@ -1391,24 +1383,24 @@ CustomizeMode.prototype = {
       tbb.addEventListener("focus", previewTheme);
       tbb.addEventListener("mouseover", previewTheme);
       tbb.addEventListener("blur", resetPreview);
       tbb.addEventListener("mouseout", resetPreview);
 
       return tbb;
     }
 
-    let themes = [aDefaultTheme];
+    let themes = [];
     let lwts = LightweightThemeManager.usedThemes;
     let currentLwt = LightweightThemeManager.currentTheme;
 
     let activeThemeID = currentLwt ? currentLwt.id : DEFAULT_THEME_ID;
 
     // Move the current theme (if any) and the light/dark themes to the start:
-    let importantThemes = [LIGHT_THEME_ID, DARK_THEME_ID];
+    let importantThemes = [DEFAULT_THEME_ID, LIGHT_THEME_ID, DARK_THEME_ID];
     if (currentLwt && !importantThemes.includes(currentLwt.id)) {
       importantThemes.push(currentLwt.id);
     }
     for (let importantTheme of importantThemes) {
       let themeIndex = lwts.findIndex(theme => theme.id == importantTheme);
       if (themeIndex > -1) {
         themes.push(...lwts.splice(themeIndex, 1));
       }
--- a/browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js
+++ b/browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js
@@ -1,15 +1,15 @@
 /* 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";
 
-const DEFAULT_THEME_ID = "{972ce4c6-7e08-4474-a285-3208198ce6fd}";
+const DEFAULT_THEME_ID = "default-theme@mozilla.org";
 const LIGHT_THEME_ID = "firefox-compact-light@mozilla.org";
 const DARK_THEME_ID = "firefox-compact-dark@mozilla.org";
 const {LightweightThemeManager} = ChromeUtils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
 
 add_task(async function() {
   Services.prefs.clearUserPref("lightweightThemes.usedThemes");
   Services.prefs.clearUserPref("lightweightThemes.recommendedThemes");
 
@@ -90,33 +90,34 @@ add_task(async function() {
     themeCount++;
     iterNode = iterNode.nextSibling;
   }
   is(themeCount, 4,
      "There should be four themes in the 'My Themes' section");
 
   let defaultTheme = header.nextSibling;
   defaultTheme.doCommand();
-  is(Services.prefs.getCharPref("lightweightThemes.selectedThemeID"), "", "No lwtheme should be selected");
+  is(Services.prefs.getCharPref("lightweightThemes.selectedThemeID"),
+     DEFAULT_THEME_ID, "Default theme should be selected");
 
   // ensure current theme isn't set to "Default"
   popupShownPromise = popupShown(popup);
   EventUtils.synthesizeMouseAtCenter(themesButton, {});
   info("Clicked on themes button a fourth time");
   await popupShownPromise;
 
   firstLWTheme = recommendedHeader.nextSibling;
   themeChangedPromise = promiseObserverNotified("lightweight-theme-changed");
   firstLWTheme.doCommand();
   info("Clicked on first theme again");
   await themeChangedPromise;
 
   // check that "Restore Defaults" button resets theme
   await gCustomizeMode.reset();
-  is(LightweightThemeManager.currentTheme, null, "Current theme reset to default");
+  is(LightweightThemeManager.currentTheme.id, DEFAULT_THEME_ID, "Current theme reset to default");
 
   await endCustomizing();
   Services.prefs.setCharPref("lightweightThemes.usedThemes", "[]");
   Services.prefs.setCharPref("lightweightThemes.recommendedThemes", "[]");
   info("Removed all recommended themes");
   await startCustomizing();
   popupShownPromise = popupShown(popup);
   EventUtils.synthesizeMouseAtCenter(themesButton, {});
--- a/browser/components/customizableui/test/browser_970511_undo_restore_default.js
+++ b/browser/components/customizableui/test/browser_970511_undo_restore_default.js
@@ -32,17 +32,17 @@ add_task(async function() {
   await themeChangedPromise;
 
   is(LightweightThemeManager.currentTheme.id, firstLWThemeId, "Theme changed to first option");
 
   await gCustomizeMode.reset();
 
   ok(CustomizableUI.inDefaultState, "In default state after reset");
   is(undoResetButton.hidden, false, "The undo button is visible after reset");
-  is(LightweightThemeManager.currentTheme, null, "Theme reset to default");
+  is(LightweightThemeManager.currentTheme.id, "default-theme@mozilla.org", "Theme reset to default");
 
   await gCustomizeMode.undoReset();
 
   is(LightweightThemeManager.currentTheme.id, firstLWThemeId, "Theme has been reset from default to original choice");
   ok(!CustomizableUI.inDefaultState, "Not in default state after undo-reset");
   is(undoResetButton.hidden, true, "The undo button is hidden after clicking on the undo button");
   is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
 
--- a/browser/installer/allowed-dupes.mn
+++ b/browser/installer/allowed-dupes.mn
@@ -76,17 +76,16 @@ browser/chrome/icons/default/default128.
 browser/chrome/pdfjs/content/web/images/findbarButton-next-rtl.png
 browser/chrome/pdfjs/content/web/images/findbarButton-next-rtl@2x.png
 browser/chrome/pdfjs/content/web/images/findbarButton-next.png
 browser/chrome/pdfjs/content/web/images/findbarButton-next@2x.png
 browser/chrome/pdfjs/content/web/images/findbarButton-previous-rtl.png
 browser/chrome/pdfjs/content/web/images/findbarButton-previous-rtl@2x.png
 browser/chrome/pdfjs/content/web/images/findbarButton-previous.png
 browser/chrome/pdfjs/content/web/images/findbarButton-previous@2x.png
-browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/icon.png
 browser/features/firefox@getpocket.com/chrome/skin/linux/menuPanel.png
 browser/features/firefox@getpocket.com/chrome/skin/linux/menuPanel@2x.png
 browser/features/firefox@getpocket.com/chrome/skin/windows/menuPanel.png
 browser/features/firefox@getpocket.com/chrome/skin/windows/menuPanel@2x.png
 chrome.manifest
 chrome/toolkit/skin/classic/global/autocomplete.css
 chrome/toolkit/skin/classic/global/button.css
 chrome/toolkit/skin/classic/global/checkbox.css
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -399,18 +399,16 @@
 #endif
 
 ; [Browser Chrome Files]
 @RESPATH@/browser/chrome.manifest
 @RESPATH@/browser/chrome/browser@JAREXT@
 @RESPATH@/browser/chrome/browser.manifest
 @RESPATH@/browser/chrome/pdfjs.manifest
 @RESPATH@/browser/chrome/pdfjs/*
-@RESPATH@/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/chrome.manifest
-@RESPATH@/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/install.rdf
 @RESPATH@/chrome/toolkit@JAREXT@
 @RESPATH@/chrome/toolkit.manifest
 @RESPATH@/chrome/recording.manifest
 @RESPATH@/chrome/recording/*
 #ifdef MOZ_GTK
 @RESPATH@/browser/chrome/icons/default/default16.png
 @RESPATH@/browser/chrome/icons/default/default32.png
 @RESPATH@/browser/chrome/icons/default/default48.png
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -620,19 +620,16 @@ safebrowsing.reportedHarmfulSite=Reporte
 # of tabs in the current browser window. It will always be 2 at least.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 ctrlTab.listAllTabs.label=;List All #1 Tabs
 
 # LOCALIZATION NOTE (addKeywordTitleAutoFill): %S will be replaced by the page's title
 # Used as the bookmark name when saving a keyword for a search field.
 addKeywordTitleAutoFill=Search %S
 
-extensions.{972ce4c6-7e08-4474-a285-3208198ce6fd}.name=Default
-extensions.{972ce4c6-7e08-4474-a285-3208198ce6fd}.description=The default theme.
-
 # safeModeRestart
 safeModeRestartPromptTitle=Restart with Add-ons Disabled
 safeModeRestartPromptMessage=Are you sure you want to disable all add-ons and restart?
 safeModeRestartButton=Restart
 
 # LOCALIZATION NOTE (browser.menu.showCharacterEncoding): Set to the string
 # "true" (spelled and capitalized exactly that way) to show the "Text
 # Encoding" menu in the main Firefox button on Windows. Any other value will
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -45,13 +45,12 @@ browser.jar:
   skin/classic/browser/preferences/preferences.css    (preferences/preferences.css)
 * skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
 * skin/classic/browser/preferences/in-content/dialog.css      (preferences/in-content/dialog.css)
   skin/classic/browser/preferences/applications.css   (preferences/applications.css)
   skin/classic/browser/tabbrowser/tabDragIndicator.png      (tabbrowser/tabDragIndicator.png)
 
   skin/classic/browser/e10s-64@2x.png (../shared/e10s-64@2x.png)
 
-[extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar:
 % override chrome://browser/skin/feeds/audioFeedIcon.png              chrome://browser/skin/feeds/feedIcon.png
 % override chrome://browser/skin/feeds/audioFeedIcon16.png            chrome://browser/skin/feeds/feedIcon16.png
 % override chrome://browser/skin/feeds/videoFeedIcon.png              chrome://browser/skin/feeds/feedIcon.png
 % override chrome://browser/skin/feeds/videoFeedIcon16.png            chrome://browser/skin/feeds/feedIcon16.png
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -56,14 +56,13 @@ browser.jar:
 * skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
 * skin/classic/browser/preferences/in-content/dialog.css      (preferences/in-content/dialog.css)
   skin/classic/browser/preferences/applications.css         (preferences/applications.css)
   skin/classic/browser/share.svg                            (share.svg)
   skin/classic/browser/tabbrowser/tabDragIndicator.png                   (tabbrowser/tabDragIndicator.png)
   skin/classic/browser/tabbrowser/tabDragIndicator@2x.png                (tabbrowser/tabDragIndicator@2x.png)
   skin/classic/browser/e10s-64@2x.png                                  (../shared/e10s-64@2x.png)
 
-[extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar:
 % override chrome://browser/skin/feeds/audioFeedIcon.png                   chrome://browser/skin/feeds/feedIcon.png
 % override chrome://browser/skin/feeds/audioFeedIcon16.png                 chrome://browser/skin/feeds/feedIcon16.png
 % override chrome://browser/skin/feeds/videoFeedIcon.png                   chrome://browser/skin/feeds/feedIcon.png
 % override chrome://browser/skin/feeds/videoFeedIcon16.png                 chrome://browser/skin/feeds/feedIcon16.png
 % override chrome://browser/skin/notification-icons/geo-detailed.svg       chrome://browser/skin/notification-icons/geo.svg
--- a/browser/themes/shared/customizableui/customizeMode.inc.css
+++ b/browser/themes/shared/customizableui/customizeMode.inc.css
@@ -153,17 +153,17 @@
 #customization-lwtheme-button > .box-inherit > .box-inherit > .button-icon {
   width: 16px;
   height: 16px;
   border-radius: 2px;
   background-size: contain;
 }
 
 #customization-lwtheme-button > .box-inherit > .box-inherit > .button-icon {
-  background-image: url("chrome://browser/content/default-theme-icon.svg");
+  background-image: url("chrome://mozapps/content/extensions/default-theme-icon.svg");
 }
 
 #customization-uidensity-button > .box-inherit > .box-inherit > .button-icon {
   background-image: url("chrome://browser/skin/customizableui/density-normal.svg");
 }
 
 #widget-overflow-fixed-list > toolbarpaletteitem[place="menu-panel"],
 toolbarpaletteitem[place="toolbar"] {
@@ -329,20 +329,16 @@ toolbarpaletteitem[place=toolbar] > tool
   padding-bottom: 0;
   padding-inline-start: 0;
 }
 
 .customization-uidensity-menuitem {
   color: inherit;
 }
 
-.customization-lwtheme-menu-theme[defaulttheme] {
-  list-style-image: url(chrome://browser/content/default-theme-icon.svg);
-}
-
 #customization-uidensity-menuitem-normal {
   list-style-image: url("chrome://browser/skin/customizableui/density-normal.svg");
 }
 
 #customization-uidensity-menuitem-compact {
   list-style-image: url("chrome://browser/skin/customizableui/density-compact.svg");
 }
 
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -56,14 +56,13 @@ browser.jar:
   skin/classic/browser/window-controls/minimize.svg              (window-controls/minimize.svg)
   skin/classic/browser/window-controls/minimize-highcontrast.svg (window-controls/minimize-highcontrast.svg)
   skin/classic/browser/window-controls/minimize-themes.svg       (window-controls/minimize-themes.svg)
   skin/classic/browser/window-controls/restore.svg               (window-controls/restore.svg)
   skin/classic/browser/window-controls/restore-highcontrast.svg  (window-controls/restore-highcontrast.svg)
   skin/classic/browser/window-controls/restore-themes.svg        (window-controls/restore-themes.svg)
   skin/classic/browser/e10s-64@2x.png                            (../shared/e10s-64@2x.png)
 
-[extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}] chrome.jar:
 % override chrome://browser/skin/page-livemarks.png                   chrome://browser/skin/feeds/feedIcon16.png
 % override chrome://browser/skin/feeds/audioFeedIcon.png              chrome://browser/skin/feeds/feedIcon.png
 % override chrome://browser/skin/feeds/audioFeedIcon16.png            chrome://browser/skin/feeds/feedIcon16.png
 % override chrome://browser/skin/feeds/videoFeedIcon.png              chrome://browser/skin/feeds/feedIcon.png
 % override chrome://browser/skin/feeds/videoFeedIcon16.png            chrome://browser/skin/feeds/feedIcon16.png
--- a/devtools/client/inspector/rules/test/browser_rules_gridline-names-autocomplete.js
+++ b/devtools/client/inspector/rules/test/browser_rules_gridline-names-autocomplete.js
@@ -24,34 +24,35 @@ const changeTestData = [
   ["l", {}, "col1-start", OPEN, SELECTED, CHANGE],
   ["VK_DOWN", {}, "col2-start", OPEN, SELECTED, CHANGE],
   ["VK_RIGHT", {}, "col2-start", !OPEN, !SELECTED, !CHANGE],
 ];
 
 // Creates a new CSS property value.
 // Checks that grid-area autocompletes column and row names.
 const newAreaTestData = [
-  ["g", {}, "grid", OPEN, SELECTED, !CHANGE],
+  ["g", {}, "gap", OPEN, SELECTED, !CHANGE],
+  ["VK_DOWN", {}, "grid", OPEN, SELECTED, !CHANGE],
   ["VK_DOWN", {}, "grid-area", OPEN, SELECTED, !CHANGE],
   ["VK_TAB", {}, "", !OPEN, !SELECTED, !CHANGE],
   "grid-line-names-updated",
   ["c", {}, "col1-start", OPEN, SELECTED, CHANGE],
   ["VK_BACK_SPACE", {}, "c", !OPEN, !SELECTED, CHANGE],
   ["VK_BACK_SPACE", {}, "", !OPEN, !SELECTED, CHANGE],
   ["r", {}, "row1-start", OPEN, SELECTED, CHANGE],
   ["r", {}, "rr", !OPEN, !SELECTED, CHANGE],
   ["VK_BACK_SPACE", {}, "r", !OPEN, !SELECTED, CHANGE],
   ["o", {}, "row1-start", OPEN, SELECTED, CHANGE],
   ["VK_RETURN", {}, "", !OPEN, !SELECTED, CHANGE],
 ];
 
 // Creates a new CSS property value.
 // Checks that grid-row only autocompletes row names.
 const newRowTestData = [
-  ["g", {}, "grid", OPEN, SELECTED, !CHANGE],
+  ["g", {}, "gap", OPEN, SELECTED, !CHANGE],
   ["r", {}, "grid", OPEN, SELECTED, !CHANGE],
   ["i", {}, "grid", OPEN, SELECTED, !CHANGE],
   ["d", {}, "grid", OPEN, SELECTED, !CHANGE],
   ["-", {}, "grid-area", OPEN, SELECTED, !CHANGE],
   ["r", {}, "grid-row", OPEN, SELECTED, !CHANGE],
   ["VK_RETURN", {}, "", !OPEN, !SELECTED, !CHANGE],
   "grid-line-names-updated",
   ["c", {}, "c", !OPEN, !SELECTED, CHANGE],
--- a/devtools/server/actors/animation-type-longhand.js
+++ b/devtools/server/actors/animation-type-longhand.js
@@ -278,18 +278,16 @@ exports.ANIMATION_TYPE_FOR_LONGHANDS = [
     "border-bottom-left-radius",
     "border-bottom-right-radius",
     "border-top-left-radius",
     "border-top-right-radius",
     "bottom",
     "column-gap",
     "column-width",
     "flex-basis",
-    "grid-column-gap",
-    "grid-row-gap",
     "height",
     "left",
     "letter-spacing",
     "line-height",
     "margin-bottom",
     "margin-left",
     "margin-right",
     "margin-top",
@@ -302,16 +300,17 @@ exports.ANIMATION_TYPE_FOR_LONGHANDS = [
     "-moz-outline-radius-topleft",
     "-moz-outline-radius-topright",
     "padding-bottom",
     "padding-left",
     "padding-right",
     "padding-top",
     "perspective",
     "right",
+    "row-gap",
     "stroke-dashoffset",
     "stroke-width",
     "-moz-tab-size",
     "text-indent",
     "top",
     "vertical-align",
     "width",
     "word-spacing",
--- a/devtools/shared/css/generated/properties-db.js
+++ b/devtools/shared/css/generated/properties-db.js
@@ -2981,20 +2981,18 @@ exports.CSS_PROPERTIES = {
       "font-variant-position",
       "font-variation-settings",
       "font-weight",
       "-moz-force-broken-image-icon",
       "grid-auto-columns",
       "grid-auto-flow",
       "grid-auto-rows",
       "grid-column-end",
-      "grid-column-gap",
       "grid-column-start",
       "grid-row-end",
-      "grid-row-gap",
       "grid-row-start",
       "grid-template-areas",
       "grid-template-columns",
       "grid-template-rows",
       "height",
       "hyphens",
       "image-orientation",
       "-moz-image-region",
@@ -3084,16 +3082,17 @@ exports.CSS_PROPERTIES = {
       "perspective",
       "perspective-origin",
       "pointer-events",
       "position",
       "quotes",
       "resize",
       "right",
       "rotate",
+      "row-gap",
       "ruby-align",
       "ruby-position",
       "scale",
       "scroll-behavior",
       "scroll-snap-coordinate",
       "scroll-snap-destination",
       "scroll-snap-points-x",
       "scroll-snap-points-y",
@@ -6162,16 +6161,34 @@ exports.CSS_PROPERTIES = {
       "bolder",
       "inherit",
       "initial",
       "lighter",
       "normal",
       "unset"
     ]
   },
+  "gap": {
+    "isInherited": false,
+    "subproperties": [
+      "row-gap",
+      "column-gap"
+    ],
+    "supports": [
+      6,
+      8
+    ],
+    "values": [
+      "calc",
+      "inherit",
+      "initial",
+      "normal",
+      "unset"
+    ]
+  },
   "grid": {
     "isInherited": false,
     "subproperties": [
       "grid-template-areas",
       "grid-template-rows",
       "grid-template-columns",
       "grid-auto-flow",
       "grid-auto-rows",
@@ -6285,26 +6302,27 @@ exports.CSS_PROPERTIES = {
       "inherit",
       "initial",
       "unset"
     ]
   },
   "grid-column-gap": {
     "isInherited": false,
     "subproperties": [
-      "grid-column-gap"
-    ],
-    "supports": [
-      6,
-      8
-    ],
-    "values": [
-      "calc",
-      "inherit",
-      "initial",
+      "column-gap"
+    ],
+    "supports": [
+      6,
+      8
+    ],
+    "values": [
+      "calc",
+      "inherit",
+      "initial",
+      "normal",
       "unset"
     ]
   },
   "grid-column-start": {
     "isInherited": false,
     "subproperties": [
       "grid-column-start"
     ],
@@ -6315,27 +6333,28 @@ exports.CSS_PROPERTIES = {
       "inherit",
       "initial",
       "unset"
     ]
   },
   "grid-gap": {
     "isInherited": false,
     "subproperties": [
-      "grid-row-gap",
-      "grid-column-gap"
-    ],
-    "supports": [
-      6,
-      8
-    ],
-    "values": [
-      "calc",
-      "inherit",
-      "initial",
+      "row-gap",
+      "column-gap"
+    ],
+    "supports": [
+      6,
+      8
+    ],
+    "values": [
+      "calc",
+      "inherit",
+      "initial",
+      "normal",
       "unset"
     ]
   },
   "grid-row": {
     "isInherited": false,
     "subproperties": [
       "grid-row-start",
       "grid-row-end"
@@ -6361,26 +6380,27 @@ exports.CSS_PROPERTIES = {
       "inherit",
       "initial",
       "unset"
     ]
   },
   "grid-row-gap": {
     "isInherited": false,
     "subproperties": [
-      "grid-row-gap"
-    ],
-    "supports": [
-      6,
-      8
-    ],
-    "values": [
-      "calc",
-      "inherit",
-      "initial",
+      "row-gap"
+    ],
+    "supports": [
+      6,
+      8
+    ],
+    "values": [
+      "calc",
+      "inherit",
+      "initial",
+      "normal",
       "unset"
     ]
   },
   "grid-row-start": {
     "isInherited": false,
     "subproperties": [
       "grid-row-start"
     ],
@@ -8349,16 +8369,33 @@ exports.CSS_PROPERTIES = {
     "values": [
       "auto",
       "calc",
       "inherit",
       "initial",
       "unset"
     ]
   },
+  "row-gap": {
+    "isInherited": false,
+    "subproperties": [
+      "row-gap"
+    ],
+    "supports": [
+      6,
+      8
+    ],
+    "values": [
+      "calc",
+      "inherit",
+      "initial",
+      "normal",
+      "unset"
+    ]
+  },
   "ruby-align": {
     "isInherited": true,
     "subproperties": [
       "ruby-align"
     ],
     "supports": [],
     "values": [
       "center",
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -2455,36 +2455,16 @@ HTMLInputElement::CreateEditor()
 {
   nsTextEditorState* state = GetEditorState();
   if (state) {
     return state->PrepareEditor();
   }
   return NS_ERROR_FAILURE;
 }
 
-NS_IMETHODIMP_(Element*)
-HTMLInputElement::GetRootEditorNode()
-{
-  nsTextEditorState* state = GetEditorState();
-  if (state) {
-    return state->GetRootNode();
-  }
-  return nullptr;
-}
-
-NS_IMETHODIMP_(Element*)
-HTMLInputElement::GetPlaceholderNode()
-{
-  nsTextEditorState* state = GetEditorState();
-  if (state) {
-    return state->GetPlaceholderNode();
-  }
-  return nullptr;
-}
-
 NS_IMETHODIMP_(void)
 HTMLInputElement::UpdateOverlayTextVisibility(bool aNotify)
 {
   nsTextEditorState* state = GetEditorState();
   if (state) {
     state->UpdateOverlayTextVisibility(aNotify);
   }
 }
@@ -2495,26 +2475,16 @@ HTMLInputElement::GetPlaceholderVisibili
   nsTextEditorState* state = GetEditorState();
   if (!state) {
     return false;
   }
 
   return state->GetPlaceholderVisibility();
 }
 
-NS_IMETHODIMP_(Element*)
-HTMLInputElement::GetPreviewNode()
-{
-  nsTextEditorState* state = GetEditorState();
-  if (state) {
-    return state->GetPreviewNode();
-  }
-  return nullptr;
-}
-
 NS_IMETHODIMP_(void)
 HTMLInputElement::SetPreviewValue(const nsAString& aValue)
 {
   nsTextEditorState* state = GetEditorState();
   if (state) {
     state->SetPreviewText(aValue, true);
   }
 }
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -241,19 +241,16 @@ public:
   NS_IMETHOD_(bool) ValueChanged() const override;
   NS_IMETHOD_(void) GetTextEditorValue(nsAString& aValue, bool aIgnoreWrap) const override;
   NS_IMETHOD_(mozilla::TextEditor*) GetTextEditor() override;
   NS_IMETHOD_(nsISelectionController*) GetSelectionController() override;
   NS_IMETHOD_(nsFrameSelection*) GetConstFrameSelection() override;
   NS_IMETHOD BindToFrame(nsTextControlFrame* aFrame) override;
   NS_IMETHOD_(void) UnbindFromFrame(nsTextControlFrame* aFrame) override;
   NS_IMETHOD CreateEditor() override;
-  NS_IMETHOD_(Element*) GetRootEditorNode() override;
-  NS_IMETHOD_(Element*) GetPlaceholderNode() override;
-  NS_IMETHOD_(Element*) GetPreviewNode() override;
   NS_IMETHOD_(void) UpdateOverlayTextVisibility(bool aNotify) override;
   NS_IMETHOD_(void) SetPreviewValue(const nsAString& aValue) override;
   NS_IMETHOD_(void) GetPreviewValue(nsAString& aValue) override;
   NS_IMETHOD_(void) EnablePreview() override;
   NS_IMETHOD_(bool) IsPreviewEnabled() override;
   NS_IMETHOD_(bool) GetPlaceholderVisibility() override;
   NS_IMETHOD_(bool) GetPreviewVisibility() override;
   NS_IMETHOD_(void) InitializeKeyboardEventListeners() override;
--- a/dom/html/HTMLTextAreaElement.cpp
+++ b/dom/html/HTMLTextAreaElement.cpp
@@ -256,46 +256,28 @@ HTMLTextAreaElement::UnbindFromFrame(nsT
 }
 
 NS_IMETHODIMP
 HTMLTextAreaElement::CreateEditor()
 {
   return mState.PrepareEditor();
 }
 
-NS_IMETHODIMP_(Element*)
-HTMLTextAreaElement::GetRootEditorNode()
-{
-  return mState.GetRootNode();
-}
-
-NS_IMETHODIMP_(Element*)
-HTMLTextAreaElement::GetPlaceholderNode()
-{
-  return mState.GetPlaceholderNode();
-}
-
 NS_IMETHODIMP_(void)
 HTMLTextAreaElement::UpdateOverlayTextVisibility(bool aNotify)
 {
   mState.UpdateOverlayTextVisibility(aNotify);
 }
 
 NS_IMETHODIMP_(bool)
 HTMLTextAreaElement::GetPlaceholderVisibility()
 {
   return mState.GetPlaceholderVisibility();
 }
 
-NS_IMETHODIMP_(Element*)
-HTMLTextAreaElement::GetPreviewNode()
-{
-  return mState.GetPreviewNode();
-}
-
 NS_IMETHODIMP_(void)
 HTMLTextAreaElement::SetPreviewValue(const nsAString& aValue)
 {
   mState.SetPreviewText(aValue, true);
 }
 
 NS_IMETHODIMP_(void)
 HTMLTextAreaElement::GetPreviewValue(nsAString& aValue)
--- a/dom/html/HTMLTextAreaElement.h
+++ b/dom/html/HTMLTextAreaElement.h
@@ -94,19 +94,16 @@ public:
   NS_IMETHOD_(bool) ValueChanged() const override;
   NS_IMETHOD_(void) GetTextEditorValue(nsAString& aValue, bool aIgnoreWrap) const override;
   NS_IMETHOD_(mozilla::TextEditor*) GetTextEditor() override;
   NS_IMETHOD_(nsISelectionController*) GetSelectionController() override;
   NS_IMETHOD_(nsFrameSelection*) GetConstFrameSelection() override;
   NS_IMETHOD BindToFrame(nsTextControlFrame* aFrame) override;
   NS_IMETHOD_(void) UnbindFromFrame(nsTextControlFrame* aFrame) override;
   NS_IMETHOD CreateEditor() override;
-  NS_IMETHOD_(Element*) GetRootEditorNode() override;
-  NS_IMETHOD_(Element*) GetPlaceholderNode() override;
-  NS_IMETHOD_(Element*) GetPreviewNode() override;
   NS_IMETHOD_(void) UpdateOverlayTextVisibility(bool aNotify) override;
   NS_IMETHOD_(bool) GetPlaceholderVisibility() override;
   NS_IMETHOD_(bool) GetPreviewVisibility() override;
   NS_IMETHOD_(void) SetPreviewValue(const nsAString& aValue) override;
   NS_IMETHOD_(void) GetPreviewValue(nsAString& aValue) override;
   NS_IMETHOD_(void) EnablePreview() override;
   NS_IMETHOD_(bool) IsPreviewEnabled() override;
   NS_IMETHOD_(void) InitializeKeyboardEventListeners() override;
--- a/dom/html/nsITextControlElement.h
+++ b/dom/html/nsITextControlElement.h
@@ -129,31 +129,16 @@ public:
   /**
    * Creates an editor for the text control.  This should happen when
    * a frame has been created for the text control element, but the created
    * editor may outlive the frame itself.
    */
   NS_IMETHOD CreateEditor() = 0;
 
   /**
-   * Get the anonymous root node for the text control.
-   */
-  NS_IMETHOD_(mozilla::dom::Element*) GetRootEditorNode() = 0;
-
-  /**
-   * Get the placeholder anonymous node for the text control.
-   */
-  NS_IMETHOD_(mozilla::dom::Element*) GetPlaceholderNode() = 0;
-
-  /**
-   * Get the preview anonymous node for the text control.
-   */
-  NS_IMETHOD_(mozilla::dom::Element*) GetPreviewNode() = 0;
-
-  /**
    * Update preview value for the text control.
    */
   NS_IMETHOD_(void) SetPreviewValue(const nsAString& aValue) = 0;
 
   /**
    * Get the current preview value for text control.
    */
   NS_IMETHOD_(void) GetPreviewValue(nsAString& aValue) = 0;
--- a/dom/html/nsTextEditorState.cpp
+++ b/dom/html/nsTextEditorState.cpp
@@ -1103,22 +1103,16 @@ nsTextEditorState::~nsTextEditorState()
 
 Element*
 nsTextEditorState::GetRootNode()
 {
   return mBoundFrame ? mBoundFrame->GetRootNode() : nullptr;
 }
 
 Element*
-nsTextEditorState::GetPlaceholderNode()
-{
-  return mBoundFrame ? mBoundFrame->GetPlaceholderNode() : nullptr;
-}
-
-Element*
 nsTextEditorState::GetPreviewNode()
 {
   return mBoundFrame ? mBoundFrame->GetPreviewNode() : nullptr;
 }
 
 void
 nsTextEditorState::Clear()
 {
--- a/dom/html/nsTextEditorState.h
+++ b/dom/html/nsTextEditorState.h
@@ -194,17 +194,16 @@ public:
   // The following methods are for textarea element to use whether default
   // value or not.
   // XXX We might have to add assertion when it is into editable,
   // or reconsider fixing bug 597525 to remove these.
   void EmptyValue() { if (mValue) mValue->Truncate(); }
   bool IsEmpty() const { return mValue ? mValue->IsEmpty() : true; }
 
   mozilla::dom::Element* GetRootNode();
-  mozilla::dom::Element* GetPlaceholderNode();
   mozilla::dom::Element* GetPreviewNode();
 
   bool IsSingleLineTextControl() const {
     return mTextCtrlElement->IsSingleLineTextControl();
   }
   bool IsTextArea() const {
     return mTextCtrlElement->IsTextArea();
   }
--- a/dom/svg/nsSVGElement.cpp
+++ b/dom/svg/nsSVGElement.cpp
@@ -530,17 +530,17 @@ nsSVGElement::ParseAttribute(int32_t aNa
     if (!foundMatch) {
       // Check for nsSVGEnum attribute
       EnumAttributesInfo enumInfo = GetEnumInfo();
       for (i = 0; i < enumInfo.mEnumCount; i++) {
         if (aAttribute == *enumInfo.mEnumInfo[i].mName) {
           RefPtr<nsAtom> valAtom = NS_Atomize(aValue);
           rv = enumInfo.mEnums[i].SetBaseValueAtom(valAtom, this);
           if (NS_FAILED(rv)) {
-            enumInfo.Reset(i);
+            enumInfo.SetUnknownValue(i);
           } else {
             aResult.SetTo(valAtom);
             didSetResult = true;
           }
           foundMatch = true;
           break;
         }
       }
@@ -1503,17 +1503,18 @@ nsSVGElement::PrependLocalTransformsTo(
 }
 
 nsSVGElement::LengthAttributesInfo
 nsSVGElement::GetLengthInfo()
 {
   return LengthAttributesInfo(nullptr, nullptr, 0);
 }
 
-void nsSVGElement::LengthAttributesInfo::Reset(uint8_t aAttrEnum)
+void
+nsSVGElement::LengthAttributesInfo::Reset(uint8_t aAttrEnum)
 {
   mLengths[aAttrEnum].Init(mLengthInfo[aAttrEnum].mCtxType,
                            aAttrEnum,
                            mLengthInfo[aAttrEnum].mDefaultValue,
                            mLengthInfo[aAttrEnum].mDefaultUnitType);
 }
 
 void
@@ -1853,17 +1854,18 @@ nsSVGElement::DidAnimatePathSegList()
 }
 
 nsSVGElement::NumberAttributesInfo
 nsSVGElement::GetNumberInfo()
 {
   return NumberAttributesInfo(nullptr, nullptr, 0);
 }
 
-void nsSVGElement::NumberAttributesInfo::Reset(uint8_t aAttrEnum)
+void
+nsSVGElement::NumberAttributesInfo::Reset(uint8_t aAttrEnum)
 {
   mNumbers[aAttrEnum].Init(aAttrEnum,
                            mNumberInfo[aAttrEnum].mDefaultValue);
 }
 
 void
 nsSVGElement::DidChangeNumber(uint8_t aAttrEnum)
 {
@@ -1915,17 +1917,18 @@ nsSVGElement::GetAnimatedNumberValues(fl
 }
 
 nsSVGElement::NumberPairAttributesInfo
 nsSVGElement::GetNumberPairInfo()
 {
   return NumberPairAttributesInfo(nullptr, nullptr, 0);
 }
 
-void nsSVGElement::NumberPairAttributesInfo::Reset(uint8_t aAttrEnum)
+void
+nsSVGElement::NumberPairAttributesInfo::Reset(uint8_t aAttrEnum)
 {
   mNumberPairs[aAttrEnum].Init(aAttrEnum,
                                mNumberPairInfo[aAttrEnum].mDefaultValue1,
                                mNumberPairInfo[aAttrEnum].mDefaultValue2);
 }
 
 nsAttrValue
 nsSVGElement::WillChangeNumberPair(uint8_t aAttrEnum)
@@ -1964,17 +1967,18 @@ nsSVGElement::DidAnimateNumberPair(uint8
 }
 
 nsSVGElement::IntegerAttributesInfo
 nsSVGElement::GetIntegerInfo()
 {
   return IntegerAttributesInfo(nullptr, nullptr, 0);
 }
 
-void nsSVGElement::IntegerAttributesInfo::Reset(uint8_t aAttrEnum)
+void
+nsSVGElement::IntegerAttributesInfo::Reset(uint8_t aAttrEnum)
 {
   mIntegers[aAttrEnum].Init(aAttrEnum,
                             mIntegerInfo[aAttrEnum].mDefaultValue);
 }
 
 void
 nsSVGElement::DidChangeInteger(uint8_t aAttrEnum)
 {
@@ -2026,17 +2030,18 @@ nsSVGElement::GetAnimatedIntegerValues(i
 }
 
 nsSVGElement::IntegerPairAttributesInfo
 nsSVGElement::GetIntegerPairInfo()
 {
   return IntegerPairAttributesInfo(nullptr, nullptr, 0);
 }
 
-void nsSVGElement::IntegerPairAttributesInfo::Reset(uint8_t aAttrEnum)
+void
+nsSVGElement::IntegerPairAttributesInfo::Reset(uint8_t aAttrEnum)
 {
   mIntegerPairs[aAttrEnum].Init(aAttrEnum,
                                 mIntegerPairInfo[aAttrEnum].mDefaultValue1,
                                 mIntegerPairInfo[aAttrEnum].mDefaultValue2);
 }
 
 nsAttrValue
 nsSVGElement::WillChangeIntegerPair(uint8_t aAttrEnum)
@@ -2076,17 +2081,18 @@ nsSVGElement::DidAnimateIntegerPair(uint
 }
 
 nsSVGElement::AngleAttributesInfo
 nsSVGElement::GetAngleInfo()
 {
   return AngleAttributesInfo(nullptr, nullptr, 0);
 }
 
-void nsSVGElement::AngleAttributesInfo::Reset(uint8_t aAttrEnum)
+void
+nsSVGElement::AngleAttributesInfo::Reset(uint8_t aAttrEnum)
 {
   mAngles[aAttrEnum].Init(aAttrEnum,
                           mAngleInfo[aAttrEnum].mDefaultValue,
                           mAngleInfo[aAttrEnum].mDefaultUnitType);
 }
 
 nsAttrValue
 nsSVGElement::WillChangeAngle(uint8_t aAttrEnum)
@@ -2124,17 +2130,18 @@ nsSVGElement::DidAnimateAngle(uint8_t aA
 }
 
 nsSVGElement::BooleanAttributesInfo
 nsSVGElement::GetBooleanInfo()
 {
   return BooleanAttributesInfo(nullptr, nullptr, 0);
 }
 
-void nsSVGElement::BooleanAttributesInfo::Reset(uint8_t aAttrEnum)
+void
+nsSVGElement::BooleanAttributesInfo::Reset(uint8_t aAttrEnum)
 {
   mBooleans[aAttrEnum].Init(aAttrEnum,
                             mBooleanInfo[aAttrEnum].mDefaultValue);
 }
 
 void
 nsSVGElement::DidChangeBoolean(uint8_t aAttrEnum)
 {
@@ -2163,23 +2170,31 @@ nsSVGElement::DidAnimateBoolean(uint8_t 
 }
 
 nsSVGElement::EnumAttributesInfo
 nsSVGElement::GetEnumInfo()
 {
   return EnumAttributesInfo(nullptr, nullptr, 0);
 }
 
-void nsSVGElement::EnumAttributesInfo::Reset(uint8_t aAttrEnum)
+void
+nsSVGElement::EnumAttributesInfo::Reset(uint8_t aAttrEnum)
 {
   mEnums[aAttrEnum].Init(aAttrEnum,
                          mEnumInfo[aAttrEnum].mDefaultValue);
 }
 
 void
+nsSVGElement::EnumAttributesInfo::SetUnknownValue(uint8_t aAttrEnum)
+{
+  // Fortunately in SVG every enum's unknown value is 0
+  mEnums[aAttrEnum].Init(aAttrEnum, 0);
+}
+
+void
 nsSVGElement::DidChangeEnum(uint8_t aAttrEnum)
 {
   EnumAttributesInfo info = GetEnumInfo();
 
   NS_ASSERTION(info.mEnumCount > 0,
                "DidChangeEnum on element with no enum attribs");
   NS_ASSERTION(aAttrEnum < info.mEnumCount, "aAttrEnum out of range");
 
@@ -2327,35 +2342,38 @@ nsSVGElement::DidAnimateTransformList(in
 }
 
 nsSVGElement::StringAttributesInfo
 nsSVGElement::GetStringInfo()
 {
   return StringAttributesInfo(nullptr, nullptr, 0);
 }
 
-void nsSVGElement::StringAttributesInfo::Reset(uint8_t aAttrEnum)
+void
+nsSVGElement::StringAttributesInfo::Reset(uint8_t aAttrEnum)
 {
   mStrings[aAttrEnum].Init(aAttrEnum);
 }
 
-void nsSVGElement::GetStringBaseValue(uint8_t aAttrEnum, nsAString& aResult) const
+void
+nsSVGElement::GetStringBaseValue(uint8_t aAttrEnum, nsAString& aResult) const
 {
   nsSVGElement::StringAttributesInfo info = const_cast<nsSVGElement*>(this)->GetStringInfo();
 
   NS_ASSERTION(info.mStringCount > 0,
                "GetBaseValue on element with no string attribs");
 
   NS_ASSERTION(aAttrEnum < info.mStringCount, "aAttrEnum out of range");
 
   GetAttr(info.mStringInfo[aAttrEnum].mNamespaceID,
           *info.mStringInfo[aAttrEnum].mName, aResult);
 }
 
-void nsSVGElement::SetStringBaseValue(uint8_t aAttrEnum, const nsAString& aValue)
+void
+nsSVGElement::SetStringBaseValue(uint8_t aAttrEnum, const nsAString& aValue)
 {
   nsSVGElement::StringAttributesInfo info = GetStringInfo();
 
   NS_ASSERTION(info.mStringCount > 0,
                "SetBaseValue on element with no string attribs");
 
   NS_ASSERTION(aAttrEnum < info.mStringCount, "aAttrEnum out of range");
 
--- a/dom/svg/nsSVGElement.h
+++ b/dom/svg/nsSVGElement.h
@@ -511,16 +511,17 @@ protected:
 
     EnumAttributesInfo(nsSVGEnum *aEnums,
                        EnumInfo *aEnumInfo,
                        uint32_t aEnumCount) :
       mEnums(aEnums), mEnumInfo(aEnumInfo), mEnumCount(aEnumCount)
       {}
 
     void Reset(uint8_t aAttrEnum);
+    void SetUnknownValue(uint8_t aAttrEnum);
   };
 
   struct NumberListInfo {
     nsStaticAtom** mName;
   };
 
   struct NumberListAttributesInfo {
     SVGAnimatedNumberList* mNumberLists;
--- a/js/public/MemoryMetrics.h
+++ b/js/public/MemoryMetrics.h
@@ -504,17 +504,18 @@ struct NotableScriptSourceInfo : public 
     NotableScriptSourceInfo(const NotableScriptSourceInfo& info) = delete;
 };
 
 struct HelperThreadStats
 {
 #define FOR_EACH_SIZE(macro) \
     macro(_, MallocHeap, stateData) \
     macro(_, MallocHeap, parseTask) \
-    macro(_, MallocHeap, ionBuilder)
+    macro(_, MallocHeap, ionBuilder) \
+    macro(_, MallocHeap, wasmCompile)
 
     explicit HelperThreadStats()
       : FOR_EACH_SIZE(ZERO_SIZE)
         idleThreadCount(0),
         activeThreadCount(0)
     { }
 
     FOR_EACH_SIZE(DECL_SIZE)
--- a/js/src/gc/Nursery.cpp
+++ b/js/src/gc/Nursery.cpp
@@ -720,20 +720,18 @@ js::Nursery::collect(JS::gcreason::Reaso
 
     // The analysis marks TenureCount as not problematic for GC hazards because
     // it is only used here, and ObjectGroup pointers are never
     // nursery-allocated.
     MOZ_ASSERT(!IsNurseryAllocable(AllocKind::OBJECT_GROUP));
 
     TenureCountCache tenureCounts;
     previousGC.reason = JS::gcreason::NO_REASON;
-    mozilla::Maybe<AutoTraceSession> session;
     if (!isEmpty()) {
-        session.emplace(rt, JS::HeapState::MinorCollecting);
-        doCollection(reason, session.ref(), tenureCounts);
+        doCollection(reason, tenureCounts);
     } else {
         previousGC.nurseryUsedBytes = 0;
         previousGC.nurseryCapacity = spaceToEnd(maxChunkCount());
         previousGC.nurseryLazyCapacity = spaceToEnd(allocatedChunkCount());
         previousGC.tenuredBytes = 0;
     }
 
     // Resize the nursery.
@@ -758,49 +756,52 @@ js::Nursery::collect(JS::gcreason::Reaso
                 if (group->canPreTenure()) {
                     AutoCompartment ac(cx, group);
                     group->setShouldPreTenure(cx);
                     pretenureCount++;
                 }
             }
         }
     }
+
+    mozilla::Maybe<AutoTraceSession> session;
     for (ZonesIter zone(rt, SkipAtoms); !zone.done(); zone.next()) {
         if (shouldPretenure && zone->allocNurseryStrings && zone->tenuredStrings >= 30 * 1000) {
-            MOZ_ASSERT(session.isSome(), "discarding JIT code must be in an AutoTraceSession");
+            if (!session.isSome())
+                session.emplace(rt, JS::HeapState::MinorCollecting);
             CancelOffThreadIonCompile(zone);
             bool preserving = zone->isPreservingCode();
             zone->setPreservingCode(false);
             zone->discardJitCode(rt->defaultFreeOp());
             zone->setPreservingCode(preserving);
             for (CompartmentsInZoneIter c(zone); !c.done(); c.next()) {
                 if (jit::JitCompartment* jitComp = c->jitCompartment()) {
                     jitComp->discardStubs();
                     jitComp->stringsCanBeInNursery = false;
                 }
             }
             zone->allocNurseryStrings = false;
         }
         zone->tenuredStrings = 0;
     }
+    session.reset(); // End the minor GC session, if running one.
     endProfile(ProfileKey::Pretenure);
 
     // We ignore gcMaxBytes when allocating for minor collection. However, if we
     // overflowed, we disable the nursery. The next time we allocate, we'll fail
     // because gcBytes >= gcMaxBytes.
     if (rt->gc.usage.gcBytes() >= rt->gc.tunables.gcMaxBytes())
         disable();
     // Disable the nursery if the user changed the configuration setting.  The
     // nursery can only be re-enabled by resetting the configurationa and
     // restarting firefox.
     if (chunkCountLimit_ == 0)
         disable();
 
     endProfile(ProfileKey::Total);
-    session.reset(); // End the minor GC session, if running one.
     rt->gc.incMinorGcNumber();
 
     TimeDuration totalTime = profileDurations_[ProfileKey::Total];
     rt->addTelemetry(JS_TELEMETRY_GC_MINOR_US, totalTime.ToMicroseconds());
     rt->addTelemetry(JS_TELEMETRY_GC_MINOR_REASON, reason);
     if (totalTime.ToMilliseconds() > 1.0)
         rt->addTelemetry(JS_TELEMETRY_GC_MINOR_REASON_LONG, reason);
     rt->addTelemetry(JS_TELEMETRY_GC_NURSERY_BYTES, sizeOfHeapCommitted());
@@ -826,21 +827,20 @@ js::Nursery::collect(JS::gcreason::Reaso
                     entry.group->print();
                 }
             }
         }
     }
 }
 
 void
-js::Nursery::doCollection(JS::gcreason::Reason reason,
-                          AutoTraceSession& session,
-                          TenureCountCache& tenureCounts)
+js::Nursery::doCollection(JS::gcreason::Reason reason, TenureCountCache& tenureCounts)
 {
     JSRuntime* rt = runtime();
+    AutoTraceSession session(rt, JS::HeapState::MinorCollecting);
     AutoSetThreadIsPerformingGC performingGC;
     AutoStopVerifyingBarriers av(rt, false);
     AutoDisableProxyCheck disableStrictProxyChecking;
     mozilla::DebugOnly<AutoEnterOOMUnsafeRegion> oomUnsafeRegion;
 
     const size_t initialNurseryCapacity = spaceToEnd(maxChunkCount());
     const size_t initialNurseryUsedBytes = initialNurseryCapacity - freeSpace();
 
--- a/js/src/gc/Nursery.h
+++ b/js/src/gc/Nursery.h
@@ -512,19 +512,17 @@ class Nursery
 
     uintptr_t position() const { return position_; }
 
     JSRuntime* runtime() const { return runtime_; }
 
     /* Common internal allocator function. */
     void* allocate(size_t size);
 
-    void doCollection(JS::gcreason::Reason reason,
-                      gc::AutoTraceSession& sesssion,
-                      gc::TenureCountCache& tenureCounts);
+    void doCollection(JS::gcreason::Reason reason, gc::TenureCountCache& tenureCounts);
 
     /*
      * Move the object at |src| in the Nursery to an already-allocated cell
      * |dst| in Tenured.
      */
     void collectToFixedPoint(TenuringTracer& trc, gc::TenureCountCache& tenureCounts);
 
     /* Handle relocation of slots/elements pointers stored in Ion frames. */
--- a/js/src/gc/Statistics.cpp
+++ b/js/src/gc/Statistics.cpp
@@ -105,17 +105,17 @@ struct PhaseKindInfo
 };
 
 // PhaseInfo objects form a tree.
 struct PhaseInfo
 {
     Phase parent;
     Phase firstChild;
     Phase nextSibling;
-    Phase nextInPhase;
+    Phase nextWithPhaseKind;
     PhaseKind phaseKind;
     uint8_t depth;
     const char* name;
     const char* path;
 };
 
 // A table of PhaseInfo indexed by Phase.
 using PhaseTable = EnumeratedArray<Phase, Phase::LIMIT, PhaseInfo>;
@@ -161,17 +161,17 @@ Statistics::lookupChildPhase(PhaseKind p
 
     MOZ_ASSERT(phaseKind < PhaseKind::LIMIT);
 
     // Search all expanded phases that correspond to the required
     // phase to find the one whose parent is the current expanded phase.
     Phase phase;
     for (phase = phaseKinds[phaseKind].firstPhase;
          phase != Phase::NONE;
-         phase = phases[phase].nextInPhase)
+         phase = phases[phase].nextWithPhaseKind)
     {
         if (phases[phase].parent == currentPhase())
             break;
     }
 
     MOZ_RELEASE_ASSERT(phase != Phase::NONE,
                        "Requested child phase not found under current phase");
 
@@ -629,17 +629,17 @@ Statistics::formatJsonDescription(uint64
     const double mmu50 = computeMMU(TimeDuration::FromMilliseconds(50));
     json.property("mmu_20ms", int(mmu20 * 100)); // #12
     json.property("mmu_50ms", int(mmu50 * 100)); // #13
 
     TimeDuration sccTotal, sccLongest;
     sccDurations(&sccTotal, &sccLongest);
     json.property("scc_sweep_total", sccTotal, JSONPrinter::MILLISECONDS); // #14
     json.property("scc_sweep_max_pause", sccLongest, JSONPrinter::MILLISECONDS); // #15
-    
+
     if (nonincrementalReason_ != AbortReason::None)
         json.property("nonincremental_reason", ExplainAbortReason(nonincrementalReason_)); // #16
     json.property("allocated_bytes", preBytes); // #17
     uint32_t addedChunks = getCount(STAT_NEW_CHUNK);
     if (addedChunks)
         json.property("added_chunks", addedChunks); // #18
     uint32_t removedChunks = getCount(STAT_DESTROY_CHUNK);
     if (removedChunks)
@@ -764,20 +764,20 @@ Statistics::initialize()
             MOZ_ASSERT(i == phases[firstChild].parent);
             MOZ_ASSERT(phases[i].depth == phases[firstChild].depth - 1);
         }
         auto nextSibling = phases[i].nextSibling;
         if (nextSibling != Phase::NONE) {
             MOZ_ASSERT(parent == phases[nextSibling].parent);
             MOZ_ASSERT(phases[i].depth == phases[nextSibling].depth);
         }
-        auto nextInPhase = phases[i].nextInPhase;
-        if (nextInPhase != Phase::NONE) {
-            MOZ_ASSERT(phases[i].phaseKind == phases[nextInPhase].phaseKind);
-            MOZ_ASSERT(parent != phases[nextInPhase].parent);
+        auto nextWithPhaseKind = phases[i].nextWithPhaseKind;
+        if (nextWithPhaseKind != Phase::NONE) {
+            MOZ_ASSERT(phases[i].phaseKind == phases[nextWithPhaseKind].phaseKind);
+            MOZ_ASSERT(parent != phases[nextWithPhaseKind].parent);
         }
     }
     for (auto i : AllPhaseKinds()) {
         MOZ_ASSERT(phases[phaseKinds[i].firstPhase].phaseKind == i);
         for (auto j : AllPhaseKinds()) {
             MOZ_ASSERT_IF(i != j,
                           phaseKinds[i].telemetryBucket != phaseKinds[j].telemetryBucket);
         }
@@ -820,17 +820,17 @@ Statistics::getMaxGCPauseSinceClear()
 // Sum up the time for a phase, including instances of the phase with different
 // parents.
 static TimeDuration
 SumPhase(PhaseKind phaseKind, const Statistics::PhaseTimeTable& times)
 {
     TimeDuration sum = 0;
     for (Phase phase = phaseKinds[phaseKind].firstPhase;
          phase != Phase::NONE;
-         phase = phases[phase].nextInPhase)
+         phase = phases[phase].nextWithPhaseKind)
     {
         sum += times[phase];
     }
     return sum;
 }
 
 static bool
 CheckSelfTime(Phase parent,
@@ -865,17 +865,17 @@ LongestPhaseSelfTimeInMajorGC(const Stat
     // time.
     for (auto i : AllPhases()) {
         Phase parent = phases[i].parent;
         if (parent != Phase::NONE) {
             bool ok = CheckSelfTime(parent, i, times, selfTimes, times[i]);
 
             // This happens very occasionally in release builds. Skip collecting
             // longest phase telemetry if it does.
-            MOZ_ASSERT(ok, "Inconsistent time data");
+            MOZ_ASSERT(ok, "Inconsistent time data; see bug 1400153");
             if (!ok)
                 return PhaseKind::NONE;
 
             selfTimes[parent] -= times[i];
         }
     }
 
     // Sum expanded phases corresponding to the same phase.
@@ -1093,17 +1093,22 @@ Statistics::endSlice()
     // Do this after the slice callback since it uses these values.
     if (last) {
         for (auto& count : counts)
             count = 0;
 
         // Clear the timers at the end of a GC, preserving the data for PhaseKind::MUTATOR.
         auto mutatorStartTime = phaseStartTimes[Phase::MUTATOR];
         auto mutatorTime = phaseTimes[Phase::MUTATOR];
-        PodZero(&phaseStartTimes);
+        for (mozilla::TimeStamp& t : phaseStartTimes)
+            t = TimeStamp();
+#ifdef DEBUG
+        for (mozilla::TimeStamp& t : phaseEndTimes)
+            t = TimeStamp();
+#endif
         PodZero(&phaseTimes);
         phaseStartTimes[Phase::MUTATOR] = mutatorStartTime;
         phaseTimes[Phase::MUTATOR] = mutatorTime;
     }
 
     aborted = false;
 }
 
@@ -1207,18 +1212,17 @@ Statistics::recordPhaseBegin(Phase phase
     MOZ_ASSERT(phaseStack.length() < MAX_PHASE_NESTING);
 
     Phase current = currentPhase();
     MOZ_ASSERT(phases[phase].parent == current);
 
     TimeStamp now = TimeStamp::Now();
 
     if (current != Phase::NONE) {
-        // Sadly this happens sometimes.
-        MOZ_ASSERT(now >= phaseStartTimes[currentPhase()]);
+        MOZ_ASSERT(now >= phaseStartTimes[currentPhase()], "Inconsistent time data; see bug 1400153");
         if (now < phaseStartTimes[currentPhase()]) {
             now = phaseStartTimes[currentPhase()];
             aborted = true;
         }
     }
 
     phaseStack.infallibleAppend(phase);
     phaseStartTimes[phase] = now;
@@ -1228,33 +1232,55 @@ void
 Statistics::recordPhaseEnd(Phase phase)
 {
     MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime));
 
     MOZ_ASSERT(phaseStartTimes[phase]);
 
     TimeStamp now = TimeStamp::Now();
 
-    // Sadly this happens sometimes.
-    MOZ_ASSERT(now >= phaseStartTimes[phase]);
+    // Make sure this phase ends after it starts.
+    MOZ_ASSERT(now >= phaseStartTimes[phase], "Inconsistent time data; see bug 1400153");
+
+#ifdef DEBUG
+    // Make sure this phase ends after all of its children. Note that some
+    // children might not have run in this instance, in which case they will
+    // have run in a previous instance of this parent or not at all.
+    for (Phase kid = phases[phase].firstChild; kid != Phase::NONE; kid = phases[kid].nextSibling) {
+        if (phaseEndTimes[kid].IsNull())
+            continue;
+        if (phaseEndTimes[kid] > now)
+            fprintf(stderr, "Parent %s ended at %.3fms, before child %s ended at %.3fms?\n",
+                    phases[phase].name,
+                    t(now - TimeStamp::ProcessCreation()),
+                    phases[kid].name,
+                    t(phaseEndTimes[kid] - TimeStamp::ProcessCreation()));
+        MOZ_ASSERT(phaseEndTimes[kid] <= now, "Inconsistent time data; see bug 1400153");
+    }
+#endif
+
     if (now < phaseStartTimes[phase]) {
         now = phaseStartTimes[phase];
         aborted = true;
     }
 
     if (phase == Phase::MUTATOR)
         timedGCStart = now;
 
     phaseStack.popBack();
 
     TimeDuration t = now - phaseStartTimes[phase];
     if (!slices_.empty())
         slices_.back().phaseTimes[phase] += t;
     phaseTimes[phase] += t;
     phaseStartTimes[phase] = TimeStamp();
+
+#ifdef DEBUG
+    phaseEndTimes[phase] = now;
+#endif
 }
 
 void
 Statistics::endPhase(PhaseKind phaseKind)
 {
     Phase phase = currentPhase();
     MOZ_ASSERT(phase != Phase::NONE);
     MOZ_ASSERT(phases[phase].phaseKind == phaseKind);
--- a/js/src/gc/Statistics.h
+++ b/js/src/gc/Statistics.h
@@ -284,16 +284,21 @@ struct Statistics
 
     gc::AbortReason nonincrementalReason_;
 
     SliceDataVector slices_;
 
     /* Most recent time when the given phase started. */
     EnumeratedArray<Phase, Phase::LIMIT, TimeStamp> phaseStartTimes;
 
+#ifdef DEBUG
+    /* Most recent time when the given phase ended. */
+    EnumeratedArray<Phase, Phase::LIMIT, TimeStamp> phaseEndTimes;
+#endif
+
     /* Bookkeeping for GC timings when timingMutator is true */
     TimeStamp timedGCStart;
     TimeDuration timedGCTime;
 
     /* Total time in a given phase for this GC. */
     PhaseTimeTable phaseTimes;
     PhaseTimeTable parallelTimes;
 
--- a/js/src/vm/HelperThreads.cpp
+++ b/js/src/vm/HelperThreads.cpp
@@ -18,16 +18,17 @@
 #include "threading/CpuCount.h"
 #include "util/NativeStack.h"
 #include "vm/Debugger.h"
 #include "vm/ErrorReporting.h"
 #include "vm/SharedImmutableStringsCache.h"
 #include "vm/Time.h"
 #include "vm/TraceLogging.h"
 #include "vm/Xdr.h"
+#include "wasm/WasmGenerator.h"
 
 #include "gc/PrivateIterators-inl.h"
 #include "vm/JSCompartment-inl.h"
 #include "vm/JSContext-inl.h"
 #include "vm/JSObject-inl.h"
 #include "vm/JSScript-inl.h"
 #include "vm/NativeObject-inl.h"
 
@@ -1171,16 +1172,22 @@ GlobalHelperThreadState::addSizeOfInclud
     // Report IonBuilders on wait lists
     for (auto builder : ionWorklist_)
         htStats.ionBuilder += builder->sizeOfIncludingThis(mallocSizeOf);
     for (auto builder : ionFinishedList_)
         htStats.ionBuilder += builder->sizeOfIncludingThis(mallocSizeOf);
     for (auto builder : ionFreeList_)
         htStats.ionBuilder += builder->sizeOfIncludingThis(mallocSizeOf);
 
+    // Report wasm::CompileTasks on wait lists
+    for (auto task : wasmWorklist_tier1_)
+        htStats.wasmCompile += task->sizeOfIncludingThis(mallocSizeOf);
+    for (auto task : wasmWorklist_tier2_)
+        htStats.wasmCompile += task->sizeOfIncludingThis(mallocSizeOf);
+
     // Report number of helper threads.
     MOZ_ASSERT(htStats.idleThreadCount == 0);
     if (threads) {
         for (auto& thread : *threads) {
             if (thread.idle())
                 htStats.idleThreadCount++;
             else
                 htStats.activeThreadCount++;
--- a/js/src/wasm/WasmGenerator.cpp
+++ b/js/src/wasm/WasmGenerator.cpp
@@ -1020,8 +1020,33 @@ ModuleGenerator::finishTier2(Module& mod
     if (MOZ_UNLIKELY(JitOptions.wasmDelayTier2)) {
         // Introduce an artificial delay when testing wasmDelayTier2, since we
         // want to exercise both tier1 and tier2 code in this case.
         std::this_thread::sleep_for(std::chrono::milliseconds(500));
     }
 
     return module.finishTier2(Move(linkDataTier_), Move(tier2), env_);
 }
+
+size_t
+CompiledCode::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const
+{
+    size_t trapSitesSize = 0;
+    for (const TrapSiteVector& vec : trapSites)
+        trapSitesSize += vec.sizeOfExcludingThis(mallocSizeOf);
+
+    return bytes.sizeOfExcludingThis(mallocSizeOf) +
+           codeRanges.sizeOfExcludingThis(mallocSizeOf) +
+           callSites.sizeOfExcludingThis(mallocSizeOf) +
+           callSiteTargets.sizeOfExcludingThis(mallocSizeOf) +
+           trapSitesSize +
+           callFarJumps.sizeOfExcludingThis(mallocSizeOf) +
+           symbolicAccesses.sizeOfExcludingThis(mallocSizeOf) +
+           codeLabels.sizeOfExcludingThis(mallocSizeOf);
+}
+
+size_t
+CompileTask::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const
+{
+    return lifo.sizeOfExcludingThis(mallocSizeOf) +
+           inputs.sizeOfExcludingThis(mallocSizeOf) +
+           output.sizeOfExcludingThis(mallocSizeOf);
+}
--- a/js/src/wasm/WasmGenerator.h
+++ b/js/src/wasm/WasmGenerator.h
@@ -14,16 +14,18 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 #ifndef wasm_generator_h
 #define wasm_generator_h
 
+#include "mozilla/MemoryReporting.h"
+
 #include "jit/MacroAssembler.h"
 #include "wasm/WasmCompile.h"
 #include "wasm/WasmModule.h"
 #include "wasm/WasmValidate.h"
 
 namespace js {
 namespace wasm {
 
@@ -88,16 +90,18 @@ struct CompiledCode
                codeRanges.empty() &&
                callSites.empty() &&
                callSiteTargets.empty() &&
                trapSites.empty() &&
                callFarJumps.empty() &&
                symbolicAccesses.empty() &&
                codeLabels.empty();
     }
+
+    size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const;
 };
 
 // The CompileTaskState of a ModuleGenerator contains the mutable state shared
 // between helper threads executing CompileTasks. Each CompileTask started on a
 // helper thread eventually either ends up in the 'finished' list or increments
 // 'numFailed'.
 
 struct CompileTaskState
@@ -123,16 +127,22 @@ struct CompileTask
     FuncCompileInputVector     inputs;
     CompiledCode               output;
 
     CompileTask(const ModuleEnvironment& env, ExclusiveCompileTaskState& state, size_t defaultChunkSize)
       : env(env),
         state(state),
         lifo(defaultChunkSize)
     {}
+
+    size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const;
+    size_t sizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const
+    {
+        return mallocSizeOf(this) + sizeOfExcludingThis(mallocSizeOf);
+    }
 };
 
 // A ModuleGenerator encapsulates the creation of a wasm module. During the
 // lifetime of a ModuleGenerator, a sequence of FunctionGenerators are created
 // and destroyed to compile the individual function bodies. After generating all
 // functions, ModuleGenerator::finish() must be called to complete the
 // compilation and extract the resulting wasm module.
 
--- a/js/xpconnect/src/XPCJSRuntime.cpp
+++ b/js/xpconnect/src/XPCJSRuntime.cpp
@@ -2494,16 +2494,20 @@ JSReporter::CollectReports(WindowPaths* 
 
     REPORT_BYTES(NS_LITERAL_CSTRING("explicit/js-non-window/helper-thread/parse-task"),
         KIND_HEAP, gStats.helperThread.parseTask,
         "The memory used by ParseTasks waiting in HelperThreadState.");
 
     REPORT_BYTES(NS_LITERAL_CSTRING("explicit/js-non-window/helper-thread/ion-builder"),
         KIND_HEAP, gStats.helperThread.ionBuilder,
         "The memory used by IonBuilders waiting in HelperThreadState.");
+
+    REPORT_BYTES(NS_LITERAL_CSTRING("explicit/js-non-window/helper-thread/wasm-compile"),
+        KIND_HEAP, gStats.helperThread.parseTask,
+        "The memory used by Wasm compilations waiting in HelperThreadState.");
 }
 
 static nsresult
 JSSizeOfTab(JSObject* objArg, size_t* jsObjectsSize, size_t* jsStringsSize,
             size_t* jsPrivateSize, size_t* jsOtherSize)
 {
     JSContext* cx = XPCJSContext::Get()->Context();
     JS::RootedObject obj(cx, objArg);
--- a/js/xpconnect/src/XPCWrappedNative.cpp
+++ b/js/xpconnect/src/XPCWrappedNative.cpp
@@ -1346,20 +1346,25 @@ CallMethodHelper::GetArraySizeFromParam(
     // it in the right slot so that we can find it again when cleaning up the params.
     // from the array.
     if (paramIndex >= mArgc && maybeArray.isObject()) {
         MOZ_ASSERT(mMethodInfo->GetParam(paramIndex).IsOptional());
         RootedObject arrayOrNull(mCallContext, maybeArray.isObject() ? &maybeArray.toObject()
                                                                      : nullptr);
 
         bool isArray;
-        if (!JS_IsArrayObject(mCallContext, maybeArray, &isArray) ||
-            !isArray ||
-            !JS_GetArrayLength(mCallContext, arrayOrNull, &GetDispatchParam(paramIndex)->val.u32))
-        {
+        bool ok = false;
+        if (JS_IsArrayObject(mCallContext, maybeArray, &isArray) && isArray) {
+            ok = JS_GetArrayLength(mCallContext, arrayOrNull, &GetDispatchParam(paramIndex)->val.u32);
+        } else if (JS_IsTypedArrayObject(&maybeArray.toObject())) {
+            GetDispatchParam(paramIndex)->val.u32 = JS_GetTypedArrayLength(&maybeArray.toObject());
+            ok = true;
+        }
+
+        if (!ok) {
             return Throw(NS_ERROR_XPC_CANT_CONVERT_OBJECT_TO_ARRAY, mCallContext);
         }
     }
 
     *result = GetDispatchParam(paramIndex)->val.u32;
 
     return true;
 }
--- a/layout/base/RestyleManager.cpp
+++ b/layout/base/RestyleManager.cpp
@@ -2587,16 +2587,18 @@ RestyleManager::ProcessPostTraversal(
   Element* aElement,
   ComputedStyle* aParentContext,
   ServoRestyleState& aRestyleState,
   ServoPostTraversalFlags aFlags)
 {
   nsIFrame* styleFrame = nsLayoutUtils::GetStyleFrame(aElement);
   nsIFrame* primaryFrame = aElement->GetPrimaryFrame();
 
+  MOZ_ASSERT(aElement->HasServoData(), "How in the world?");
+
   // NOTE(emilio): This is needed because for table frames the bit is set on the
   // table wrapper (which is the primary frame), not on the table itself.
   const bool isOutOfFlow =
     primaryFrame &&
     primaryFrame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW);
 
   // Grab the change hint from Servo.
   bool wasRestyled;
--- a/layout/forms/nsTextControlFrame.cpp
+++ b/layout/forms/nsTextControlFrame.cpp
@@ -923,46 +923,34 @@ nsTextControlFrame::ScrollSelectionIntoV
                                            nsISelectionController::SELECTION_FOCUS_REGION,
                                            nsISelectionController::SCROLL_FIRST_ANCESTOR_ONLY);
   }
 
   return NS_ERROR_FAILURE;
 }
 
 nsresult
-nsTextControlFrame::GetRootNodeAndInitializeEditor(nsIDOMElement **aRootElement)
-{
-  NS_ENSURE_ARG_POINTER(aRootElement);
-
-  RefPtr<TextEditor> textEditor = GetTextEditor();
-  if (!textEditor) {
-    return NS_OK;
-  }
-  return textEditor->GetRootElement(aRootElement);
-}
-
-nsresult
 nsTextControlFrame::SelectAllOrCollapseToEndOfText(bool aSelect)
 {
-  nsCOMPtr<nsIDOMElement> rootElement;
-  nsresult rv = GetRootNodeAndInitializeEditor(getter_AddRefs(rootElement));
-  NS_ENSURE_SUCCESS(rv, rv);
+  nsresult rv = EnsureEditorInitialized();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
 
-  nsCOMPtr<nsIContent> rootContent = do_QueryInterface(rootElement);
   nsCOMPtr<nsINode> rootNode;
-  rootNode = rootContent;
+  rootNode= mRootNode;
 
-  NS_ENSURE_TRUE(rootNode && rootContent, NS_ERROR_FAILURE);
+  NS_ENSURE_TRUE(rootNode, NS_ERROR_FAILURE);
 
-  int32_t numChildren = rootContent->GetChildCount();
+  int32_t numChildren = mRootNode->GetChildCount();
 
   if (numChildren > 0) {
     // We never want to place the selection after the last
     // br under the root node!
-    nsIContent *child = rootContent->GetLastChild();
+    nsIContent *child = mRootNode->GetLastChild();
     if (child) {
       if (child->IsHTMLElement(nsGkAtoms::br)) {
         child = child->GetPreviousSibling();
         --numChildren;
       } else if (child->IsText() && !child->Length()) {
         // Editor won't remove text node when empty value.
         --numChildren;
       }
@@ -1042,33 +1030,34 @@ nsTextControlFrame::OffsetToDOMPoint(uin
                                      nsINode** aResult,
                                      uint32_t* aPosition)
 {
   NS_ENSURE_ARG_POINTER(aResult && aPosition);
 
   *aResult = nullptr;
   *aPosition = 0;
 
-  nsCOMPtr<nsIDOMElement> rootElement;
-  nsresult rv = GetRootNodeAndInitializeEditor(getter_AddRefs(rootElement));
-  NS_ENSURE_SUCCESS(rv, rv);
-  nsCOMPtr<nsINode> rootNode(do_QueryInterface(rootElement));
+  nsresult rv = EnsureEditorInitialized();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
 
+  RefPtr<Element> rootNode = mRootNode;
   NS_ENSURE_TRUE(rootNode, NS_ERROR_FAILURE);
 
   nsCOMPtr<nsINodeList> nodeList = rootNode->ChildNodes();
   uint32_t length = nodeList->Length();
 
   NS_ASSERTION(length <= 2, "We should have one text node and one mozBR at most");
 
   nsCOMPtr<nsINode> firstNode = nodeList->Item(0);
   Text* textNode = firstNode ? firstNode->GetAsText() : nullptr;
 
   if (length == 0) {
-    NS_IF_ADDREF(*aResult = rootNode);
+    rootNode.forget(aResult);
     *aPosition = 0;
   } else if (textNode) {
     uint32_t textLength = textNode->Length();
     if (length == 2 && aOffset == textLength) {
       // If we're at the end of the text node and we have a trailing BR node,
       // set the selection on the BR node.
       rootNode.forget(aResult);
       *aPosition = 1;
@@ -1347,43 +1336,37 @@ nsTextControlFrame::GetOwnedFrameSelecti
   NS_ASSERTION(txtCtrl, "Content not a text control element");
 
   return txtCtrl->GetConstFrameSelection();
 }
 
 UniquePtr<PresState>
 nsTextControlFrame::SaveState()
 {
-  nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent());
-  NS_ASSERTION(txtCtrl, "Content not a text control element");
-
-  nsIContent* rootNode = txtCtrl->GetRootEditorNode();
-  if (rootNode) {
+  if (mRootNode) {
     // Query the nsIStatefulFrame from the HTMLScrollFrame
-    nsIStatefulFrame* scrollStateFrame = do_QueryFrame(rootNode->GetPrimaryFrame());
+    nsIStatefulFrame* scrollStateFrame =
+      do_QueryFrame(mRootNode->GetPrimaryFrame());
     if (scrollStateFrame) {
       return scrollStateFrame->SaveState();
     }
   }
 
   return nullptr;
 }
 
 NS_IMETHODIMP
 nsTextControlFrame::RestoreState(PresState* aState)
 {
   NS_ENSURE_ARG_POINTER(aState);
 
-  nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent());
-  NS_ASSERTION(txtCtrl, "Content not a text control element");
-
-  nsIContent* rootNode = txtCtrl->GetRootEditorNode();
-  if (rootNode) {
+  if (mRootNode) {
     // Query the nsIStatefulFrame from the HTMLScrollFrame
-    nsIStatefulFrame* scrollStateFrame = do_QueryFrame(rootNode->GetPrimaryFrame());
+    nsIStatefulFrame* scrollStateFrame =
+      do_QueryFrame(mRootNode->GetPrimaryFrame());
     if (scrollStateFrame) {
       return scrollStateFrame->RestoreState(aState);
     }
   }
 
   // Most likely, we don't have our anonymous content constructed yet, which
   // would cause us to end up here.  In this case, we'll just store the scroll
   // pos ourselves, and forward it to the scroll frame later when it's created.
@@ -1419,32 +1402,31 @@ nsTextControlFrame::BuildDisplayList(nsD
   // opacity creating stacking contexts that then get sorted with stacking
   // contexts external to us.
   nsDisplayList* content = aLists.Content();
   nsDisplayListSet set(content, content, content, content, content, content);
 
   while (kid) {
     // If the frame is the placeholder or preview frame, we should only show
     // it if it has to be visible.
-    if (!((kid->GetContent() == txtCtrl->GetPlaceholderNode() &&
+    if (!((kid->GetContent() == mPlaceholderDiv &&
            !txtCtrl->GetPlaceholderVisibility()) ||
-          (kid->GetContent() == txtCtrl->GetPreviewNode() &&
+          (kid->GetContent() == mPreviewDiv &&
            !txtCtrl->GetPreviewVisibility()))) {
       BuildDisplayListForChild(aBuilder, kid, set, 0);
     }
     kid = kid->GetNextSibling();
   }
 }
 
 mozilla::dom::Element*
 nsTextControlFrame::GetPseudoElement(CSSPseudoElementType aType)
 {
   if (aType == CSSPseudoElementType::placeholder) {
-    nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent());
-    return txtCtrl->GetPlaceholderNode();
+    return mPlaceholderDiv;
   }
 
   return nsContainerFrame::GetPseudoElement(aType);
 }
 
 NS_IMETHODIMP
 nsTextControlFrame::EditorInitializer::Run()
 {
--- a/layout/forms/nsTextControlFrame.h
+++ b/layout/forms/nsTextControlFrame.h
@@ -188,20 +188,16 @@ protected:
 
 public: //for methods who access nsTextControlFrame directly
   void SetValueChanged(bool aValueChanged);
 
   mozilla::dom::Element* GetRootNode() const {
     return mRootNode;
   }
 
-  mozilla::dom::Element* GetPlaceholderNode() const {
-    return mPlaceholderDiv;
-  }
-
   mozilla::dom::Element* GetPreviewNode() const {
     return mPreviewDiv;
   }
 
   // called by the focus listener
   nsresult MaybeBeginSecureKeyboardInput();
   void MaybeEndSecureKeyboardInput();
 
@@ -322,26 +318,16 @@ private:
   //helper methods
   nsresult SetSelectionInternal(nsINode* aStartNode, uint32_t aStartOffset,
                                 nsINode* aEndNode, uint32_t aEndOffset,
                                 SelectionDirection aDirection = eNone);
   nsresult SelectAllOrCollapseToEndOfText(bool aSelect);
   nsresult SetSelectionEndPoints(uint32_t aSelStart, uint32_t aSelEnd,
                                  SelectionDirection aDirection = eNone);
 
-  /**
-   * Return the root DOM element, and implicitly initialize the editor if
-   * needed.
-   *
-   * XXXbz This function is slow.  Very slow.  Consider using
-   * EnsureEditorInitialized() if you need that, and
-   * nsITextControlElement::GetRootEditorNode on our content if you need that.
-   */
-  nsresult GetRootNodeAndInitializeEditor(nsIDOMElement **aRootElement);
-
   void FinishedInitializer() {
     DeleteProperty(TextControlInitializer());
   }
 
   const nsAString& CachedValue() const
   {
     return mCachedValue;
   }
--- a/layout/generic/nsColumnSetFrame.cpp
+++ b/layout/generic/nsColumnSetFrame.cpp
@@ -283,21 +283,20 @@ nsColumnSetFrame::GetAvailableContentBSi
   WritingMode wm = aReflowInput.GetWritingMode();
   LogicalMargin bp = aReflowInput.ComputedLogicalBorderPadding();
   bp.ApplySkipSides(GetLogicalSkipSides(&aReflowInput));
   bp.BEnd(wm) = aReflowInput.ComputedLogicalBorderPadding().BEnd(wm);
   return std::max(0, aReflowInput.AvailableBSize() - bp.BStartEnd(wm));
 }
 
 static nscoord
-GetColumnGap(nsColumnSetFrame*    aFrame,
-             const nsStyleColumn* aColStyle,
-             nscoord              aPercentageBasis)
+GetColumnGap(nsColumnSetFrame* aFrame,
+             nscoord           aPercentageBasis)
 {
-  const auto& columnGap = aColStyle->mColumnGap;
+  const auto& columnGap = aFrame->StylePosition()->mColumnGap;
   if (columnGap.GetUnit() == eStyleUnit_Normal) {
     return aFrame->StyleFont()->mFont.size;
   }
   return nsLayoutUtils::ResolveGapToLength(columnGap, aPercentageBasis);
 }
 
 nsColumnSetFrame::ReflowConfig
 nsColumnSetFrame::ChooseColumnStrategy(const ReflowInput& aReflowInput,
@@ -325,17 +324,17 @@ nsColumnSetFrame::ChooseColumnStrategy(c
   nscoord colBSize = GetAvailableContentBSize(aReflowInput);
 
   if (aReflowInput.ComputedBSize() != NS_INTRINSICSIZE) {
     colBSize = aReflowInput.ComputedBSize();
   } else if (aReflowInput.ComputedMaxBSize() != NS_INTRINSICSIZE) {
     colBSize = std::min(colBSize, aReflowInput.ComputedMaxBSize());
   }
 
-  nscoord colGap = GetColumnGap(this, colStyle, aReflowInput.ComputedISize());
+  nscoord colGap = GetColumnGap(this, aReflowInput.ComputedISize());
   int32_t numColumns = colStyle->mColumnCount;
 
   // If column-fill is set to 'balance', then we want to balance the columns.
   const bool isBalancing = colStyle->mColumnFill == NS_STYLE_COLUMN_FILL_BALANCE
                            && !aForceAuto;
   if (isBalancing) {
     const uint32_t MAX_NESTED_COLUMN_BALANCING = 2;
     uint32_t cnt = 0;
@@ -509,17 +508,17 @@ nsColumnSetFrame::GetMinISize(gfxContext
   } else {
     NS_ASSERTION(colStyle->mColumnCount > 0,
                  "column-count and column-width can't both be auto");
     // As available width reduces to zero, we still have mColumnCount columns,
     // so multiply the child's min-width by the number of columns (n) and
     // include n-1 column gaps.
     colISize = iSize;
     iSize *= colStyle->mColumnCount;
-    nscoord colGap = GetColumnGap(this, colStyle, NS_UNCONSTRAINEDSIZE);
+    nscoord colGap = GetColumnGap(this, NS_UNCONSTRAINEDSIZE);
     iSize += colGap * (colStyle->mColumnCount - 1);
     // The multiplication above can make 'width' negative (integer overflow),
     // so use std::max to protect against that.
     iSize = std::max(iSize, colISize);
   }
   // XXX count forced column breaks here? Maybe we should return the child's
   // min-width times the minimum number of columns.
   return iSize;
@@ -530,17 +529,17 @@ nsColumnSetFrame::GetPrefISize(gfxContex
 {
   // Our preferred width is our desired column width, if specified, otherwise
   // the child's preferred width, times the number of columns, plus the width
   // of any required column gaps
   // XXX what about forced column breaks here?
   nscoord result = 0;
   DISPLAY_PREF_WIDTH(this, result);
   const nsStyleColumn* colStyle = StyleColumn();
-  nscoord colGap = GetColumnGap(this, colStyle, NS_UNCONSTRAINEDSIZE);
+  nscoord colGap = GetColumnGap(this, NS_UNCONSTRAINEDSIZE);
 
   nscoord colISize;
   if (colStyle->mColumnWidth.GetUnit() == eStyleUnit_Coord) {
     colISize = colStyle->mColumnWidth.GetCoordValue();
   } else if (mFrames.FirstChild()) {
     colISize = mFrames.FirstChild()->GetPrefISize(aRenderingContext);
   } else {
     colISize = 0;
--- a/layout/generic/nsGridContainerFrame.cpp
+++ b/layout/generic/nsGridContainerFrame.cpp
@@ -2370,19 +2370,19 @@ struct MOZ_STACK_CLASS nsGridContainerFr
 };
 
 void
 nsGridContainerFrame::GridReflowInput::CalculateTrackSizes(
   const Grid&        aGrid,
   const LogicalSize& aContentBox,
   SizingConstraint   aConstraint)
 {
-  mCols.Initialize(mColFunctions, mGridStyle->mGridColumnGap,
+  mCols.Initialize(mColFunctions, mGridStyle->mColumnGap,
                    aGrid.mGridColEnd, aContentBox.ISize(mWM));
-  mRows.Initialize(mRowFunctions, mGridStyle->mGridRowGap,
+  mRows.Initialize(mRowFunctions, mGridStyle->mRowGap,
                    aGrid.mGridRowEnd, aContentBox.BSize(mWM));
 
   mCols.CalculateSizes(*this, mGridItems, mColFunctions,
                        aContentBox.ISize(mWM), &GridArea::mCols,
                        aConstraint);
   mCols.AlignJustifyContent(mGridStyle, mWM, aContentBox);
   // Column positions and sizes are now final.
   mCols.mCanResolveLineRangeSize = true;
@@ -3126,26 +3126,26 @@ nsGridContainerFrame::Grid::PlaceGridIte
   // Initialize the end lines of the Explicit Grid (mExplicitGridCol[Row]End).
   // This is determined by the larger of the number of rows/columns defined
   // by 'grid-template-areas' and the 'grid-template-rows'/'-columns', plus one.
   // Also initialize the Implicit Grid (mGridCol[Row]End) to the same values.
   // Note that this is for a grid with a 1,1 origin.  We'll change that
   // to a 0,0 based grid after placing definite lines.
   auto areas = gridStyle->mGridTemplateAreas.get();
   uint32_t numRepeatCols = aState.mColFunctions.InitRepeatTracks(
-                             gridStyle->mGridColumnGap,
+                             gridStyle->mColumnGap,
                              aComputedMinSize.ISize(aState.mWM),
                              aComputedSize.ISize(aState.mWM),
                              aComputedMaxSize.ISize(aState.mWM));
   mGridColEnd = mExplicitGridColEnd =
     aState.mColFunctions.ComputeExplicitGridEnd(areas ? areas->mNColumns + 1 : 1);
   LineNameMap colLineNameMap(gridStyle->GridTemplateColumns(), numRepeatCols);
 
   uint32_t numRepeatRows = aState.mRowFunctions.InitRepeatTracks(
-                             gridStyle->mGridRowGap,
+                             gridStyle->mRowGap,
                              aComputedMinSize.BSize(aState.mWM),
                              aComputedSize.BSize(aState.mWM),
                              aComputedMaxSize.BSize(aState.mWM));
   mGridRowEnd = mExplicitGridRowEnd =
     aState.mRowFunctions.ComputeExplicitGridEnd(areas ? areas->NRows() + 1 : 1);
   LineNameMap rowLineNameMap(gridStyle->GridTemplateRows(), numRepeatRows);
 
   // http://dev.w3.org/csswg/css-grid/#line-placement
@@ -6365,25 +6365,25 @@ nsGridContainerFrame::IntrinsicISize(gfx
                      &max.BSize(state.mWM));
   }
 
   Grid grid;
   grid.PlaceGridItems(state, min, sz, max);  // XXX optimize
   if (grid.mGridColEnd == 0) {
     return 0;
   }
-  state.mCols.Initialize(state.mColFunctions, state.mGridStyle->mGridColumnGap,
+  state.mCols.Initialize(state.mColFunctions, state.mGridStyle->mColumnGap,
                          grid.mGridColEnd, NS_UNCONSTRAINEDSIZE);
   auto constraint = aType == nsLayoutUtils::MIN_ISIZE ?
     SizingConstraint::eMinContent : SizingConstraint::eMaxContent;
   state.mCols.CalculateSizes(state, state.mGridItems, state.mColFunctions,
                              NS_UNCONSTRAINEDSIZE, &GridArea::mCols,
                              constraint);
   state.mCols.mGridGap =
-    nsLayoutUtils::ResolveGapToLength(state.mGridStyle->mGridColumnGap,
+    nsLayoutUtils::ResolveGapToLength(state.mGridStyle->mColumnGap,
                                       NS_UNCONSTRAINEDSIZE);
   nscoord length = 0;
   for (const TrackSize& sz : state.mCols.mSizes) {
     length += sz.mBase;
   }
   return length + state.mCols.SumOfGridGaps();
 }
 
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -2396,18 +2396,19 @@ TreatAsOpaque(nsDisplayItem* aItem, nsDi
 bool
 nsDisplayList::ComputeVisibilityForSublist(nsDisplayListBuilder* aBuilder,
                                            nsRegion* aVisibleRegion,
                                            const nsRect& aListVisibleBounds)
 {
 #ifdef DEBUG
   nsRegion r;
   r.And(*aVisibleRegion, GetBounds(aBuilder));
-  NS_ASSERTION(r.GetBounds().IsEqualInterior(aListVisibleBounds),
-               "bad aListVisibleBounds");
+  // XXX this fails sometimes:
+  NS_WARNING_ASSERTION(r.GetBounds().IsEqualInterior(aListVisibleBounds),
+                       "bad aListVisibleBounds");
 #endif
 
   bool anyVisible = false;
 
   AutoTArray<nsDisplayItem*, 512> elements;
   MoveListTo(this, &elements);
 
   for (int32_t i = elements.Length() - 1; i >= 0; --i) {
--- a/layout/reftests/svg/filters/feBlend-1-ref.svg
+++ b/layout/reftests/svg/filters/feBlend-1-ref.svg
@@ -11,10 +11,9 @@
 <rect x="0" y="50" width="50" height="50" fill="#DFB53F"/>
 <rect x="50" y="50" width="50" height="50" fill="#B5DF3F"/>
 <rect x="100" y="50" width="50" height="50" fill="#DFDF3F"/>
 <rect x="150" y="50" width="50" height="50" fill="#DFDF3F"/>
 <rect x="200" y="50" width="50" height="50" fill="#DFC88E"/>
 <rect x="250" y="50" width="50" height="50" fill="#B5DF3F"/>
 <rect x="300" y="50" width="50" height="50" fill="#DFC88E"/>
 <rect x="350" y="50" width="50" height="50" fill="#B5CC3F"/>
-<rect x="0" y="100" width="50" height="50" fill="#DFB53F"/>
 </svg>
--- a/layout/style/nsCSSPropAliasList.h
+++ b/layout/style/nsCSSPropAliasList.h
@@ -592,13 +592,28 @@ CSS_PROP_ALIAS(-webkit-transition-timing
                WEBKIT_PREFIX_PREF)
 CSS_PROP_ALIAS(-webkit-user-select,
                _webkit_user_select,
                _moz_user_select,
                WebkitUserSelect,
                WEBKIT_PREFIX_PREF)
 #undef WEBKIT_PREFIX_PREF
 
+CSS_PROP_ALIAS(grid-column-gap,
+               grid_column_gap,
+               column_gap,
+               GridColumnGap,
+               "")
+CSS_PROP_ALIAS(grid-gap,
+               grid_gap,
+               gap,
+               GridGap,
+               "")
+CSS_PROP_ALIAS(grid-row-gap,
+               grid_row_gap,
+               row_gap,
+               GridRowGap,
+               "")
 CSS_PROP_ALIAS(word-wrap,
                word_wrap,
                overflow_wrap,
                WordWrap,
                "")
--- a/layout/style/nsCSSPropList.h
+++ b/layout/style/nsCSSPropList.h
@@ -1359,16 +1359,22 @@ CSS_PROP_(
     -moz-force-broken-image-icon,
     _moz_force_broken_image_icon,
     CSS_PROP_DOMPROP_PREFIXED(ForceBrokenImageIcon),
     0,
     "",
     VARIANT_HI,
     nullptr) // bug 58646
 CSS_PROP_SHORTHAND(
+    gap,
+    gap,
+    Gap,
+    CSS_PROPERTY_PARSE_FUNCTION,
+    "")
+CSS_PROP_SHORTHAND(
     grid,
     grid,
     Grid,
     CSS_PROPERTY_PARSE_FUNCTION,
     "")
 CSS_PROP_SHORTHAND(
     grid-area,
     grid_area,
@@ -1409,60 +1415,38 @@ CSS_PROP_(
     grid-column-end,
     grid_column_end,
     GridColumnEnd,
     CSS_PROPERTY_PARSE_FUNCTION,
     "",
     0,
     nullptr)
 CSS_PROP_(
-    grid-column-gap,
-    grid_column_gap,
-    GridColumnGap,
-    0,
-    "",
-    VARIANT_HLP | VARIANT_CALC,
-    nullptr)
-CSS_PROP_(
     grid-column-start,
     grid_column_start,
     GridColumnStart,
     CSS_PROPERTY_PARSE_FUNCTION,
     "",
     0,
     nullptr)
 CSS_PROP_SHORTHAND(
-    grid-gap,
-    grid_gap,
-    GridGap,
-    CSS_PROPERTY_PARSE_FUNCTION,
-    "")
-CSS_PROP_SHORTHAND(
     grid-row,
     grid_row,
     GridRow,
     CSS_PROPERTY_PARSE_FUNCTION,
     "")
 CSS_PROP_(
     grid-row-end,
     grid_row_end,
     GridRowEnd,
     CSS_PROPERTY_PARSE_FUNCTION,
     "",
     0,
     nullptr)
 CSS_PROP_(
-    grid-row-gap,
-    grid_row_gap,
-    GridRowGap,
-    0,
-    "",
-    VARIANT_HLP | VARIANT_CALC,
-    nullptr)
-CSS_PROP_(
     grid-row-start,
     grid_row_start,
     GridRowStart,
     CSS_PROPERTY_PARSE_FUNCTION,
     "",
     0,
     nullptr)
 CSS_PROP_SHORTHAND(
@@ -2379,16 +2363,24 @@ CSS_PROP_(
     rotate,
     rotate,
     Rotate,
     CSS_PROPERTY_PARSE_FUNCTION,
     "layout.css.individual-transform.enabled",
     0,
     nullptr)
 CSS_PROP_(
+    row-gap,
+    row_gap,
+    RowGap,
+    0,
+    "",
+    VARIANT_HLP | VARIANT_NORMAL | VARIANT_CALC,
+    nullptr)
+CSS_PROP_(
     ruby-align,
     ruby_align,
     RubyAlign,
     0,
     "",
     VARIANT_HK,
     kRubyAlignKTable)
 CSS_PROP_(
--- a/layout/style/nsCSSProps.cpp
+++ b/layout/style/nsCSSProps.cpp
@@ -2667,19 +2667,19 @@ static const nsCSSPropertyID gGridRowSub
 static const nsCSSPropertyID gGridAreaSubpropTable[] = {
   eCSSProperty_grid_row_start,
   eCSSProperty_grid_column_start,
   eCSSProperty_grid_row_end,
   eCSSProperty_grid_column_end,
   eCSSProperty_UNKNOWN
 };
 
-static const nsCSSPropertyID gGridGapSubpropTable[] = {
-  eCSSProperty_grid_row_gap,
-  eCSSProperty_grid_column_gap,
+static const nsCSSPropertyID gGapSubpropTable[] = {
+  eCSSProperty_row_gap,
+  eCSSProperty_column_gap,
   eCSSProperty_UNKNOWN
 };
 
 static const nsCSSPropertyID gOverflowSubpropTable[] = {
   eCSSProperty_overflow_x,
   eCSSProperty_overflow_y,
   eCSSProperty_UNKNOWN
 };
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -1247,31 +1247,16 @@ nsComputedDOMStyle::DoGetColumnWidth()
 
   // XXX fix the auto case. When we actually have a column frame, I think
   // we should return the computed column width.
   SetValueToCoord(val, StyleColumn()->mColumnWidth, true);
   return val.forget();
 }
 
 already_AddRefed<CSSValue>
-nsComputedDOMStyle::DoGetColumnGap()
-{
-  RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
-
-  const nsStyleColumn* column = StyleColumn();
-  if (column->mColumnGap.GetUnit() == eStyleUnit_Normal) {
-    val->SetIdent(eCSSKeyword_normal);
-  } else {
-    SetValueToCoord(val, StyleColumn()->mColumnGap, true);
-  }
-
-  return val.forget();
-}
-
-already_AddRefed<CSSValue>
 nsComputedDOMStyle::DoGetColumnFill()
 {
   RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
   val->SetIdent(
     nsCSSProps::ValueToKeywordEnum(StyleColumn()->mColumnFill,
                                    nsCSSProps::kColumnFillKTable));
   return val.forget();
 }
@@ -3424,28 +3409,38 @@ nsComputedDOMStyle::DoGetGridRowStart()
 
 already_AddRefed<CSSValue>
 nsComputedDOMStyle::DoGetGridRowEnd()
 {
   return GetGridLine(StylePosition()->mGridRowEnd);
 }
 
 already_AddRefed<CSSValue>
-nsComputedDOMStyle::DoGetGridColumnGap()
-{
-  RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
-  SetValueToCoord(val, StylePosition()->mGridColumnGap, true);
-  return val.forget();
-}
-
-already_AddRefed<CSSValue>
-nsComputedDOMStyle::DoGetGridRowGap()
-{
-  RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
-  SetValueToCoord(val, StylePosition()->mGridRowGap, true);
+nsComputedDOMStyle::DoGetColumnGap()
+{
+  RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
+  const auto& columnGap = StylePosition()->mColumnGap;
+  if (columnGap.GetUnit() == eStyleUnit_Normal) {
+    val->SetIdent(eCSSKeyword_normal);
+  } else {
+    SetValueToCoord(val, columnGap, true);
+  }
+  return val.forget();
+}
+
+already_AddRefed<CSSValue>
+nsComputedDOMStyle::DoGetRowGap()
+{
+  RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
+  const auto& rowGap = StylePosition()->mRowGap;
+  if (rowGap.GetUnit() == eStyleUnit_Normal) {
+    val->SetIdent(eCSSKeyword_normal);
+  } else {
+    SetValueToCoord(val, rowGap, true);
+  }
   return val.forget();
 }
 
 already_AddRefed<CSSValue>
 nsComputedDOMStyle::DoGetPaddingTop()
 {
   return GetPaddingWidthFor(eSideTop);
 }
--- a/layout/style/nsComputedDOMStyle.h
+++ b/layout/style/nsComputedDOMStyle.h
@@ -311,18 +311,16 @@ private:
   already_AddRefed<CSSValue> DoGetGridAutoRows();
   already_AddRefed<CSSValue> DoGetGridTemplateAreas();
   already_AddRefed<CSSValue> DoGetGridTemplateColumns();
   already_AddRefed<CSSValue> DoGetGridTemplateRows();
   already_AddRefed<CSSValue> DoGetGridColumnStart();
   already_AddRefed<CSSValue> DoGetGridColumnEnd();
   already_AddRefed<CSSValue> DoGetGridRowStart();
   already_AddRefed<CSSValue> DoGetGridRowEnd();
-  already_AddRefed<CSSValue> DoGetGridColumnGap();
-  already_AddRefed<CSSValue> DoGetGridRowGap();
 
   /* StyleImageLayer properties */
   already_AddRefed<CSSValue> DoGetImageLayerImage(const nsStyleImageLayers& aLayers);
   already_AddRefed<CSSValue> DoGetImageLayerPosition(const nsStyleImageLayers& aLayers);
   already_AddRefed<CSSValue> DoGetImageLayerPositionX(const nsStyleImageLayers& aLayers);
   already_AddRefed<CSSValue> DoGetImageLayerPositionY(const nsStyleImageLayers& aLayers);
   already_AddRefed<CSSValue> DoGetImageLayerRepeat(const nsStyleImageLayers& aLayers);
   already_AddRefed<CSSValue> DoGetImageLayerSize(const nsStyleImageLayers& aLayers);
@@ -534,17 +532,16 @@ private:
   already_AddRefed<CSSValue> DoGetWindowTransform();
   already_AddRefed<CSSValue> DoGetWindowTransformOrigin();
 
   /* Column properties */
   already_AddRefed<CSSValue> DoGetColumnCount();
   already_AddRefed<CSSValue> DoGetColumnFill();
   already_AddRefed<CSSValue> DoGetColumnSpan();
   already_AddRefed<CSSValue> DoGetColumnWidth();
-  already_AddRefed<CSSValue> DoGetColumnGap();
   already_AddRefed<CSSValue> DoGetColumnRuleWidth();
   already_AddRefed<CSSValue> DoGetColumnRuleStyle();
   already_AddRefed<CSSValue> DoGetColumnRuleColor();
 
   /* CSS Transitions */
   already_AddRefed<CSSValue> DoGetTransitionProperty();
   already_AddRefed<CSSValue> DoGetTransitionDuration();
   already_AddRefed<CSSValue> DoGetTransitionDelay();
@@ -572,16 +569,18 @@ private:
 
   /* CSS Box Alignment properties */
   already_AddRefed<CSSValue> DoGetAlignContent();
   already_AddRefed<CSSValue> DoGetAlignItems();
   already_AddRefed<CSSValue> DoGetAlignSelf();
   already_AddRefed<CSSValue> DoGetJustifyContent();
   already_AddRefed<CSSValue> DoGetJustifyItems();
   already_AddRefed<CSSValue> DoGetJustifySelf();
+  already_AddRefed<CSSValue> DoGetColumnGap();
+  already_AddRefed<CSSValue> DoGetRowGap();
 
   /* SVG properties */
   already_AddRefed<CSSValue> DoGetFill();
   already_AddRefed<CSSValue> DoGetStroke();
   already_AddRefed<CSSValue> DoGetMarkerEnd();
   already_AddRefed<CSSValue> DoGetMarkerMid();
   already_AddRefed<CSSValue> DoGetMarkerStart();
   already_AddRefed<CSSValue> DoGetStrokeDasharray();
--- a/layout/style/nsComputedDOMStylePropertyList.h
+++ b/layout/style/nsComputedDOMStylePropertyList.h
@@ -149,20 +149,18 @@ COMPUTED_STYLE_PROP(font_variant_ligatur
 COMPUTED_STYLE_PROP(font_variant_numeric,          FontVariantNumeric)
 COMPUTED_STYLE_PROP(font_variant_position,         FontVariantPosition)
 COMPUTED_STYLE_PROP(font_variation_settings,       FontVariationSettings)
 COMPUTED_STYLE_PROP(font_weight,                   FontWeight)
 COMPUTED_STYLE_PROP(grid_auto_columns,             GridAutoColumns)
 COMPUTED_STYLE_PROP(grid_auto_flow,                GridAutoFlow)
 COMPUTED_STYLE_PROP(grid_auto_rows,                GridAutoRows)
 COMPUTED_STYLE_PROP(grid_column_end,               GridColumnEnd)
-COMPUTED_STYLE_PROP(grid_column_gap,               GridColumnGap)
 COMPUTED_STYLE_PROP(grid_column_start,             GridColumnStart)
 COMPUTED_STYLE_PROP(grid_row_end,                  GridRowEnd)
-COMPUTED_STYLE_PROP(grid_row_gap,                  GridRowGap)
 COMPUTED_STYLE_PROP(grid_row_start,                GridRowStart)
 COMPUTED_STYLE_PROP(grid_template_areas,           GridTemplateAreas)
 COMPUTED_STYLE_PROP(grid_template_columns,         GridTemplateColumns)
 COMPUTED_STYLE_PROP(grid_template_rows,            GridTemplateRows)
 COMPUTED_STYLE_PROP(height,                        Height)
 COMPUTED_STYLE_PROP(hyphens,                       Hyphens)
 COMPUTED_STYLE_PROP(image_orientation,             ImageOrientation)
 COMPUTED_STYLE_PROP(ime_mode,                      IMEMode)
@@ -219,16 +217,17 @@ COMPUTED_STYLE_PROP(page_break_inside,  
 COMPUTED_STYLE_PROP(perspective,                   Perspective)
 COMPUTED_STYLE_PROP(perspective_origin,            PerspectiveOrigin)
 COMPUTED_STYLE_PROP(pointer_events,                PointerEvents)
 COMPUTED_STYLE_PROP(position,                      Position)
 COMPUTED_STYLE_PROP(quotes,                        Quotes)
 COMPUTED_STYLE_PROP(resize,                        Resize)
 COMPUTED_STYLE_PROP(right,                         Right)
 COMPUTED_STYLE_PROP(rotate,                        Rotate)
+COMPUTED_STYLE_PROP(row_gap,                       RowGap)
 COMPUTED_STYLE_PROP(ruby_align,                    RubyAlign)
 COMPUTED_STYLE_PROP(ruby_position,                 RubyPosition)
 COMPUTED_STYLE_PROP(scale,                         Scale)
 COMPUTED_STYLE_PROP(scroll_behavior,               ScrollBehavior)
 COMPUTED_STYLE_PROP(scroll_snap_coordinate,        ScrollSnapCoordinate)
 COMPUTED_STYLE_PROP(scroll_snap_destination,       ScrollSnapDestination)
 COMPUTED_STYLE_PROP(scroll_snap_points_x,          ScrollSnapPointsX)
 COMPUTED_STYLE_PROP(scroll_snap_points_y,          ScrollSnapPointsY)
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -751,17 +751,16 @@ nsStyleXUL::CalcDifference(const nsStyle
 // --------------------
 // nsStyleColumn
 //
 /* static */ const uint32_t nsStyleColumn::kMaxColumnCount;
 
 nsStyleColumn::nsStyleColumn(const nsPresContext* aContext)
   : mColumnCount(NS_STYLE_COLUMN_COUNT_AUTO)
   , mColumnWidth(eStyleUnit_Auto)
-  , mColumnGap(eStyleUnit_Normal)
   , mColumnRuleColor(StyleComplexColor::CurrentColor())
   , mColumnRuleStyle(NS_STYLE_BORDER_STYLE_NONE)
   , mColumnFill(NS_STYLE_COLUMN_FILL_BALANCE)
   , mColumnSpan(NS_STYLE_COLUMN_SPAN_NONE)
   , mColumnRuleWidth((StaticPresData::Get()
                         ->GetBorderWidthTable())[NS_STYLE_BORDER_WIDTH_MEDIUM])
   , mTwipsPerPixel(aContext->AppUnitsPerDevPixel())
 {
@@ -771,17 +770,16 @@ nsStyleColumn::nsStyleColumn(const nsPre
 nsStyleColumn::~nsStyleColumn()
 {
   MOZ_COUNT_DTOR(nsStyleColumn);
 }
 
 nsStyleColumn::nsStyleColumn(const nsStyleColumn& aSource)
   : mColumnCount(aSource.mColumnCount)
   , mColumnWidth(aSource.mColumnWidth)
-  , mColumnGap(aSource.mColumnGap)
   , mColumnRuleColor(aSource.mColumnRuleColor)
   , mColumnRuleStyle(aSource.mColumnRuleStyle)
   , mColumnFill(aSource.mColumnFill)
   , mColumnSpan(aSource.mColumnSpan)
   , mColumnRuleWidth(aSource.mColumnRuleWidth)
   , mTwipsPerPixel(aSource.mTwipsPerPixel)
 {
   MOZ_COUNT_CTOR(nsStyleColumn);
@@ -796,17 +794,16 @@ nsStyleColumn::CalcDifference(const nsSt
       mColumnSpan != aNewData.mColumnSpan) {
     // We force column count changes to do a reframe, because it's tricky to handle
     // some edge cases where the column count gets smaller and content overflows.
     // XXX not ideal
     return nsChangeHint_ReconstructFrame;
   }
 
   if (mColumnWidth != aNewData.mColumnWidth ||
-      mColumnGap != aNewData.mColumnGap ||
       mColumnFill != aNewData.mColumnFill) {
     return NS_STYLE_HINT_REFLOW;
   }
 
   if (GetComputedColumnRuleWidth() != aNewData.GetComputedColumnRuleWidth() ||
       mColumnRuleStyle != aNewData.mColumnRuleStyle ||
       mColumnRuleColor != aNewData.mColumnRuleColor) {
     return NS_STYLE_HINT_VISUAL;
@@ -1518,18 +1515,18 @@ nsStylePosition::nsStylePosition(const n
   , mJustifySelf(NS_STYLE_JUSTIFY_AUTO)
   , mFlexDirection(NS_STYLE_FLEX_DIRECTION_ROW)
   , mFlexWrap(NS_STYLE_FLEX_WRAP_NOWRAP)
   , mObjectFit(NS_STYLE_OBJECT_FIT_FILL)
   , mOrder(NS_STYLE_ORDER_INITIAL)
   , mFlexGrow(0.0f)
   , mFlexShrink(1.0f)
   , mZIndex(eStyleUnit_Auto)
-  , mGridColumnGap(nscoord(0), nsStyleCoord::CoordConstructor)
-  , mGridRowGap(nscoord(0), nsStyleCoord::CoordConstructor)
+  , mColumnGap(eStyleUnit_Normal)
+  , mRowGap(eStyleUnit_Normal)
 {
   MOZ_COUNT_CTOR(nsStylePosition);
 
   // positioning values not inherited
 
   mObjectPosition.SetInitialPercentValues(0.5f);
 
   nsStyleCoord  autoCoord(eStyleUnit_Auto);
@@ -1582,18 +1579,18 @@ nsStylePosition::nsStylePosition(const n
   , mFlexGrow(aSource.mFlexGrow)
   , mFlexShrink(aSource.mFlexShrink)
   , mZIndex(aSource.mZIndex)
   , mGridTemplateAreas(aSource.mGridTemplateAreas)
   , mGridColumnStart(aSource.mGridColumnStart)
   , mGridColumnEnd(aSource.mGridColumnEnd)
   , mGridRowStart(aSource.mGridRowStart)
   , mGridRowEnd(aSource.mGridRowEnd)
-  , mGridColumnGap(aSource.mGridColumnGap)
-  , mGridRowGap(aSource.mGridRowGap)
+  , mColumnGap(aSource.mColumnGap)
+  , mRowGap(aSource.mRowGap)
 {
   MOZ_COUNT_CTOR(nsStylePosition);
 
   if (aSource.mGridTemplateColumns) {
     mGridTemplateColumns =
       MakeUnique<nsStyleGridTemplate>(*aSource.mGridTemplateColumns);
   }
   if (aSource.mGridTemplateRows) {
@@ -1707,18 +1704,18 @@ nsStylePosition::CalcDifference(const ns
 
   // Properties that apply to grid items:
   // FIXME: only for grid items
   // (ie. parent frame is 'display: grid' or 'display: inline-grid')
   if (mGridColumnStart != aNewData.mGridColumnStart ||
       mGridColumnEnd != aNewData.mGridColumnEnd ||
       mGridRowStart != aNewData.mGridRowStart ||
       mGridRowEnd != aNewData.mGridRowEnd ||
-      mGridColumnGap != aNewData.mGridColumnGap ||
-      mGridRowGap != aNewData.mGridRowGap) {
+      mColumnGap != aNewData.mColumnGap ||
+      mRowGap != aNewData.mRowGap) {
     return hint |
            nsChangeHint_AllReflowHints;
   }
 
   // Changing 'justify-content/items/self' might affect the positioning,
   // but it won't affect any sizing.
   if (mJustifyContent != aNewData.mJustifyContent ||
       mJustifyItems != aNewData.mJustifyItems ||
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -1502,18 +1502,18 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
 
   // nullptr for 'none'
   RefPtr<mozilla::css::GridTemplateAreasValue> mGridTemplateAreas;
 
   nsStyleGridLine mGridColumnStart;
   nsStyleGridLine mGridColumnEnd;
   nsStyleGridLine mGridRowStart;
   nsStyleGridLine mGridRowEnd;
-  nsStyleCoord    mGridColumnGap;       // [reset] coord, percent, calc
-  nsStyleCoord    mGridRowGap;          // [reset] coord, percent, calc
+  nsStyleCoord    mColumnGap;       // [reset] normal, coord, percent, calc
+  nsStyleCoord    mRowGap;          // [reset] normal, coord, percent, calc
 
   // FIXME: Logical-coordinate equivalents to these WidthDepends... and
   // HeightDepends... methods have been introduced (see below); we probably
   // want to work towards removing the physical methods, and using the logical
   // ones in all cases.
 
   bool WidthDependsOnContainer() const
     {
@@ -2926,17 +2926,16 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
   /**
    * This is the maximum number of columns we can process. It's used in both
    * nsColumnSetFrame and nsRuleNode.
    */
   static const uint32_t kMaxColumnCount = 1000;
 
   uint32_t     mColumnCount; // [reset] see nsStyleConsts.h
   nsStyleCoord mColumnWidth; // [reset] coord, auto
-  nsStyleCoord mColumnGap;   // [reset] <length-percentage> | normal
 
   mozilla::StyleComplexColor mColumnRuleColor; // [reset]
   uint8_t      mColumnRuleStyle;  // [reset]
   uint8_t      mColumnFill;  // [reset] see nsStyleConsts.h
   uint8_t      mColumnSpan;  // [reset] see nsStyleConsts.h
 
   void SetColumnRuleWidth(nscoord aWidth) {
     mColumnRuleWidth = NS_ROUND_BORDER_TO_PIXELS(aWidth, mTwipsPerPixel);
--- a/layout/style/test/property_database.js
+++ b/layout/style/test/property_database.js
@@ -1700,34 +1700,16 @@ var gCSSProperties = {
   },
   "-moz-column-fill": {
     domProp: "MozColumnFill",
     inherited: false,
     type: CSS_TYPE_SHORTHAND_AND_LONGHAND,
     alias_for: "column-fill",
     subproperties: [ "column-fill" ]
   },
-  "column-gap": {
-    domProp: "columnGap",
-    inherited: false,
-    type: CSS_TYPE_LONGHAND,
-    initial_values: [ "normal" ],
-    other_values: [ "2px", "1em", "4em", "3%", "calc(3%)", "calc(1em - 3%)",
-      "calc(2px)",
-      "calc(-2px)",
-      "calc(0px)",
-      "calc(0pt)",
-      "calc(5em)",
-      "calc(-2em + 3em)",
-      "calc(3*25px)",
-      "calc(25px*3)",
-      "calc(3*25px + 5em)",
-    ],
-    invalid_values: [ "-3%", "-1px", "4" ]
-  },
   "-moz-column-gap": {
     domProp: "MozColumnGap",
     inherited: false,
     type: CSS_TYPE_SHORTHAND_AND_LONGHAND,
     alias_for: "column-gap",
     subproperties: [ "column-gap" ]
   },
   "column-rule": {
@@ -7221,43 +7203,65 @@ gCSSProperties["grid-area"] = {
     "auto / auto",
     "auto / auto / auto",
     "auto / auto / auto / auto"
   ],
   other_values: gridAreaOtherValues,
   invalid_values: gridAreaInvalidValues
 };
 
-gCSSProperties["grid-column-gap"] = {
-  domProp: "gridColumnGap",
+gCSSProperties["column-gap"] = {
+  domProp: "columnGap",
   inherited: false,
   type: CSS_TYPE_LONGHAND,
-  initial_values: [ "0" ],
+  initial_values: [ "normal" ],
   other_values: [ "2px", "2%", "1em", "calc(1px + 1em)", "calc(1%)",
                   "calc(1% + 1ch)" , "calc(1px - 99%)" ],
   invalid_values: [ "-1px", "auto", "none", "1px 1px", "-1%", "fit-content(1px)" ],
 };
+gCSSProperties["grid-column-gap"] = {
+  domProp: "gridColumnGap",
+  inherited: false,
+  type: CSS_TYPE_SHORTHAND_AND_LONGHAND,
+  alias_for: "column-gap",
+  subproperties: [ "column-gap" ]
+};
+gCSSProperties["row-gap"] = {
+  domProp: "rowGap",
+  inherited: false,
+  type: CSS_TYPE_LONGHAND,
+  initial_values: [ "normal" ],
+  other_values: [ "2px", "2%", "1em", "calc(1px + 1em)", "calc(1%)",
+                  "calc(1% + 1ch)" , "calc(1px - 99%)" ],
+  invalid_values: [ "-1px", "auto", "none", "1px 1px", "-1%", "min-content" ],
+};
 gCSSProperties["grid-row-gap"] = {
   domProp: "gridRowGap",
   inherited: false,
-  type: CSS_TYPE_LONGHAND,
-  initial_values: [ "0" ],
-  other_values: [ "2px", "2%", "1em", "calc(1px + 1em)", "calc(1%)",
-                  "calc(1% + 1ch)" , "calc(1px - 99%)" ],
-  invalid_values: [ "-1px", "auto", "none", "1px 1px", "-1%", "min-content" ],
+  type: CSS_TYPE_SHORTHAND_AND_LONGHAND,
+  alias_for: "row-gap",
+  subproperties: [ "row-gap" ]
+};
+gCSSProperties["gap"] = {
+  domProp: "gap",
+  inherited: false,
+  type: CSS_TYPE_TRUE_SHORTHAND,
+  subproperties: [ "column-gap", "row-gap" ],
+  initial_values: [ "normal", "normal normal" ],
+  other_values: [ "1ch 0", "1px 1%", "1em 1px", "calc(1px) calc(1%)",
+                  "normal 0", "1% normal" ],
+  invalid_values: [ "-1px", "1px -1px", "1px 1px 1px", "inherit 1px",
+                    "1px auto" ]
 };
 gCSSProperties["grid-gap"] = {
   domProp: "gridGap",
   inherited: false,
   type: CSS_TYPE_TRUE_SHORTHAND,
-  subproperties: [ "grid-column-gap", "grid-row-gap" ],
-  initial_values: [ "0", "0 0" ],
-  other_values: [ "1ch 0", "1px 1%", "1em 1px", "calc(1px) calc(1%)" ],
-  invalid_values: [ "-1px", "1px -1px", "1px 1px 1px", "inherit 1px",
-                    "1px auto" ]
+  alias_for: "gap",
+  subproperties: [ "column-gap", "row-gap" ],
 };
 
 if (IsCSSPropertyPrefEnabled("layout.css.contain.enabled")) {
   gCSSProperties["contain"] = {
     domProp: "contain",
     inherited: false,
     type: CSS_TYPE_LONGHAND,
     initial_values: [ "none" ],
--- a/layout/style/test/test_transitions_per_property.html
+++ b/layout/style/test/test_transitions_per_property.html
@@ -68,18 +68,16 @@ var supported_properties = {
     "border-top-left-radius": [ test_radius_transition ],
     "border-top-right-radius": [ test_radius_transition ],
     "-moz-box-flex": [ test_float_zeroToOne_transition,
                        test_float_aboveOne_transition,
                        test_float_zeroToOne_clamped ],
     "box-shadow": [ test_shadow_transition ],
     "column-count": [ test_pos_integer_or_auto_transition,
                       test_integer_at_least_one_clamping ],
-    "column-gap": [ test_length_transition,
-                    test_length_clamped ],
     "column-rule-color": [ test_color_transition,
                            test_true_currentcolor_transition ],
     "column-rule-width": [ test_length_transition,
                            test_length_clamped ],
     "column-width": [ test_length_transition,
                       test_length_clamped ],
     "-moz-image-region": [ test_rect_transition ],
     "-moz-outline-radius-bottomleft": [ test_radius_transition ],
@@ -164,18 +162,18 @@ var supported_properties = {
                    test_length_percent_calc_transition,
                    test_length_clamped, test_percent_clamped ],
     "font-size-adjust": [ test_float_zeroToOne_transition,
                           test_float_aboveOne_transition,
                           /* FIXME: font-size-adjust treats zero specially */
                           /* test_float_zeroToOne_clamped */ ],
     "font-stretch": [ test_percent_transition, test_percent_clamped ],
     "font-weight": [ test_font_weight ],
-    "grid-column-gap": [ test_grid_gap ],
-    "grid-row-gap": [ test_grid_gap ],
+    "column-gap": [ test_grid_gap ],
+    "row-gap": [ test_grid_gap ],
     "height": [ test_length_transition, test_percent_transition,
                 test_length_percent_calc_transition,
                 test_length_clamped, test_percent_clamped ],
     "left": [ test_length_transition, test_percent_transition,
               test_length_percent_calc_transition,
               test_length_unclamped, test_percent_unclamped ],
     "letter-spacing": [ test_length_transition, test_length_unclamped ],
     "lighting-color": [ test_color_transition,
--- a/mobile/android/geckoview/build.gradle
+++ b/mobile/android/geckoview/build.gradle
@@ -184,17 +184,18 @@ android.libraryVariants.all { variant ->
         destinationDir = new File(destinationDir, variant.baseName)
         classpath = files(variant.javaCompile.classpath.files)
 
         source = files(variant.javaCompile.source)
         exclude '**/R.java', '**/BuildConfig.java'
         include 'org/mozilla/geckoview/**'
         options.addPathOption('sourcepath', ':').setValue(
             variant.sourceSets.collect({ it.javaDirectories }).flatten() +
-            variant.generateBuildConfig.sourceOutputDir)
+            variant.generateBuildConfig.sourceOutputDir +
+            variant.aidlCompile.sourceOutputDir)
 
         // javadoc 8 has a bug that requires the rt.jar file from the JRE to be
         // in the bootclasspath (https://stackoverflow.com/a/30458820).
         options.bootClasspath = [
             file("${System.properties['java.home']}/lib/rt.jar")] + android.bootClasspath
         options.memberLevel = JavadocMemberLevel.PROTECTED
         options.source = 7
         options.links("https://d.android.com/reference/")
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntime.java
@@ -29,16 +29,17 @@ public final class GeckoRuntime implemen
 
     /**
      * Get the default runtime for the given context.
      * This will create and initialize the runtime with the default settings.
      *
      * Note: Only use this for session-less apps.
      *       For regular apps, use create() and createSession() instead.
      *
+     * @param context An application context for the default runtime.
      * @return The (static) default runtime for the context.
      */
     public static synchronized @NonNull GeckoRuntime getDefault(
             final @NonNull Context context) {
         Log.d(LOGTAG, "getDefault");
         if (sDefaultRuntime == null) {
             sDefaultRuntime = new GeckoRuntime();
             sDefaultRuntime.attachTo(context);
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java
@@ -35,72 +35,78 @@ public final class GeckoRuntimeSettings 
         public @NonNull GeckoRuntimeSettings build() {
             return new GeckoRuntimeSettings(mSettings);
         }
 
         /**
          * Set the content process hint flag.
          *
          * @param use If true, this will reload the content process for future use.
+         * @return This Builder instance.
          */
         public @NonNull Builder useContentProcessHint(final boolean use) {
             mSettings.mUseContentProcess = use;
             return this;
         }
 
         /**
          * Set the custom Gecko process arguments.
          *
          * @param args The Gecko process arguments.
+         * @return This Builder instance.
          */
         public @NonNull Builder arguments(final @NonNull String[] args) {
             if (args == null) {
                 throw new IllegalArgumentException("Arguments must not  be null");
             }
             mSettings.mArgs = args;
             return this;
         }
 
         /**
          * Set the custom Gecko intent extras.
          *
          * @param extras The Gecko intent extras.
+         * @return This Builder instance.
          */
         public @NonNull Builder extras(final @NonNull Bundle extras) {
             if (extras == null) {
                 throw new IllegalArgumentException("Extras must not  be null");
             }
             mSettings.mExtras = extras;
             return this;
         }
 
         /**
          * Set whether JavaScript support should be enabled.
          *
          * @param flag A flag determining whether JavaScript should be enabled.
+         * @return This Builder instance.
          */
         public @NonNull Builder javaScriptEnabled(final boolean flag) {
             mSettings.mJavaScript.set(flag);
             return this;
         }
 
         /**
          * Set whether remote debugging support should be enabled.
          *
          * @param enabled True if remote debugging should be enabled.
+         * @return This Builder instance.
          */
         public @NonNull Builder remoteDebuggingEnabled(final boolean enabled) {
             mSettings.mRemoteDebugging.set(enabled);
             return this;
         }
 
         /**
          * Set whether support for web fonts should be enabled.
          *
          * @param flag A flag determining whether web fonts should be enabled.
+         * @return This Builder instance.
          */
         public @NonNull Builder webFontsEnabled(final boolean flag) {
             mSettings.mWebFonts.set(flag);
             return this;
         }
     }
 
     /* package */ GeckoRuntime runtime;
@@ -217,16 +223,17 @@ public final class GeckoRuntimeSettings 
     public boolean getJavaScriptEnabled() {
         return mJavaScript.get();
     }
 
     /**
      * Set whether JavaScript support should be enabled.
      *
      * @param flag A flag determining whether JavaScript should be enabled.
+     * @return This GeckoRuntimeSettings instance.
      */
     public @NonNull GeckoRuntimeSettings setJavaScriptEnabled(final boolean flag) {
         mJavaScript.set(flag);
         return this;
     }
 
     /**
      * Get whether remote debugging support is enabled.
@@ -236,16 +243,17 @@ public final class GeckoRuntimeSettings 
     public boolean getRemoteDebuggingEnabled() {
         return mRemoteDebugging.get();
     }
 
     /**
      * Set whether remote debugging support should be enabled.
      *
      * @param enabled True if remote debugging should be enabled.
+     * @return This GeckoRuntimeSettings instance.
      */
     public @NonNull GeckoRuntimeSettings setRemoteDebuggingEnabled(final boolean enabled) {
         mRemoteDebugging.set(enabled);
         return this;
     }
 
     /**
      * Get whether web fonts support is enabled.
@@ -255,16 +263,17 @@ public final class GeckoRuntimeSettings 
     public boolean getWebFontsEnabled() {
         return mWebFonts.get();
     }
 
     /**
      * Set whether support for web fonts should be enabled.
      *
      * @param flag A flag determining whether web fonts should be enabled.
+     * @return This GeckoRuntimeSettings instance.
      */
     public @NonNull GeckoRuntimeSettings setWebFontsEnabled(final boolean flag) {
         mWebFonts.set(flag);
         return this;
     }
 
     @Override // Parcelable
     public int describeContents() {
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
@@ -55,26 +55,28 @@ public class SessionAccessibility {
             public void handleMessage(final String event, final GeckoBundle message,
                                       final EventCallback callback) {
                 sendAccessibilityEvent(message);
             }
         }, "GeckoView:AccessibilityEvent", null);
     }
 
     /**
-      * Get the GeckoView instance that delegates accessibility to this session.
+      * Get the View instance that delegates accessibility to this session.
       *
-      * @return GeckoView instance.
+      * @return View instance.
       */
     public View getView() {
         return mView;
     }
 
     /**
-      * Set the GeckoView instance that should delegate accessibility to this session.
+      * Set the View instance that should delegate accessibility to this session.
+      *
+      * @param view View instance.
       */
     public void setView(final View view) {
         if (mView != null) {
             mView.setAccessibilityDelegate(null);
         }
 
         mView = view;
         mLastItem = false;
--- a/mobile/android/themes/core/aboutMemory.css
+++ b/mobile/android/themes/core/aboutMemory.css
@@ -5,16 +5,17 @@
 /*
  * The version used for desktop is located at
  * toolkit/components/aboutmemory/content/aboutMemory.css.
  * Mobile-specific stuff is at the bottom of this file.
  */
 
 html {
   background: -moz-Dialog;
+  color: -moz-DialogText;
   font: message-box;
 }
 
 body {
   padding: 0 2em;
   margin: 0;
   min-width: 45em;
   margin: auto;
@@ -26,25 +27,27 @@ div.ancillary {
 }
 
 div.section {
   padding: 2em;
   margin: 1em 0em;
   border: 1px solid ThreeDShadow;
   border-radius: 10px;
   background: -moz-Field;
+  color: -moz-FieldText;
 }
 
 div.opsRow {
   padding: 0.5em;
   margin-right: 0.5em;
   margin-top: 0.5em;
   border: 1px solid ThreeDShadow;
   border-radius: 10px;
   background: -moz-Field;
+  color: -moz-FieldText;
   display: inline-block;
 }
 
 div.opsRowLabel {
   display: block;
   margin-bottom: 0.2em;
   font-weight: bold;
 }
@@ -60,51 +63,56 @@ div.non-verbose pre.entries {
 }
 
 h1 {
   padding: 0;
   margin: 0;
 }
 
 h2 {
-  background: #ddd;
   padding-left: .1em;
 }
 
+.accuracyWarning, .badInputWarning, .invalid {
+  /*
+   * Technically this should be used with the default background colour,
+   * instead we're using the default field background colour,
+   * I hope this will be okay.
+   */
+  color: -moz-activehyperlinktext;
+}
+
 .accuracyWarning {
-  color: #d22;
 }
 
 .badInputWarning {
-  color: #f00;
 }
 
 .treeline {
-  color: #888;
+  color: -moz-FieldText;
+  opacity: 0.5;
+}
+
+/*
+ * We might like to style these but cannot find a colour that always
+ * contrasts with the background colour.
+ */
+.mrValue, .mrName, .mrNote {
 }
 
 .mrValue {
   font-weight: bold;
-  color: #400;
 }
 
 .mrPerc {
 }
 
 .mrSep {
 }
 
-.mrName {
-  color: #004;
-}
-
-.mrNote {
-  color: #604;
-}
-
 .hasKids {
   cursor: pointer;
 }
 
 .hasKids:hover {
   text-decoration: underline;
 }
 
@@ -125,21 +133,16 @@ h2 {
 .debug {
   font-size: 80%;
 }
 
 .hidden {
   display: none;
 }
 
-.invalid {
-  color: #fff;
-  background-color: #f00;
-}
-
 /* Mobile-specific parts go here. */
 
 /* buttons are different sizes and overlapping without this */
 button {
   margin: 1%;
   padding: 2%;
 }
 
--- a/security/manager/ssl/tests/unit/xpcshell.ini
+++ b/security/manager/ssl/tests/unit/xpcshell.ini
@@ -1,11 +1,12 @@
 [DEFAULT]
 head = head_psm.js
 tags = psm
+firefox-appdir = browser
 support-files =
   bad_certs/**
   ocsp_certs/**
   test_baseline_requirements/**
   test_broken_fips/**
   test_cert_eku/**
   test_cert_embedded_null/**
   test_cert_isBuiltInRoot_reload/**
--- a/services/sync/tests/unit/test_prefs_store.js
+++ b/services/sync/tests/unit/test_prefs_store.js
@@ -104,18 +104,17 @@ add_task(async function run_test() {
     Assert.equal(prefs.get("testing.synced.url"), "https://www.example.com");
     Assert.equal(prefs.get("testing.unsynced.url"), "https://www.example.com/2");
     Assert.equal(Svc.Prefs.get("prefs.sync.testing.somepref"), true);
 
     _("Enable persona");
     // Ensure we don't go to the network to fetch personas and end up leaking
     // stuff.
     Services.io.offline = true;
-    Assert.ok(!prefs.get("lightweightThemes.selectedThemeID"));
-    Assert.equal(LightweightThemeManager.currentTheme, null);
+    Assert.equal(LightweightThemeManager.currentTheme.id, "default-theme@mozilla.org");
 
     let persona1 = makePersona();
     let persona2 = makePersona();
     let usedThemes = JSON.stringify([persona1, persona2]);
     record.value = {
       "lightweightThemes.selectedThemeID": persona1.id,
       "lightweightThemes.usedThemes": usedThemes
     };
@@ -125,18 +124,17 @@ add_task(async function run_test() {
               persona1));
 
     _("Disable persona");
     record.value = {
       "lightweightThemes.selectedThemeID": null,
       "lightweightThemes.usedThemes": usedThemes
     };
     await store.update(record);
-    Assert.ok(!prefs.get("lightweightThemes.selectedThemeID"));
-    Assert.equal(LightweightThemeManager.currentTheme, null);
+    Assert.equal(LightweightThemeManager.currentTheme.id, "default-theme@mozilla.org");
 
     _("Only the current app's preferences are applied.");
     record = new PrefRec("prefs", "some-fake-app");
     record.value = {
       "testing.int": 98
     };
     await store.update(record);
     Assert.equal(prefs.get("testing.int"), 42);
--- a/servo/components/style/properties/longhand/column.mako.rs
+++ b/servo/components/style/properties/longhand/column.mako.rs
@@ -26,27 +26,16 @@
     animation_value_type="AnimatedColumnCount",
     extra_prefixes="moz",
     spec="https://drafts.csswg.org/css-multicol/#propdef-column-count",
     servo_restyle_damage="rebuild_and_reflow",
 )}
 
 
 
-${helpers.predefined_type(
-    "column-gap",
-    "length::NonNegativeLengthOrPercentageOrNormal",
-    "Either::Second(Normal)",
-    extra_prefixes="moz",
-    servo_pref="layout.columns.enabled",
-    animation_value_type="NonNegativeLengthOrPercentageOrNormal",
-    spec="https://drafts.csswg.org/css-multicol/#propdef-column-gap",
-    servo_restyle_damage = "reflow",
-)}
-
 ${helpers.single_keyword("column-fill", "balance auto", extra_prefixes="moz",
                          products="gecko", animation_value_type="discrete",
                          spec="https://drafts.csswg.org/css-multicol/#propdef-column-fill")}
 
 ${helpers.predefined_type("column-rule-width",
                           "BorderSideWidth",
                           "::values::computed::NonNegativeLength::new(3.)",
                           initial_specified_value="specified::BorderSideWidth::Medium",
--- a/servo/components/style/properties/longhand/position.mako.rs
+++ b/servo/components/style/properties/longhand/position.mako.rs
@@ -297,23 +297,16 @@ macro_rules! impl_align_conversions {
                           "Position",
                           "computed::Position::zero()",
                           products="gecko",
                           boxed=True,
                           spec="https://drafts.csswg.org/css-images-3/#the-object-position",
                           animation_value_type="ComputedValue")}
 
 % for kind in ["row", "column"]:
-    ${helpers.predefined_type("grid-%s-gap" % kind,
-                              "NonNegativeLengthOrPercentage",
-                              "computed::NonNegativeLengthOrPercentage::zero()",
-                              spec="https://drafts.csswg.org/css-grid/#propdef-grid-%s-gap" % kind,
-                              animation_value_type="NonNegativeLengthOrPercentage",
-                              products="gecko")}
-
     % for range in ["start", "end"]:
         ${helpers.predefined_type("grid-%s-%s" % (kind, range),
                                   "GridLine",
                                   "Default::default()",
                                   animation_value_type="discrete",
                                   spec="https://drafts.csswg.org/css-grid/#propdef-grid-%s-%s" % (kind, range),
                                   products="gecko",
                                   boxed=True)}
@@ -350,8 +343,28 @@ macro_rules! impl_align_conversions {
                           spec="https://drafts.csswg.org/css-grid/#propdef-grid-auto-flow")}
 
 ${helpers.predefined_type("grid-template-areas",
                           "GridTemplateAreas",
                           initial_value="computed::GridTemplateAreas::none()",
                           products="gecko",
                           animation_value_type="discrete",
                           spec="https://drafts.csswg.org/css-grid/#propdef-grid-template-areas")}
+
+${helpers.predefined_type("column-gap",
+                          "length::NonNegativeLengthOrPercentageOrNormal",
+                          "Either::Second(Normal)",
+                          alias="grid-column-gap",
+                          extra_prefixes="moz",
+                          servo_pref="layout.columns.enabled",
+                          spec="https://drafts.csswg.org/css-align-3/#propdef-column-gap",
+                          animation_value_type="NonNegativeLengthOrPercentageOrNormal",
+                          servo_restyle_damage = "reflow")}
+
+// no need for -moz- prefixed alias for this property
+${helpers.predefined_type("row-gap",
+                          "length::NonNegativeLengthOrPercentageOrNormal",
+                          "Either::Second(Normal)",
+                          alias="grid-row-gap",
+                          servo_pref="layout.columns.enabled",
+                          spec="https://drafts.csswg.org/css-align-3/#propdef-row-gap",
+                          animation_value_type="NonNegativeLengthOrPercentageOrNormal",
+                          servo_restyle_damage = "reflow")}
--- a/servo/components/style/properties/shorthand/position.mako.rs
+++ b/servo/components/style/properties/shorthand/position.mako.rs
@@ -103,40 +103,40 @@
             // browsers currently agree on using `0%`. This is a spec
             // change which hasn't been adopted by browsers:
             // https://github.com/w3c/csswg-drafts/commit/2c446befdf0f686217905bdd7c92409f6bd3921b
             flex_basis: basis.unwrap_or(FlexBasis::zero_percent()),
         })
     }
 </%helpers:shorthand>
 
-<%helpers:shorthand name="grid-gap" sub_properties="grid-row-gap grid-column-gap"
-                    spec="https://drafts.csswg.org/css-grid/#propdef-grid-gap"
+<%helpers:shorthand name="gap" alias="grid-gap" sub_properties="row-gap column-gap"
+                    spec="https://drafts.csswg.org/css-align-3/#gap-shorthand"
                     products="gecko">
-  use properties::longhands::{grid_row_gap, grid_column_gap};
+  use properties::longhands::{row_gap, column_gap};
 
   pub fn parse_value<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>)
                              -> Result<Longhands, ParseError<'i>> {
-      let row_gap = grid_row_gap::parse(context, input)?;
-      let column_gap = input.try(|input| grid_column_gap::parse(context, input)).unwrap_or(row_gap.clone());
+      let r_gap = row_gap::parse(context, input)?;
+      let c_gap = input.try(|input| column_gap::parse(context, input)).unwrap_or(r_gap.clone());
 
       Ok(expanded! {
-        grid_row_gap: row_gap,
-        grid_column_gap: column_gap,
+        row_gap: r_gap,
+        column_gap: c_gap,
       })
   }
 
   impl<'a> ToCss for LonghandsToSerialize<'a>  {
       fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
-          if self.grid_row_gap == self.grid_column_gap {
-            self.grid_row_gap.to_css(dest)
+          if self.row_gap == self.column_gap {
+            self.row_gap.to_css(dest)
           } else {
-            self.grid_row_gap.to_css(dest)?;
+            self.row_gap.to_css(dest)?;
             dest.write_str(" ")?;
-            self.grid_column_gap.to_css(dest)
+            self.column_gap.to_css(dest)
           }
       }
   }
 
 </%helpers:shorthand>
 
 % for kind in ["row", "column"]:
 <%helpers:shorthand name="grid-${kind}" sub_properties="grid-${kind}-start grid-${kind}-end"
--- a/taskcluster/ci/release-generate-checksums/kind.yml
+++ b/taskcluster/ci/release-generate-checksums/kind.yml
@@ -35,16 +35,17 @@ job-defaults:
          - name: public/build/SHA512SUMMARY
            path: /builds/worker/SHA512SUMMARY
            type: file
          - name: public/build/SHA512SUMS
            path: /builds/worker/SHA512SUMS
            type: file
    run:
       using: mozharness
+      config: []  # See extra-config below
       actions: [create-virtualenv collect-individual-checksums create-big-checksums create-summary]
       options:
          - "version={version}"
          - "build-number={build_number}"
       script: "mozharness/scripts/release/generate-checksums.py"
    treeherder:
       symbol: Rel(GenChcks)
       kind: test
@@ -52,58 +53,52 @@ job-defaults:
 
 jobs:
    firefox:
       shipping-product: firefox
       attributes:
          build_platform: linux64
          build_type: opt
       run:
-         config:
+         extra-config:
             by-project:
-               mozilla-release:
-                  - releases/checksums_firefox.py
-               mozilla-beta:
-                  - releases/checksums_firefox.py
-               maple:
-                  - releases/dev_checksums_firefox.py
+               mozilla-(release|beta):
+                  stage_product: "firefox"
+                  bucket_name: "net-mozaws-prod-delivery-firefox"
                default:
-                  - releases/dev_checksums_firefox.py
+                  stage_product: "firefox"
+                  bucket_name: "net-mozaws-stage-delivery-firefox"
       treeherder:
          platform: linux64/opt
 
    fennec:
       shipping-product: fennec
       attributes:
          build_platform: android-nightly
          build_type: opt
       run:
-         config:
+         extra-config:
             by-project:
-               mozilla-release:
-                  - releases/checksums_fennec.py
-               mozilla-beta:
-                  - releases/checksums_fennec.py
-               maple:
-                  - releases/dev_checksums_fennec.py
+               mozilla-(release|beta):
+                  stage_product: "mobile"
+                  bucket_name: "net-mozaws-prod-delivery-archive"
                default:
-                  - releases/dev_checksums_fennec.py
+                  stage_product: "mobile"
+                  bucket_name: "net-mozaws-stage-delivery-archive"
       treeherder:
          platform: Android/opt
 
    devedition:
       shipping-product: devedition
       attributes:
          build_platform: linux64-devedition
          build_type: opt
       run:
-         config:
+         extra-config:
             by-project:
-               mozilla-release:
-                  - releases/checksums_devedition.py
                mozilla-beta:
-                  - releases/checksums_devedition.py
-               maple:
-                  - releases/dev_checksums_devedition.py
+                  stage_product: "devedition"
+                  bucket_name: "net-mozaws-prod-delivery-archive"
                default:
-                  - releases/dev_checksums_devedition.py
+                  stage_product: "devedition"
+                  bucket_name: "net-mozaws-stage-delivery-archive"
       treeherder:
          platform: linux64-devedition/opt
--- a/taskcluster/scripts/builder/build-linux.sh
+++ b/taskcluster/scripts/builder/build-linux.sh
@@ -48,17 +48,17 @@ export LIBRARY_PATH=$LIBRARY_PATH:$WORKS
 
 if [[ -n ${USE_SCCACHE} ]]; then
     # Point sccache at the Taskcluster proxy for AWS credentials.
     export AWS_IAM_CREDENTIALS_URL="http://taskcluster/auth/v1/aws/s3/read-write/taskcluster-level-${MOZ_SCM_LEVEL}-sccache-${TASKCLUSTER_WORKER_GROUP}/?format=iam-role-compat"
 fi
 
 # test required parameters are supplied
 if [[ -z ${MOZHARNESS_SCRIPT} ]]; then fail "MOZHARNESS_SCRIPT is not set"; fi
-if [[ -z ${MOZHARNESS_CONFIG} ]]; then fail "MOZHARNESS_CONFIG is not set"; fi
+if [[ -z "${MOZHARNESS_CONFIG}" && -z "${EXTRA_MOZHARNESS_CONFIG}" ]]; then fail "MOZHARNESS_CONFIG or EXTRA_MOZHARNESS_CONFIG is not set"; fi
 
 # run XVfb in the background, if necessary
 if $NEED_XVFB; then
     . /builds/worker/scripts/xvfb.sh
 
     cleanup() {
         local rv=$?
         cleanup_xvfb
--- a/taskcluster/taskgraph/parameters.py
+++ b/taskcluster/taskgraph/parameters.py
@@ -183,17 +183,17 @@ def load_parameters_file(filename, stric
         # reading parameters from a local parameters.yml file
         f = open(filename)
     except IOError:
         # fetching parameters.yml using task task-id, project or supplied url
         task_id = None
         if filename.startswith("task-id="):
             task_id = filename.split("=")[1]
         elif filename.startswith("project="):
-            index = "gecko.v2.{project}.latest.firefox.decision".format(
+            index = "gecko.v2.{project}.latest.taskgraph.decision".format(
                 project=filename.split("=")[1],
             )
             task_id = find_task_id(index)
 
         if task_id:
             filename = get_artifact_url(task_id, 'public/parameters.yml')
         f = urllib.urlopen(filename)
 
--- a/taskcluster/taskgraph/transforms/release_generate_checksums.py
+++ b/taskcluster/taskgraph/transforms/release_generate_checksums.py
@@ -20,17 +20,18 @@ logger = logging.getLogger(__name__)
 
 transforms = TransformSequence()
 
 
 @transforms.add
 def handle_keyed_by(config, jobs):
     """Resolve fields that can be keyed by project, etc."""
     fields = [
-            "run.config",
+        "run.config",
+        "run.extra-config",
     ]
     for job in jobs:
         job = copy.deepcopy(job)
         for field in fields:
             resolve_keyed_by(
                 item=job,
                 field=field,
                 item_name=job['name'],
deleted file mode 100644
--- a/testing/mozharness/configs/releases/checksums_devedition.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# lint_ignore=E501
-config = {
-    "stage_product": "devedition",
-    "bucket_name": "net-mozaws-prod-delivery-archive",
-}
deleted file mode 100644
--- a/testing/mozharness/configs/releases/checksums_fennec.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# lint_ignore=E501
-config = {
-    "stage_product": "mobile",
-    "bucket_name": "net-mozaws-prod-delivery-archive",
-}
deleted file mode 100644
--- a/testing/mozharness/configs/releases/checksums_firefox.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# lint_ignore=E501
-config = {
-    "stage_product": "firefox",
-    "bucket_name": "net-mozaws-prod-delivery-firefox",
-}
deleted file mode 100644
--- a/testing/mozharness/configs/releases/dev_checksums_devedition.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# lint_ignore=E501
-config = {
-    "stage_product": "devedition",
-    "bucket_name": "net-mozaws-stage-delivery-archive",
-}
deleted file mode 100644
--- a/testing/mozharness/configs/releases/dev_checksums_fennec.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# lint_ignore=E501
-config = {
-    "stage_product": "mobile",
-    "bucket_name": "net-mozaws-stage-delivery-archive",
-}
deleted file mode 100644
--- a/testing/mozharness/configs/releases/dev_checksums_firefox.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# lint_ignore=E501
-config = {
-    "stage_product": "firefox",
-    "bucket_name": "net-mozaws-stage-delivery-firefox",
-}
--- a/testing/mozharness/mozharness/base/config.py
+++ b/testing/mozharness/mozharness/base/config.py
@@ -280,18 +280,19 @@ class BaseConfig(object):
             type="string", default=os.getcwd(),
             help="Specify the absolute path of the parent of the working directory"
         )
         self.config_parser.add_option(
             "--extra-config-path", action='extend', dest="config_paths",
             type="string", help="Specify additional paths to search for config files.",
         )
         self.config_parser.add_option(
-            "-c", "--config-file", "--cfg", action="extend", dest="config_files",
-            type="string", help="Specify a config file; can be repeated"
+            "-c", "--config-file", "--cfg", action="extend",
+            dest="config_files", default=[], type="string",
+            help="Specify a config file; can be repeated",
         )
         self.config_parser.add_option(
             "-C", "--opt-config-file", "--opt-cfg", action="extend",
             dest="opt_config_files", type="string", default=[],
             help="Specify an optional config file, like --config-file but with no "
                  "error if the file is missing; can be repeated"
         )
         self.config_parser.add_option(
@@ -484,45 +485,46 @@ class BaseConfig(object):
         defaults = self.config_parser.defaults.copy()
 
         if not options.config_files:
             if self.require_config_file:
                 if options.list_actions:
                     self.list_actions()
                 print("Required config file not set! (use --config-file option)")
                 raise SystemExit(-1)
+
+        # this is what get_cfgs_from_files returns. It will represent each
+        # config file name and its assoctiated dict
+        # eg ('builds/branch_specifics.py', {'foo': 'bar'})
+        # let's store this to self for things like --interpret-config-files
+        self.all_cfg_files_and_dicts.extend(self.get_cfgs_from_files(
+            # append opt_config to allow them to overwrite previous configs
+            options.config_files + options.opt_config_files, options=options
+        ))
+        config = {}
+        if (self.append_env_variables_from_configs
+                or options.append_env_variables_from_configs):
+            # We only append values from various configs for the 'env' entry
+            # For everything else we follow the standard behaviour
+            for i, (c_file, c_dict) in enumerate(self.all_cfg_files_and_dicts):
+                for v in c_dict.keys():
+                    if v == 'env' and v in config:
+                        config[v].update(c_dict[v])
+                    else:
+                        config[v] = c_dict[v]
         else:
-            # this is what get_cfgs_from_files returns. It will represent each
-            # config file name and its assoctiated dict
-            # eg ('builds/branch_specifics.py', {'foo': 'bar'})
-            # let's store this to self for things like --interpret-config-files
-            self.all_cfg_files_and_dicts.extend(self.get_cfgs_from_files(
-                # append opt_config to allow them to overwrite previous configs
-                options.config_files + options.opt_config_files, options=options
-            ))
-            config = {}
-            if (self.append_env_variables_from_configs
-                    or options.append_env_variables_from_configs):
-                # We only append values from various configs for the 'env' entry
-                # For everything else we follow the standard behaviour
-                for i, (c_file, c_dict) in enumerate(self.all_cfg_files_and_dicts):
-                    for v in c_dict.keys():
-                        if v == 'env' and v in config:
-                            config[v].update(c_dict[v])
-                        else:
-                            config[v] = c_dict[v]
-            else:
-                for i, (c_file, c_dict) in enumerate(self.all_cfg_files_and_dicts):
-                    config.update(c_dict)
-            # assign or update self._config depending on if it exists or not
-            #    NOTE self._config will be passed to ReadOnlyConfig's init -- a
-            #    dict subclass with immutable locking capabilities -- and serve
-            #    as the keys/values that make up that instance. Ultimately,
-            #    this becomes self.config during BaseScript's init
-            self.set_config(config)
+            for i, (c_file, c_dict) in enumerate(self.all_cfg_files_and_dicts):
+                config.update(c_dict)
+        # assign or update self._config depending on if it exists or not
+        #    NOTE self._config will be passed to ReadOnlyConfig's init -- a
+        #    dict subclass with immutable locking capabilities -- and serve
+        #    as the keys/values that make up that instance. Ultimately,
+        #    this becomes self.config during BaseScript's init
+        self.set_config(config)
+
         for key in defaults.keys():
             value = getattr(options, key)
             if value is None:
                 continue
             # Don't override config_file defaults with config_parser defaults
             if key in defaults and value == defaults[key] and key in self._config:
                 continue
             self._config[key] = value
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-align/gaps/gap-normal-computed-001.html.ini
+++ /dev/null
@@ -1,10 +0,0 @@
-[gap-normal-computed-001.html]
-  [row-gap:normal computes to normal on multicol elements]
-    expected: FAIL
-
-  [row-gap:normal computes to normal on grid]
-    expected: FAIL
-
-  [row-gap:normal (cross axis) computes to normal on flexbox]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-align/gaps/gap-parsing-001.html.ini
+++ /dev/null
@@ -1,79 +0,0 @@
-[gap-parsing-001.html]
-  [Default gap is 'normal']
-    expected: FAIL
-
-  [gap accepts pixels]
-    expected: FAIL
-
-  [gap accepts pixels 2]
-    expected: FAIL
-
-  [gap accepts pixels combined with percentage]
-    expected: FAIL
-
-  [gap accepts em]
-    expected: FAIL
-
-  [gap accepts em 2]
-    expected: FAIL
-
-  [gap accepts vw]
-    expected: FAIL
-
-  [gap accepts vw and vh]
-    expected: FAIL
-
-  [gap accepts percentage]
-    expected: FAIL
-
-  [gap accepts percentage 2]
-    expected: FAIL
-
-  [gap accepts calc()]
-    expected: FAIL
-
-  [gap accepts calc() 2]
-    expected: FAIL
-
-  [Initial gap is 'normal']
-    expected: FAIL
-
-  [Initial gap is 'normal' 2]
-    expected: FAIL
-
-  [Initial inherited gap is 'normal']
-    expected: FAIL
-
-  [gap is inheritable]
-    expected: FAIL
-
-  [Negative gap is invalid]
-    expected: FAIL
-
-  ['max-content' gap is invalid]
-    expected: FAIL
-
-  ['none' gap is invalid]
-    expected: FAIL
-
-  [Angle gap is invalid]
-    expected: FAIL
-
-  [Resolution gap is invalid]
-    expected: FAIL
-
-  [Time gap is invalid]
-    expected: FAIL
-
-  [gap with three values is invalid]
-    expected: FAIL
-
-  [gap with slash is invalid]
-    expected: FAIL
-
-  [gap with one wrong value is invalid]
-    expected: FAIL
-
-  [gap accepts calc() mixing fixed and percentage values]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-align/gaps/grid-column-gap-parsing-001.html.ini
+++ /dev/null
@@ -1,22 +0,0 @@
-[grid-column-gap-parsing-001.html]
-  [grid-column-gap accepts pixels]
-    expected: FAIL
-
-  [grid-column-gap accepts em]
-    expected: FAIL
-
-  [grid-column-gap accepts vw]
-    expected: FAIL
-
-  [grid-column-gap accepts percentage]
-    expected: FAIL
-
-  [grid-column-gap accepts calc()]
-    expected: FAIL
-
-  [grid-column-gap accepts calc() mixing fixed and percentage values]
-    expected: FAIL
-
-  [grid-column-gap is inheritable]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-align/gaps/grid-gap-parsing-001.html.ini
+++ /dev/null
@@ -1,154 +0,0 @@
-[grid-gap-parsing-001.html]
-  [Default gap is 'normal']
-    expected: FAIL
-
-  [gap accepts pixels]
-    expected: FAIL
-
-  [gap accepts pixels 2]
-    expected: FAIL
-
-  [gap accepts pixels combined with percentage]
-    expected: FAIL
-
-  [gap accepts em]
-    expected: FAIL
-
-  [gap accepts em 2]
-    expected: FAIL
-
-  [gap accepts vw]
-    expected: FAIL
-
-  [gap accepts vw and vh]
-    expected: FAIL
-
-  [gap accepts percentage]
-    expected: FAIL
-
-  [gap accepts percentage 2]
-    expected: FAIL
-
-  [gap accepts calc()]
-    expected: FAIL
-
-  [gap accepts calc() 2]
-    expected: FAIL
-
-  [Initial gap is 'normal']
-    expected: FAIL
-
-  [Initial gap is 'normal' 2]
-    expected: FAIL
-
-  [Initial inherited gap is 'normal']
-    expected: FAIL
-
-  [gap is inheritable]
-    expected: FAIL
-
-  [Negative gap is invalid]
-    expected: FAIL
-
-  ['max-content' gap is invalid]
-    expected: FAIL
-
-  ['none' gap is invalid]
-    expected: FAIL
-
-  [Angle gap is invalid]
-    expected: FAIL
-
-  [Resolution gap is invalid]
-    expected: FAIL
-
-  [Time gap is invalid]
-    expected: FAIL
-
-  [gap with three values is invalid]
-    expected: FAIL
-
-  [gap with slash is invalid]
-    expected: FAIL
-
-  [gap with one wrong value is invalid]
-    expected: FAIL
-
-  [Default grid-gap is 'normal']
-    expected: FAIL
-
-  [grid-gap accepts pixels]
-    expected: FAIL
-
-  [grid-gap accepts pixels 2]
-    expected: FAIL
-
-  [grid-gap accepts pixels combined with percentage]
-    expected: FAIL
-
-  [grid-gap accepts em]
-    expected: FAIL
-
-  [grid-gap accepts em 2]
-    expected: FAIL
-
-  [grid-gap accepts vw]
-    expected: FAIL
-
-  [grid-gap accepts vw and vh]
-    expected: FAIL
-
-  [grid-gap accepts percentage]
-    expected: FAIL
-
-  [grid-gap accepts percentage 2]
-    expected: FAIL
-
-  [grid-gap accepts calc()]
-    expected: FAIL
-
-  [grid-gap accepts calc() mixing fixed and percentage values]
-    expected: FAIL
-
-  [grid-gap accepts calc() 2]
-    expected: FAIL
-
-  [Initial grid-gap is 'normal']
-    expected: FAIL
-
-  [Initial grid-gap is 'normal' 2]
-    expected: FAIL
-
-  [Initial inherited grid-gap is 'normal']
-    expected: FAIL
-
-  [grid-gap is inheritable]
-    expected: FAIL
-
-  [Negative grid-gap is invalid]
-    expected: FAIL
-
-  ['max-content' grid-gap is invalid]
-    expected: FAIL
-
-  ['none' grid-gap is invalid]
-    expected: FAIL
-
-  [Angle grid-gap is invalid]
-    expected: FAIL
-
-  [Resolution grid-gap is invalid]
-    expected: FAIL
-
-  [Time grid-gap is invalid]
-    expected: FAIL
-
-  [grid-gap with three values is invalid]
-    expected: FAIL
-
-  [grid-gap with slash is invalid]
-    expected: FAIL
-
-  [grid-gap with one wrong value is invalid]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-align/gaps/grid-row-gap-parsing-001.html.ini
+++ /dev/null
@@ -1,100 +0,0 @@
-[grid-row-gap-parsing-001.html]
-  [Default row-gap is 'normal']
-    expected: FAIL
-
-  [row-gap accepts pixels]
-    expected: FAIL
-
-  [row-gap accepts em]
-    expected: FAIL
-
-  [row-gap accepts percentage]
-    expected: FAIL
-
-  [row-gap accepts calc()]
-    expected: FAIL
-
-  [Initial row-gap is 'normal']
-    expected: FAIL
-
-  [Initial row-gap is 'normal' 2]
-    expected: FAIL
-
-  [Initial inherited row-gap is 'normal']
-    expected: FAIL
-
-  [row-gap is inheritable]
-    expected: FAIL
-
-  [Negative row-gap is invalid]
-    expected: FAIL
-
-  ['max-content' row-gap is invalid]
-    expected: FAIL
-
-  ['none' row-gap is invalid]
-    expected: FAIL
-
-  [row-gap with multiple values is invalid]
-    expected: FAIL
-
-  [Angle row-gap is invalid]
-    expected: FAIL
-
-  [Resolution row-gap is invalid]
-    expected: FAIL
-
-  [Time row-gap is invalid]
-    expected: FAIL
-
-  [Default grid-row-gap is 'normal']
-    expected: FAIL
-
-  [grid-row-gap accepts pixels]
-    expected: FAIL
-
-  [grid-row-gap accepts em]
-    expected: FAIL
-
-  [grid-row-gap accepts percentage]
-    expected: FAIL
-
-  [grid-row-gap accepts calc()]
-    expected: FAIL
-
-  [grid-row-gap accepts calc() mixing fixed and percentage values]
-    expected: FAIL
-
-  [Initial grid-row-gap is 'normal']
-    expected: FAIL
-
-  [Initial grid-row-gap is 'normal' 2]
-    expected: FAIL
-
-  [Initial inherited grid-row-gap is 'normal']
-    expected: FAIL
-
-  [grid-row-gap is inheritable]
-    expected: FAIL
-
-  [Negative grid-row-gap is invalid]
-    expected: FAIL
-
-  ['max-content' grid-row-gap is invalid]
-    expected: FAIL
-
-  ['none' grid-row-gap is invalid]
-    expected: FAIL
-
-  [grid-row-gap with multiple values is invalid]
-    expected: FAIL
-
-  [Angle grid-row-gap is invalid]
-    expected: FAIL
-
-  [Resolution grid-row-gap is invalid]
-    expected: FAIL
-
-  [Time grid-row-gap is invalid]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-align/gaps/row-gap-animation-001.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[row-gap-animation-001.html]
-  [row-gap is interpolable]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-align/gaps/row-gap-animation-002.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[row-gap-animation-002.html]
-  [row-gap: normal is not interpolable]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-align/gaps/row-gap-animation-003.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[row-gap-animation-003.html]
-  [Default row-gap is not interpolable]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-align/gaps/row-gap-parsing-001.html.ini
+++ /dev/null
@@ -1,52 +0,0 @@
-[row-gap-parsing-001.html]
-  [Default row-gap is 'normal']
-    expected: FAIL
-
-  [row-gap accepts pixels]
-    expected: FAIL
-
-  [row-gap accepts em]
-    expected: FAIL
-
-  [row-gap accepts percentage]
-    expected: FAIL
-
-  [row-gap accepts calc()]
-    expected: FAIL
-
-  [Initial row-gap is 'normal']
-    expected: FAIL
-
-  [Initial row-gap is 'normal' 2]
-    expected: FAIL
-
-  [Initial inherited row-gap is 'normal']
-    expected: FAIL
-
-  [row-gap is inheritable]
-    expected: FAIL
-
-  [Negative row-gap is invalid]
-    expected: FAIL
-
-  ['max-content' row-gap is invalid]
-    expected: FAIL
-
-  ['none' row-gap is invalid]
-    expected: FAIL
-
-  [row-gap with multiple values is invalid]
-    expected: FAIL
-
-  [Angle row-gap is invalid]
-    expected: FAIL
-
-  [Resolution row-gap is invalid]
-    expected: FAIL
-
-  [Time row-gap is invalid]
-    expected: FAIL
-
-  [row-gap accepts calc() mixing fixed and percentage values]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-grid/alignment/grid-gutters-001.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[grid-gutters-001.html]
-  expected: FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-grid/alignment/grid-gutters-003.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[grid-gutters-003.html]
-  expected: FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-grid/alignment/grid-gutters-005.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[grid-gutters-005.html]
-  expected: FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-grid/alignment/grid-gutters-007.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[grid-gutters-007.html]
-  expected: FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-grid/alignment/grid-gutters-009.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[grid-gutters-009.html]
-  expected: FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-grid/alignment/grid-gutters-011.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[grid-gutters-011.html]
-  expected: FAIL
--- a/toolkit/components/aboutmemory/content/aboutMemory.css
+++ b/toolkit/components/aboutmemory/content/aboutMemory.css
@@ -5,16 +5,17 @@
 /*
  * The version used for mobile is located at
  * mobile/android/themes/core/aboutMemory.css.
  * Desktop-specific stuff is at the bottom of this file.
  */
 
 html {
   background: -moz-Dialog;
+  color: -moz-DialogText;
   font: message-box;
 }
 
 body {
   padding: 0 2em;
   margin: 0;
   min-width: 45em;
   margin: auto;
@@ -26,25 +27,27 @@ div.ancillary {
 }
 
 div.section {
   padding: 2em;
   margin: 1em 0em;
   border: 1px solid ThreeDShadow;
   border-radius: 10px;
   background: -moz-Field;
+  color: -moz-FieldText;
 }
 
 div.opsRow {
   padding: 0.5em;
   margin-right: 0.5em;
   margin-top: 0.5em;
   border: 1px solid ThreeDShadow;
   border-radius: 10px;
   background: -moz-Field;
+  color: -moz-FieldText;
   display: inline-block;
 }
 
 div.opsRowLabel {
   display: block;
   margin-bottom: 0.2em;
   font-weight: bold;
 }
@@ -61,61 +64,66 @@ div.non-verbose pre.entries {
 
 h1 {
   padding: 0;
   margin: 0;
   display: inline;  /* allow subsequent text to the right of the heading */
 }
 
 h2 {
-  background: #ddd;
   padding-left: .1em;
 }
 
 h3 {
   display: inline;  /* allow subsequent text to the right of the heading */
 }
 
 a.upDownArrow {
   font-size: 130%;
   text-decoration: none;
   -moz-user-select: none;  /* no need to include this when cutting+pasting */
 }
 
+.accuracyWarning, .badInputWarning, .invalid {
+  /*
+   * Technically this should be used with the default background colour,
+   * instead we're using the default field background colour,
+   * I hope this will be okay.
+   */
+  color: -moz-activehyperlinktext;
+}
+
 .accuracyWarning {
-  color: #d22;
 }
 
 .badInputWarning {
-  color: #f00;
 }
 
 .treeline {
-  color: #888;
+  color: -moz-FieldText;
+  opacity: 0.5;
+}
+
+/*
+ * We might like to style these but cannot find a colour that always
+ * contrasts with the background colour.
+ */
+.mrValue, .mrName, .mrNote {
 }
 
 .mrValue {
   font-weight: bold;
-  color: #400;
 }
 
 .mrPerc {
 }
 
 .mrSep {
 }
 
-.mrName {
-  color: #004;
-}
-
-.mrNote {
-  color: #604;
-}
-
 .hasKids {
   cursor: pointer;
 }
 
 .hasKids:hover {
   text-decoration: underline;
 }
 
@@ -136,19 +144,14 @@ a.upDownArrow {
 .debug {
   font-size: 80%;
 }
 
 .hidden {
   display: none;
 }
 
-.invalid {
-  color: #fff;
-  background-color: #f00;
-}
-
 /* Desktop-specific parts go here. */
 
 .hasKids:hover {
   text-decoration: underline;
 }
 
--- a/toolkit/components/extensions/test/browser/browser_ext_management_themes.js
+++ b/toolkit/components/extensions/test/browser/browser_ext_management_themes.js
@@ -119,17 +119,16 @@ add_task(async function test_management_
     homepageURL: "http://mochi.test:8888/data/index.html",
     headerURL: "http://mochi.test:8888/data/header.png",
     previewURL: "http://mochi.test:8888/data/preview.png",
     iconURL: "http://mochi.test:8888/data/icon.png",
     textcolor: Math.random().toString(),
     accentcolor: Math.random().toString(),
   };
   is(await extension.awaitMessage("onInstalled"), "Bling", "LWT installed");
-  is(await extension.awaitMessage("onDisabled"), "Default", "default disabled");
   is(await extension.awaitMessage("onEnabled"), "Bling", "LWT enabled");
 
   await theme.startup();
   is(await extension.awaitMessage("onInstalled"), "Simple theme test", "webextension theme installed");
   is(await extension.awaitMessage("onDisabled"), "Bling", "LWT disabled");
   // no enabled event when installed.
 
   extension.sendMessage("test");
--- a/toolkit/components/extensions/test/xpcshell/test_ext_management.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_management.js
@@ -29,17 +29,17 @@ add_task(async function test_management_
 
   async function background() {
     browser.test.onMessage.addListener(async (msg, id) => {
       let addon = await browser.management.get(id);
       browser.test.sendMessage("addon", addon);
     });
 
     let addons = await browser.management.getAll();
-    browser.test.assertEq(addons.length, 2, "management.getAll returned two extensions.");
+    browser.test.assertEq(addons.length, 3, "management.getAll returned three add-ons.");
     browser.test.sendMessage("addons", addons);
   }
 
   let extension1 = ExtensionTestUtils.loadExtension({
     manifest: getManifest(id1),
     useAddonManager: "temporary",
   });
 
--- a/toolkit/components/telemetry/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -508,17 +508,17 @@ EnvironmentAddonBuilder.prototype = {
         () => this._shutdownBlocker());
     } catch (err) {
       return Promise.reject(err);
     }
 
     this._pendingTask = (async () => {
       try {
         // Gather initial addons details
-        await this._updateAddons();
+        await this._updateAddons(true);
 
         if (!this._environment._addonsAreFull) {
           // The addon database has not been loaded, so listen for the event
           // triggered by the AddonManager when it is loaded so we can
           // immediately gather full data at that time.
           await new Promise(resolve => {
             const ADDON_LOAD_NOTIFICATION = "xpi-database-loaded";
             Services.obs.addObserver({
@@ -623,33 +623,37 @@ EnvironmentAddonBuilder.prototype = {
   },
 
   /**
    * Collect the addon data for the environment.
    *
    * This should only be called from _pendingTask; otherwise we risk
    * running this during addon manager shutdown.
    *
+   * @param {boolean} [atStartup]
+   *        True if this is the first check we're performing at startup. In that
+   *        situation, we defer some more expensive initialization.
+   *
    * @returns Promise<Object> This returns a Promise resolved with a status object with the following members:
    *   changed - Whether the environment changed.
    *   oldEnvironment - Only set if a change occured, contains the environment data before the change.
    */
-  async _updateAddons() {
+  async _updateAddons(atStartup) {
     this._environment._log.trace("_updateAddons");
     let personaId = null;
     let theme = LightweightThemeManager.currentTheme;
     if (theme) {
       personaId = theme.id;
     }
 
     let addons = {
       activeAddons: await this._getActiveAddons(),
       theme: await this._getActiveTheme(),
-      activePlugins: this._getActivePlugins(),
-      activeGMPlugins: await this._getActiveGMPlugins(),
+      activePlugins: this._getActivePlugins(atStartup),
+      activeGMPlugins: await this._getActiveGMPlugins(atStartup),
       activeExperiment: {},
       persona: personaId,
     };
 
     let result = {
       changed: !this._environment._currentEnvironment.addons ||
                !ObjectUtils.deepEqual(addons, this._environment._currentEnvironment.addons),
     };
@@ -747,22 +751,27 @@ EnvironmentAddonBuilder.prototype = {
       };
     }
 
     return activeTheme;
   },
 
   /**
    * Get the plugins data in object form.
+   *
+   * @param {boolean} [atStartup]
+   *        True if this is the first check we're performing at startup. In that
+   *        situation, we defer some more expensive initialization.
+   *
    * @return Object containing the plugins data.
    */
-  _getActivePlugins() {
+  _getActivePlugins(atStartup) {
     // If we haven't yet loaded the blocklist, pass back dummy data for now,
     // and add an observer to update this data as soon as we get it.
-    if (!Services.blocklist.isLoaded) {
+    if (atStartup || !Services.blocklist.isLoaded) {
       if (!this._blocklistObserverAdded) {
         Services.obs.addObserver(this, BLOCKLIST_LOADED_TOPIC);
         this._blocklistObserverAdded = true;
       }
       return [{
         name: "dummy", version: "0.1", description: "Blocklist unavailable",
         blocklisted: false, disabled: true, clicktoplay: false,
         mimeTypes: ["text/there.is.only.blocklist"],
@@ -799,31 +808,36 @@ EnvironmentAddonBuilder.prototype = {
       }
     }
 
     return activePlugins;
   },
 
   /**
    * Get the GMPlugins data in object form.
+   *
+   * @param {boolean} [atStartup]
+   *        True if this is the first check we're performing at startup. In that
+   *        situation, we defer some more expensive initialization.
+   *
    * @return Object containing the GMPlugins data.
    *
    * This should only be called from _pendingTask; otherwise we risk
    * running this during addon manager shutdown.
    */
-  async _getActiveGMPlugins() {
+  async _getActiveGMPlugins(atStartup) {
     // If we haven't yet loaded the blocklist, pass back dummy data for now,
     // and add an observer to update this data as soon as we get it.
-    if (!Services.blocklist.isLoaded) {
+    if (atStartup || !Services.blocklist.isLoaded) {
       if (!this._blocklistObserverAdded) {
         Services.obs.addObserver(this, BLOCKLIST_LOADED_TOPIC);
         this._blocklistObserverAdded = true;
       }
       return {
-        "dummy-gmp": {version: "0.1", userDisabled: false, applyBackgroundUpdates: true}
+        "dummy-gmp": {version: "0.1", userDisabled: false, applyBackgroundUpdates: 1}
       };
     }
     // Request plugins, asynchronously.
     let allPlugins = await AddonManager.getAddonsByTypes(["plugin"]);
 
     let activeGMPlugins = {};
     for (let plugin of allPlugins) {
       // Only get info for active GMplugins.
--- a/toolkit/components/telemetry/tests/unit/xpcshell.ini
+++ b/toolkit/components/telemetry/tests/unit/xpcshell.ini
@@ -6,28 +6,26 @@ firefox-appdir = browser
 support-files =
   ../search/chrome.manifest
   ../search/searchTest.jar
   dictionary.xpi
   experiment.xpi
   engine.xml
   system.xpi
   restartless.xpi
-  theme.xpi
   testUnicodePDB32.dll
   testNoPDB32.dll
   testUnicodePDB64.dll
   testNoPDB64.dll
   !/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
 generated-files =
   dictionary.xpi
   experiment.xpi
   system.xpi
   restartless.xpi
-  theme.xpi
 
 [test_MigratePendingPings.js]
 [test_TelemetryHistograms.js]
 [test_TelemetryStorage.js]
 [test_SubsessionChaining.js]
 tags = addons
 [test_TelemetryEnvironment.js]
 skip-if = os == "android"
--- a/toolkit/locales/en-US/chrome/global/extensions.properties
+++ b/toolkit/locales/en-US/chrome/global/extensions.properties
@@ -32,8 +32,13 @@ saveaspdf.saveasdialog.title = Save As
 
 #LOCALIZATION NOTE (newTabControlled.message2) %S is the icon and name of the extension which updated the New Tab page.
 newTabControlled.message2 = An extension, %S, changed the page you see when you open a new tab.
 newTabControlled.learnMore = Learn more
 
 #LOCALIZATION NOTE (homepageControlled.message) %S is the icon and name of the extension which updated the homepage.
 homepageControlled.message = An extension, %S, changed what you see when you open your homepage and new windows.
 homepageControlled.learnMore = Learn more
+
+# LOCALIZATION NOTE (defaultTheme.name): This is displayed in about:addons -> Appearance
+defaultTheme.name=Default
+defaultTheme.description=The default theme.
+
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -40,16 +40,18 @@ const PREF_WEBEXT_PERM_PROMPTS        = 
 const UPDATE_REQUEST_VERSION          = 2;
 
 const XMLURI_BLOCKLIST                = "http://www.mozilla.org/2006/addons-blocklist";
 
 const KEY_PROFILEDIR                  = "ProfD";
 const KEY_APPDIR                      = "XCurProcD";
 const FILE_BLOCKLIST                  = "blocklist.xml";
 
+const DEFAULT_THEME_ID                = "default-theme@mozilla.org";
+
 const BRANCH_REGEXP                   = /^([^\.]+\.[0-9]+[a-z]*).*/gi;
 const PREF_EM_CHECK_COMPATIBILITY_BASE = "extensions.checkCompatibility";
 var PREF_EM_CHECK_COMPATIBILITY = MOZ_COMPATIBILITY_NIGHTLY ?
                                   PREF_EM_CHECK_COMPATIBILITY_BASE + ".nightly" :
                                   undefined;
 
 const VALID_TYPES_REGEXP = /^[\w\-]+$/;
 
@@ -65,16 +67,17 @@ const URI_XPINSTALL_DIALOG = "chrome://m
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/AsyncShutdown.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm",
   Extension: "resource://gre/modules/Extension.jsm",
   FileUtils: "resource://gre/modules/FileUtils.jsm",
+  LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
   PromptUtils: "resource://gre/modules/SharedPromptUtils.jsm",
 });
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS",
                                       PREF_WEBEXT_PERM_PROMPTS, false);
 
 // Initialize the WebExtension process script service as early as possible,
 // since it needs to be able to track things like new frameLoader globals that
@@ -833,16 +836,37 @@ var AddonManagerInternal = {
 
       gStartupComplete = true;
       this.recordTimestamp("AMI_startup_end");
     } catch (e) {
       logger.error("startup failed", e);
       AddonManagerPrivate.recordException("AMI", "startup failed", e);
     }
 
+    let brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
+    let extensionsBundle = Services.strings.createBundle(
+      "chrome://global/locale/extensions.properties");
+
+    // When running in xpcshell tests, the default theme may already
+    // exist.
+    if (!LightweightThemeManager._builtInThemes.has(DEFAULT_THEME_ID)) {
+      let author = "Mozilla";
+      try {
+        author = brandBundle.GetStringFromName("vendorShortName");
+      } catch (e) {}
+
+      LightweightThemeManager.addBuiltInTheme({
+        id: DEFAULT_THEME_ID,
+        name: extensionsBundle.GetStringFromName("defaultTheme.name"),
+        description: extensionsBundle.GetStringFromName("defaultTheme.description"),
+        iconURL: "chrome://mozapps/content/extensions/default-theme-icon.svg",
+        author,
+      });
+    }
+
     logger.debug("Completed startup sequence");
     this.callManagerListeners("onStartup");
   },
 
   /**
    * Registers a new AddonProvider.
    *
    * @param {string} aProvider -The provider to register
--- a/toolkit/mozapps/extensions/AddonManagerStartup.cpp
+++ b/toolkit/mozapps/extensions/AddonManagerStartup.cpp
@@ -59,17 +59,16 @@ AddonManagerStartup::GetSingleton()
   if (!singleton) {
     singleton = new AddonManagerStartup();
     ClearOnShutdown(&singleton);
   }
   return *singleton;
 }
 
 AddonManagerStartup::AddonManagerStartup()
-  : mInitialized(false)
 {}
 
 
 nsIFile*
 AddonManagerStartup::ProfileDir()
 {
   if (!mProfileDir) {
     nsresult rv;
@@ -408,18 +407,16 @@ public:
 
   bool Enabled() { return GetBool("enabled"); }
 
   double LastModifiedTime() { return GetNumber("lastModifiedTime"); }
 
 
   Result<nsCOMPtr<nsIFile>, nsresult> FullPath();
 
-  NSLocationType LocationType();
-
   Result<bool, nsresult> UpdateLastModifiedTime();
 
 
 private:
   nsString mId;
   InstallLocation& mLocation;
 };
 
@@ -436,26 +433,16 @@ Addon::FullPath()
 
   // If not an absolute path, fall back to a relative path from the location.
   MOZ_TRY(NS_NewLocalFile(mLocation.Path(), false, getter_AddRefs(file)));
 
   MOZ_TRY(file->AppendRelativePath(path));
   return Move(file);
 }
 
-NSLocationType
-Addon::LocationType()
-{
-  nsString type = GetString("type", "extension");
-  if (type.LowerCaseEqualsLiteral("theme")) {
-    return NS_SKIN_LOCATION;
-  }
-  return NS_EXTENSION_LOCATION;
-}
-
 Result<bool, nsresult>
 Addon::UpdateLastModifiedTime()
 {
   nsCOMPtr<nsIFile> file;
   MOZ_TRY_VAR(file, FullPath());
 
   bool result;
   if (NS_FAILED(file->Exists(&result)) || !result) {
@@ -503,42 +490,16 @@ InstallLocation::InstallLocation(JSConte
   mAddonsIter.emplace(cx, mAddonsObj, this);
 }
 
 
 /*****************************************************************************
  * XPC interfacing
  *****************************************************************************/
 
-Result<Ok, nsresult>
-AddonManagerStartup::AddInstallLocation(Addon& addon)
-{
-  nsCOMPtr<nsIFile> file;
-  MOZ_TRY_VAR(file, addon.FullPath());
-
-  nsString path;
-  MOZ_TRY(file->GetPath(path));
-
-  auto type = addon.LocationType();
-
-  if (type == NS_SKIN_LOCATION) {
-    mThemePaths.AppendElement(file);
-  } else {
-    return Ok();
-  }
-
-  if (StringTail(path, 4).LowerCaseEqualsLiteral(".xpi")) {
-    XRE_AddJarManifestLocation(type, file);
-  } else {
-    nsCOMPtr<nsIFile> manifest = CloneAndAppend(file, "chrome.manifest");
-    XRE_AddManifestLocation(type, manifest);
-  }
-  return Ok();
-}
-
 nsresult
 AddonManagerStartup::ReadStartupData(JSContext* cx, JS::MutableHandleValue locations)
 {
   locations.set(JS::UndefinedValue());
 
   nsCOMPtr<nsIFile> file = CloneAndAppend(ProfileDir(), "addonStartup.json.lz4");
 
   nsCString data;
@@ -577,44 +538,16 @@ AddonManagerStartup::ReadStartupData(JSC
       }
     }
   }
 
   return NS_OK;
 }
 
 nsresult
-AddonManagerStartup::InitializeExtensions(JS::HandleValue locations, JSContext* cx)
-{
-  NS_ENSURE_FALSE(mInitialized, NS_ERROR_UNEXPECTED);
-  NS_ENSURE_TRUE(locations.isObject(), NS_ERROR_INVALID_ARG);
-
-  mInitialized = true;
-
-  if (!Preferences::GetBool("extensions.defaultProviders.enabled", true)) {
-    return NS_OK;
-  }
-
-  JS::RootedObject locs(cx, &locations.toObject());
-  for (auto e1 : PropertyIter(cx, locs)) {
-    InstallLocation loc(e1);
-
-    for (auto e2 : loc.Addons()) {
-      Addon addon(e2);
-
-      if (addon.Enabled() && !addon.Bootstrapped()) {
-        Unused << AddInstallLocation(addon);
-      }
-    }
-  }
-
-  return NS_OK;
-}
-
-nsresult
 AddonManagerStartup::EncodeBlob(JS::HandleValue value, JSContext* cx, JS::MutableHandleValue result)
 {
   StructuredCloneData holder;
 
   ErrorResult rv;
   holder.Write(cx, value, rv);
   if (rv.Failed()) {
     return rv.StealNSResult();
@@ -702,29 +635,16 @@ AddonManagerStartup::EnumerateZipFile(ns
 
   *countOut = results.Length();
   *entriesOut = strResults.release();
 
   return NS_OK;
 }
 
 nsresult
-AddonManagerStartup::Reset()
-{
-  MOZ_RELEASE_ASSERT(xpc::IsInAutomation());
-
-  mInitialized = false;
-
-  mExtensionPaths.Clear();
-  mThemePaths.Clear();
-
-  return NS_OK;
-}
-
-nsresult
 AddonManagerStartup::InitializeURLPreloader()
 {
   MOZ_RELEASE_ASSERT(xpc::IsInAutomation());
 
   URLPreloader::ReInitialize();
 
   return NS_OK;
 }
--- a/toolkit/mozapps/extensions/AddonManagerStartup.h
+++ b/toolkit/mozapps/extensions/AddonManagerStartup.h
@@ -33,38 +33,21 @@ public:
   static AddonManagerStartup& GetSingleton();
 
   static already_AddRefed<AddonManagerStartup> GetInstance()
   {
     RefPtr<AddonManagerStartup> inst = &GetSingleton();
     return inst.forget();
   }
 
-  const nsCOMArray<nsIFile>& ExtensionPaths()
-  {
-    return mExtensionPaths;
-  }
-
-  const nsCOMArray<nsIFile>& ThemePaths()
-  {
-    return mExtensionPaths;
-  }
-
 private:
-  Result<Ok, nsresult> AddInstallLocation(Addon& addon);
-
   nsIFile* ProfileDir();
 
   nsCOMPtr<nsIFile> mProfileDir;
 
-  nsCOMArray<nsIFile> mExtensionPaths;
-  nsCOMArray<nsIFile> mThemePaths;
-
-  bool mInitialized;
-
 protected:
   virtual ~AddonManagerStartup() = default;
 };
 
 } // namespace mozilla
 
 #define NS_ADDONMANAGERSTARTUP_CONTRACTID \
   "@mozilla.org/addons/addon-manager-startup;1"
--- a/toolkit/mozapps/extensions/LightweightThemeManager.jsm
+++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm
@@ -7,22 +7,22 @@
 var EXPORTED_SYMBOLS = ["LightweightThemeManager"];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
 /* globals AddonManagerPrivate*/
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 const ID_SUFFIX              = "@personas.mozilla.org";
-const PREF_LWTHEME_TO_SELECT = "extensions.lwThemeToSelect";
 const ADDON_TYPE             = "theme";
 const ADDON_TYPE_WEBEXT      = "webextension-theme";
 
 const URI_EXTENSION_STRINGS  = "chrome://mozapps/locale/extensions/extensions.properties";
 
+const DEFAULT_THEME_ID = "default-theme@mozilla.org";
 const DEFAULT_MAX_USED_THEMES_COUNT = 30;
 
 const MAX_PREVIEW_SECONDS = 30;
 
 const MANDATORY = ["id", "name"];
 const OPTIONAL = ["headerURL", "footerURL", "textcolor", "accentcolor",
                   "iconURL", "previewURL", "author", "description",
                   "homepageURL", "updateURL", "version"];
@@ -115,28 +115,28 @@ var LightweightThemeManager = {
       themes = JSON.parse(_prefs.getStringPref("usedThemes"));
     } catch (e) { }
 
     themes.push(...this._builtInThemes.values());
     return themes;
   },
 
   get currentTheme() {
-    let selectedThemeID = _prefs.getCharPref("selectedThemeID", "");
+    let selectedThemeID = _prefs.getStringPref("selectedThemeID", DEFAULT_THEME_ID);
 
     let data = null;
     if (selectedThemeID) {
       data = this.getUsedTheme(selectedThemeID);
     }
     return data;
   },
 
   get currentThemeForDisplay() {
     var data = this.currentTheme;
-    if (!data && _fallbackThemeData)
+    if ((!data || data.id == DEFAULT_THEME_ID) && _fallbackThemeData)
       data = _fallbackThemeData;
 
     if (data && PERSIST_ENABLED) {
       for (let key in PERSIST_FILES) {
         try {
           if (data[key] && _prefs.getBoolPref("persisted." + key))
             data[key] = _getLocalImageURI(PERSIST_FILES[key]).spec
                         + "?" + data.id + ";" + _version(data);
@@ -184,17 +184,17 @@ var LightweightThemeManager = {
 
   addBuiltInTheme(theme) {
     if (!theme || !theme.id || this.usedThemes.some(t => t.id == theme.id)) {
       throw new Error("Trying to add invalid builtIn theme");
     }
 
     this._builtInThemes.set(theme.id, theme);
 
-    if (_prefs.getCharPref("selectedThemeID") == theme.id) {
+    if (_prefs.getStringPref("selectedThemeID", DEFAULT_THEME_ID) == theme.id) {
       this.currentTheme = theme;
     }
   },
 
   forgetBuiltInTheme(id) {
     if (!this._builtInThemes.has(id)) {
       let currentTheme = this.currentTheme;
       if (currentTheme && currentTheme.id == id) {
@@ -318,25 +318,16 @@ var LightweightThemeManager = {
     Services.obs.notifyObservers(null, "lightweight-theme-changed");
   },
 
   /**
    * Starts the Addons provider and enables the new lightweight theme if
    * necessary.
    */
   startup() {
-    if (Services.prefs.prefHasUserValue(PREF_LWTHEME_TO_SELECT)) {
-      let id = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT);
-      if (id)
-        this.themeChanged(this.getUsedTheme(id));
-      else
-        this.themeChanged(null);
-      Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT);
-    }
-
     _prefs.addObserver("", _prefObserver);
   },
 
   /**
    * Shuts down the provider.
    */
   shutdown() {
     _prefs.removeObserver("", _prefObserver);
@@ -345,78 +336,54 @@ var LightweightThemeManager = {
   /**
    * Called when a new add-on has been enabled when only one add-on of that type
    * can be enabled.
    *
    * @param  aId
    *         The ID of the newly enabled add-on
    * @param  aType
    *         The type of the newly enabled add-on
-   * @param  aPendingRestart
-   *         true if the newly enabled add-on will only become enabled after a
-   *         restart
    */
-  addonChanged(aId, aType, aPendingRestart) {
+  addonChanged(aId, aType) {
     if (aType != ADDON_TYPE && aType != ADDON_TYPE_WEBEXT)
       return;
 
     let id = _getInternalID(aId);
     let current = this.currentTheme;
 
-    try {
-      let next = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT);
-      if (id == next && aPendingRestart)
-        return;
-
-      Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT);
-      if (next) {
-        AddonManagerPrivate.callAddonListeners("onOperationCancelled",
-                                               new AddonWrapper(this.getUsedTheme(next)));
-      } else if (id == current.id) {
-        AddonManagerPrivate.callAddonListeners("onOperationCancelled",
-                                               new AddonWrapper(current));
-        return;
-      }
-    } catch (e) {
+    if (current && id == current.id) {
+      AddonManagerPrivate.callAddonListeners("onOperationCancelled",
+                                             new AddonWrapper(current));
+      return;
     }
 
     if (current) {
-      if (current.id == id)
+      if (current.id == id || (!aId && current.id == DEFAULT_THEME_ID))
         return;
       _themeIDBeingDisabled = current.id;
       let wrapper = new AddonWrapper(current);
-      if (aPendingRestart) {
-        Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, "");
-        AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, true);
-      } else {
-        AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false);
-        this.themeChanged(null);
-        AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
-      }
+
+      AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false);
+      this.themeChanged(null);
+      AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
       _themeIDBeingDisabled = null;
     }
 
     if (id) {
       let theme = this.getUsedTheme(id);
       // WebExtension themes have an ID, but no LWT wrapper, so bail out here.
       if (!theme)
         return;
       _themeIDBeingEnabled = id;
       let wrapper = new AddonWrapper(theme);
-      if (aPendingRestart) {
-        AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, true);
-        Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, id);
 
-        // Flush the preferences to disk so they survive any crash
-        Services.prefs.savePrefFile(null);
-      } else {
-        AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, false);
-        this.themeChanged(theme);
-        AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
-      }
+      AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, false);
+      this.themeChanged(theme);
+      AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
+
       _themeIDBeingEnabled = null;
     }
   },
 
   /**
    * Called to get an Addon with a particular ID.
    *
    * @param  aId
@@ -459,17 +426,17 @@ let themeFor = wrapper => wrapperMap.get
  * consumers of the AddonManager API.
  */
 function AddonWrapper(aTheme) {
   wrapperMap.set(this, aTheme);
 }
 
 AddonWrapper.prototype = {
   get id() {
-    return themeFor(this).id + ID_SUFFIX;
+    return _getExternalID(themeFor(this).id);
   },
 
   get type() {
     return ADDON_TYPE;
   },
 
   get isActive() {
     let current = LightweightThemeManager.currentTheme;
@@ -517,35 +484,30 @@ AddonWrapper.prototype = {
   get permissions() {
     let permissions = 0;
 
     // Do not allow uninstall of builtIn themes.
     if (!LightweightThemeManager._builtInThemes.has(themeFor(this).id))
       permissions = AddonManager.PERM_CAN_UNINSTALL;
     if (this.userDisabled)
       permissions |= AddonManager.PERM_CAN_ENABLE;
-    else
+    else if (themeFor(this).id != DEFAULT_THEME_ID)
       permissions |= AddonManager.PERM_CAN_DISABLE;
     return permissions;
   },
 
   get userDisabled() {
     let id = themeFor(this).id;
     if (_themeIDBeingEnabled == id)
       return false;
     if (_themeIDBeingDisabled == id)
       return true;
 
-    try {
-      let toSelect = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT);
-      return id != toSelect;
-    } catch (e) {
-      let current = LightweightThemeManager.currentTheme;
-      return !current || current.id != id;
-    }
+    let current = LightweightThemeManager.currentTheme;
+    return !current || current.id != id;
   },
 
   set userDisabled(val) {
     if (val == this.userDisabled)
       return val;
 
     if (val)
       LightweightThemeManager.currentTheme = null;
@@ -633,22 +595,30 @@ AddonWrapper.prototype = {
  *          The ID to be converted
  *
  * @return  the lightweight theme ID or null if the ID was not for a lightweight
  *          theme.
  */
 function _getInternalID(id) {
   if (!id)
     return null;
+  if (id == DEFAULT_THEME_ID)
+    return id;
   let len = id.length - ID_SUFFIX.length;
   if (len > 0 && id.substring(len) == ID_SUFFIX)
     return id.substring(0, len);
   return null;
 }
 
+function _getExternalID(id) {
+  if (id == DEFAULT_THEME_ID)
+    return id;
+  return id + ID_SUFFIX;
+}
+
 function _setCurrentTheme(aData, aLocal) {
   aData = _sanitizeTheme(aData, null, aLocal);
 
   let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
   cancel.data = false;
   Services.obs.notifyObservers(cancel, "lightweight-theme-change-requested",
                                JSON.stringify(aData));
 
@@ -686,17 +656,17 @@ function _setCurrentTheme(aData, aLocal)
     if (isInstall)
       AddonManagerPrivate.callAddonListeners("onInstalled", wrapper);
   }
 
   if (cancel.data)
     return null;
 
   if (notify) {
-    AddonManagerPrivate.notifyAddonChanged(aData ? aData.id + ID_SUFFIX : null,
+    AddonManagerPrivate.notifyAddonChanged(aData ? _getExternalID(aData.id) : null,
                                            ADDON_TYPE, false);
   }
 
   return LightweightThemeManager.currentTheme;
 }
 
 function _sanitizeTheme(aData, aBaseURI, aLocal) {
   if (!aData || typeof aData != "object")
--- a/toolkit/mozapps/extensions/amIAddonManagerStartup.idl
+++ b/toolkit/mozapps/extensions/amIAddonManagerStartup.idl
@@ -17,23 +17,16 @@ interface amIAddonManagerStartup : nsISu
    *
    * Returns null for an empty or nonexistent state file, but throws for an
    * invalid one.
    */
   [implicit_jscontext]
   jsval readStartupData();
 
   /**
-   * Initializes the chrome registry for the enabled, non-restartless add-on
-   * in the given state data.
-   */
-  [implicit_jscontext]
-  void initializeExtensions(in jsval locations);
-
-  /**
    * Registers a set of dynamic chrome registry entries, and returns an object
    * with a `destruct()` method which must be called in order to unregister
    * the entries.
    *
    * @param manifestURI The base manifest URI for the entries. URL values are
    *        resolved relative to this URI.
    * @param entries An array of arrays, each containing a registry entry as it
    *        would appar in a chrome.manifest file. Only the following entry
@@ -63,25 +56,16 @@ interface amIAddonManagerStartup : nsISu
    * @param file The zip file to enumerate.
    * @param pattern The pattern to match, as passed to nsIZipReader.findEntries.
    */
   void enumerateZipFile(in nsIFile file, in AUTF8String pattern,
                         [optional] out unsigned long count,
                         [retval, array, size_is(count)] out wstring entries);
 
   /**
-   * Resets the internal state of the startup service, and allows
-   * initializeExtensions() to be called again. Does *not* fully unregister
-   * chrome registry locations for previously registered add-ons.
-   *
-   * NOT FOR USE OUTSIDE OF UNIT TESTS.
-   */
-  void reset();
-
-  /**
    * Initializes the URL Preloader.
    *
    * NOT FOR USE OUTSIDE OF UNIT TESTS.
    */
   void initializeURLPreloader();
 
 };
 
rename from browser/base/content/default-theme-icon.svg
rename to toolkit/mozapps/extensions/content/default-theme-icon.svg
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -240,17 +240,18 @@ function isLegacyExtension(addon) {
     legacy = true;
   }
   if (addon.type == "theme") {
     // The logic here is kind of clunky but we want to mark complete
     // themes as legacy.  There's no explicit flag for complete
     // themes so explicitly check for new style themes (for which
     // isWebExtension is true) or lightweight themes (which have
     // ids that end with @personas.mozilla.org)
-    legacy = !(addon.isWebExtension || addon.id.endsWith("@personas.mozilla.org"));
+    legacy = !(addon.isWebExtension || addon.id.endsWith("@personas.mozilla.org") ||
+               addon.id == "default-theme@mozilla.org");
   }
 
   if (legacy && (addon.hidden || addon.signedState == AddonManager.SIGNEDSTATE_PRIVILEGED)) {
     legacy = false;
   }
   // Exceptions that can slip through above: the default theme plus
   // test pilot addons until we get SIGNEDSTATE_PRIVILEGED deployed.
   if (legacy && legacyWarningExceptions.includes(addon.id)) {
--- a/toolkit/mozapps/extensions/internal/AddonRepository.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm
@@ -366,17 +366,17 @@ var AddonRepository = {
   },
 
   /**
    * Asynchronously get a cached add-on by id. The add-on (or null if the
    * add-on is not found) is passed to the specified callback. If caching is
    * disabled, null is passed to the specified callback.
    *
    * The callback variant exists only for existing code in XPIProvider.jsm
-   * and XPIProviderUtils.jsm that requires a synchronous callback, yuck.
+   * and XPIDatabase.jsm that requires a synchronous callback, yuck.
    *
    * @param  aId
    *         The id of the add-on to get
    */
   async getCachedAddonByID(aId, aCallback) {
     if (!aId || !this.cacheEnabled) {
       if (aCallback) {
         aCallback(null);
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -782,24 +782,19 @@ var AddonTestUtils = {
         // simulate real-world usage.
         let XPIscope = ChromeUtils.import("resource://gre/modules/addons/XPIProvider.jsm", {});
         // This would be cleaner if I could get it as the rejection reason from
         // the AddonManagerInternal.shutdown() promise
         let shutdownError = XPIscope.XPIDatabase._saveError;
 
         AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider);
         Cu.unload("resource://gre/modules/addons/XPIProvider.jsm");
+        Cu.unload("resource://gre/modules/addons/XPIDatabase.jsm");
         Cu.unload("resource://gre/modules/addons/XPIInstall.jsm");
 
-        // We need to set this in order reset the startup service, which
-        // is only possible when running in automation.
-        Services.prefs.setBoolPref(PREF_DISABLE_SECURITY, true);
-
-        aomStartup.reset();
-
         if (shutdownError)
           throw shutdownError;
 
         return true;
       });
   },
 
   promiseRestartManager(newVersion) {
@@ -954,16 +949,18 @@ var AddonTestUtils = {
    */
   writeFilesToZip(zipFile, files, flags = 0) {
     if (typeof zipFile == "string")
       zipFile = nsFile(zipFile);
 
     var zipW = ZipWriter(zipFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | flags);
 
     for (let [path, data] of Object.entries(files)) {
+      if (typeof data === "object" && ChromeUtils.getClassName(data) === "Object")
+        data = JSON.stringify(data);
       if (!(data instanceof ArrayBuffer))
         data = new TextEncoder("utf-8").encode(data).buffer;
 
       let stream = ArrayBufferInputStream(data, 0, data.byteLength);
 
       // Note these files are being created in the XPI archive with date "0" which is 1970-01-01.
       zipW.addEntryStream(path, 0, Ci.nsIZipWriter.COMPRESSION_NONE,
                           stream, false);
rename from toolkit/mozapps/extensions/internal/XPIProviderUtils.js
rename to toolkit/mozapps/extensions/internal/XPIDatabase.jsm
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -1,190 +1,1323 @@
 /* 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";
 
-// These are injected from XPIProvider.jsm
-/* globals ADDON_SIGNING, SIGNED_TYPES, BOOTSTRAP_REASONS, DB_SCHEMA,
-          AddonInternal, XPIProvider, XPIStates, syncLoadManifestFromFile,
-          isUsableAddon, recordAddonTelemetry,
-          flushChromeCaches, descriptorToPath, DEFAULT_SKIN */
+/**
+ * This file contains most of the logic required to maintain the
+ * extensions database, including querying and modifying extension
+ * metadata. In general, we try to avoid loading it during startup when
+ * at all possible. Please keep that in mind when deciding whether to
+ * add code here or elsewhere.
+ */
+
+/* eslint "valid-jsdoc": [2, {requireReturn: false, requireReturnDescription: false, prefer: {return: "returns"}}] */
+
+var EXPORTED_SYMBOLS = ["AddonInternal", "XPIDatabase", "XPIDatabaseReconcile"];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AddonManager: "resource://gre/modules/AddonManager.jsm",
   AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
   AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm",
+  AddonSettings: "resource://gre/modules/addons/AddonSettings.jsm",
+  AppConstants: "resource://gre/modules/AppConstants.jsm",
   DeferredTask: "resource://gre/modules/DeferredTask.jsm",
   FileUtils: "resource://gre/modules/FileUtils.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   Services: "resource://gre/modules/Services.jsm",
+
+  UpdateChecker: "resource://gre/modules/addons/XPIInstall.jsm",
+  XPIInstall: "resource://gre/modules/addons/XPIInstall.jsm",
+  XPIInternal: "resource://gre/modules/addons/XPIProvider.jsm",
 });
 
+const {nsIBlocklistService} = Ci;
+
+// These are injected from XPIProvider.jsm
+/* globals
+ *         BOOTSTRAP_REASONS,
+ *         DB_SCHEMA,
+ *         SIGNED_TYPES,
+ *         XPIProvider,
+ *         XPIStates,
+ *         descriptorToPath,
+ *         isTheme,
+ *         isWebExtension,
+ *         recordAddonTelemetry,
+ */
+
+for (let sym of [
+  "BOOTSTRAP_REASONS",
+  "DB_SCHEMA",
+  "SIGNED_TYPES",
+  "XPIProvider",
+  "XPIStates",
+  "descriptorToPath",
+  "isTheme",
+  "isWebExtension",
+  "recordAddonTelemetry",
+]) {
+  XPCOMUtils.defineLazyGetter(this, sym, () => XPIInternal[sym]);
+}
+
 ChromeUtils.import("resource://gre/modules/Log.jsm");
 const LOGGER_ID = "addons.xpi-utils";
 
 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
                                        "initWithPath");
 
 // Create a new logger for use by the Addons XPI Provider Utils
 // (Requires AddonManager.jsm)
 var logger = Log.repository.getLogger(LOGGER_ID);
 
 const KEY_PROFILEDIR                  = "ProfD";
 const FILE_JSON_DB                    = "extensions.json";
 
 // The last version of DB_SCHEMA implemented in SQLITE
 const LAST_SQLITE_DB_SCHEMA           = 14;
+
+const PREF_BLOCKLIST_ITEM_URL         = "extensions.blocklist.itemURL";
 const PREF_DB_SCHEMA                  = "extensions.databaseSchema";
+const PREF_EM_AUTO_DISABLED_SCOPES    = "extensions.autoDisableScopes";
+const PREF_EM_EXTENSION_FORMAT        = "extensions.";
 const PREF_PENDING_OPERATIONS         = "extensions.pendingOperations";
-const PREF_EM_AUTO_DISABLED_SCOPES    = "extensions.autoDisableScopes";
+const PREF_XPI_SIGNATURES_DEV_ROOT    = "xpinstall.signatures.dev-root";
+
+const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
 const KEY_APP_SYSTEM_ADDONS           = "app-system-addons";
 const KEY_APP_SYSTEM_DEFAULTS         = "app-system-defaults";
+const KEY_APP_SYSTEM_LOCAL            = "app-system-local";
+const KEY_APP_SYSTEM_SHARE            = "app-system-share";
 const KEY_APP_GLOBAL                  = "app-global";
+const KEY_APP_PROFILE                 = "app-profile";
 const KEY_APP_TEMPORARY               = "app-temporary";
 
+// Properties to cache and reload when an addon installation is pending
+const PENDING_INSTALL_METADATA =
+    ["syncGUID", "targetApplications", "userDisabled", "softDisabled",
+     "existingAddonID", "sourceURI", "releaseNotesURI", "installDate",
+     "updateDate", "applyBackgroundUpdates", "compatibilityOverrides"];
+
+const COMPATIBLE_BY_DEFAULT_TYPES = {
+  extension: true,
+  dictionary: true
+};
+
+// Properties that exist in the install manifest
+const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
+const PROP_LOCALE_MULTI  = ["developers", "translators", "contributors"];
+
 // Properties to save in JSON file
 const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type",
-                          "internalName", "updateURL", "optionsURL",
+                          "updateURL", "optionsURL",
                           "optionsType", "optionsBrowserStyle", "aboutURL",
                           "defaultLocale", "visible", "active", "userDisabled",
                           "appDisabled", "pendingUninstall", "installDate",
                           "updateDate", "applyBackgroundUpdates", "bootstrap", "path",
                           "skinnable", "size", "sourceURI", "releaseNotesURI",
                           "softDisabled", "foreignInstall",
                           "strictCompatibility", "locales", "targetApplications",
                           "targetPlatforms", "signedState",
                           "seen", "dependencies", "hasEmbeddedWebExtension",
                           "userPermissions", "icons", "iconURL", "icon64URL",
                           "blocklistState", "blocklistURL", "startupData"];
 
+const LEGACY_TYPES = new Set([
+  "extension",
+]);
+
 // Time to wait before async save of XPI JSON database, in milliseconds
 const ASYNC_SAVE_DELAY_MS = 20;
 
+// Note: When adding/changing/removing items here, remember to change the
+// DB schema version to ensure changes are picked up ASAP.
+const STATIC_BLOCKLIST_PATTERNS = [
+  { creator: "Mozilla Corp.",
+    level: nsIBlocklistService.STATE_BLOCKED,
+    blockID: "i162" },
+  { creator: "Mozilla.org",
+    level: nsIBlocklistService.STATE_BLOCKED,
+    blockID: "i162" }
+];
+
+function findMatchingStaticBlocklistItem(aAddon) {
+  for (let item of STATIC_BLOCKLIST_PATTERNS) {
+    if ("creator" in item && typeof item.creator == "string") {
+      if ((aAddon.defaultLocale && aAddon.defaultLocale.creator == item.creator) ||
+          (aAddon.selectedLocale && aAddon.selectedLocale.creator == item.creator)) {
+        return item;
+      }
+    }
+  }
+  return null;
+}
+
 /**
  * Asynchronously fill in the _repositoryAddon field for one addon
+ *
+ * @param {AddonInternal} aAddon
+ *        The add-on to annotate.
+ * @returns {AddonInternal}
+ *        The annotated add-on.
  */
 async function getRepositoryAddon(aAddon) {
   if (aAddon) {
     aAddon._repositoryAddon = await AddonRepository.getCachedAddonByID(aAddon.id);
   }
   return aAddon;
 }
 
 /**
  * Copies properties from one object to another. If no target object is passed
  * a new object will be created and returned.
  *
- * @param  aObject
- *         An object to copy from
- * @param  aProperties
- *         An array of properties to be copied
- * @param  aTarget
- *         An optional target object to copy the properties to
- * @return the object that the properties were copied onto
+ * @param {object} aObject
+ *        An object to copy from
+ * @param {string[]} aProperties
+ *        An array of properties to be copied
+ * @param {object?} [aTarget]
+ *        An optional target object to copy the properties to
+ * @returns {Object}
+ *        The object that the properties were copied onto
  */
 function copyProperties(aObject, aProperties, aTarget) {
   if (!aTarget)
     aTarget = {};
   aProperties.forEach(function(aProp) {
     if (aProp in aObject)
       aTarget[aProp] = aObject[aProp];
   });
   return aTarget;
 }
 
+// Maps instances of AddonInternal to AddonWrapper
+const wrapperMap = new WeakMap();
+let addonFor = wrapper => wrapperMap.get(wrapper);
+
+const EMPTY_ARRAY = Object.freeze([]);
+
+let AddonWrapper;
+
+/**
+ * The AddonInternal is an internal only representation of add-ons. It may
+ * have come from the database (see DBAddonInternal in XPIDatabase.jsm)
+ * or an install manifest.
+ */
+class AddonInternal {
+  constructor() {
+    this._hasResourceCache = new Map();
+
+    this._wrapper = null;
+    this._selectedLocale = null;
+    this.active = false;
+    this.visible = false;
+    this.userDisabled = false;
+    this.appDisabled = false;
+    this.softDisabled = false;
+    this.blocklistState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+    this.blocklistURL = null;
+    this.sourceURI = null;
+    this.releaseNotesURI = null;
+    this.foreignInstall = false;
+    this.seen = true;
+    this.skinnable = false;
+    this.startupData = null;
+
+    /**
+     * @property {Array<string>} dependencies
+     *   An array of bootstrapped add-on IDs on which this add-on depends.
+     *   The add-on will remain appDisabled if any of the dependent
+     *   add-ons is not installed and enabled.
+     */
+    this.dependencies = EMPTY_ARRAY;
+    this.hasEmbeddedWebExtension = false;
+  }
+
+  get wrapper() {
+    if (!this._wrapper) {
+      this._wrapper = new AddonWrapper(this);
+    }
+    return this._wrapper;
+  }
+
+  get selectedLocale() {
+    if (this._selectedLocale)
+      return this._selectedLocale;
+
+    /**
+     * this.locales is a list of objects that have property `locales`.
+     * It's value is an array of locale codes.
+     *
+     * First, we reduce this nested structure to a flat list of locale codes.
+     */
+    const locales = [].concat(...this.locales.map(loc => loc.locales));
+
+    let requestedLocales = Services.locale.getRequestedLocales();
+
+    /**
+     * If en-US is not in the list, add it as the last fallback.
+     */
+    if (!requestedLocales.includes("en-US")) {
+      requestedLocales.push("en-US");
+    }
+
+    /**
+     * Then we negotiate best locale code matching the app locales.
+     */
+    let bestLocale = Services.locale.negotiateLanguages(
+      requestedLocales,
+      locales,
+      "und",
+      Services.locale.langNegStrategyLookup
+    )[0];
+
+    /**
+     * If no match has been found, we'll assign the default locale as
+     * the selected one.
+     */
+    if (bestLocale === "und") {
+      this._selectedLocale = this.defaultLocale;
+    } else {
+      /**
+       * Otherwise, we'll go through all locale entries looking for the one
+       * that has the best match in it's locales list.
+       */
+      this._selectedLocale = this.locales.find(loc =>
+        loc.locales.includes(bestLocale));
+    }
+
+    return this._selectedLocale;
+  }
+
+  get providesUpdatesSecurely() {
+    return !this.updateURL || this.updateURL.startsWith("https:");
+  }
+
+  get isCorrectlySigned() {
+    switch (this._installLocation.name) {
+      case KEY_APP_SYSTEM_ADDONS:
+        // System add-ons must be signed by the system key.
+        return this.signedState == AddonManager.SIGNEDSTATE_SYSTEM;
+
+      case KEY_APP_SYSTEM_DEFAULTS:
+      case KEY_APP_TEMPORARY:
+        // Temporary and built-in system add-ons do not require signing.
+        return true;
+
+      case KEY_APP_SYSTEM_SHARE:
+      case KEY_APP_SYSTEM_LOCAL:
+        // On UNIX platforms except OSX, an additional location for system
+        // add-ons exists in /usr/{lib,share}/mozilla/extensions. Add-ons
+        // installed there do not require signing.
+        if (Services.appinfo.OS != "Darwin")
+          return true;
+        break;
+    }
+
+    if (this.signedState === AddonManager.SIGNEDSTATE_NOT_REQUIRED)
+      return true;
+    return this.signedState > AddonManager.SIGNEDSTATE_MISSING;
+  }
+
+  get unpack() {
+    return this.type === "dictionary";
+  }
+
+  get isCompatible() {
+    return this.isCompatibleWith();
+  }
+
+  get disabled() {
+    return (this.userDisabled || this.appDisabled || this.softDisabled);
+  }
+
+  get isPlatformCompatible() {
+    if (this.targetPlatforms.length == 0)
+      return true;
+
+    let matchedOS = false;
+
+    // If any targetPlatform matches the OS and contains an ABI then we will
+    // only match a targetPlatform that contains both the current OS and ABI
+    let needsABI = false;
+
+    // Some platforms do not specify an ABI, test against null in that case.
+    let abi = null;
+    try {
+      abi = Services.appinfo.XPCOMABI;
+    } catch (e) { }
+
+    // Something is causing errors in here
+    try {
+      for (let platform of this.targetPlatforms) {
+        if (platform.os == Services.appinfo.OS) {
+          if (platform.abi) {
+            needsABI = true;
+            if (platform.abi === abi)
+              return true;
+          } else {
+            matchedOS = true;
+          }
+        }
+      }
+    } catch (e) {
+      let message = "Problem with addon " + this.id + " targetPlatforms "
+                    + JSON.stringify(this.targetPlatforms);
+      logger.error(message, e);
+      AddonManagerPrivate.recordException("XPI", message, e);
+      // don't trust this add-on
+      return false;
+    }
+
+    return matchedOS && !needsABI;
+  }
+
+  isCompatibleWith(aAppVersion, aPlatformVersion) {
+    let app = this.matchingTargetApplication;
+    if (!app)
+      return false;
+
+    // set reasonable defaults for minVersion and maxVersion
+    let minVersion = app.minVersion || "0";
+    let maxVersion = app.maxVersion || "*";
+
+    if (!aAppVersion)
+      aAppVersion = Services.appinfo.version;
+    if (!aPlatformVersion)
+      aPlatformVersion = Services.appinfo.platformVersion;
+
+    let version;
+    if (app.id == Services.appinfo.ID)
+      version = aAppVersion;
+    else if (app.id == TOOLKIT_ID)
+      version = aPlatformVersion;
+
+    // Only extensions and dictionaries can be compatible by default; themes
+    // and language packs always use strict compatibility checking.
+    if (this.type in COMPATIBLE_BY_DEFAULT_TYPES &&
+        !AddonManager.strictCompatibility && !this.strictCompatibility) {
+
+      // The repository can specify compatibility overrides.
+      // Note: For now, only blacklisting is supported by overrides.
+      let overrides = AddonRepository.getCompatibilityOverridesSync(this.id);
+      if (overrides) {
+        let override = AddonRepository.findMatchingCompatOverride(this.version,
+                                                                  overrides);
+        if (override) {
+          return false;
+        }
+      }
+
+      // Extremely old extensions should not be compatible by default.
+      let minCompatVersion;
+      if (app.id == Services.appinfo.ID)
+        minCompatVersion = XPIProvider.minCompatibleAppVersion;
+      else if (app.id == TOOLKIT_ID)
+        minCompatVersion = XPIProvider.minCompatiblePlatformVersion;
+
+      if (minCompatVersion &&
+          Services.vc.compare(minCompatVersion, maxVersion) > 0)
+        return false;
+
+      return Services.vc.compare(version, minVersion) >= 0;
+    }
+
+    return (Services.vc.compare(version, minVersion) >= 0) &&
+           (Services.vc.compare(version, maxVersion) <= 0);
+  }
+
+  get matchingTargetApplication() {
+    let app = null;
+    for (let targetApp of this.targetApplications) {
+      if (targetApp.id == Services.appinfo.ID)
+        return targetApp;
+      if (targetApp.id == TOOLKIT_ID)
+        app = targetApp;
+    }
+    return app;
+  }
+
+  async findBlocklistEntry() {
+    let staticItem = findMatchingStaticBlocklistItem(this);
+    if (staticItem) {
+      let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ITEM_URL);
+      return {
+        state: staticItem.level,
+        url: url.replace(/%blockID%/g, staticItem.blockID)
+      };
+    }
+
+    return Services.blocklist.getAddonBlocklistEntry(this.wrapper);
+  }
+
+  async updateBlocklistState(options = {}) {
+    let {applySoftBlock = true, oldAddon = null, updateDatabase = true} = options;
+
+    if (oldAddon) {
+      this.userDisabled = oldAddon.userDisabled;
+      this.softDisabled = oldAddon.softDisabled;
+      this.blocklistState = oldAddon.blocklistState;
+    }
+    let oldState = this.blocklistState;
+
+    let entry = await this.findBlocklistEntry();
+    let newState = entry ? entry.state : Services.blocklist.STATE_NOT_BLOCKED;
+
+    this.blocklistState = newState;
+    this.blocklistURL = entry && entry.url;
+
+    let userDisabled, softDisabled;
+    // After a blocklist update, the blocklist service manually applies
+    // new soft blocks after displaying a UI, in which cases we need to
+    // skip updating it here.
+    if (applySoftBlock && oldState != newState) {
+      if (newState == Services.blocklist.STATE_SOFTBLOCKED) {
+        if (this.type == "theme") {
+          userDisabled = true;
+        } else {
+          softDisabled = !this.userDisabled;
+        }
+      } else {
+        softDisabled = false;
+      }
+    }
+
+    if (this.inDatabase && updateDatabase) {
+      XPIDatabase.updateAddonDisabledState(this, userDisabled, softDisabled);
+      XPIDatabase.saveChanges();
+    } else {
+      this.appDisabled = !XPIDatabase.isUsableAddon(this);
+      if (userDisabled !== undefined) {
+        this.userDisabled = userDisabled;
+      }
+      if (softDisabled !== undefined) {
+        this.softDisabled = softDisabled;
+      }
+    }
+  }
+
+  applyCompatibilityUpdate(aUpdate, aSyncCompatibility) {
+    for (let targetApp of this.targetApplications) {
+      for (let updateTarget of aUpdate.targetApplications) {
+        if (targetApp.id == updateTarget.id && (aSyncCompatibility ||
+            Services.vc.compare(targetApp.maxVersion, updateTarget.maxVersion) < 0)) {
+          targetApp.minVersion = updateTarget.minVersion;
+          targetApp.maxVersion = updateTarget.maxVersion;
+        }
+      }
+    }
+    this.appDisabled = !XPIDatabase.isUsableAddon(this);
+  }
+
+  /**
+   * toJSON is called by JSON.stringify in order to create a filtered version
+   * of this object to be serialized to a JSON file. A new object is returned
+   * with copies of all non-private properties. Functions, getters and setters
+   * are not copied.
+   *
+   * @returns {Object}
+   *       An object containing copies of the properties of this object
+   *       ignoring private properties, functions, getters and setters.
+   */
+  toJSON() {
+    let obj = {};
+    for (let prop in this) {
+      // Ignore the wrapper property
+      if (prop == "wrapper")
+        continue;
+
+      // Ignore private properties
+      if (prop.substring(0, 1) == "_")
+        continue;
+
+      // Ignore getters
+      if (this.__lookupGetter__(prop))
+        continue;
+
+      // Ignore setters
+      if (this.__lookupSetter__(prop))
+        continue;
+
+      // Ignore functions
+      if (typeof this[prop] == "function")
+        continue;
+
+      obj[prop] = this[prop];
+    }
+
+    return obj;
+  }
+
+  /**
+   * When an add-on install is pending its metadata will be cached in a file.
+   * This method reads particular properties of that metadata that may be newer
+   * than that in the install manifest, like compatibility information.
+   *
+   * @param {Object} aObj
+   *        A JS object containing the cached metadata
+   */
+  importMetadata(aObj) {
+    for (let prop of PENDING_INSTALL_METADATA) {
+      if (!(prop in aObj))
+        continue;
+
+      this[prop] = aObj[prop];
+    }
+
+    // Compatibility info may have changed so update appDisabled
+    this.appDisabled = !XPIDatabase.isUsableAddon(this);
+  }
+
+  permissions() {
+    let permissions = 0;
+
+    // Add-ons that aren't installed cannot be modified in any way
+    if (!(this.inDatabase))
+      return permissions;
+
+    if (!this.appDisabled) {
+      if (this.userDisabled || this.softDisabled) {
+        permissions |= AddonManager.PERM_CAN_ENABLE;
+      } else if (this.type != "theme") {
+        permissions |= AddonManager.PERM_CAN_DISABLE;
+      }
+    }
+
+    // Add-ons that are in locked install locations, or are pending uninstall
+    // cannot be upgraded or uninstalled
+    if (!this._installLocation.locked && !this.pendingUninstall) {
+      // System add-on upgrades are triggered through a different mechanism (see updateSystemAddons())
+      let isSystem = this._installLocation.isSystem;
+      // Add-ons that are installed by a file link cannot be upgraded.
+      if (!this._installLocation.isLinkedAddon(this.id) && !isSystem) {
+        permissions |= AddonManager.PERM_CAN_UPGRADE;
+      }
+
+      permissions |= AddonManager.PERM_CAN_UNINSTALL;
+    }
+
+    if (Services.policies &&
+        !Services.policies.isAllowed(`modify-extension:${this.id}`)) {
+      permissions &= ~AddonManager.PERM_CAN_UNINSTALL;
+      permissions &= ~AddonManager.PERM_CAN_DISABLE;
+    }
+
+    return permissions;
+  }
+}
+
+/**
+ * The AddonWrapper wraps an Addon to provide the data visible to consumers of
+ * the public API.
+ *
+ * @param {AddonInternal} aAddon
+ *        The add-on object to wrap.
+ */
+AddonWrapper = class {
+  constructor(aAddon) {
+    wrapperMap.set(this, aAddon);
+  }
+
+  get __AddonInternal__() {
+    return AppConstants.DEBUG ? addonFor(this) : undefined;
+  }
+
+  get seen() {
+    return addonFor(this).seen;
+  }
+
+  get hasEmbeddedWebExtension() {
+    return addonFor(this).hasEmbeddedWebExtension;
+  }
+
+  markAsSeen() {
+    addonFor(this).seen = true;
+    XPIDatabase.saveChanges();
+  }
+
+  get type() {
+    return XPIInternal.getExternalType(addonFor(this).type);
+  }
+
+  get isWebExtension() {
+    return isWebExtension(addonFor(this).type);
+  }
+
+  get temporarilyInstalled() {
+    return addonFor(this)._installLocation == XPIInternal.TemporaryInstallLocation;
+  }
+
+  get aboutURL() {
+    return this.isActive ? addonFor(this).aboutURL : null;
+  }
+
+  get optionsURL() {
+    if (!this.isActive) {
+      return null;
+    }
+
+    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 policy = WebExtensionPolicy.getByID(addon.id);
+        if (!policy) {
+          return null;
+        }
+        let base = policy.getURL();
+        return new URL(addon.optionsURL, base).href;
+      }
+      return addon.optionsURL;
+    }
+
+    return null;
+  }
+
+  get optionsType() {
+    if (!this.isActive)
+      return null;
+
+    let addon = addonFor(this);
+    let hasOptionsURL = !!this.optionsURL;
+
+    if (addon.optionsType) {
+      switch (parseInt(addon.optionsType, 10)) {
+      case AddonManager.OPTIONS_TYPE_TAB:
+      case AddonManager.OPTIONS_TYPE_INLINE_BROWSER:
+        return hasOptionsURL ? addon.optionsType : null;
+      }
+      return null;
+    }
+
+    return null;
+  }
+
+  get optionsBrowserStyle() {
+    let addon = addonFor(this);
+    return addon.optionsBrowserStyle;
+  }
+
+  get iconURL() {
+    return AddonManager.getPreferredIconURL(this, 48);
+  }
+
+  get icon64URL() {
+    return AddonManager.getPreferredIconURL(this, 64);
+  }
+
+  get icons() {
+    let addon = addonFor(this);
+    let icons = {};
+
+    if (addon._repositoryAddon) {
+      for (let size in addon._repositoryAddon.icons) {
+        icons[size] = addon._repositoryAddon.icons[size];
+      }
+    }
+
+    if (addon.icons) {
+      for (let size in addon.icons) {
+        icons[size] = this.getResourceURI(addon.icons[size]).spec;
+      }
+    } else {
+      // legacy add-on that did not update its icon data yet
+      if (this.hasResource("icon.png")) {
+        icons[32] = icons[48] = this.getResourceURI("icon.png").spec;
+      }
+      if (this.hasResource("icon64.png")) {
+        icons[64] = this.getResourceURI("icon64.png").spec;
+      }
+    }
+
+    let canUseIconURLs = this.isActive;
+    if (canUseIconURLs && addon.iconURL) {
+      icons[32] = addon.iconURL;
+      icons[48] = addon.iconURL;
+    }
+
+    if (canUseIconURLs && addon.icon64URL) {
+      icons[64] = addon.icon64URL;
+    }
+
+    Object.freeze(icons);
+    return icons;
+  }
+
+  get screenshots() {
+    let addon = addonFor(this);
+    let repositoryAddon = addon._repositoryAddon;
+    if (repositoryAddon && ("screenshots" in repositoryAddon)) {
+      let repositoryScreenshots = repositoryAddon.screenshots;
+      if (repositoryScreenshots && repositoryScreenshots.length > 0)
+        return repositoryScreenshots;
+    }
+
+    if (isTheme(addon.type) && this.hasResource("preview.png")) {
+      let url = this.getResourceURI("preview.png").spec;
+      return [new AddonManagerPrivate.AddonScreenshot(url)];
+    }
+
+    return null;
+  }
+
+  get applyBackgroundUpdates() {
+    return addonFor(this).applyBackgroundUpdates;
+  }
+  set applyBackgroundUpdates(val) {
+    let addon = addonFor(this);
+    if (val != AddonManager.AUTOUPDATE_DEFAULT &&
+        val != AddonManager.AUTOUPDATE_DISABLE &&
+        val != AddonManager.AUTOUPDATE_ENABLE) {
+      val = val ? AddonManager.AUTOUPDATE_DEFAULT :
+                  AddonManager.AUTOUPDATE_DISABLE;
+    }
+
+    if (val == addon.applyBackgroundUpdates)
+      return val;
+
+    XPIDatabase.setAddonProperties(addon, {
+      applyBackgroundUpdates: val
+    });
+    AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]);
+
+    return val;
+  }
+
+  set syncGUID(val) {
+    let addon = addonFor(this);
+    if (addon.syncGUID == val)
+      return val;
+
+    if (addon.inDatabase)
+      XPIDatabase.setAddonSyncGUID(addon, val);
+
+    addon.syncGUID = val;
+
+    return val;
+  }
+
+  get install() {
+    let addon = addonFor(this);
+    if (!("_install" in addon) || !addon._install)
+      return null;
+    return addon._install.wrapper;
+  }
+
+  get pendingUpgrade() {
+    let addon = addonFor(this);
+    return addon.pendingUpgrade ? addon.pendingUpgrade.wrapper : null;
+  }
+
+  get scope() {
+    let addon = addonFor(this);
+    if (addon._installLocation)
+      return addon._installLocation.scope;
+
+    return AddonManager.SCOPE_PROFILE;
+  }
+
+  get pendingOperations() {
+    let addon = addonFor(this);
+    let pending = 0;
+    if (!(addon.inDatabase)) {
+      // Add-on is pending install if there is no associated install (shouldn't
+      // happen here) or if the install is in the process of or has successfully
+      // completed the install. If an add-on is pending install then we ignore
+      // any other pending operations.
+      if (!addon._install || addon._install.state == AddonManager.STATE_INSTALLING ||
+          addon._install.state == AddonManager.STATE_INSTALLED)
+        return AddonManager.PENDING_INSTALL;
+    } else if (addon.pendingUninstall) {
+      // If an add-on is pending uninstall then we ignore any other pending
+      // operations
+      return AddonManager.PENDING_UNINSTALL;
+    }
+
+    if (addon.active && addon.disabled)
+      pending |= AddonManager.PENDING_DISABLE;
+    else if (!addon.active && !addon.disabled)
+      pending |= AddonManager.PENDING_ENABLE;
+
+    if (addon.pendingUpgrade)
+      pending |= AddonManager.PENDING_UPGRADE;
+
+    return pending;
+  }
+
+  get operationsRequiringRestart() {
+    return 0;
+  }
+
+  get isDebuggable() {
+    return this.isActive && addonFor(this).bootstrap;
+  }
+
+  get permissions() {
+    return addonFor(this).permissions();
+  }
+
+  get isActive() {
+    let addon = addonFor(this);
+    if (!addon.active)
+      return false;
+    if (!Services.appinfo.inSafeMode)
+      return true;
+    return addon.bootstrap && XPIInternal.canRunInSafeMode(addon);
+  }
+
+  get startupPromise() {
+    let addon = addonFor(this);
+    if (!addon.bootstrap || !this.isActive)
+      return null;
+
+    let activeAddon = XPIProvider.activeAddons.get(addon.id);
+    if (activeAddon)
+      return activeAddon.startupPromise || null;
+    return null;
+  }
+
+  updateBlocklistState(applySoftBlock = true) {
+    return addonFor(this).updateBlocklistState({applySoftBlock});
+  }
+
+  get userDisabled() {
+    let addon = addonFor(this);
+    return addon.softDisabled || addon.userDisabled;
+  }
+  set userDisabled(val) {
+    let addon = addonFor(this);
+    if (val == this.userDisabled) {
+      return val;
+    }
+
+    if (addon.inDatabase) {
+      // hidden and system add-ons should not be user disabled,
+      // as there is no UI to re-enable them.
+      if (this.hidden) {
+        throw new Error(`Cannot disable hidden add-on ${addon.id}`);
+      }
+      XPIDatabase.updateAddonDisabledState(addon, val);
+    } else {
+      addon.userDisabled = val;
+      // When enabling remove the softDisabled flag
+      if (!val)
+        addon.softDisabled = false;
+    }
+
+    return val;
+  }
+
+  set softDisabled(val) {
+    let addon = addonFor(this);
+    if (val == addon.softDisabled)
+      return val;
+
+    if (addon.inDatabase) {
+      // When softDisabling a theme just enable the active theme
+      if (isTheme(addon.type) && val && !addon.userDisabled) {
+        if (isWebExtension(addon.type))
+          XPIDatabase.updateAddonDisabledState(addon, undefined, val);
+      } else {
+        XPIDatabase.updateAddonDisabledState(addon, undefined, val);
+      }
+    } else if (!addon.userDisabled) {
+      // Only set softDisabled if not already disabled
+      addon.softDisabled = val;
+    }
+
+    return val;
+  }
+
+  get hidden() {
+    let addon = addonFor(this);
+    if (addon._installLocation.name == KEY_APP_TEMPORARY)
+      return false;
+
+    return addon._installLocation.isSystem;
+  }
+
+  get isSystem() {
+    let addon = addonFor(this);
+    return addon._installLocation.isSystem;
+  }
+
+  // Returns true if Firefox Sync should sync this addon. Only addons
+  // in the profile install location are considered syncable.
+  get isSyncable() {
+    let addon = addonFor(this);
+    return (addon._installLocation.name == KEY_APP_PROFILE);
+  }
+
+  get userPermissions() {
+    return addonFor(this).userPermissions;
+  }
+
+  isCompatibleWith(aAppVersion, aPlatformVersion) {
+    return addonFor(this).isCompatibleWith(aAppVersion, aPlatformVersion);
+  }
+
+  uninstall(alwaysAllowUndo) {
+    let addon = addonFor(this);
+    XPIProvider.uninstallAddon(addon, alwaysAllowUndo);
+  }
+
+  cancelUninstall() {
+    let addon = addonFor(this);
+    XPIProvider.cancelUninstallAddon(addon);
+  }
+
+  findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
+    new UpdateChecker(addonFor(this), aListener, aReason, aAppVersion, aPlatformVersion);
+  }
+
+  // Returns true if there was an update in progress, false if there was no update to cancel
+  cancelUpdate() {
+    let addon = addonFor(this);
+    if (addon._updateCheck) {
+      addon._updateCheck.cancel();
+      return true;
+    }
+    return false;
+  }
+
+  hasResource(aPath) {
+    let addon = addonFor(this);
+    if (addon._hasResourceCache.has(aPath))
+      return addon._hasResourceCache.get(aPath);
+
+    let bundle = addon._sourceBundle.clone();
+
+    // Bundle may not exist any more if the addon has just been uninstalled,
+    // but explicitly first checking .exists() results in unneeded file I/O.
+    try {
+      var isDir = bundle.isDirectory();
+    } catch (e) {
+      addon._hasResourceCache.set(aPath, false);
+      return false;
+    }
+
+    if (isDir) {
+      if (aPath)
+        aPath.split("/").forEach(part => bundle.append(part));
+      let result = bundle.exists();
+      addon._hasResourceCache.set(aPath, result);
+      return result;
+    }
+
+    let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+                    createInstance(Ci.nsIZipReader);
+    try {
+      zipReader.open(bundle);
+      let result = zipReader.hasEntry(aPath);
+      addon._hasResourceCache.set(aPath, result);
+      return result;
+    } catch (e) {
+      addon._hasResourceCache.set(aPath, false);
+      return false;
+    } finally {
+      zipReader.close();
+    }
+  }
+
+  /**
+   * Reloads the add-on.
+   *
+   * For temporarily installed add-ons, this uninstalls and re-installs the
+   * add-on. Otherwise, the addon is disabled and then re-enabled, and the cache
+   * is flushed.
+   *
+   * @returns {Promise}
+   */
+  reload() {
+    return new Promise((resolve) => {
+      const addon = addonFor(this);
+
+      logger.debug(`reloading add-on ${addon.id}`);
+
+      if (!this.temporarilyInstalled) {
+        let addonFile = addon.getResourceURI;
+        XPIDatabase.updateAddonDisabledState(addon, true);
+        Services.obs.notifyObservers(addonFile, "flush-cache-entry");
+        XPIDatabase.updateAddonDisabledState(addon, false);
+        resolve();
+      } else {
+        // This function supports re-installing an existing add-on.
+        resolve(AddonManager.installTemporaryAddon(addon._sourceBundle));
+      }
+    });
+  }
+
+  /**
+   * Returns a URI to the selected resource or to the add-on bundle if aPath
+   * is null. URIs to the bundle will always be file: URIs. URIs to resources
+   * will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is
+   * still an XPI file.
+   *
+   * @param {string?} aPath
+   *        The path in the add-on to get the URI for or null to get a URI to
+   *        the file or directory the add-on is installed as.
+   * @returns {nsIURI}
+   */
+  getResourceURI(aPath) {
+    let addon = addonFor(this);
+    if (!aPath)
+      return Services.io.newFileURI(addon._sourceBundle);
+
+    return XPIInternal.getURIForResourceInFile(addon._sourceBundle, aPath);
+  }
+};
+
+function chooseValue(aAddon, aObj, aProp) {
+  let repositoryAddon = aAddon._repositoryAddon;
+  let objValue = aObj[aProp];
+
+  if (repositoryAddon && (aProp in repositoryAddon) &&
+      (objValue === undefined || objValue === null)) {
+    return [repositoryAddon[aProp], true];
+  }
+
+  return [objValue, false];
+}
+
+function defineAddonWrapperProperty(name, getter) {
+  Object.defineProperty(AddonWrapper.prototype, name, {
+    get: getter,
+    enumerable: true,
+  });
+}
+
+["id", "syncGUID", "version", "isCompatible", "isPlatformCompatible",
+ "providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled",
+ "softDisabled", "skinnable", "size", "foreignInstall",
+ "strictCompatibility", "updateURL", "dependencies",
+ "signedState", "isCorrectlySigned"].forEach(function(aProp) {
+   defineAddonWrapperProperty(aProp, function() {
+     let addon = addonFor(this);
+     return (aProp in addon) ? addon[aProp] : undefined;
+   });
+});
+
+["fullDescription", "developerComments", "supportURL",
+ "contributionURL", "averageRating", "reviewCount",
+ "reviewURL", "weeklyDownloads"].forEach(function(aProp) {
+  defineAddonWrapperProperty(aProp, function() {
+    let addon = addonFor(this);
+    if (addon._repositoryAddon)
+      return addon._repositoryAddon[aProp];
+
+    return null;
+  });
+});
+
+["installDate", "updateDate"].forEach(function(aProp) {
+  defineAddonWrapperProperty(aProp, function() {
+    return new Date(addonFor(this)[aProp]);
+  });
+});
+
+["sourceURI", "releaseNotesURI"].forEach(function(aProp) {
+  defineAddonWrapperProperty(aProp, function() {
+    let addon = addonFor(this);
+
+    // Temporary Installed Addons do not have a "sourceURI",
+    // But we can use the "_sourceBundle" as an alternative,
+    // which points to the path of the addon xpi installed
+    // or its source dir (if it has been installed from a
+    // directory).
+    if (aProp == "sourceURI" && this.temporarilyInstalled) {
+      return Services.io.newFileURI(addon._sourceBundle);
+    }
+
+    let [target, fromRepo] = chooseValue(addon, addon, aProp);
+    if (!target)
+      return null;
+    if (fromRepo)
+      return target;
+    return Services.io.newURI(target);
+  });
+});
+
+PROP_LOCALE_SINGLE.forEach(function(aProp) {
+  defineAddonWrapperProperty(aProp, function() {
+    let addon = addonFor(this);
+    // Override XPI creator if repository creator is defined
+    if (aProp == "creator" &&
+        addon._repositoryAddon && addon._repositoryAddon.creator) {
+      return addon._repositoryAddon.creator;
+    }
+
+    let result = null;
+
+    if (addon.active) {
+      try {
+        let pref = PREF_EM_EXTENSION_FORMAT + addon.id + "." + aProp;
+        let value = Services.prefs.getPrefType(pref) != Ci.nsIPrefBranch.PREF_INVALID ? Services.prefs.getComplexValue(pref, Ci.nsIPrefLocalizedString).data : null;
+        if (value)
+          result = value;
+      } catch (e) {
+      }
+    }
+
+    if (result == null)
+      [result] = chooseValue(addon, addon.selectedLocale, aProp);
+
+    if (aProp == "creator")
+      return result ? new AddonManagerPrivate.AddonAuthor(result) : null;
+
+    return result;
+  });
+});
+
+PROP_LOCALE_MULTI.forEach(function(aProp) {
+  defineAddonWrapperProperty(aProp, function() {
+    let addon = addonFor(this);
+    let results = null;
+    let usedRepository = false;
+
+    if (addon.active) {
+      let pref = PREF_EM_EXTENSION_FORMAT + addon.id + "." +
+                 aProp.substring(0, aProp.length - 1);
+      let list = Services.prefs.getChildList(pref, {});
+      if (list.length > 0) {
+        list.sort();
+        results = [];
+        for (let childPref of list) {
+          let value = Services.prefs.getPrefType(childPref) != Ci.nsIPrefBranch.PREF_INVALID ? Services.prefs.getComplexValue(childPref, Ci.nsIPrefLocalizedString).data : null;
+          if (value)
+            results.push(value);
+        }
+      }
+    }
+
+    if (results == null)
+      [results, usedRepository] = chooseValue(addon, addon.selectedLocale, aProp);
+
+    if (results && !usedRepository) {
+      results = results.map(function(aResult) {
+        return new AddonManagerPrivate.AddonAuthor(aResult);
+      });
+    }
+
+    return results;
+  });
+});
+
+
 /**
  * The DBAddonInternal is a special AddonInternal that has been retrieved from
  * the database. The constructor will initialize the DBAddonInternal with a set
  * of fields, which could come from either the JSON store or as an
  * XPIProvider.AddonInternal created from an addon's manifest
  * @constructor
- * @param aLoaded
+ * @param {Object} aLoaded
  *        Addon data fields loaded from JSON or the addon manifest.
  */
-function DBAddonInternal(aLoaded) {
-  AddonInternal.call(this);
-
-  if (aLoaded.descriptor) {
-    if (!aLoaded.path) {
-      aLoaded.path = descriptorToPath(aLoaded.descriptor);
+class DBAddonInternal extends AddonInternal {
+  constructor(aLoaded) {
+    super();
+
+    if (aLoaded.descriptor) {
+      if (!aLoaded.path) {
+        aLoaded.path = descriptorToPath(aLoaded.descriptor);
+      }
+      delete aLoaded.descriptor;
     }
-    delete aLoaded.descriptor;
+
+    copyProperties(aLoaded, PROP_JSON_FIELDS, this);
+
+    if (!this.dependencies)
+      this.dependencies = [];
+    Object.freeze(this.dependencies);
+
+    if (aLoaded._installLocation) {
+      this._installLocation = aLoaded._installLocation;
+      this.location = aLoaded._installLocation.name;
+    } else if (aLoaded.location) {
+      this._installLocation = XPIProvider.installLocationsByName[this.location];
+    }
+
+    this._key = this.location + ":" + this.id;
+
+    if (!aLoaded._sourceBundle) {
+      throw new Error("Expected passed argument to contain a path");
+    }
+
+    this._sourceBundle = aLoaded._sourceBundle;
   }
 
-  copyProperties(aLoaded, PROP_JSON_FIELDS, this);
-
-  if (!this.dependencies)
-    this.dependencies = [];
-  Object.freeze(this.dependencies);
-
-  if (aLoaded._installLocation) {
-    this._installLocation = aLoaded._installLocation;
-    this.location = aLoaded._installLocation.name;
-  } else if (aLoaded.location) {
-    this._installLocation = XPIProvider.installLocationsByName[this.location];
-  }
-
-  this._key = this.location + ":" + this.id;
-
-  if (!aLoaded._sourceBundle) {
-    throw new Error("Expected passed argument to contain a path");
-  }
-
-  this._sourceBundle = aLoaded._sourceBundle;
-}
-
-DBAddonInternal.prototype = Object.create(AddonInternal.prototype);
-Object.assign(DBAddonInternal.prototype, {
   applyCompatibilityUpdate(aUpdate, aSyncCompatibility) {
     let wasCompatible = this.isCompatible;
 
     this.targetApplications.forEach(function(aTargetApp) {
       aUpdate.targetApplications.forEach(function(aUpdateTarget) {
         if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility ||
             Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) {
           aTargetApp.minVersion = aUpdateTarget.minVersion;
           aTargetApp.maxVersion = aUpdateTarget.maxVersion;
           XPIDatabase.saveChanges();
         }
       });
     });
 
     if (wasCompatible != this.isCompatible)
-      XPIProvider.updateAddonDisabledState(this);
-  },
+      XPIDatabase.updateAddonDisabledState(this);
+  }
 
   toJSON() {
     return copyProperties(this, PROP_JSON_FIELDS);
-  },
+  }
 
   get inDatabase() {
     return true;
   }
-});
+}
+
+/**
+ * @typedef {Map<string, DBAddonInternal>} AddonDB
+ */
 
 /**
- * Internal interface: find an addon from an already loaded addonDB
+ * Internal interface: find an addon from an already loaded addonDB.
+ *
+ * @param {AddonDB} addonDB
+ *        The add-on database.
+ * @param {function(DBAddonInternal) : boolean} aFilter
+ *        The filter predecate. The first add-on for which it returns
+ *        true will be returned.
+ * @returns {DBAddonInternal?}
+ *        The first matching add-on, if one is found.
  */
 function _findAddon(addonDB, aFilter) {
   for (let addon of addonDB.values()) {
     if (aFilter(addon)) {
       return addon;
     }
   }
   return null;
 }
 
 /**
  * Internal interface to get a filtered list of addons from a loaded addonDB
+ *
+ * @param {AddonDB} addonDB
+ *        The add-on database.
+ * @param {function(DBAddonInternal) : boolean} aFilter
+ *        The filter predecate. Add-ons which match this predicate will
+ *        be returned.
+ * @returns {Array<DBAddonInternal>}
+ *        The list of matching add-ons.
  */
 function _filterDB(addonDB, aFilter) {
   return Array.from(addonDB.values()).filter(aFilter);
 }
 
 this.XPIDatabase = {
   // true if the database connection has been opened
   initialized: false,
@@ -265,16 +1398,18 @@ this.XPIDatabase = {
     }
 
     await this._saveTask.finalize();
   },
 
   /**
    * Converts the current internal state of the XPI addon database to
    * a JSON.stringify()-ready structure
+   *
+   * @returns {Object}
    */
   toJSON() {
     if (!this.addonDB) {
       // We never loaded the database?
       throw new Error("Attempt to save database without loading it first");
     }
 
     let toSave = {
@@ -293,20 +1428,21 @@ this.XPIDatabase = {
    * 1) Perfectly good, up to date database
    * 2) Out of date JSON database needs to be upgraded => upgrade
    * 3) JSON database exists but is mangled somehow => build new JSON
    * 4) no JSON DB, but a usable SQLITE db we can upgrade from => upgrade
    * 5) useless SQLITE DB => build new JSON
    * 6) usable RDF DB => upgrade
    * 7) useless RDF DB => build new JSON
    * 8) Nothing at all => build new JSON
-   * @param  aRebuildOnError
-   *         A boolean indicating whether add-on information should be loaded
-   *         from the install locations if the database needs to be rebuilt.
-   *         (if false, caller is XPIProvider.checkForChanges() which will rebuild)
+   *
+   * @param {boolean} aRebuildOnError
+   *        A boolean indicating whether add-on information should be loaded
+   *        from the install locations if the database needs to be rebuilt.
+   *        (if false, caller is XPIProvider.checkForChanges() which will rebuild)
    */
   syncLoadDB(aRebuildOnError) {
     this.migrateData = null;
     let fstream = null;
     let data = "";
     try {
       let readTimer = AddonManagerPrivate.simpleTimer("XPIDB_syncRead_MS");
       logger.debug("Opening XPI database " + this.jsonFile.path);
@@ -352,23 +1488,25 @@ this.XPIDatabase = {
       AddonManagerPrivate.recordSimpleMeasure("XPIDB_overlapped_load", 1);
     }
     this._dbPromise = Promise.resolve(this.addonDB);
     Services.obs.notifyObservers(this.addonDB, "xpi-database-loaded");
   },
 
   /**
    * Parse loaded data, reconstructing the database if the loaded data is not valid
-   * @param aRebuildOnError
+   *
+   * @param {string} aData
+   *        The stringified add-on JSON to parse.
+   * @param {boolean} aRebuildOnError
    *        If true, synchronously reconstruct the database from installed add-ons
    */
   parseDB(aData, aRebuildOnError) {
     let parseTimer = AddonManagerPrivate.simpleTimer("XPIDB_parseDB_MS");
     try {
-      // dump("Loaded JSON:\n" + aData + "\n");
       let inputAddons = JSON.parse(aData);
       // Now do some sanity checks on our JSON db
       if (!("schemaVersion" in inputAddons) || !("addons" in inputAddons)) {
         parseTimer.done();
         // Content of JSON file is bad, need to rebuild from scratch
         logger.error("bad JSON file contents");
         AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", "badJSON");
         let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildBadJSON_MS");
@@ -424,16 +1562,19 @@ this.XPIDatabase = {
       let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildReadFailed_MS");
       this.rebuildDatabase(aRebuildOnError);
       rebuildTimer.done();
     }
   },
 
   /**
    * Upgrade database from earlier (sqlite or RDF) version if available
+   *
+   * @param {boolean} aRebuildOnError
+   *        If true, synchronously reconstruct the database from installed add-ons
    */
   upgradeDB(aRebuildOnError) {
     let upgradeTimer = AddonManagerPrivate.simpleTimer("XPIDB_upgradeDB_MS");
 
     let schemaVersion = Services.prefs.getIntPref(PREF_DB_SCHEMA, 0);
     if (schemaVersion > LAST_SQLITE_DB_SCHEMA) {
       // we've upgraded before but the JSON file is gone, fall through
       // and rebuild from scratch
@@ -442,16 +1583,21 @@ this.XPIDatabase = {
 
     this.rebuildDatabase(aRebuildOnError);
     upgradeTimer.done();
   },
 
   /**
    * Reconstruct when the DB file exists but is unreadable
    * (for example because read permission is denied)
+   *
+   * @param {Error} aError
+   *        The error that triggered the rebuild.
+   * @param {boolean} aRebuildOnError
+   *        If true, synchronously reconstruct the database from installed add-ons
    */
   rebuildUnreadableDB(aError, aRebuildOnError) {
     let rebuildTimer = AddonManagerPrivate.simpleTimer("XPIDB_rebuildUnreadableDB_MS");
     logger.warn("Extensions database " + this.jsonFile.path +
         " exists but is not readable; rebuilding", aError);
     // Remember the error message until we try and write at least once, so
     // we know at shutdown time that there was a problem
     this._loadError = aError;
@@ -460,18 +1606,19 @@ this.XPIDatabase = {
     rebuildTimer.done();
   },
 
   /**
    * Open and read the XPI database asynchronously, upgrading if
    * necessary. If any DB load operation fails, we need to
    * synchronously rebuild the DB from the installed extensions.
    *
-   * @return Promise<Map> resolves to the Map of loaded JSON data stored
-   *         in this.addonDB; never rejects.
+   * @returns {Promise<AddonDB>}
+   *        Resolves to the Map of loaded JSON data stored in
+   *        this.addonDB; never rejects.
    */
   asyncLoadDB() {
     // Already started (and possibly finished) loading
     if (this._dbPromise) {
       return this._dbPromise;
     }
 
     logger.debug("Starting async load of XPI database " + this.jsonFile.path);
@@ -518,20 +1665,21 @@ this.XPIDatabase = {
 
     return this._dbPromise;
   },
 
   /**
    * Rebuild the database from addon install directories. If this.migrateData
    * is available, uses migrated information for settings on the addons found
    * during rebuild
-   * @param aRebuildOnError
-   *         A boolean indicating whether add-on information should be loaded
-   *         from the install locations if the database needs to be rebuilt.
-   *         (if false, caller is XPIProvider.checkForChanges() which will rebuild)
+   *
+   * @param {boolean} aRebuildOnError
+   *        A boolean indicating whether add-on information should be loaded
+   *        from the install locations if the database needs to be rebuilt.
+   *        (if false, caller is XPIProvider.checkForChanges() which will rebuild)
    */
   rebuildDatabase(aRebuildOnError) {
     this.addonDB = new Map();
     this.initialized = true;
 
     if (XPIStates.size == 0) {
       // No extensions installed, so we're done
       logger.debug("Rebuilding XPI database with no extensions");
@@ -597,39 +1745,44 @@ this.XPIDatabase = {
       delete this._saveTask;
       // re-enable the schema version setter
       delete this._schemaVersionSet;
     }
   },
 
   /**
    * Asynchronously list all addons that match the filter function
-   * @param  aFilter
-   *         Function that takes an addon instance and returns
-   *         true if that addon should be included in the selected array
-   * @return a Promise that resolves to the list of add-ons matching aFilter or
-   *         an empty array if none match
+   *
+   * @param {function(DBAddonInternal) : boolean} aFilter
+   *        Function that takes an addon instance and returns
+   *        true if that addon should be included in the selected array
+   *
+   * @returns {Array<DBAddonInternal>}
+   *        A Promise that resolves to the list of add-ons matching
+   *        aFilter or an empty array if none match
    */
   async getAddonList(aFilter) {
     try {
       let addonDB = await this.asyncLoadDB();
       let addonList = _filterDB(addonDB, aFilter);
       let addons = await Promise.all(addonList.map(addon => getRepositoryAddon(addon)));
       return addons;
     } catch (error) {
       logger.error("getAddonList failed", error);
       return [];
     }
   },
 
   /**
-   * (Possibly asynchronously) get the first addon that matches the filter function
-   * @param  aFilter
-   *         Function that takes an addon instance and returns
-   *         true if that addon should be selected
+   * Get the first addon that matches the filter function
+   *
+   * @param {function(DBAddonInternal) : boolean} aFilter
+   *        Function that takes an addon instance and returns
+   *        true if that addon should be selected
+   * @returns {Promise<DBAddonInternal?>}
    */
   getAddon(aFilter) {
     return this.asyncLoadDB()
       .then(addonDB => getRepositoryAddon(_findAddon(addonDB, aFilter)))
       .catch(
         error => {
           logger.error("getAddon failed", error);
         });
@@ -638,68 +1791,72 @@ this.XPIDatabase = {
   syncGetAddon(aFilter) {
     return _findAddon(this.addonDB, aFilter);
   },
 
   /**
    * Asynchronously gets an add-on with a particular ID in a particular
    * install location.
    *
-   * @param  aId
-   *         The ID of the add-on to retrieve
-   * @param  aLocation
-   *         The name of the install location
+   * @param {string} aId
+   *        The ID of the add-on to retrieve
+   * @param {string} aLocation
+   *        The name of the install location
+   * @returns {Promise<DBAddonInternal?>}
    */
   getAddonInLocation(aId, aLocation) {
     return this.asyncLoadDB().then(
         addonDB => getRepositoryAddon(addonDB.get(aLocation + ":" + aId)));
   },
 
   /**
    * Asynchronously get all the add-ons in a particular install location.
    *
-   * @param  aLocation
-   *         The name of the install location
+   * @param {string} aLocation
+   *        The name of the install location
+   * @returns {Promise<Array<DBAddonInternal>>}
    */
   getAddonsInLocation(aLocation) {
     return this.getAddonList(aAddon => aAddon._installLocation.name == aLocation);
   },
 
   /**
    * Asynchronously gets the add-on with the specified ID that is visible.
    *
-   * @param  aId
-   *         The ID of the add-on to retrieve
+   * @param {string} aId
+   *        The ID of the add-on to retrieve
+   * @returns {Promise<DBAddonInternal?>}
    */
   getVisibleAddonForID(aId) {
     return this.getAddon(aAddon => ((aAddon.id == aId) && aAddon.visible));
   },
 
   syncGetVisibleAddonForID(aId) {
     return this.syncGetAddon(aAddon => ((aAddon.id == aId) && aAddon.visible));
   },
 
   /**
    * Asynchronously gets the visible add-ons, optionally restricting by type.
    *
-   * @param  aTypes
-   *         An array of types to include or null to include all types
+   * @param {Array<string>?} aTypes
+   *        An array of types to include or null to include all types
+   * @returns {Promise<Array<DBAddonInternal>>}
    */
   getVisibleAddons(aTypes) {
     return this.getAddonList(aAddon => (aAddon.visible &&
                                         (!aTypes || (aTypes.length == 0) ||
                                          (aTypes.indexOf(aAddon.type) > -1))));
   },
 
   /**
    * Synchronously gets all add-ons of a particular type(s).
    *
-   * @param  aType, aType2, ...
-   *         The type(s) of add-on to retrieve
-   * @return an array of DBAddonInternals
+   * @param {Array<string>} aTypes
+   *        The type(s) of add-on to retrieve
+   * @returns {Array<DBAddonInternal>}
    */
   getAddonsByType(...aTypes) {
     if (!this.addonDB) {
       // jank-tastic! Must synchronously load DB if the theme switches from
       // an XPI theme to a lightweight theme before the DB has loaded,
       // because we're called from sync XPIProvider.addonChanged
       logger.warn(`Synchronous load of XPI database due to ` +
                   `getAddonsByType([${aTypes.join(", ")}]) ` +
@@ -707,83 +1864,175 @@ this.XPIDatabase = {
       AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_byType", XPIProvider.runPhase);
       this.syncLoadDB(true);
     }
 
     return _filterDB(this.addonDB, aAddon => aTypes.includes(aAddon.type));
   },
 
   /**
-   * Synchronously gets an add-on with a particular internalName.
-   *
-   * @param  aInternalName
-   *         The internalName of the add-on to retrieve
-   * @return a DBAddonInternal
-   */
-  getVisibleAddonForInternalName(aInternalName) {
-    if (!this.addonDB) {
-      // This may be called when the DB hasn't otherwise been loaded
-      logger.warn(`Synchronous load of XPI database due to ` +
-                  `getVisibleAddonForInternalName. Stack: ${Error().stack}`);
-      AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_forInternalName",
-          XPIProvider.runPhase);
-      this.syncLoadDB(true);
-    }
-
-    return _findAddon(this.addonDB,
-                      aAddon => aAddon.visible &&
-                                (aAddon.internalName == aInternalName));
-  },
-
-  /**
    * Asynchronously gets all add-ons with pending operations.
    *
-   * @param  aTypes
-   *         The types of add-ons to retrieve or null to get all types
+   * @param {Array<string>?} aTypes
+   *        The types of add-ons to retrieve or null to get all types
+   * @returns {Promise<Array<DBAddonInternal>>}
    */
   getVisibleAddonsWithPendingOperations(aTypes) {
     return this.getAddonList(
         aAddon => (aAddon.visible &&
                    aAddon.pendingUninstall &&
                    (!aTypes || (aTypes.length == 0) || (aTypes.indexOf(aAddon.type) > -1))));
   },
 
   /**
    * Asynchronously get an add-on by its Sync GUID.
    *
-   * @param  aGUID
-   *         Sync GUID of add-on to fetch
+   * @param {string} aGUID
+   *        Sync GUID of add-on to fetch
+   * @returns {Promise<DBAddonInternal?>}
    */
   getAddonBySyncGUID(aGUID) {
     return this.getAddon(aAddon => aAddon.syncGUID == aGUID);
   },
 
   /**
    * Synchronously gets all add-ons in the database.
    * This is only called from the preference observer for the default
    * compatibility version preference, so we can return an empty list if
    * we haven't loaded the database yet.
    *
-   * @return  an array of DBAddonInternals
+   * @returns {Array<DBAddonInternal>}
    */
   getAddons() {
     if (!this.addonDB) {
       return [];
     }
     return _filterDB(this.addonDB, aAddon => true);
   },
 
+
+  /**
+   * Returns true if signing is required for the given add-on type.
+   *
+   * @param {string} aType
+   *        The add-on type to check.
+   * @returns {boolean}
+   */
+  mustSign(aType) {
+    if (!SIGNED_TYPES.has(aType))
+      return false;
+
+    if (aType == "webextension-langpack") {
+      return AddonSettings.LANGPACKS_REQUIRE_SIGNING;
+    }
+
+    return AddonSettings.REQUIRE_SIGNING;
+  },
+
+  /**
+   * Determine if this addon should be disabled due to being legacy
+   *
+   * @param {Addon} addon The addon to check
+   *
+   * @returns {boolean} Whether the addon should be disabled for being legacy
+   */
+  isDisabledLegacy(addon) {
+    return (!AddonSettings.ALLOW_LEGACY_EXTENSIONS &&
+            LEGACY_TYPES.has(addon.type) &&
+
+            // Legacy add-ons are allowed in the system location.
+            !addon._installLocation.isSystem &&
+
+            // Legacy extensions may be installed temporarily in
+            // non-release builds.
+            !(AppConstants.MOZ_ALLOW_LEGACY_EXTENSIONS &&
+              addon._installLocation.name == KEY_APP_TEMPORARY) &&
+
+            // Properly signed legacy extensions are allowed.
+            addon.signedState !== AddonManager.SIGNEDSTATE_PRIVILEGED);
+  },
+
+  /**
+   * Calculates whether an add-on should be appDisabled or not.
+   *
+   * @param {AddonInternal} aAddon
+   *        The add-on to check
+   * @returns {boolean}
+   *        True if the add-on should not be appDisabled
+   */
+  isUsableAddon(aAddon) {
+    if (this.mustSign(aAddon.type) && !aAddon.isCorrectlySigned) {
+      logger.warn(`Add-on ${aAddon.id} is not correctly signed.`);
+      if (Services.prefs.getBoolPref(PREF_XPI_SIGNATURES_DEV_ROOT, false)) {
+        logger.warn(`Preference ${PREF_XPI_SIGNATURES_DEV_ROOT} is set.`);
+      }
+      return false;
+    }
+
+    if (aAddon.blocklistState == nsIBlocklistService.STATE_BLOCKED) {
+      logger.warn(`Add-on ${aAddon.id} is blocklisted.`);
+      return false;
+    }
+
+    // If we can't read it, it's not usable:
+    if (aAddon.brokenManifest) {
+      return false;
+    }
+
+    if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely) {
+      logger.warn(`Updates for add-on ${aAddon.id} must be provided over HTTPS.`);
+      return false;
+    }
+
+
+    if (!aAddon.isPlatformCompatible) {
+      logger.warn(`Add-on ${aAddon.id} is not compatible with platform.`);
+      return false;
+    }
+
+    if (aAddon.dependencies.length) {
+      let isActive = id => {
+        let active = XPIProvider.activeAddons.get(id);
+        return active && !active.disable;
+      };
+
+      if (aAddon.dependencies.some(id => !isActive(id)))
+        return false;
+    }
+
+    if (this.isDisabledLegacy(aAddon)) {
+      logger.warn(`disabling legacy extension ${aAddon.id}`);
+      return false;
+    }
+
+    if (AddonManager.checkCompatibility) {
+      if (!aAddon.isCompatible) {
+        logger.warn(`Add-on ${aAddon.id} is not compatible with application version.`);
+        return false;
+      }
+    } else {
+      let app = aAddon.matchingTargetApplication;
+      if (!app) {
+        logger.warn(`Add-on ${aAddon.id} is not compatible with target application.`);
+        return false;
+      }
+    }
+
+    return true;
+  },
+
   /**
    * Synchronously adds an AddonInternal's metadata to the database.
    *
-   * @param  aAddon
-   *         AddonInternal to add
-   * @param  aPath
-   *         The file path of the add-on
-   * @return The DBAddonInternal that was added to the database
+   * @param {AddonInternal} aAddon
+   *        AddonInternal to add
+   * @param {string} aPath
+   *        The file path of the add-on
+   * @returns {DBAddonInternal}
+   *        the DBAddonInternal that was added to the database
    */
   addAddonMetadata(aAddon, aPath) {
     if (!this.addonDB) {
       AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_addMetadata",
           XPIProvider.runPhase);
       this.syncLoadDB(false);
     }
 
@@ -797,23 +2046,24 @@ this.XPIDatabase = {
     this.saveChanges();
     return newAddon;
   },
 
   /**
    * Synchronously updates an add-on's metadata in the database. Currently just
    * removes and recreates.
    *
-   * @param  aOldAddon
-   *         The DBAddonInternal to be replaced
-   * @param  aNewAddon
-   *         The new AddonInternal to add
-   * @param  aPath
-   *         The file path of the add-on
-   * @return The DBAddonInternal that was added to the database
+   * @param {DBAddonInternal} aOldAddon
+   *        The DBAddonInternal to be replaced
+   * @param {AddonInternal} aNewAddon
+   *        The new AddonInternal to add
+   * @param {string} aPath
+   *        The file path of the add-on
+   * @returns {DBAddonInternal}
+   *        The DBAddonInternal that was added to the database
    */
   updateAddonMetadata(aOldAddon, aNewAddon, aPath) {
     this.removeAddonMetadata(aOldAddon);
     aNewAddon.syncGUID = aOldAddon.syncGUID;
     aNewAddon.installDate = aOldAddon.installDate;
     aNewAddon.applyBackgroundUpdates = aOldAddon.applyBackgroundUpdates;
     aNewAddon.foreignInstall = aOldAddon.foreignInstall;
     aNewAddon.seen = aOldAddon.seen;
@@ -821,18 +2071,18 @@ this.XPIDatabase = {
 
     // addAddonMetadata does a saveChanges()
     return this.addAddonMetadata(aNewAddon, aPath);
   },
 
   /**
    * Synchronously removes an add-on from the database.
    *
-   * @param  aAddon
-   *         The DBAddonInternal being removed
+   * @param {DBAddonInternal} aAddon
+   *        The DBAddonInternal being removed
    */
   removeAddonMetadata(aAddon) {
     this.addonDB.delete(aAddon._key);
     this.saveChanges();
   },
 
   updateXPIStates(addon) {
     let xpiState = XPIStates.getAddon(addon.location, addon.id);
@@ -841,18 +2091,18 @@ this.XPIDatabase = {
       XPIStates.save();
     }
   },
 
   /**
    * Synchronously marks a DBAddonInternal as visible marking all other
    * instances with the same ID as not visible.
    *
-   * @param  aAddon
-   *         The DBAddonInternal to make visible
+   * @param {DBAddonInternal} aAddon
+   *        The DBAddonInternal to make visible
    */
   makeAddonVisible(aAddon) {
     logger.debug("Make addon " + aAddon._key + " visible");
     for (let [, otherAddon] of this.addonDB) {
       if ((otherAddon.id == aAddon.id) && (otherAddon._key != aAddon._key)) {
         logger.debug("Hide addon " + otherAddon._key);
         otherAddon.visible = false;
         otherAddon.active = false;
@@ -864,18 +2114,22 @@ this.XPIDatabase = {
     this.updateXPIStates(aAddon);
     this.saveChanges();
   },
 
   /**
    * Synchronously marks a given add-on ID visible in a given location,
    * instances with the same ID as not visible.
    *
-   * @param  aAddon
-   *         The DBAddonInternal to make visible
+   * @param {string} aId
+   *        The ID of the add-on to make visible
+   * @param {InstallLocation} aLocation
+   *        The location in which to make the add-on visible.
+   * @returns {DBAddonInternal?}
+   *        The add-on instance which was marked visible, if any.
    */
   makeAddonLocationVisible(aId, aLocation) {
     logger.debug(`Make addon ${aId} visible in location ${aLocation}`);
     let result;
     for (let [, addon] of this.addonDB) {
       if (addon.id != aId) {
         continue;
       }
@@ -894,36 +2148,36 @@ this.XPIDatabase = {
     }
     this.saveChanges();
     return result;
   },
 
   /**
    * Synchronously sets properties for an add-on.
    *
-   * @param  aAddon
-   *         The DBAddonInternal being updated
-   * @param  aProperties
-   *         A dictionary of properties to set
+   * @param {DBAddonInternal} aAddon
+   *        The DBAddonInternal being updated
+   * @param {Object} aProperties
+   *        A dictionary of properties to set
    */
   setAddonProperties(aAddon, aProperties) {
     for (let key in aProperties) {
       aAddon[key] = aProperties[key];
     }
     this.saveChanges();
   },
 
   /**
    * Synchronously sets the Sync GUID for an add-on.
    * Only called when the database is already loaded.
    *
-   * @param  aAddon
-   *         The DBAddonInternal being updated
-   * @param  aGUID
-   *         GUID string to set the value to
+   * @param {DBAddonInternal} aAddon
+   *        The DBAddonInternal being updated
+   * @param {string} aGUID
+   *        GUID string to set the value to
    * @throws if another addon already has the specified GUID
    */
   setAddonSyncGUID(aAddon, aGUID) {
     // Need to make sure no other addon has this GUID
     function excludeSyncGUID(otherAddon) {
       return (otherAddon._key != aAddon._key) && (otherAddon.syncGUID == aGUID);
     }
     let otherAddon = _findAddon(this.addonDB, excludeSyncGUID);
@@ -933,18 +2187,20 @@ this.XPIDatabase = {
     }
     aAddon.syncGUID = aGUID;
     this.saveChanges();
   },
 
   /**
    * Synchronously updates an add-on's active flag in the database.
    *
-   * @param  aAddon
-   *         The DBAddonInternal to update
+   * @param {DBAddonInternal} aAddon
+   *        The DBAddonInternal to update
+   * @param {boolean} aActive
+   *        The new active state for the add-on.
    */
   updateAddonActive(aAddon, aActive) {
     logger.debug("Updating active state for add-on " + aAddon.id + " to " + aActive);
 
     aAddon.active = aActive;
     this.saveChanges();
   },
 
@@ -963,22 +2219,182 @@ this.XPIDatabase = {
     for (let [, addon] of this.addonDB) {
       let newActive = (addon.visible && !addon.disabled && !addon.pendingUninstall);
       if (newActive != addon.active) {
         addon.active = newActive;
         this.saveChanges();
       }
     }
   },
+
+  /**
+   * Updates the disabled state for an add-on. Its appDisabled property will be
+   * calculated and if the add-on is changed the database will be saved and
+   * appropriate notifications will be sent out to the registered AddonListeners.
+   *
+   * @param {DBAddonInternal} aAddon
+   *        The DBAddonInternal to update
+   * @param {boolean?} [aUserDisabled]
+   *        Value for the userDisabled property. If undefined the value will
+   *        not change
+   * @param {boolean?} [aSoftDisabled]
+   *        Value for the softDisabled property. If undefined the value will
+   *        not change. If true this will force userDisabled to be true
+   * @param {boolean?} [aBecauseSelecting]
+   *        True if we're disabling this add-on because we're selecting
+   *        another.
+   * @returns {boolean?}
+   *       A tri-state indicating the action taken for the add-on:
+   *           - undefined: The add-on did not change state
+   *           - true: The add-on because disabled
+   *           - false: The add-on became enabled
+   * @throws if addon is not a DBAddonInternal
+   */
+  updateAddonDisabledState(aAddon, aUserDisabled, aSoftDisabled, aBecauseSelecting) {
+    if (!(aAddon.inDatabase))
+      throw new Error("Can only update addon states for installed addons.");
+    if (aUserDisabled !== undefined && aSoftDisabled !== undefined) {
+      throw new Error("Cannot change userDisabled and softDisabled at the " +
+                      "same time");
+    }
+
+    if (aUserDisabled === undefined) {
+      aUserDisabled = aAddon.userDisabled;
+    } else if (!aUserDisabled) {
+      // If enabling the add-on then remove softDisabled
+      aSoftDisabled = false;
+    }
+
+    // If not changing softDisabled or the add-on is already userDisabled then
+    // use the existing value for softDisabled
+    if (aSoftDisabled === undefined || aUserDisabled)
+      aSoftDisabled = aAddon.softDisabled;
+
+    let appDisabled = !this.isUsableAddon(aAddon);
+    // No change means nothing to do here
+    if (aAddon.userDisabled == aUserDisabled &&
+        aAddon.appDisabled == appDisabled &&
+        aAddon.softDisabled == aSoftDisabled)
+      return undefined;
+
+    let wasDisabled = aAddon.disabled;
+    let isDisabled = aUserDisabled || aSoftDisabled || appDisabled;
+
+    // If appDisabled changes but addon.disabled doesn't,
+    // no onDisabling/onEnabling is sent - so send a onPropertyChanged.
+    let appDisabledChanged = aAddon.appDisabled != appDisabled;
+
+    // Update the properties in the database.
+    this.setAddonProperties(aAddon, {
+      userDisabled: aUserDisabled,
+      appDisabled,
+      softDisabled: aSoftDisabled
+    });
+
+    let wrapper = aAddon.wrapper;
+
+    if (appDisabledChanged) {
+      AddonManagerPrivate.callAddonListeners("onPropertyChanged",
+                                             wrapper,
+                                             ["appDisabled"]);
+    }
+
+    // If the add-on is not visible or the add-on is not changing state then
+    // there is no need to do anything else
+    if (!aAddon.visible || (wasDisabled == isDisabled))
+      return undefined;
+
+    // Flag that active states in the database need to be updated on shutdown
+    Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
+
+    // Sync with XPIStates.
+    let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
+    if (xpiState) {
+      xpiState.syncWithDB(aAddon);
+      XPIStates.save();
+    } else {
+      // There should always be an xpiState
+      logger.warn("No XPIState for ${id} in ${location}", aAddon);
+    }
+
+    // Have we just gone back to the current state?
+    if (isDisabled != aAddon.active) {
+      AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
+    } else {
+      if (isDisabled) {
+        AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false);
+      } else {
+        AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, false);
+      }
+
+      this.updateAddonActive(aAddon, !isDisabled);
+
+      if (isDisabled) {
+        if (aAddon.bootstrap && XPIProvider.activeAddons.has(aAddon.id)) {
+          XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
+                                          BOOTSTRAP_REASONS.ADDON_DISABLE);
+          XPIProvider.unloadBootstrapScope(aAddon.id);
+        }
+        AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
+      } else {
+        if (aAddon.bootstrap) {
+          XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup",
+                                          BOOTSTRAP_REASONS.ADDON_ENABLE);
+        }
+        AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
+      }
+    }
+
+    // Notify any other providers that a new theme has been enabled
+    if (isTheme(aAddon.type)) {
+      if (!isDisabled) {
+        AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type);
+
+        if (xpiState) {
+          xpiState.syncWithDB(aAddon);
+          XPIStates.save();
+        }
+      } else if (isDisabled && !aBecauseSelecting) {
+        AddonManagerPrivate.notifyAddonChanged(null, "theme");
+      }
+    }
+
+    return isDisabled;
+  },
+
+  /**
+   * Record a bit of per-addon telemetry.
+   *
+   * Yes, this description is extremely helpful. How dare you question its
+   * utility?
+   *
+   * @param {AddonInternal} aAddon
+   *        The addon to record
+   */
+  recordAddonTelemetry(aAddon) {
+    let locale = aAddon.defaultLocale;
+    if (locale) {
+      if (locale.name)
+        XPIProvider.setTelemetry(aAddon.id, "name", locale.name);
+      if (locale.creator)
+        XPIProvider.setTelemetry(aAddon.id, "creator", locale.creator);
+    }
+  },
 };
 
 this.XPIDatabaseReconcile = {
   /**
    * Returns a map of ID -> add-on. When the same add-on ID exists in multiple
    * install locations the highest priority location is chosen.
+   *
+   * @param {Map<String, AddonInternal>} addonMap
+   *        The add-on map to flatten.
+   * @param {string?} [hideLocation]
+   *        An optional location from which to hide any add-ons.
+   * @returns {Map<string, AddonInternal>}
    */
   flattenByID(addonMap, hideLocation) {
     let map = new Map();
 
     for (let installLocation of XPIProvider.installLocations) {
       if (installLocation.name == hideLocation)
         continue;
 
@@ -992,16 +2408,20 @@ this.XPIDatabaseReconcile = {
       }
     }
 
     return map;
   },
 
   /**
    * Finds the visible add-ons from the map.
+   *
+   * @param {Map<String, AddonInternal>} addonMap
+   *        The add-on map to filter.
+   * @returns {Map<string, AddonInternal>}
    */
   getVisibleAddons(addonMap) {
     let map = new Map();
 
     for (let addons of addonMap.values()) {
       for (let [id, addon] of addons) {
         if (!addon.visible)
           continue;
@@ -1021,32 +2441,33 @@ this.XPIDatabaseReconcile = {
   /**
    * Called to add the metadata for an add-on in one of the install locations
    * to the database. This can be called in three different cases. Either an
    * add-on has been dropped into the location from outside of Firefox, or
    * an add-on has been installed through the application, or the database
    * has been upgraded or become corrupt and add-on data has to be reloaded
    * into it.
    *
-   * @param  aInstallLocation
-   *         The install location containing the add-on
-   * @param  aId
-   *         The ID of the add-on
-   * @param  aAddonState
-   *         The new state of the add-on
-   * @param  aNewAddon
-   *         The manifest for the new add-on if it has already been loaded
-   * @param  aOldAppVersion
-   *         The version of the application last run with this profile or null
-   *         if it is a new profile or the version is unknown
-   * @param  aOldPlatformVersion
-   *         The version of the platform last run with this profile or null
-   *         if it is a new profile or the version is unknown
-   * @return a boolean indicating if flushing caches is required to complete
-   *         changing this add-on
+   * @param {InstallLocation} aInstallLocation
+   *        The install location containing the add-on
+   * @param {string} aId
+   *        The ID of the add-on
+   * @param {XPIState} aAddonState
+   *        The new state of the add-on
+   * @param {AddonInternal?} [aNewAddon]
+   *        The manifest for the new add-on if it has already been loaded
+   * @param {string?} [aOldAppVersion]
+   *        The version of the application last run with this profile or null
+   *        if it is a new profile or the version is unknown
+   * @param {string?} [aOldPlatformVersion]
+   *        The version of the platform last run with this profile or null
+   *        if it is a new profile or the version is unknown
+   * @returns {boolean}
+   *        A boolean indicating if flushing caches is required to complete
+   *        changing this add-on
    */
   addMetadata(aInstallLocation, aId, aAddonState, aNewAddon, aOldAppVersion,
               aOldPlatformVersion) {
     logger.debug("New add-on " + aId + " installed in " + aInstallLocation.name);
 
     // If we had staged data for this add-on or we aren't recovering from a
     // corrupt database and we don't have migration data for this add-on then
     // this must be a new install.
@@ -1056,17 +2477,17 @@ this.XPIDatabaseReconcile = {
     // must be something dropped directly into the install location
     let isDetectedInstall = isNewInstall && !aNewAddon;
 
     // Load the manifest if necessary and sanity check the add-on ID
     try {
       if (!aNewAddon) {
         // Load the manifest from the add-on.
         let file = new nsIFile(aAddonState.path);
-        aNewAddon = syncLoadManifestFromFile(file, aInstallLocation);
+        aNewAddon = XPIInstall.syncLoadManifestFromFile(file, aInstallLocation);
       }
       // The add-on in the manifest should match the add-on ID.
       if (aNewAddon.id != aId) {
         throw new Error("Invalid addon ID: expected addon ID " + aId +
                         ", found " + aNewAddon.id + " in manifest");
       }
     } catch (e) {
       logger.warn("addMetadata: Add-on " + aId + " is invalid", e);
@@ -1088,21 +2509,17 @@ this.XPIDatabaseReconcile = {
 
     // Assume that add-ons in the system add-ons install location aren't
     // foreign and should default to enabled.
     aNewAddon.foreignInstall = isDetectedInstall &&
                                aInstallLocation.name != KEY_APP_SYSTEM_ADDONS &&
                                aInstallLocation.name != KEY_APP_SYSTEM_DEFAULTS;
 
     // appDisabled depends on whether the add-on is a foreignInstall so update
-    aNewAddon.appDisabled = !isUsableAddon(aNewAddon);
-
-    // The default theme is never a foreign install
-    if (aNewAddon.type == "theme" && aNewAddon.internalName == DEFAULT_SKIN)
-      aNewAddon.foreignInstall = false;
+    aNewAddon.appDisabled = !XPIDatabase.isUsableAddon(aNewAddon);
 
     if (isDetectedInstall && aNewAddon.foreignInstall) {
       // If the add-on is a foreign install and is in a scope where add-ons
       // that were dropped in should default to disabled then disable it
       let disablingScopes = Services.prefs.getIntPref(PREF_EM_AUTO_DISABLED_SCOPES, 0);
       if (aInstallLocation.scope & disablingScopes) {
         logger.warn("Disabling foreign installed add-on " + aNewAddon.id + " in "
             + aInstallLocation.name);
@@ -1112,52 +2529,51 @@ this.XPIDatabaseReconcile = {
     }
 
     return XPIDatabase.addAddonMetadata(aNewAddon, aAddonState.path);
   },
 
   /**
    * Called when an add-on has been removed.
    *
-   * @param  aOldAddon
-   *         The AddonInternal as it appeared the last time the application
-   *         ran
-   * @return a boolean indicating if flushing caches is required to complete
-   *         changing this add-on
+   * @param {AddonInternal} aOldAddon
+   *        The AddonInternal as it appeared the last time the application
+   *        ran
    */
   removeMetadata(aOldAddon) {
     // This add-on has disappeared
     logger.debug("Add-on " + aOldAddon.id + " removed from " + aOldAddon.location);
     XPIDatabase.removeAddonMetadata(aOldAddon);
   },
 
   /**
    * Updates an add-on's metadata and determines. This is called when either the
    * add-on's install directory path or last modified time has changed.
    *
-   * @param  aInstallLocation
-   *         The install location containing the add-on
-   * @param  aOldAddon
-   *         The AddonInternal as it appeared the last time the application
-   *         ran
-   * @param  aAddonState
-   *         The new state of the add-on
-   * @param  aNewAddon
-   *         The manifest for the new add-on if it has already been loaded
-   * @return a boolean indicating if flushing caches is required to complete
-   *         changing this add-on
+   * @param {InstallLocation} aInstallLocation
+   *        The install location containing the add-on
+   * @param {AddonInternal} aOldAddon
+   *        The AddonInternal as it appeared the last time the application
+   *        ran
+   * @param {XPIState} aAddonState
+   *        The new state of the add-on
+   * @param {AddonInternal?} [aNewAddon]
+   *        The manifest for the new add-on if it has already been loaded
+   * @returns {boolean?}
+   *        A boolean indicating if flushing caches is required to complete
+   *        changing this add-on
    */
   updateMetadata(aInstallLocation, aOldAddon, aAddonState, aNewAddon) {
     logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name);
 
     try {
       // If there isn't an updated install manifest for this add-on then load it.
       if (!aNewAddon) {
         let file = new nsIFile(aAddonState.path);
-        aNewAddon = syncLoadManifestFromFile(file, aInstallLocation, aOldAddon);
+        aNewAddon = XPIInstall.syncLoadManifestFromFile(file, aInstallLocation, aOldAddon);
       }
 
       // The ID in the manifest that was loaded must match the ID of the old
       // add-on.
       if (aNewAddon.id != aOldAddon.id)
         throw new Error("Incorrect id in install manifest for existing add-on " + aOldAddon.id);
     } catch (e) {
       logger.warn("updateMetadata: Add-on " + aOldAddon.id + " is invalid", e);
@@ -1177,61 +2593,62 @@ this.XPIDatabaseReconcile = {
     // Update the database
     return XPIDatabase.updateAddonMetadata(aOldAddon, aNewAddon, aAddonState.path);
   },
 
   /**
    * Updates an add-on's path for when the add-on has moved in the
    * filesystem but hasn't changed in any other way.
    *
-   * @param  aInstallLocation
-   *         The install location containing the add-on
-   * @param  aOldAddon
-   *         The AddonInternal as it appeared the last time the application
-   *         ran
-   * @param  aAddonState
-   *         The new state of the add-on
-   * @return a boolean indicating if flushing caches is required to complete
-   *         changing this add-on
+   * @param {InstallLocation} aInstallLocation
+   *        The install location containing the add-on
+   * @param {AddonInternal} aOldAddon
+   *        The AddonInternal as it appeared the last time the application
+   *        ran
+   * @param {XPIState} aAddonState
+   *        The new state of the add-on
+   * @returns {AddonInternal}
    */
   updatePath(aInstallLocation, aOldAddon, aAddonState) {
     logger.debug("Add-on " + aOldAddon.id + " moved to " + aAddonState.path);
     aOldAddon.path = aAddonState.path;
     aOldAddon._sourceBundle = new nsIFile(aAddonState.path);
 
     return aOldAddon;
   },
 
   /**
    * Called when no change has been detected for an add-on's metadata but the
    * application has changed so compatibility may have changed.
    *
-   * @param  aInstallLocation
-   *         The install location containing the add-on
-   * @param  aOldAddon
-   *         The AddonInternal as it appeared the last time the application
-   *         ran
-   * @param  aAddonState
-   *         The new state of the add-on
-   * @param  aReloadMetadata
-   *         A boolean which indicates whether metadata should be reloaded from
-   *         the addon manifests. Default to false.
-   * @return the new addon.
+   * @param {InstallLocation} aInstallLocation
+   *        The install location containing the add-on
+   * @param {AddonInternal} aOldAddon
+   *        The AddonInternal as it appeared the last time the application
+   *        ran
+   * @param {XPIState} aAddonState
+   *        The new state of the add-on
+   * @param {boolean} [aReloadMetadata = false]
+   *        A boolean which indicates whether metadata should be reloaded from
+   *        the addon manifests. Default to false.
+   * @returns {DBAddonInternal}
+   *        The new addon.
    */
   updateCompatibility(aInstallLocation, aOldAddon, aAddonState, aReloadMetadata) {
     logger.debug("Updating compatibility for add-on " + aOldAddon.id + " in " + aInstallLocation.name);
 
-    let checkSigning = aOldAddon.signedState === undefined && ADDON_SIGNING &&
-                       SIGNED_TYPES.has(aOldAddon.type);
+    let checkSigning = (aOldAddon.signedState === undefined &&
+                        AddonSettings.ADDON_SIGNING &&
+                        SIGNED_TYPES.has(aOldAddon.type));
 
     let manifest = null;
     if (checkSigning || aReloadMetadata) {
       try {
         let file = new nsIFile(aAddonState.path);
-        manifest = syncLoadManifestFromFile(file, aInstallLocation);
+        manifest = XPIInstall.syncLoadManifestFromFile(file, aInstallLocation);
       } catch (err) {
         // If we can no longer read the manifest, it is no longer compatible.
         aOldAddon.brokenManifest = true;
         aOldAddon.appDisabled = true;
         return aOldAddon;
       }
     }
 
@@ -1250,44 +2667,45 @@ this.XPIDatabaseReconcile = {
       let remove = ["syncGUID", "foreignInstall", "visible", "active",
                     "userDisabled", "applyBackgroundUpdates", "sourceURI",
                     "releaseNotesURI", "targetApplications"];
 
       let props = PROP_JSON_FIELDS.filter(a => !remove.includes(a));
       copyProperties(manifest, props, aOldAddon);
     }
 
-    aOldAddon.appDisabled = !isUsableAddon(aOldAddon);
+    aOldAddon.appDisabled = !XPIDatabase.isUsableAddon(aOldAddon);
 
     return aOldAddon;
   },
 
   /**
    * Compares the add-ons that are currently installed to those that were
    * known to be installed when the application last ran and applies any
    * changes found to the database. Also sends "startupcache-invalidate" signal to
    * observerservice if it detects that data may have changed.
-   * Always called after XPIProviderUtils.js and extensions.json have been loaded.
+   * Always called after XPIDatabase.jsm and extensions.json have been loaded.
    *
-   * @param  aManifests
-   *         A dictionary of cached AddonInstalls for add-ons that have been
-   *         installed
-   * @param  aUpdateCompatibility
-   *         true to update add-ons appDisabled property when the application
-   *         version has changed
-   * @param  aOldAppVersion
-   *         The version of the application last run with this profile or null
-   *         if it is a new profile or the version is unknown
-   * @param  aOldPlatformVersion
-   *         The version of the platform last run with this profile or null
-   *         if it is a new profile or the version is unknown
-   * @param  aSchemaChange
-   *         The schema has changed and all add-on manifests should be re-read.
-   * @return a boolean indicating if a change requiring flushing the caches was
-   *         detected
+   * @param {Object} aManifests
+   *        A dictionary of cached AddonInstalls for add-ons that have been
+   *        installed
+   * @param {boolean} aUpdateCompatibility
+   *        true to update add-ons appDisabled property when the application
+   *        version has changed
+   * @param {string?} [aOldAppVersion]
+   *        The version of the application last run with this profile or null
+   *        if it is a new profile or the version is unknown
+   * @param {string?} [aOldPlatformVersion]
+   *        The version of the platform last run with this profile or null
+   *        if it is a new profile or the version is unknown
+   * @param {boolean} aSchemaChange
+   *        The schema has changed and all add-on manifests should be re-read.
+   * @returns {boolean}
+   *        A boolean indicating if a change requiring flushing the caches was
+   *        detected
    */
   processFileChanges(aManifests, aUpdateCompatibility, aOldAppVersion, aOldPlatformVersion,
                      aSchemaChange) {
     let loadedManifest = (aInstallLocation, aId) => {
       if (!(aInstallLocation.name in aManifests))
         return null;
       if (!(aId in aManifests[aInstallLocation.name]))
         return null;
@@ -1337,17 +2755,17 @@ this.XPIDatabaseReconcile = {
       // ran
       let dbAddons = previousAddons.get(installLocation.name);
       if (dbAddons) {
         for (let [id, oldAddon] of dbAddons) {
           // Check if the add-on is still installed
           let xpiState = states && states.get(id);
           if (xpiState) {
             // Here the add-on was present in the database and on disk
-            recordAddonTelemetry(oldAddon);
+            XPIDatabase.recordAddonTelemetry(oldAddon);
 
             // Check if the add-on has been changed outside the XPI provider
             if (oldAddon.updateDate != xpiState.mtime) {
               // Did time change in the wrong direction?
               if (xpiState.mtime < oldAddon.updateDate) {
                 XPIProvider.setTelemetry(oldAddon.id, "olderFile", {
                   mtime: xpiState.mtime,
                   oldtime: oldAddon.updateDate
@@ -1455,21 +2873,17 @@ this.XPIDatabaseReconcile = {
       if (!previousAddon) {
         // If we had a manifest for this add-on it was a staged install and
         // so wasn't something recovered from a corrupt database
         let wasStaged = !!loadedManifest(currentAddon._installLocation, id);
 
         // We might be recovering from a corrupt database, if so use the
         // list of known active add-ons to update the new add-on
         if (!wasStaged && XPIDatabase.activeBundles) {
-          // For themes we know which is active by the current skin setting
-          if (currentAddon.type == "theme")
-            isActive = currentAddon.internalName == DEFAULT_SKIN;
-          else
-            isActive = XPIDatabase.activeBundles.includes(currentAddon.path);
+          isActive = XPIDatabase.activeBundles.includes(currentAddon.path);
 
           if (currentAddon.type == "webextension-theme")
             currentAddon.userDisabled = !isActive;
 
           // If the add-on wasn't active and it isn't already disabled in some way
           // then it was probably either softDisabled or userDisabled
           if (!isActive && !currentAddon.disabled) {
             // If the add-on is softblocked then assume it is softDisabled
@@ -1510,17 +2924,17 @@ this.XPIDatabaseReconcile = {
 
             XPIProvider.callBootstrapMethod(previousAddon, previousAddon._sourceBundle,
                                             "uninstall", installReason,
                                             { newVersion: currentAddon.version });
             XPIProvider.unloadBootstrapScope(previousAddon.id);
           }
 
           // Make sure to flush the cache when an old add-on has gone away
-          flushChromeCaches();
+          XPIInstall.flushChromeCaches();
 
           if (currentAddon.bootstrap) {
             // Visible bootstrapped add-ons need to have their install method called
             let file = currentAddon._sourceBundle.clone();
             XPIProvider.callBootstrapMethod(currentAddon, file,
                                             "install", installReason,
                                             { oldVersion: previousAddon.version });
             if (currentAddon.disabled)
@@ -1553,17 +2967,17 @@ this.XPIDatabaseReconcile = {
         XPIProvider.callBootstrapMethod(previousAddon, previousAddon._sourceBundle,
                                         "uninstall", BOOTSTRAP_REASONS.ADDON_UNINSTALL);
         XPIProvider.unloadBootstrapScope(previousAddon.id);
       }
       AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED, id);
       XPIStates.removeAddon(previousAddon.location, id);
 
       // Make sure to flush the cache when an old add-on has gone away
-      flushChromeCaches();
+      XPIInstall.flushChromeCaches();
     }
 
     // Make sure add-ons from hidden locations are marked invisible and inactive
     let locationAddonMap = currentAddons.get(hideLocation);
     if (locationAddonMap) {
       for (let addon of locationAddonMap.values()) {
         addon.visible = false;
         addon.active = false;
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -1,101 +1,111 @@
 /* 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 file contains most of the logic required to install extensions.
+ * In general, we try to avoid loading it until extension installation
+ * or update is required. Please keep that in mind when deciding whether
+ * to add code here or elsewhere.
+ */
+
+/* eslint "valid-jsdoc": [2, {requireReturn: false, requireReturnDescription: false, prefer: {return: "returns"}}] */
+
 var EXPORTED_SYMBOLS = [
-  "DownloadAddonInstall",
-  "LocalAddonInstall",
   "UpdateChecker",
   "XPIInstall",
-  "loadManifestFromFile",
   "verifyBundleSignedState",
 ];
 
 /* globals DownloadAddonInstall, LocalAddonInstall */
 
 Cu.importGlobalProperties(["TextDecoder", "TextEncoder", "fetch"]);
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
 
-ChromeUtils.defineModuleGetter(this, "AddonRepository",
-                               "resource://gre/modules/addons/AddonRepository.jsm");
-ChromeUtils.defineModuleGetter(this, "AddonSettings",
-                               "resource://gre/modules/addons/AddonSettings.jsm");
-ChromeUtils.defineModuleGetter(this, "AppConstants",
-                               "resource://gre/modules/AppConstants.jsm");
-ChromeUtils.defineModuleGetter(this, "CertUtils",
-                               "resource://gre/modules/CertUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "ExtensionData",
-                               "resource://gre/modules/Extension.jsm");
-ChromeUtils.defineModuleGetter(this, "FileUtils",
-                               "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm",
+  AddonSettings: "resource://gre/modules/addons/AddonSettings.jsm",
+  AppConstants: "resource://gre/modules/AppConstants.jsm",
+  CertUtils: "resource://gre/modules/CertUtils.jsm",
+  ExtensionData: "resource://gre/modules/Extension.jsm",
+  FileUtils: "resource://gre/modules/FileUtils.jsm",
+  NetUtil: "resource://gre/modules/NetUtil.jsm",
+  OS: "resource://gre/modules/osfile.jsm",
+  ProductAddonChecker: "resource://gre/modules/addons/ProductAddonChecker.jsm",
+  UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
+  ZipUtils: "resource://gre/modules/ZipUtils.jsm",
+
+  AddonInternal: "resource://gre/modules/addons/XPIDatabase.jsm",
+  XPIDatabase: "resource://gre/modules/addons/XPIDatabase.jsm",
+  XPIInternal: "resource://gre/modules/addons/XPIProvider.jsm",
+  XPIProvider: "resource://gre/modules/addons/XPIProvider.jsm",
+});
+
 XPCOMUtils.defineLazyGetter(this, "IconDetails", () => {
   return ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm", {}).ExtensionParent.IconDetails;
 });
-ChromeUtils.defineModuleGetter(this, "LightweightThemeManager",
-                               "resource://gre/modules/LightweightThemeManager.jsm");
-ChromeUtils.defineModuleGetter(this, "NetUtil",
-                               "resource://gre/modules/NetUtil.jsm");
-ChromeUtils.defineModuleGetter(this, "OS",
-                               "resource://gre/modules/osfile.jsm");
-ChromeUtils.defineModuleGetter(this, "ZipUtils",
-                               "resource://gre/modules/ZipUtils.jsm");
 
 const {nsIBlocklistService} = Ci;
 
 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
                                        "initWithPath");
+
+const BinaryOutputStream = Components.Constructor("@mozilla.org/binaryoutputstream;1",
+                                                  "nsIBinaryOutputStream", "setOutputStream");
 const CryptoHash = Components.Constructor("@mozilla.org/security/hash;1",
                                           "nsICryptoHash", "initWithString");
+const FileOutputStream = Components.Constructor("@mozilla.org/network/file-output-stream;1",
+                                                "nsIFileOutputStream", "init");
 const ZipReader = Components.Constructor("@mozilla.org/libjar/zip-reader;1",
                                          "nsIZipReader", "open");
 
 const RDFDataSource = Components.Constructor(
   "@mozilla.org/rdf/datasource;1?name=in-memory-datasource", "nsIRDFDataSource");
 const parseRDFString = Components.Constructor(
   "@mozilla.org/rdf/xml-parser;1", "nsIRDFXMLParser", "parseString");
 
 XPCOMUtils.defineLazyServiceGetters(this, {
   gCertDB: ["@mozilla.org/security/x509certdb;1", "nsIX509CertDB"],
   gRDF: ["@mozilla.org/rdf/rdf-service;1", "nsIRDFService"],
 });
 
-ChromeUtils.defineModuleGetter(this, "XPIInternal",
-                               "resource://gre/modules/addons/XPIProvider.jsm");
-ChromeUtils.defineModuleGetter(this, "XPIProvider",
-                               "resource://gre/modules/addons/XPIProvider.jsm");
 
 const PREF_ALLOW_NON_RESTARTLESS      = "extensions.legacy.non-restartless.enabled";
-
-const DEFAULT_SKIN = "classic/1.0";
-
-/* globals AddonInternal, BOOTSTRAP_REASONS, KEY_APP_SYSTEM_ADDONS, KEY_APP_SYSTEM_DEFAULTS, KEY_APP_TEMPORARY, TEMPORARY_ADDON_SUFFIX, SIGNED_TYPES, TOOLKIT_ID, XPIDatabase, XPIStates, getExternalType, isTheme, isUsableAddon, isWebExtension, mustSign, recordAddonTelemetry */
+const PREF_DISTRO_ADDONS_PERMS        = "extensions.distroAddons.promptForPermissions";
+const PREF_INSTALL_REQUIRESECUREORIGIN = "extensions.install.requireSecureOrigin";
+const PREF_PENDING_OPERATIONS         = "extensions.pendingOperations";
+const PREF_SYSTEM_ADDON_UPDATE_URL    = "extensions.systemAddon.update.url";
+const PREF_XPI_ENABLED                = "xpinstall.enabled";
+const PREF_XPI_DIRECT_WHITELISTED     = "xpinstall.whitelist.directRequest";
+const PREF_XPI_FILE_WHITELISTED       = "xpinstall.whitelist.fileRequest";
+const PREF_XPI_WHITELIST_REQUIRED     = "xpinstall.whitelist.required";
+
+/* globals BOOTSTRAP_REASONS, KEY_APP_SYSTEM_ADDONS, KEY_APP_SYSTEM_DEFAULTS, KEY_APP_TEMPORARY, PREF_BRANCH_INSTALLED_ADDON, PREF_SYSTEM_ADDON_SET, TEMPORARY_ADDON_SUFFIX, SIGNED_TYPES, TOOLKIT_ID, XPI_PERMISSION, XPIStates, getExternalType, isTheme, isWebExtension */
 const XPI_INTERNAL_SYMBOLS = [
-  "AddonInternal",
   "BOOTSTRAP_REASONS",
   "KEY_APP_SYSTEM_ADDONS",
   "KEY_APP_SYSTEM_DEFAULTS",
   "KEY_APP_TEMPORARY",
+  "PREF_BRANCH_INSTALLED_ADDON",
+  "PREF_SYSTEM_ADDON_SET",
   "SIGNED_TYPES",
   "TEMPORARY_ADDON_SUFFIX",
   "TOOLKIT_ID",
-  "XPIDatabase",
+  "XPI_PERMISSION",
   "XPIStates",
   "getExternalType",
   "isTheme",
-  "isUsableAddon",
   "isWebExtension",
-  "mustSign",
-  "recordAddonTelemetry",
 ];
 
 for (let name of XPI_INTERNAL_SYMBOLS) {
   XPCOMUtils.defineLazyGetter(this, name, () => XPIInternal[name]);
 }
 
 /**
  * Returns a nsIFile instance for the given path, relative to the given
@@ -124,53 +134,58 @@ function getFile(path, base = null) {
   let file = base.clone();
   file.appendRelativePath(path);
   return file;
 }
 
 /**
  * Sends local and remote notifications to flush a JAR file cache entry
  *
- * @param aJarFile
+ * @param {nsIFile} aJarFile
  *        The ZIP/XPI/JAR file as a nsIFile
  */
 function flushJarCache(aJarFile) {
   Services.obs.notifyObservers(aJarFile, "flush-cache-entry");
   Services.mm.broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path);
 }
 
 const PREF_EM_UPDATE_BACKGROUND_URL   = "extensions.update.background.url";
 const PREF_EM_UPDATE_URL              = "extensions.update.url";
 const PREF_XPI_SIGNATURES_DEV_ROOT    = "xpinstall.signatures.dev-root";
 const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
 const FILE_WEB_MANIFEST               = "manifest.json";
 
+const KEY_PROFILEDIR                  = "ProfD";
 const KEY_TEMPDIR                     = "TmpD";
 
+const KEY_APP_PROFILE                 = "app-profile";
+
+const DIR_STAGE                       = "staged";
+const DIR_TRASH                       = "trash";
+
 const RDFURI_INSTALL_MANIFEST_ROOT    = "urn:mozilla:install-manifest";
 const PREFIX_NS_EM                    = "http://www.mozilla.org/2004/em-rdf#";
 
 // Properties that exist in the install manifest
 const PROP_METADATA      = ["id", "version", "type", "internalName", "updateURL",
                             "optionsURL", "optionsType", "aboutURL",
                             "iconURL", "icon64URL"];
 const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
 const PROP_LOCALE_MULTI  = ["developers", "translators", "contributors"];
 const PROP_TARGETAPP     = ["id", "minVersion", "maxVersion"];
 
 // Map new string type identifiers to old style nsIUpdateItem types.
 // Retired values:
-// 8 = locale
 // 32 = multipackage xpi file
 // 8 = locale
 // 256 = apiextension
 // 128 = experiment
+// theme = 4
 const TYPES = {
   extension: 2,
-  theme: 4,
   dictionary: 64,
 };
 
 const COMPATIBLE_BY_DEFAULT_TYPES = {
   extension: true,
   dictionary: true,
 };
 
@@ -390,74 +405,93 @@ XPIPackage = class XPIPackage extends Pa
 
   flushCache() {
     flushJarCache(this.file);
     this.needFlush = false;
   }
 };
 
 /**
- * Sets permissions on a file
+ * Determine the reason to pass to an extension's bootstrap methods when
+ * switch between versions.
  *
- * @param  aFile
- *         The file or directory to operate on.
- * @param  aPermissions
- *         The permissions to set
+ * @param {string} oldVersion The version of the existing extension instance.
+ * @param {string} newVersion The version of the extension being installed.
+ *
+ * @returns {integer}
+ *        BOOSTRAP_REASONS.ADDON_UPGRADE or BOOSTRAP_REASONS.ADDON_DOWNGRADE
  */
-function setFilePermissions(aFile, aPermissions) {
-  try {
-    aFile.permissions = aPermissions;
-  } catch (e) {
-    logger.warn("Failed to set permissions " + aPermissions.toString(8) + " on " +
-         aFile.path, e);
-  }
+function newVersionReason(oldVersion, newVersion) {
+  return Services.vc.compare(oldVersion, newVersion) <= 0 ?
+         BOOTSTRAP_REASONS.ADDON_UPGRADE :
+         BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
+}
+
+// Behaves like Promise.all except waits for all promises to resolve/reject
+// before resolving/rejecting itself
+function waitForAllPromises(promises) {
+  return new Promise((resolve, reject) => {
+    let shouldReject = false;
+    let rejectValue = null;
+
+    let newPromises = promises.map(
+      p => p.catch(value => {
+        shouldReject = true;
+        rejectValue = value;
+      })
+    );
+    Promise.all(newPromises)
+           .then((results) => shouldReject ? reject(rejectValue) : resolve(results));
+  });
 }
 
 function EM_R(aProperty) {
   return gRDF.GetResource(PREFIX_NS_EM + aProperty);
 }
 
 /**
  * Converts an RDF literal, resource or integer into a string.
  *
- * @param  aLiteral
- *         The RDF object to convert
- * @return a string if the object could be converted or null
+ * @param {nsISupports} aLiteral
+ *        The RDF object to convert
+ * @returns {string?}
+ *        A string if the object could be converted or null
  */
 function getRDFValue(aLiteral) {
   if (aLiteral instanceof Ci.nsIRDFLiteral)
     return aLiteral.Value;
   if (aLiteral instanceof Ci.nsIRDFResource)
     return aLiteral.Value;
   if (aLiteral instanceof Ci.nsIRDFInt)
     return aLiteral.Value;
   return null;
 }
 
 /**
  * Gets an RDF property as a string
  *
- * @param  aDs
- *         The RDF datasource to read the property from
- * @param  aResource
- *         The RDF resource to read the property from
- * @param  aProperty
- *         The property to read
- * @return a string if the property existed or null
+ * @param {nsIRDFDataSource} aDs
+ *        The RDF datasource to read the property from
+ * @param {nsIRDFResource} aResource
+ *        The RDF resource to read the property from
+ * @param {string} aProperty
+ *        The property to read
+ * @returns {string?}
+ *        A string if the property existed or null
  */
 function getRDFProperty(aDs, aResource, aProperty) {
   return getRDFValue(aDs.GetTarget(aResource, EM_R(aProperty), true));
 }
 
 /**
  * Reads an AddonInternal object from a manifest stream.
  *
- * @param  aUri
- *         A |file:| or |jar:| URL for the manifest
- * @return an AddonInternal object
+ * @param {nsIURI} aUri
+ *        A |file:| or |jar:| URL for the manifest
+ * @returns {AddonInternal}
  * @throws if the install manifest in the stream is corrupt or could not
  *         be read
  */
 async function loadManifestFromWebManifest(aUri) {
   // We're passed the URI for the manifest file. Get the URI for its
   // parent directory.
   let uri = Services.io.newURI("./", null, aUri);
 
@@ -575,21 +609,21 @@ async function loadManifestFromWebManife
   addon.softDisabled = addon.blocklistState == nsIBlocklistService.STATE_SOFTBLOCKED;
 
   return addon;
 }
 
 /**
  * Reads an AddonInternal object from an RDF stream.
  *
- * @param  aUri
- *         The URI that the manifest is being read from
- * @param  aData
- *         The manifest text
- * @return an AddonInternal object
+ * @param {nsIURI} aUri
+ *        The URI that the manifest is being read from
+ * @param {string} aData
+ *        The manifest text
+ * @returns {AddonInternal}
  * @throws if the install manifest in the RDF stream is corrupt or could not
  *         be read
  */
 async function loadManifestFromRDF(aUri, aData) {
   function getPropertyArray(aDs, aSource, aProperty) {
     let values = [];
     let targets = aDs.GetTargets(aSource, EM_R(aProperty), true);
     while (targets.hasMoreElements())
@@ -597,28 +631,29 @@ async function loadManifestFromRDF(aUri,
 
     return values;
   }
 
   /**
    * Reads locale properties from either the main install manifest root or
    * an em:localized section in the install manifest.
    *
-   * @param  aDs
-   *         The nsIRDFDatasource to read from
-   * @param  aSource
-   *         The nsIRDFResource to read the properties from
-   * @param  isDefault
-   *         True if the locale is to be read from the main install manifest
-   *         root
-   * @param  aSeenLocales
-   *         An array of locale names already seen for this install manifest.
-   *         Any locale names seen as a part of this function will be added to
-   *         this array
-   * @return an object containing the locale properties
+   * @param {nsIRDFDataSource} aDs
+   *         The datasource to read from.
+   * @param {nsIRDFResource} aSource
+   *         The resource to read the properties from.
+   * @param {boolean} isDefault
+   *        True if the locale is to be read from the main install manifest
+   *        root
+   * @param {string[]} aSeenLocales
+   *        An array of locale names already seen for this install manifest.
+   *        Any locale names seen as a part of this function will be added to
+   *        this array
+   * @returns {Object}
+   *        an object containing the locale properties
    */
   function readLocale(aDs, aSource, isDefault, aSeenLocales) {
     let locale = { };
     if (!isDefault) {
       locale.locales = [];
       let targets = ds.GetTargets(aSource, EM_R("locale"), true);
       while (targets.hasMoreElements()) {
         let localeName = getRDFValue(targets.getNext());
@@ -796,26 +831,17 @@ async function loadManifestFromRDF(aUri,
       platform.abi = targetPlatform.substring(pos + 1);
     } else {
       platform.os = targetPlatform;
     }
 
     addon.targetPlatforms.push(platform);
   }
 
-  // A theme's userDisabled value is true if the theme is not the selected skin
-  // or if there is an active lightweight theme. We ignore whether softblocking
-  // is in effect since it would change the active theme.
-  if (isTheme(addon.type)) {
-    addon.userDisabled = !!LightweightThemeManager.currentTheme ||
-                         addon.internalName != DEFAULT_SKIN;
-  } else {
-    addon.userDisabled = false;
-  }
-
+  addon.userDisabled = false;
   addon.softDisabled = addon.blocklistState == nsIBlocklistService.STATE_SOFTBLOCKED;
   addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
 
   // icons will be filled by the calling function
   addon.icons = {};
   addon.userPermissions = null;
 
   return addon;
@@ -905,60 +931,97 @@ var loadManifest = async function(aPacka
       }
     }
     if (!addon.id && aInstallLocation.name == KEY_APP_TEMPORARY) {
       addon.id = generateTemporaryInstallID(aPackage.file);
     }
   }
 
   await addon.updateBlocklistState({oldAddon: aOldAddon});
-  addon.appDisabled = !isUsableAddon(addon);
+  addon.appDisabled = !XPIDatabase.isUsableAddon(addon);
 
   defineSyncGUID(addon);
 
   return addon;
 };
 
+/**
+ * Loads an add-on's manifest from the given file or directory.
+ *
+ * @param {nsIFile} aFile
+ *        The file to load the manifest from.
+ * @param {InstallLocation} aInstallLocation
+ *        The install location the add-on is installed in, or will be
+ *        installed to.
+ * @param {AddonInternal?} aOldAddon
+ *        The currently-installed add-on with the same ID, if one exist.
+ *        This is used to migrate user settings like the add-on's
+ *        disabled state.
+ * @returns {AddonInternal}
+ *        The parsed Addon object for the file's manifest.
+ */
 var loadManifestFromFile = async function(aFile, aInstallLocation, aOldAddon) {
   let pkg = Package.get(aFile);
   try {
     let addon = await loadManifest(pkg, aInstallLocation, aOldAddon);
     return addon;
   } finally {
     pkg.close();
   }
 };
 
+/*
+ * A synchronous method for loading an add-on's manifest. Do not use
+ * this.
+ */
+function syncLoadManifestFromFile(aFile, aInstallLocation, aOldAddon) {
+  return XPIInternal.awaitPromise(loadManifestFromFile(aFile, aInstallLocation, aOldAddon));
+}
+
 function flushChromeCaches() {
   // Init this, so it will get the notification.
   Services.obs.notifyObservers(null, "startupcache-invalidate");
   // Flush message manager cached scripts
   Services.obs.notifyObservers(null, "message-manager-flush-caches");
   // Also dispatch this event to child processes
   Services.mm.broadcastAsyncMessage(MSG_MESSAGE_MANAGER_CACHES_FLUSH, null);
 }
 
 /**
  * Creates and returns a new unique temporary file. The caller should delete
  * the file when it is no longer needed.
  *
- * @return an nsIFile that points to a randomly named, initially empty file in
- *         the OS temporary files directory
+ * @returns {nsIFile}
+ *       An nsIFile that points to a randomly named, initially empty file in
+ *       the OS temporary files directory
  */
 function getTemporaryFile() {
   let file = FileUtils.getDir(KEY_TEMPDIR, []);
   let random = Math.round(Math.random() * 36 ** 3).toString(36);
   file.append("tmp-" + random + ".xpi");
   file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
   return file;
 }
 
 /**
  * Returns the signedState for a given return code and certificate by verifying
  * it against the expected ID.
+ *
+ * @param {nsresult} aRv
+ *        The result code returned by the signature checker for the
+ *        signature check operation.
+ * @param {nsIX509Cert?} aCert
+ *        The certificate the add-on was signed with, if a valid
+ *        certificate exists.
+ * @param {string?} aAddonID
+ *        The expected ID of the add-on. If passed, this must match the
+ *        ID in the certificate's CN field.
+ * @returns {number}
+ *        A SIGNEDSTATE result code constant, as defined on the
+ *        AddonManager class.
  */
 function getSignedStatus(aRv, aCert, aAddonID) {
   let expectedCommonName = aAddonID;
   if (aAddonID && aAddonID.length > 64) {
     let data = new Uint8Array(new TextEncoder().encode(aAddonID));
 
     let crypto = CryptoHash("sha256");
     crypto.update(data, data.length);
@@ -1010,46 +1073,48 @@ function shouldVerifySignedState(aAddon)
   // of the signed types.
   return AddonSettings.ADDON_SIGNING && SIGNED_TYPES.has(aAddon.type);
 }
 
 /**
  * Verifies that a bundle's contents are all correctly signed by an
  * AMO-issued certificate
  *
- * @param  aBundle
- *         the nsIFile for the bundle to check, either a directory or zip file
- * @param  aAddon
- *         the add-on object to verify
- * @return a Promise that resolves to an AddonManager.SIGNEDSTATE_* constant.
+ * @param {nsIFile}aBundle
+ *        The nsIFile for the bundle to check, either a directory or zip file.
+ * @param {AddonInternal} aAddon
+ *        The add-on object to verify.
+ * @returns {Prommise<number>}
+ *        A Promise that resolves to an AddonManager.SIGNEDSTATE_* constant.
  */
 var verifyBundleSignedState = async function(aBundle, aAddon) {
   let pkg = Package.get(aBundle);
   try {
     let {signedState} = await pkg.verifySignedState(aAddon);
     return signedState;
   } finally {
     pkg.close();
   }
 };
 
 /**
  * Replaces %...% strings in an addon url (update and updateInfo) with
  * appropriate values.
  *
- * @param  aAddon
- *         The AddonInternal representing the add-on
- * @param  aUri
- *         The uri to escape
- * @param  aUpdateType
- *         An optional number representing the type of update, only applicable
- *         when creating a url for retrieving an update manifest
- * @param  aAppVersion
- *         The optional application version to use for %APP_VERSION%
- * @return the appropriately escaped uri.
+ * @param {AddonInternal} aAddon
+ *        The AddonInternal representing the add-on
+ * @param {string} aUri
+ *        The URI to escape
+ * @param {integer?} aUpdateType
+ *        An optional number representing the type of update, only applicable
+ *        when creating a url for retrieving an update manifest
+ * @param {string?} aAppVersion
+ *        The optional application version to use for %APP_VERSION%
+ * @returns {string}
+ *       The appropriately escaped URI.
  */
 function escapeAddonURI(aAddon, aUri, aUpdateType, aAppVersion) {
   let uri = AddonManager.escapeAddonURI(aAddon, aUri, aAppVersion);
 
   // If there is an updateType then replace the UPDATE_TYPE string
   if (aUpdateType)
     uri = uri.replace(/%UPDATE_TYPE%/g, aUpdateType);
 
@@ -1068,16 +1133,28 @@ function escapeAddonURI(aAddon, aUri, aU
     compatMode = "ignore";
   else if (AddonManager.strictCompatibility)
     compatMode = "strict";
   uri = uri.replace(/%COMPATIBILITY_MODE%/g, compatMode);
 
   return uri;
 }
 
+/**
+ * Converts an iterable of addon objects into a map with the add-on's ID as key.
+ *
+ * @param {sequence<AddonInternal>} addons
+ *        A sequence of AddonInternal objects.
+ *
+ * @returns {Map<string, AddonInternal>}
+ */
+function addonMap(addons) {
+  return new Map(addons.map(a => [a.id, a]));
+}
+
 async function removeAsync(aFile) {
   let info = null;
   try {
     info = await OS.File.stat(aFile.path);
     if (info.isDir)
       await OS.File.removeDir(aFile.path);
     else
       await OS.File.remove(aFile.path);
@@ -1086,18 +1163,18 @@ async function removeAsync(aFile) {
       throw e;
     // The file has already gone away
   }
 }
 
 /**
  * Recursively removes a directory or file fixing permissions when necessary.
  *
- * @param  aFile
- *         The nsIFile to remove
+ * @param {nsIFile} aFile
+ *        The nsIFile to remove
  */
 function recursiveRemove(aFile) {
   let isDir = null;
 
   try {
     isDir = aFile.isDirectory();
   } catch (e) {
     // If the file has already gone away then don't worry about it, this can
@@ -1135,23 +1212,268 @@ function recursiveRemove(aFile) {
     aFile.remove(true);
   } catch (e) {
     logger.error("Failed to remove empty directory " + aFile.path, e);
     throw e;
   }
 }
 
 /**
+ * Sets permissions on a file
+ *
+ * @param {nsIFile} aFile
+ *        The file or directory to operate on.
+ * @param {integer} aPermissions
+ *        The permissions to set
+ */
+function setFilePermissions(aFile, aPermissions) {
+  try {
+    aFile.permissions = aPermissions;
+  } catch (e) {
+    logger.warn("Failed to set permissions " + aPermissions.toString(8) + " on " +
+         aFile.path, e);
+  }
+}
+
+/**
+ * Write a given string to a file
+ *
+ * @param {nsIFile} file
+ *        The nsIFile instance to write into
+ * @param {string} string
+ *        The string to write
+ */
+function writeStringToFile(file, string) {
+  let fileStream = new FileOutputStream(
+    file, (FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+           FileUtils.MODE_TRUNCATE),
+    FileUtils.PERMS_FILE, 0);
+
+  try {
+    let binStream = new BinaryOutputStream(fileStream);
+
+    binStream.writeByteArray(new TextEncoder().encode(string));
+  } finally {
+    fileStream.close();
+  }
+}
+
+/**
+ * A safe way to install a file or the contents of a directory to a new
+ * directory. The file or directory is moved or copied recursively and if
+ * anything fails an attempt is made to rollback the entire operation. The
+ * operation may also be rolled back to its original state after it has
+ * completed by calling the rollback method.
+ *
+ * Operations can be chained. Calling move or copy multiple times will remember
+ * the whole set and if one fails all of the operations will be rolled back.
+ */
+function SafeInstallOperation() {
+  this._installedFiles = [];
+  this._createdDirs = [];
+}
+
+SafeInstallOperation.prototype = {
+  _installedFiles: null,
+  _createdDirs: null,
+
+  _installFile(aFile, aTargetDirectory, aCopy) {
+    let oldFile = aCopy ? null : aFile.clone();
+    let newFile = aFile.clone();
+    try {
+      if (aCopy) {
+        newFile.copyTo(aTargetDirectory, null);
+        // copyTo does not update the nsIFile with the new.
+        newFile = getFile(aFile.leafName, aTargetDirectory);
+        // Windows roaming profiles won't properly sync directories if a new file
+        // has an older lastModifiedTime than a previous file, so update.
+        newFile.lastModifiedTime = Date.now();
+      } else {
+        newFile.moveTo(aTargetDirectory, null);
+      }
+    } catch (e) {
+      logger.error("Failed to " + (aCopy ? "copy" : "move") + " file " + aFile.path +
+            " to " + aTargetDirectory.path, e);
+      throw e;
+    }
+    this._installedFiles.push({ oldFile, newFile });
+  },
+
+  _installDirectory(aDirectory, aTargetDirectory, aCopy) {
+    if (aDirectory.contains(aTargetDirectory)) {
+      let err = new Error(`Not installing ${aDirectory} into its own descendent ${aTargetDirectory}`);
+      logger.error(err);
+      throw err;
+    }
+
+    let newDir = getFile(aDirectory.leafName, aTargetDirectory);
+    try {
+      newDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+    } catch (e) {
+      logger.error("Failed to create directory " + newDir.path, e);
+      throw e;
+    }
+    this._createdDirs.push(newDir);
+
+    // Use a snapshot of the directory contents to avoid possible issues with
+    // iterating over a directory while removing files from it (the YAFFS2
+    // embedded filesystem has this issue, see bug 772238), and to remove
+    // normal files before their resource forks on OSX (see bug 733436).
+    let entries = getDirectoryEntries(aDirectory, true);
+    for (let entry of entries) {
+      try {
+        this._installDirEntry(entry, newDir, aCopy);
+      } catch (e) {
+        logger.error("Failed to " + (aCopy ? "copy" : "move") + " entry " +
+                     entry.path, e);
+        throw e;
+      }
+    }
+
+    // If this is only a copy operation then there is nothing else to do
+    if (aCopy)
+      return;
+
+    // The directory should be empty by this point. If it isn't this will throw
+    // and all of the operations will be rolled back
+    try {
+      setFilePermissions(aDirectory, FileUtils.PERMS_DIRECTORY);
+      aDirectory.remove(false);
+    } catch (e) {
+      logger.error("Failed to remove directory " + aDirectory.path, e);
+      throw e;
+    }
+
+    // Note we put the directory move in after all the file moves so the
+    // directory is recreated before all the files are moved back
+    this._installedFiles.push({ oldFile: aDirectory, newFile: newDir });
+  },
+
+  _installDirEntry(aDirEntry, aTargetDirectory, aCopy) {
+    let isDir = null;
+
+    try {
+      isDir = aDirEntry.isDirectory() && !aDirEntry.isSymlink();
+    } catch (e) {
+      // If the file has already gone away then don't worry about it, this can
+      // happen on OSX where the resource fork is automatically moved with the
+      // data fork for the file. See bug 733436.
+      if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
+        return;
+
+      logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path +
+            " to " + aTargetDirectory.path);
+      throw e;
+    }
+
+    try {
+      if (isDir)
+        this._installDirectory(aDirEntry, aTargetDirectory, aCopy);
+      else
+        this._installFile(aDirEntry, aTargetDirectory, aCopy);
+    } catch (e) {
+      logger.error("Failure " + (aCopy ? "copying" : "moving") + " " + aDirEntry.path +
+            " to " + aTargetDirectory.path);
+      throw e;
+    }
+  },
+
+  /**
+   * Moves a file or directory into a new directory. If an error occurs then all
+   * files that have been moved will be moved back to their original location.
+   *
+   * @param {nsIFile} aFile
+   *        The file or directory to be moved.
+   * @param {nsIFile} aTargetDirectory
+   *        The directory to move into, this is expected to be an empty
+   *        directory.
+   */
+  moveUnder(aFile, aTargetDirectory) {
+    try {
+      this._installDirEntry(aFile, aTargetDirectory, false);
+    } catch (e) {
+      this.rollback();
+      throw e;
+    }
+  },
+
+  /**
+   * Renames a file to a new location.  If an error occurs then all
+   * files that have been moved will be moved back to their original location.
+   *
+   * @param {nsIFile} aOldLocation
+   *        The old location of the file.
+   * @param {nsIFile} aNewLocation
+   *        The new location of the file.
+   */
+  moveTo(aOldLocation, aNewLocation) {
+    try {
+      let oldFile = aOldLocation.clone(), newFile = aNewLocation.clone();
+      oldFile.moveTo(newFile.parent, newFile.leafName);
+      this._installedFiles.push({ oldFile, newFile, isMoveTo: true});
+    } catch (e) {
+      this.rollback();
+      throw e;
+    }
+  },
+
+  /**
+   * Copies a file or directory into a new directory. If an error occurs then
+   * all new files that have been created will be removed.
+   *
+   * @param {nsIFile} aFile
+   *        The file or directory to be copied.
+   * @param {nsIFile} aTargetDirectory
+   *        The directory to copy into, this is expected to be an empty
+   *        directory.
+   */
+  copy(aFile, aTargetDirectory) {
+    try {
+      this._installDirEntry(aFile, aTargetDirectory, true);
+    } catch (e) {
+      this.rollback();
+      throw e;
+    }
+  },
+
+  /**
+   * Rolls back all the moves that this operation performed. If an exception
+   * occurs here then both old and new directories are left in an indeterminate
+   * state
+   */
+  rollback() {
+    while (this._installedFiles.length > 0) {
+      let move = this._installedFiles.pop();
+      if (move.isMoveTo) {
+        move.newFile.moveTo(move.oldDir.parent, move.oldDir.leafName);
+      } else if (move.newFile.isDirectory() && !move.newFile.isSymlink()) {
+        let oldDir = getFile(move.oldFile.leafName, move.oldFile.parent);
+        oldDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+      } else if (!move.oldFile) {
+        // No old file means this was a copied file
+        move.newFile.remove(true);
+      } else {
+        move.newFile.moveTo(move.oldFile.parent, null);
+      }
+    }
+
+    while (this._createdDirs.length > 0)
+      recursiveRemove(this._createdDirs.pop());
+  }
+};
+
+/**
  * Gets a snapshot of directory entries.
  *
- * @param  aDir
- *         Directory to look at
- * @param  aSortEntries
- *         True to sort entries by filename
- * @return An array of nsIFile, or an empty array if aDir is not a readable directory
+ * @param {nsIFile} aDir
+ *        Directory to look at
+ * @param {boolean} aSortEntries
+ *        True to sort entries by filename
+ * @returns {nsIFile[]}
+ *        An array of nsIFile, or an empty array if aDir is not a readable directory
  */
 function getDirectoryEntries(aDir, aSortEntries) {
   let dirEnum;
   try {
     dirEnum = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
     let entries = [];
     while (dirEnum.hasMoreElements())
       entries.push(dirEnum.nextFile);
@@ -1188,37 +1510,37 @@ function getHashStringForCrypto(aCrypto)
 /**
  * Base class for objects that manage the installation of an addon.
  * This class isn't instantiated directly, see the derived classes below.
  */
 class AddonInstall {
   /**
    * Instantiates an AddonInstall.
    *
-   * @param  installLocation
-   *         The install location the add-on will be installed into
-   * @param  url
-   *         The nsIURL to get the add-on from. If this is an nsIFileURL then
-   *         the add-on will not need to be downloaded
-   * @param  options
-   *         Additional options for the install
-   * @param  options.hash
-   *         An optional hash for the add-on
-   * @param  options.existingAddon
-   *         The add-on this install will update if known
-   * @param  options.name
-   *         An optional name for the add-on
-   * @param  options.type
-   *         An optional type for the add-on
-   * @param  options.icons
-   *         Optional icons for the add-on
-   * @param  options.version
-   *         An optional version for the add-on
-   * @param  options.promptHandler
-   *         A callback to prompt the user before installing.
+   * @param {InstallLocation} installLocation
+   *        The install location the add-on will be installed into
+   * @param {nsIURL} url
+   *        The nsIURL to get the add-on from. If this is an nsIFileURL then
+   *        the add-on will not need to be downloaded
+   * @param {Object} [options = {}]
+   *        Additional options for the install
+   * @param {string} [options.hash]
+   *        An optional hash for the add-on
+   * @param {AddonInternal} [options.existingAddon]
+   *        The add-on this install will update if known
+   * @param {string} [options.name]
+   *        An optional name for the add-on
+   * @param {string} [options.type]
+   *        An optional type for the add-on
+   * @param {object} [options.icons]
+   *        Optional icons for the add-on
+   * @param {string} [options.version]
+   *        An optional version for the add-on
+   * @param {function(string) : Promise<void>} [options.promptHandler]
+   *        A callback to prompt the user before installing.
    */
   constructor(installLocation, url, options = {}) {
     this.wrapper = new AddonInstallWrapper(this);
     this.installLocation = installLocation;
     this.sourceURI = url;
 
     if (options.hash) {
       let hashSplit = options.hash.toLowerCase().split(":");
@@ -1264,16 +1586,17 @@ class AddonInstall {
 
   /**
    * Starts installation of this add-on from whatever state it is currently at
    * if possible.
    *
    * Note this method is overridden to handle additional state in
    * the subclassses below.
    *
+   * @returns {Promise<Addon>}
    * @throws if installation cannot proceed from the current state
    */
   install() {
     switch (this.state) {
     case AddonManager.STATE_DOWNLOADED:
       this.checkPrompt();
       break;
     case AddonManager.STATE_PROMPTS_DONE:
@@ -1359,29 +1682,29 @@ class AddonInstall {
                       " from this state (" + this.state + ")");
     }
   }
 
   /**
    * Adds an InstallListener for this instance if the listener is not already
    * registered.
    *
-   * @param  aListener
-   *         The InstallListener to add
+   * @param {InstallListener} aListener
+   *        The InstallListener to add
    */
   addListener(aListener) {
     if (!this.listeners.some(function(i) { return i == aListener; }))
       this.listeners.push(aListener);
   }
 
   /**
    * Removes an InstallListener for this instance if it is registered.
    *
-   * @param  aListener
-   *         The InstallListener to remove
+   * @param {InstallListener} aListener
+   *        The InstallListener to remove
    */
   removeListener(aListener) {
     this.listeners = this.listeners.filter(function(i) {
       return i != aListener;
     });
   }
 
   /**
@@ -1415,20 +1738,19 @@ class AddonInstall {
     if (this.releaseNotesURI)
       this.addon.releaseNotesURI = this.releaseNotesURI.spec;
   }
 
   /**
    * Called after the add-on is a local file and the signature and install
    * manifest can be read.
    *
-   * @param  aCallback
-   *         A function to call when the manifest has been loaded
-   * @throws if the add-on does not contain a valid install manifest or the
-   *         XPI is incorrectly signed
+   * @param {nsIFile} file
+   *        The file from which to load the manifest.
+   * @returns {Promise<void>}
    */
   async loadManifest(file) {
     let pkg;
     try {
       pkg = Package.get(file);
     } catch (e) {
       return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
     }
@@ -1453,17 +1775,17 @@ class AddonInstall {
         }
 
         if (isWebExtension(this.existingAddon.type) && !isWebExtension(this.addon.type)) {
           return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
                                  "WebExtensions may not be updated to other extension types"]);
         }
       }
 
-      if (mustSign(this.addon.type)) {
+      if (XPIDatabase.mustSign(this.addon.type)) {
         if (this.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
           // This add-on isn't properly signed by a signature that chains to the
           // trusted root.
           let state = this.addon.signedState;
           this.addon = null;
 
           if (state == AddonManager.SIGNEDSTATE_MISSING)
             return Promise.reject([AddonManager.ERROR_SIGNEDSTATE_REQUIRED,
@@ -1496,17 +1818,17 @@ class AddonInstall {
         [repoAddon] = await AddonRepository.cacheAddons([this.addon.id]);
       } catch (err) {
         logger.debug(`Error getting metadata for ${this.addon.id}: ${err.message}`);
       }
     }
 
     this.addon._repositoryAddon = repoAddon;
     this.name = this.name || this.addon._repositoryAddon.name;
-    this.addon.appDisabled = !isUsableAddon(this.addon);
+    this.addon.appDisabled = !XPIDatabase.isUsableAddon(this.addon);
     return undefined;
   }
 
   getIcon(desiredSize = 64) {
     if (!this.addon.icons || !this.file) {
       return null;
     }
 
@@ -1708,17 +2030,17 @@ class AddonInstall {
           XPIProvider.callBootstrapMethod(this.addon, file, "startup",
                                           reason, extraParams);
         } else {
           // XXX this makes it dangerous to do some things in onInstallEnded
           // listeners because important cleanup hasn't been done yet
           XPIProvider.unloadBootstrapScope(this.addon.id);
         }
       }
-      recordAddonTelemetry(this.addon);
+      XPIDatabase.recordAddonTelemetry(this.addon);
 
       // Notify providers that a new theme has been enabled.
       if (isTheme(this.addon.type) && this.addon.active)
         AddonManagerPrivate.notifyAddonChanged(this.addon.id, this.addon.type);
     })().catch((e) => {
       logger.warn(`Failed to install ${this.file.path} from ${this.sourceURI.spec} to ${stagedAddon.path}`, e);
 
       if (stagedAddon.exists())
@@ -1731,17 +2053,26 @@ class AddonInstall {
       this._callInstallListeners("onInstallFailed");
     }).then(() => {
       this.removeTemporaryFile();
       return this.installLocation.releaseStagingDir();
     });
   }
 
   /**
-   * Stages an upgrade for next application restart.
+   * Stages an add-on for install.
+   *
+   * @param {boolean} restartRequired
+   *        If true, the final installation will be deferred until the
+   *        next app startup.
+   * @param {AddonInternal} stagedAddon
+   *        The AddonInternal object for the staged install.
+   * @param {boolean} isUpgrade
+   *        True if this installation is an upgrade for an existing
+   *        add-on.
    */
   async stageInstall(restartRequired, stagedAddon, isUpgrade) {
     // First stage the file regardless of whether restarting is necessary
     if (this.addon.unpack) {
       logger.debug("Addon " + this.addon.id + " will be installed as " +
                    "an unpacked directory");
       stagedAddon.leafName = this.addon.id;
       await OS.File.makeDir(stagedAddon.path);
@@ -1766,30 +2097,33 @@ class AddonInstall {
         delete this.existingAddon.pendingUpgrade;
         this.existingAddon.pendingUpgrade = this.addon;
       }
     }
   }
 
   /**
    * Removes any previously staged upgrade.
+   *
+   * @param {nsIFile} stagingDir
+   *        The staging directory from which to unstage the install.
    */
-  async unstageInstall(stagedAddon) {
+  async unstageInstall(stagingDir) {
     XPIStates.getLocation(this.installLocation.name).unstageAddon(this.addon.id);
 
-    await removeAsync(getFile(this.addon.id, stagedAddon));
-
-    await removeAsync(getFile(`${this.addon.id}.xpi`, stagedAddon));
+    await removeAsync(getFile(this.addon.id, stagingDir));
+
+    await removeAsync(getFile(`${this.addon.id}.xpi`, stagingDir));
   }
 
   /**
     * Postone a pending update, until restart or until the add-on resumes.
     *
-    * @param {Function} resumeFn - a function for the add-on to run
-    *                                    when resuming.
+    * @param {function} resumeFn
+    *        A function for the add-on to run when resuming.
     */
   async postpone(resumeFn) {
     this.state = AddonManager.STATE_POSTPONED;
 
     let stagingDir = this.installLocation.getStagingDir();
 
     await this.installLocation.requestStagingDir();
     await this.unstageInstall(stagingDir);
@@ -1843,19 +2177,16 @@ class AddonInstall {
     return AddonManagerPrivate.callInstallListeners(event, this.listeners, this.wrapper,
                                                     ...args);
   }
 }
 
 var LocalAddonInstall = class extends AddonInstall {
   /**
    * Initialises this install to be an install from a local file.
-   *
-   * @returns Promise
-   *          A Promise that resolves when the object is ready to use.
    */
   async init() {
     this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file;
 
     if (!this.file.exists()) {
       logger.warn("XPI file " + this.file.path + " does not exist");
       this.state = AddonManager.STATE_DOWNLOAD_FAILED;
       this.error = AddonManager.ERROR_NETWORK_FAILURE;
@@ -1944,39 +2275,39 @@ var LocalAddonInstall = class extends Ad
     return super.install();
   }
 };
 
 var DownloadAddonInstall = class extends AddonInstall {
   /**
    * Instantiates a DownloadAddonInstall
    *
-   * @param  installLocation
-   *         The InstallLocation the add-on will be installed into
-   * @param  url
-   *         The nsIURL to get the add-on from
-   * @param  options
-   *         Additional options for the install
-   * @param  options.hash
-   *         An optional hash for the add-on
-   * @param  options.existingAddon
-   *         The add-on this install will update if known
-   * @param  options.browser
-   *         The browser performing the install, used to display
-   *         authentication prompts.
-   * @param  options.name
-   *         An optional name for the add-on
-   * @param  options.type
-   *         An optional type for the add-on
-   * @param  options.icons
-   *         Optional icons for the add-on
-   * @param  options.version
-   *         An optional version for the add-on
-   * @param  options.promptHandler
-   *         A callback to prompt the user before installing.
+   * @param {InstallLocation} installLocation
+   *        The InstallLocation the add-on will be installed into
+   * @param {nsIURL} url
+   *        The nsIURL to get the add-on from
+   * @param {Object} [options = {}]
+   *        Additional options for the install
+   * @param {string} [options.hash]
+   *        An optional hash for the add-on
+   * @param {AddonInternal} [options.existingAddon]
+   *        The add-on this install will update if known
+   * @param {XULElement} [options.browser]
+   *        The browser performing the install, used to display
+   *        authentication prompts.
+   * @param {string} [options.name]
+   *        An optional name for the add-on
+   * @param {string} [options.type]
+   *        An optional type for the add-on
+   * @param {Object} [options.icons]
+   *        Optional icons for the add-on
+   * @param {string} [options.version]
+   *        An optional version for the add-on
+   * @param {function(string) : Promise<void>} [options.promptHandler]
+   *        A callback to prompt the user before installing.
    */
   constructor(installLocation, url, options = {}) {
     super(installLocation, url, options);
 
     this.browser = options.browser;
 
     this.state = AddonManager.STATE_AVAILABLE;
 
@@ -2097,30 +2428,30 @@ var DownloadAddonInstall = class extends
       logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
       this.state = AddonManager.STATE_DOWNLOAD_FAILED;
       this.error = AddonManager.ERROR_NETWORK_FAILURE;
       XPIProvider.removeActiveInstall(this);
       this._callInstallListeners("onDownloadFailed");
     }
   }
 
-  /**
+  /*
    * Update the crypto hasher with the new data and call the progress listeners.
    *
    * @see nsIStreamListener
    */
   onDataAvailable(aRequest, aContext, aInputstream, aOffset, aCount) {
     this.crypto.updateFromStream(aInputstream, aCount);
     this.progress += aCount;
     if (!this._callInstallListeners("onDownloadProgress")) {
       // TODO cancel the download and make it available again (bug 553024)
     }
   }
 
-  /**
+  /*
    * Check the redirect response for a hash of the target XPI and verify that
    * we don't end up on an insecure channel.
    *
    * @see nsIChannelEventSink
    */
   asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback) {
     if (!this.hash && aOldChannel.originalURI.schemeIs("https") &&
         aOldChannel instanceof Ci.nsIHttpChannel) {
@@ -2140,17 +2471,17 @@ var DownloadAddonInstall = class extends
     if (!this.hash)
       this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback);
     else
       aCallback.onRedirectVerifyCallback(Cr.NS_OK);
 
     this.channel = aNewChannel;
   }
 
-  /**
+  /*
    * This is the first chance to get at real headers on the channel.
    *
    * @see nsIStreamListener
    */
   onStartRequest(aRequest, aContext) {
     if (this.hash) {
       try {
         this.crypto = CryptoHash(this.hash.algorithm);
@@ -2175,17 +2506,17 @@ var DownloadAddonInstall = class extends
         this.maxProgress = aRequest.contentLength;
       } catch (e) {
       }
       logger.debug("Download started for " + this.sourceURI.spec + " to file " +
           this.file.path);
     }
   }
 
-  /**
+  /*
    * The download is complete.
    *
    * @see nsIStreamListener
    */
   onStopRequest(aRequest, aContext, aStatus) {
     this.stream.close();
     this.channel = null;
     this.badCerthandler = null;
@@ -2258,20 +2589,20 @@ var DownloadAddonInstall = class extends
     } else {
       this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
     }
   }
 
   /**
    * Notify listeners that the download failed.
    *
-   * @param  aReason
-   *         Something to log about the failure
-   * @param  error
-   *         The error code to pass to the listeners
+   * @param {string} aReason
+   *        Something to log about the failure
+   * @param {integer} aError
+   *        The error code to pass to the listeners
    */
   downloadFailed(aReason, aError) {
     logger.warn("Download of " + this.sourceURI.spec + " failed", aError);
     this.state = AddonManager.STATE_DOWNLOAD_FAILED;
     this.error = aReason;
     XPIProvider.removeActiveInstall(this);
     this._callInstallListeners("onDownloadFailed");
 
@@ -2335,22 +2666,22 @@ var DownloadAddonInstall = class extends
 
     return this.badCertHandler.getInterface(iid);
   }
 };
 
 /**
  * Creates a new AddonInstall for an update.
  *
- * @param  aCallback
- *         The callback to pass the new AddonInstall to
- * @param  aAddon
- *         The add-on being updated
- * @param  aUpdate
- *         The metadata about the new version from the update manifest
+ * @param {function} aCallback
+ *        The callback to pass the new AddonInstall to
+ * @param {AddonInternal} aAddon
+ *        The add-on being updated
+ * @param {Object} aUpdate
+ *        The metadata about the new version from the update manifest
  */
 function createUpdate(aCallback, aAddon, aUpdate) {
   let url = Services.io.newURI(aUpdate.updateURL);
 
   (async function() {
     let opts = {
       hash: aUpdate.updateHash,
       existingAddon: aAddon,
@@ -2379,18 +2710,18 @@ function createUpdate(aCallback, aAddon,
 
 // Maps instances of AddonInstall to AddonInstallWrapper
 const wrapperMap = new WeakMap();
 let installFor = wrapper => wrapperMap.get(wrapper);
 
 /**
  * Creates a wrapper for an AddonInstall that only exposes the public API
  *
- * @param  install
- *         The AddonInstall to create a wrapper for
+ * @param {AddonInstall} aInstall
+ *        The AddonInstall to create a wrapper for
  */
 function AddonInstallWrapper(aInstall) {
   wrapperMap.set(this, aInstall);
 }
 
 AddonInstallWrapper.prototype = {
   get __AddonInstallInternal__() {
     return AppConstants.DEBUG ? installFor(this) : undefined;
@@ -2447,26 +2778,26 @@ AddonInstallWrapper.prototype = {
     },
     enumerable: true,
   });
 });
 
 /**
  * Creates a new update checker.
  *
- * @param  aAddon
- *         The add-on to check for updates
- * @param  aListener
- *         An UpdateListener to notify of updates
- * @param  aReason
- *         The reason for the update check
- * @param  aAppVersion
- *         An optional application version to check for updates for
- * @param  aPlatformVersion
- *         An optional platform version to check for updates for
+ * @param {AddonInternal} aAddon
+ *        The add-on to check for updates
+ * @param {UpdateListener} aListener
+ *        An UpdateListener to notify of updates
+ * @param {integer} aReason
+ *        The reason for the update check
+ * @param {string} [aAppVersion]
+ *        An optional application version to check for updates for
+ * @param {string} [aPlatformVersion]
+ *        An optional platform version to check for updates for
  * @throws if the aListener or aReason arguments are not valid
  */
 var UpdateChecker = function(aAddon, aListener, aReason, aAppVersion, aPlatformVersion) {
   if (!aListener || !aReason)
     throw Cr.NS_ERROR_INVALID_ARG;
 
   ChromeUtils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm");
 
@@ -2505,35 +2836,37 @@ UpdateChecker.prototype = {
   appVersion: null,
   platformVersion: null,
   syncCompatibility: null,
 
   /**
    * Calls a method on the listener passing any number of arguments and
    * consuming any exceptions.
    *
-   * @param  aMethod
-   *         The method to call on the listener
+   * @param {string} aMethod
+   *        The method to call on the listener
+   * @param {any[]} aArgs
+   *        Additional arguments to pass to the listener.
    */
   callListener(aMethod, ...aArgs) {
     if (!(aMethod in this.listener))
       return;
 
     try {
       this.listener[aMethod].apply(this.listener, aArgs);
     } catch (e) {
       logger.warn("Exception calling UpdateListener method " + aMethod, e);
     }
   },
 
   /**
    * Called when AddonUpdateChecker completes the update check
    *
-   * @param  updates
-   *         The list of update details for the add-on
+   * @param {object[]} aUpdates
+   *        The list of update details for the add-on
    */
   async onUpdateCheckComplete(aUpdates) {
     XPIProvider.done(this.addon._updateCheck);
     this.addon._updateCheck = null;
     let AUC = AddonUpdateChecker;
 
     let ignoreMaxVersion = false;
     let ignoreStrictCompat = false;
@@ -2619,18 +2952,18 @@ UpdateChecker.prototype = {
     } else {
       sendUpdateAvailableMessages(this, null);
     }
   },
 
   /**
    * Called when AddonUpdateChecker fails the update check
    *
-   * @param  aError
-   *         An error status
+   * @param {any} aError
+   *        An error status
    */
   onUpdateCheckError(aError) {
     XPIProvider.done(this.addon._updateCheck);
     this.addon._updateCheck = null;
     this.callListener("onNoCompatibilityUpdateAvailable", this.addon.wrapper);
     this.callListener("onNoUpdateAvailable", this.addon.wrapper);
     this.callListener("onUpdateFinished", this.addon.wrapper, aError);
   },
@@ -2643,13 +2976,1405 @@ UpdateChecker.prototype = {
     if (parser) {
       this._parser = null;
       // This will call back to onUpdateCheckError with a CANCELLED error
       parser.cancel();
     }
   }
 };
 
+/**
+ * Creates a new AddonInstall to install an add-on from a local file.
+ *
+ * @param {nsIFile} file
+ *        The file to install
+ * @param {InstallLocation} location
+ *        The location to install to
+ * @returns {Promise<AddonInstall>}
+ *        A Promise that resolves with the new install object.
+ */
+function createLocalInstall(file, location) {
+  if (!location) {
+    location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+  }
+  let url = Services.io.newFileURI(file);
+
+  try {
+    let install = new LocalAddonInstall(location, url);
+    return install.init().then(() => install);
+  } catch (e) {
+    logger.error("Error creating install", e);
+    XPIProvider.removeActiveInstall(this);
+    return Promise.resolve(null);
+  }
+}
+
+// These are partial classes which contain the install logic for the
+// homonymous classes in XPIProvider.jsm. Those classes forward calls to
+// their install methods to these classes, with the `this` value set to
+// an instance the class as defined in XPIProvider.
+class DirectoryInstallLocation {}
+
+class MutableDirectoryInstallLocation extends DirectoryInstallLocation {
+  /**
+   * Gets the staging directory to put add-ons that are pending install and
+   * uninstall into.
+   *
+   * @returns {nsIFile}
+   */
+  getStagingDir() {
+    return getFile(DIR_STAGE, this._directory);
+  }
+
+  requestStagingDir() {
+    this._stagingDirLock++;
+
+    if (this._stagingDirPromise)
+      return this._stagingDirPromise;
+
+    OS.File.makeDir(this._directory.path);
+    let stagepath = OS.Path.join(this._directory.path, DIR_STAGE);
+    return this._stagingDirPromise = OS.File.makeDir(stagepath).catch((e) => {
+      if (e instanceof OS.File.Error && e.becauseExists)
+        return;
+      logger.error("Failed to create staging directory", e);
+      throw e;
+    });
+  }
+
+  releaseStagingDir() {
+    this._stagingDirLock--;
+
+    if (this._stagingDirLock == 0) {
+      this._stagingDirPromise = null;
+      this.cleanStagingDir();
+    }
+
+    return Promise.resolve();
+  }
+
+  /**
+   * Removes the specified files or directories in the staging directory and
+   * then if the staging directory is empty attempts to remove it.
+   *
+   * @param {string[]} [aLeafNames = []]
+   *        An array of file or directory to remove from the directory, the
+   *        array may be empty
+   */
+  cleanStagingDir(aLeafNames = []) {
+    let dir = this.getStagingDir();
+
+    for (let name of aLeafNames) {
+      let file = getFile(name, dir);
+      recursiveRemove(file);
+    }
+
+    if (this._stagingDirLock > 0)
+      return;
+
+    let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+    try {
+      if (dirEntries.nextFile)
+        return;
+    } finally {
+      dirEntries.close();
+    }
+
+    try {
+      setFilePermissions(dir, FileUtils.PERMS_DIRECTORY);
+      dir.remove(false);
+    } catch (e) {
+      logger.warn("Failed to remove staging dir", e);
+      // Failing to remove the staging directory is ignorable
+    }
+  }
+
+  /**
+   * Returns a directory that is normally on the same filesystem as the rest of
+   * the install location and can be used for temporarily storing files during
+   * safe move operations. Calling this method will delete the existing trash
+   * directory and its contents.
+   *
+   * @returns {nsIFile}
+   */
+  getTrashDir() {
+    let trashDir = getFile(DIR_TRASH, this._directory);
+    let trashDirExists = trashDir.exists();
+    try {
+      if (trashDirExists)
+        recursiveRemove(trashDir);
+      trashDirExists = false;
+    } catch (e) {
+      logger.warn("Failed to remove trash directory", e);
+    }
+    if (!trashDirExists)
+      trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+    return trashDir;
+  }
+
+  /**
+   * Installs an add-on into the install location.
+   *
+   * @param {Object} options
+   *        Installation options.
+   * @param {string} options.id
+   *        The ID of the add-on to install
+   * @param {nsIFile} options.source
+   *        The source nsIFile to install from
+   * @param {string?} [options.existingAddonID]
+   *        The ID of an existing add-on to uninstall at the same time
+   * @param {string} options.action
+   *        What to we do with the given source file:
+   *          "move"
+   *          Default action, the source files will be moved to the new
+   *          location,
+   *          "copy"
+   *          The source files will be copied,
+   *          "proxy"
+   *          A "proxy file" is going to refer to the source file path
+   * @returns {nsIFile}
+   *        An nsIFile indicating where the add-on was installed to
+   */
+  installAddon({ id, source, existingAddonID, action = "move" }) {
+    let trashDir = this.getTrashDir();
+
+    let transaction = new SafeInstallOperation();
+
+    let moveOldAddon = aId => {
+      let file = getFile(aId, this._directory);
+      if (file.exists())
+        transaction.moveUnder(file, trashDir);
+
+      file = getFile(`${aId}.xpi`, this._directory);
+      if (file.exists()) {
+        flushJarCache(file);
+        transaction.moveUnder(file, trashDir);
+      }
+    };
+
+    // If any of these operations fails the finally block will clean up the
+    // temporary directory
+    try {
+      moveOldAddon(id);
+      if (existingAddonID && existingAddonID != id) {
+        moveOldAddon(existingAddonID);
+
+        {
+          // Move the data directories.
+          /* XXX ajvincent We can't use OS.File:  installAddon isn't compatible
+           * with Promises, nor is SafeInstallOperation.  Bug 945540 has been filed
+           * for porting to OS.File.
+           */
+          let oldDataDir = FileUtils.getDir(
+            KEY_PROFILEDIR, ["extension-data", existingAddonID], false, true
+          );
+
+          if (oldDataDir.exists()) {
+            let newDataDir = FileUtils.getDir(
+              KEY_PROFILEDIR, ["extension-data", id], false, true
+            );
+            if (newDataDir.exists()) {
+              let trashData = getFile("data-directory", trashDir);
+              transaction.moveUnder(newDataDir, trashData);
+            }
+
+            transaction.moveTo(oldDataDir, newDataDir);
+          }
+        }
+      }
+
+      if (action == "copy") {
+        transaction.copy(source, this._directory);
+      } else if (action == "move") {
+        if (source.isFile())
+          flushJarCache(source);
+
+        transaction.moveUnder(source, this._directory);
+      }
+      // Do nothing for the proxy file as we sideload an addon permanently
+    } finally {
+      // It isn't ideal if this cleanup fails but it isn't worth rolling back
+      // the install because of it.
+      try {
+        recursiveRemove(trashDir);
+      } catch (e) {
+        logger.warn("Failed to remove trash directory when installing " + id, e);
+      }
+    }
+
+    let newFile = this._directory.clone();
+
+    if (action == "proxy") {
+      // When permanently installing sideloaded addon, we just put a proxy file
+      // referring to the addon sources
+      newFile.append(id);
+
+      writeStringToFile(newFile, source.path);
+    } else {
+      newFile.append(source.leafName);
+    }
+
+    try {
+      newFile.lastModifiedTime = Date.now();
+    } catch (e) {
+      logger.warn("failed to set lastModifiedTime on " + newFile.path, e);
+    }
+    this._IDToFileMap[id] = newFile;
+
+    if (existingAddonID && existingAddonID != id &&
+        existingAddonID in this._IDToFileMap) {
+      delete this._IDToFileMap[existingAddonID];
+    }
+
+    return newFile;
+  }
+
+  /**
+   * Uninstalls an add-on from this location.
+   *
+   * @param {string} aId
+   *        The ID of the add-on to uninstall
+   * @throws if the ID does not match any of the add-ons installed
+   */
+  uninstallAddon(aId) {
+    let file = this._IDToFileMap[aId];
+    if (!file) {
+      logger.warn("Attempted to remove " + aId + " from " +
+           this._name + " but it was already gone");
+      return;
+    }
+
+    file = getFile(aId, this._directory);
+    if (!file.exists())
+      file.leafName += ".xpi";
+
+    if (!file.exists()) {
+      logger.warn("Attempted to remove " + aId + " from " +
+           this._name + " but it was already gone");
+
+      delete this._IDToFileMap[aId];
+      return;
+    }
+
+    let trashDir = this.getTrashDir();
+
+    if (file.leafName != aId) {
+      logger.debug("uninstallAddon: flushing jar cache " + file.path + " for addon " + aId);
+      flushJarCache(file);
+    }
+
+    let transaction = new SafeInstallOperation();
+
+    try {
+      transaction.moveUnder(file, trashDir);
+    } finally {
+      // It isn't ideal if this cleanup fails, but it is probably better than
+      // rolling back the uninstall at this point
+      try {
+        recursiveRemove(trashDir);
+      } catch (e) {
+        logger.warn("Failed to remove trash directory when uninstalling " + aId, e);
+      }
+    }
+
+    XPIStates.removeAddon(this.name, aId);
+
+    delete this._IDToFileMap[aId];
+  }
+}
+
+class SystemAddonInstallLocation extends MutableDirectoryInstallLocation {
+  /**
+   * Saves the current set of system add-ons
+   *
+   * @param {Object} aAddonSet - object containing schema, directory and set
+   *                 of system add-on IDs and versions.
+   */
+  static _saveAddonSet(aAddonSet) {
+    Services.prefs.setStringPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(aAddonSet));
+  }
+
+  static _loadAddonSet() {
+    return XPIInternal.SystemAddonInstallLocation._loadAddonSet();
+  }
+
+  /**
+   * Gets the staging directory to put add-ons that are pending install and
+   * uninstall into.
+   *
+   * @returns {nsIFile}
+   *        Staging directory for system add-on upgrades.
+   */
+  getStagingDir() {
+    this._addonSet = SystemAddonInstallLocation._loadAddonSet();
+    let dir = null;
+    if (this._addonSet.directory) {
+      this._directory = getFile(this._addonSet.directory, this._baseDir);
+      dir = getFile(DIR_STAGE, this._directory);
+    } else {
+      logger.info("SystemAddonInstallLocation directory is missing");
+    }
+
+    return dir;
+  }
+
+  requestStagingDir() {
+    this._addonSet = SystemAddonInstallLocation._loadAddonSet();
+    if (this._addonSet.directory) {
+      this._directory = getFile(this._addonSet.directory, this._baseDir);
+    }
+    return super.requestStagingDir();
+  }
+
+  isValidAddon(aAddon) {
+    if (aAddon.appDisabled) {
+      logger.warn(`System add-on ${aAddon.id} isn't compatible with the application.`);
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Tests whether the loaded add-on information matches what is expected.
+   *
+   * @param {Map<string, AddonInternal>} aAddons
+   *        The set of add-ons to check.
+   * @returns {boolean}
+   *        True if all of the given add-ons are valid.
+   */
+  isValid(aAddons) {
+    for (let id of Object.keys(this._addonSet.addons)) {
+      if (!aAddons.has(id)) {
+        logger.warn(`Expected add-on ${id} is missing from the system add-on location.`);
+        return false;
+      }
+
+      let addon = aAddons.get(id);
+      if (addon.version != this._addonSet.addons[id].version) {
+        logger.warn(`Expected system add-on ${id} to be version ${this._addonSet.addons[id].version} but was ${addon.version}.`);
+        return false;
+      }
+
+      if (!this.isValidAddon(addon))
+        return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Resets the add-on set so on the next startup the default set will be used.
+   */
+  async resetAddonSet() {
+    logger.info("Removing all system add-on upgrades.");
+
+    // remove everything from the pref first, if uninstall
+    // fails then at least they will not be re-activated on
+    // next restart.
+    this._addonSet = { schema: 1, addons: {} };
+    SystemAddonInstallLocation._saveAddonSet(this._addonSet);
+
+    // If this is running at app startup, the pref being cleared
+    // will cause later stages of startup to notice that the
+    // old updates are now gone.
+    //
+    // Updates will only be explicitly uninstalled if they are
+    // removed restartlessly, for instance if they are no longer
+    // part of the latest update set.
+    if (this._addonSet) {
+      let ids = Object.keys(this._addonSet.addons);
+      for (let addon of await AddonManager.getAddonsByIDs(ids)) {
+        if (addon) {
+          addon.uninstall();
+        }
+      }
+    }
+  }
+
+  /**
+   * Removes any directories not currently in use or pending use after a
+   * restart. Any errors that happen here don't really matter as we'll attempt
+   * to cleanup again next time.
+   */
+  async cleanDirectories() {
+    // System add-ons directory does not exist