Merge inbound to mozilla-central. a=merge
authorMargareta Eliza Balazs <ebalazs@mozilla.com>
Tue, 24 Apr 2018 12:42:08 +0300
changeset 468764 26e53729a10976f52e75efa44e17b5e054969fec
parent 468718 dc845b3a8cbe45f88fcb0408e19f9df2e120420c (current diff)
parent 468763 2d7531046934947a11eaec3a0c64a696aad87945 (diff)
child 468824 e583796f1c09477e789945e22361fa7c5674e70a
child 468847 4acca3e3505256e14a5f9ca2d2035314051d7976
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 linux64
26e53729a109 / 61.0a1 / 20180424100107 / files
nightly mac
26e53729a109 / 61.0a1 / 20180424100107 / files
nightly win32
26e53729a109 / 61.0a1 / 20180424100107 / files
nightly win64
26e53729a109 / 61.0a1 / 20180424100107 / files
nightly linux32
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
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
layout/painting/nsDisplayList.cpp
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) {
+