Bug 1363925: Part 4 - Move XPIProvider install methods to XPIInstall. r=aswan
authorKris Maglione <maglione.k@gmail.com>
Sat, 21 Apr 2018 19:06:44 -0700
changeset 468743 801a6d33fa88b3030a3989d602a34237fb467e5c
parent 468742 cffb4b6e971a38219fab7fa49fd8d6565e986394
child 468744 afd6f3e6e6af154925a2c13bd363d38187cecf2a
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)
reviewersaswan
bugs1363925
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1363925: Part 4 - Move XPIProvider install methods to XPIInstall. r=aswan MozReview-Commit-ID: DiPA01emGA9
toolkit/mozapps/extensions/internal/XPIInstall.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -1,20 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = [
-  "DownloadAddonInstall",
-  "LocalAddonInstall",
   "UpdateChecker",
   "XPIInstall",
-  "loadManifestFromFile",
   "verifyBundleSignedState",
 ];
 
 /* globals DownloadAddonInstall, LocalAddonInstall */
 
 Cu.importGlobalProperties(["TextDecoder", "TextEncoder", "fetch"]);
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
@@ -31,22 +28,24 @@ ChromeUtils.defineModuleGetter(this, "Ce
                                "resource://gre/modules/CertUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "ExtensionData",
                                "resource://gre/modules/Extension.jsm");
 ChromeUtils.defineModuleGetter(this, "FileUtils",
                                "resource://gre/modules/FileUtils.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, "ProductAddonChecker",
+                               "resource://gre/modules/addons/ProductAddonChecker.jsm");
+ChromeUtils.defineModuleGetter(this, "UpdateUtils",
+                               "resource://gre/modules/UpdateUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "ZipUtils",
                                "resource://gre/modules/ZipUtils.jsm");
 
 const {nsIBlocklistService} = Ci;
 
 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
                                        "initWithPath");
 
@@ -71,29 +70,37 @@ XPCOMUtils.defineLazyServiceGetters(this
 
 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 PREF_DISTRO_ADDONS_PERMS        = "extensions.distroAddons.promptForPermissions";
-
-/* globals AddonInternal, 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, XPIDatabase, XPIStates, getExternalType, isTheme, isUsableAddon, isWebExtension, mustSign, recordAddonTelemetry */
+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 AddonInternal, 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, XPIDatabase, XPI_PERMISSION, XPIStates, getExternalType, isTheme, isUsableAddon, isWebExtension, mustSign, recordAddonTelemetry */
 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",
+  "XPI_PERMISSION",
   "XPIDatabase",
   "XPIStates",
   "getExternalType",
   "isTheme",
   "isUsableAddon",
   "isWebExtension",
   "mustSign",
   "recordAddonTelemetry",
@@ -400,16 +407,31 @@ XPIPackage = class XPIPackage extends Pa
   }
 
   flushCache() {
     flushJarCache(this.file);
     this.needFlush = false;
   }
 };
 
+/**
+ * Determine the reason to pass to an extension's bootstrap methods when
+ * switch between versions.
+ *
+ * @param {string} oldVersion The version of the existing extension instance.
+ * @param {string} newVersion The version of the extension being installed.
+ *
+ * @return {BOOSTRAP_REASONS.ADDON_UPGRADE|BOOSTRAP_REASONS.ADDON_DOWNGRADE}
+ */
+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(
@@ -1079,16 +1101,23 @@ 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.
+ */
+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);
@@ -3558,16 +3587,17 @@ class SystemAddonInstallLocation extends
   // old system add-on upgrade dirs get automatically removed
   uninstallAddon(aAddon) {}
 }
 
 var XPIInstall = {
   createLocalInstall,
   flushChromeCaches,
   flushJarCache,
+  newVersionReason,
   recursiveRemove,
   syncLoadManifestFromFile,
 
   /**
    * @param {string} id
    *        The expected ID of the add-on.
    * @param {nsIFile} file
    *        The XPI file to install the add-on from.
@@ -3617,11 +3647,590 @@ var XPIInstall = {
     XPIStates.addAddon(addon);
     logger.debug("Installed distribution add-on " + id);
 
     Services.prefs.setBoolPref(PREF_BRANCH_INSTALLED_ADDON + id, true);
 
     return addon;
   },
 
+  async updateSystemAddons() {
+    let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
+    if (!systemAddonLocation)
+      return;
+
+    // Don't do anything in safe mode
+    if (Services.appinfo.inSafeMode)
+      return;
+
+    // Download the list of system add-ons
+    let url = Services.prefs.getStringPref(PREF_SYSTEM_ADDON_UPDATE_URL, null);
+    if (!url) {
+      await systemAddonLocation.cleanDirectories();
+      return;
+    }
+
+    url = await UpdateUtils.formatUpdateURL(url);
+
+    logger.info(`Starting system add-on update check from ${url}.`);
+    let res = await ProductAddonChecker.getProductAddonList(url);
+
+    // If there was no list then do nothing.
+    if (!res || !res.gmpAddons) {
+      logger.info("No system add-ons list was returned.");
+      await systemAddonLocation.cleanDirectories();
+      return;
+    }
+
+    let addonList = new Map(
+      res.gmpAddons.map(spec => [spec.id, { spec, path: null, addon: null }]));
+
+    let setMatches = (wanted, existing) => {
+      if (wanted.size != existing.size)
+        return false;
+
+      for (let [id, addon] of existing) {
+        let wantedInfo = wanted.get(id);
+
+        if (!wantedInfo)
+          return false;
+        if (wantedInfo.spec.version != addon.version)
+          return false;
+      }
+
+      return true;
+    };
+
+    // If this matches the current set in the profile location then do nothing.
+    let updatedAddons = addonMap(await XPIDatabase.getAddonsInLocation(KEY_APP_SYSTEM_ADDONS));
+    if (setMatches(addonList, updatedAddons)) {
+      logger.info("Retaining existing updated system add-ons.");
+      await systemAddonLocation.cleanDirectories();
+      return;
+    }
+
+    // If this matches the current set in the default location then reset the
+    // updated set.
+    let defaultAddons = addonMap(await XPIDatabase.getAddonsInLocation(KEY_APP_SYSTEM_DEFAULTS));
+    if (setMatches(addonList, defaultAddons)) {
+      logger.info("Resetting system add-ons.");
+      systemAddonLocation.resetAddonSet();
+      await systemAddonLocation.cleanDirectories();
+      return;
+    }
+
+    // Download all the add-ons
+    async function downloadAddon(item) {
+      try {
+        let sourceAddon = updatedAddons.get(item.spec.id);
+        if (sourceAddon && sourceAddon.version == item.spec.version) {
+          // Copying the file to a temporary location has some benefits. If the
+          // file is locked and cannot be read then we'll fall back to
+          // downloading a fresh copy. It also means we don't have to remember
+          // whether to delete the temporary copy later.
+          try {
+            let path = OS.Path.join(OS.Constants.Path.tmpDir, "tmpaddon");
+            let unique = await OS.File.openUnique(path);
+            unique.file.close();
+            await OS.File.copy(sourceAddon._sourceBundle.path, unique.path);
+            // Make sure to update file modification times so this is detected
+            // as a new add-on.
+            await OS.File.setDates(unique.path);
+            item.path = unique.path;
+          } catch (e) {
+            logger.warn(`Failed make temporary copy of ${sourceAddon._sourceBundle.path}.`, e);
+          }
+        }
+        if (!item.path) {
+          item.path = await ProductAddonChecker.downloadAddon(item.spec);
+        }
+        item.addon = await loadManifestFromFile(nsIFile(item.path), systemAddonLocation);
+      } catch (e) {
+        logger.error(`Failed to download system add-on ${item.spec.id}`, e);
+      }
+    }
+    await Promise.all(Array.from(addonList.values()).map(downloadAddon));
+
+    // The download promises all resolve regardless, now check if they all
+    // succeeded
+    let validateAddon = (item) => {
+      if (item.spec.id != item.addon.id) {
+        logger.warn(`Downloaded system add-on expected to be ${item.spec.id} but was ${item.addon.id}.`);
+        return false;
+      }
+
+      if (item.spec.version != item.addon.version) {
+        logger.warn(`Expected system add-on ${item.spec.id} to be version ${item.spec.version} but was ${item.addon.version}.`);
+        return false;
+      }
+
+      if (!systemAddonLocation.isValidAddon(item.addon))
+        return false;
+
+      return true;
+    };
+
+    if (!Array.from(addonList.values()).every(item => item.path && item.addon && validateAddon(item))) {
+      throw new Error("Rejecting updated system add-on set that either could not " +
+                      "be downloaded or contained unusable add-ons.");
+    }
+
+    // Install into the install location
+    logger.info("Installing new system add-on set");
+    await systemAddonLocation.installAddonSet(Array.from(addonList.values())
+      .map(a => a.addon));
+  },
+
+  /**
+   * Called to test whether installing XPI add-ons is enabled.
+   *
+   * @return true if installing is enabled
+   */
+  isInstallEnabled() {
+    // Default to enabled if the preference does not exist
+    return Services.prefs.getBoolPref(PREF_XPI_ENABLED, true);
+  },
+
+  /**
+   * Called to test whether installing XPI add-ons by direct URL requests is
+   * whitelisted.
+   *
+   * @return true if installing by direct requests is whitelisted
+   */
+  isDirectRequestWhitelisted() {
+    // Default to whitelisted if the preference does not exist.
+    return Services.prefs.getBoolPref(PREF_XPI_DIRECT_WHITELISTED, true);
+  },
+
+  /**
+   * Called to test whether installing XPI add-ons from file referrers is
+   * whitelisted.
+   *
+   * @return true if installing from file referrers is whitelisted
+   */
+  isFileRequestWhitelisted() {
+    // Default to whitelisted if the preference does not exist.
+    return Services.prefs.getBoolPref(PREF_XPI_FILE_WHITELISTED, true);
+  },
+
+  /**
+   * Called to test whether installing XPI add-ons from a URI is allowed.
+   *
+   * @param  aInstallingPrincipal
+   *         The nsIPrincipal that initiated the install
+   * @return true if installing is allowed
+   */
+  isInstallAllowed(aInstallingPrincipal) {
+    if (!this.isInstallEnabled())
+      return false;
+
+    let uri = aInstallingPrincipal.URI;
+
+    // Direct requests without a referrer are either whitelisted or blocked.
+    if (!uri)
+      return this.isDirectRequestWhitelisted();
+
+    // Local referrers can be whitelisted.
+    if (this.isFileRequestWhitelisted() &&
+        (uri.schemeIs("chrome") || uri.schemeIs("file")))
+      return true;
+
+    XPIProvider.importPermissions();
+
+    let permission = Services.perms.testPermissionFromPrincipal(aInstallingPrincipal, XPI_PERMISSION);
+    if (permission == Ci.nsIPermissionManager.DENY_ACTION)
+      return false;
+
+    let requireWhitelist = Services.prefs.getBoolPref(PREF_XPI_WHITELIST_REQUIRED, true);
+    if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION))
+      return false;
+
+    let requireSecureOrigin = Services.prefs.getBoolPref(PREF_INSTALL_REQUIRESECUREORIGIN, true);
+    let safeSchemes = ["https", "chrome", "file"];
+    if (requireSecureOrigin && !safeSchemes.includes(uri.scheme))
+      return false;
+
+    return true;
+  },
+
+  /**
+   * Called to get an AddonInstall to download and install an add-on from a URL.
+   *
+   * @param  aUrl
+   *         The URL to be installed
+   * @param  aHash
+   *         A hash for the install
+   * @param  aName
+   *         A name for the install
+   * @param  aIcons
+   *         Icon URLs for the install
+   * @param  aVersion
+   *         A version for the install
+   * @param  aBrowser
+   *         The browser performing the install
+   */
+  async getInstallForURL(aUrl, aHash, aName, aIcons, aVersion, aBrowser) {
+    let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+    let url = Services.io.newURI(aUrl);
+
+    let options = {
+      hash: aHash,
+      browser: aBrowser,
+      name: aName,
+      icons: aIcons,
+      version: aVersion,
+    };
+
+    if (url instanceof Ci.nsIFileURL) {
+      let install = new LocalAddonInstall(location, url, options);
+      await install.init();
+      return install.wrapper;
+    }
+
+    let install = new DownloadAddonInstall(location, url, options);
+    return install.wrapper;
+  },
+
+  /**
+   * Called to get an AddonInstall to install an add-on from a local file.
+   *
+   * @param  aFile
+   *         The file to be installed
+   */
+  async getInstallForFile(aFile) {
+    let install = await createLocalInstall(aFile);
+    return install ? install.wrapper : null;
+  },
+
+  /**
+   * Temporarily installs add-on from a local XPI file or directory.
+   * As this is intended for development, the signature is not checked and
+   * the add-on does not persist on application restart.
+   *
+   * @param aFile
+   *        An nsIFile for the unpacked add-on directory or XPI file.
+   *
+   * @return See installAddonFromLocation return value.
+   */
+  installTemporaryAddon(aFile) {
+    return this.installAddonFromLocation(aFile, XPIInternal.TemporaryInstallLocation);
+  },
+
+  /**
+   * Permanently installs add-on from a local XPI file or directory.
+   * The signature is checked but the add-on persist on application restart.
+   *
+   * @param aFile
+   *        An nsIFile for the unpacked add-on directory or XPI file.
+   *
+   * @return See installAddonFromLocation return value.
+   */
+  async installAddonFromSources(aFile) {
+    let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+    return this.installAddonFromLocation(aFile, location, "proxy");
+  },
+
+  /**
+   * Installs add-on from a local XPI file or directory.
+   *
+   * @param aFile
+   *        An nsIFile for the unpacked add-on directory or XPI file.
+   * @param aInstallLocation
+   *        Define a custom install location object to use for the install.
+   * @param aInstallAction
+   *        Optional action mode to use when installing the addon
+   *        (see MutableDirectoryInstallLocation.installAddon)
+   *
+   * @return a Promise that resolves to an Addon object on success, or rejects
+   *         if the add-on is not a valid restartless add-on or if the
+   *         same ID is already installed.
+   */
+  async installAddonFromLocation(aFile, aInstallLocation, aInstallAction) {
+    if (aFile.exists() && aFile.isFile()) {
+      flushJarCache(aFile);
+    }
+    let addon = await loadManifestFromFile(aFile, aInstallLocation);
+
+    aInstallLocation.installAddon({ id: addon.id, source: aFile, action: aInstallAction });
+
+    if (addon.appDisabled) {
+      let message = `Add-on ${addon.id} is not compatible with application version.`;
+
+      let app = addon.matchingTargetApplication;
+      if (app) {
+        if (app.minVersion) {
+          message += ` add-on minVersion: ${app.minVersion}.`;
+        }
+        if (app.maxVersion) {
+          message += ` add-on maxVersion: ${app.maxVersion}.`;
+        }
+      }
+      throw new Error(message);
+    }
+
+    if (!addon.bootstrap) {
+      throw new Error(`Only restartless (bootstrap) add-ons can be installed from sources: ${addon.id}`);
+    }
+    let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL;
+    let oldAddon = await XPIDatabase.getVisibleAddonForID(addon.id);
+    let callUpdate = false;
+
+    let extraParams = {};
+    extraParams.temporarilyInstalled = aInstallLocation === XPIInternal.TemporaryInstallLocation;
+    if (oldAddon) {
+      if (!oldAddon.bootstrap) {
+        logger.warn("Non-restartless Add-on is already installed", addon.id);
+        throw new Error("Non-restartless add-on with ID "
+                        + oldAddon.id + " is already installed");
+      } else {
+        logger.warn("Addon with ID " + oldAddon.id + " already installed,"
+                    + " older version will be disabled");
+
+        addon.installDate = oldAddon.installDate;
+
+        let existingAddonID = oldAddon.id;
+        let existingAddon = oldAddon._sourceBundle;
+
+        // We'll be replacing a currently active bootstrapped add-on so
+        // call its uninstall method
+        let newVersion = addon.version;
+        let oldVersion = oldAddon.version;
+
+        installReason = newVersionReason(oldVersion, newVersion);
+        let uninstallReason = installReason;
+
+        extraParams.newVersion = newVersion;
+        extraParams.oldVersion = oldVersion;
+
+        callUpdate = isWebExtension(oldAddon.type) && isWebExtension(addon.type);
+
+        if (oldAddon.active) {
+          XPIProvider.callBootstrapMethod(oldAddon, existingAddon,
+                                          "shutdown", uninstallReason,
+                                          extraParams);
+        }
+
+        if (!callUpdate) {
+          XPIProvider.callBootstrapMethod(oldAddon, existingAddon,
+                                          "uninstall", uninstallReason, extraParams);
+        }
+        XPIProvider.unloadBootstrapScope(existingAddonID);
+        flushChromeCaches();
+      }
+    } else {
+      addon.installDate = Date.now();
+    }
+
+    let file = addon._sourceBundle;
+
+    let method = callUpdate ? "update" : "install";
+    XPIProvider.callBootstrapMethod(addon, file, method, installReason, extraParams);
+    addon.state = AddonManager.STATE_INSTALLED;
+    logger.debug("Install of temporary addon in " + aFile.path + " completed.");
+    addon.visible = true;
+    addon.enabled = true;
+    addon.active = true;
+    // WebExtension themes are installed as disabled, fix that here.
+    addon.userDisabled = false;
+
+    addon = XPIDatabase.addAddonMetadata(addon, file.path);
+
+    XPIStates.addAddon(addon);
+    XPIDatabase.saveChanges();
+    XPIStates.save();
+
+    AddonManagerPrivate.callAddonListeners("onInstalling", addon.wrapper,
+                                           false);
+    XPIProvider.callBootstrapMethod(addon, file, "startup", installReason, extraParams);
+    AddonManagerPrivate.callInstallListeners("onExternalInstall",
+                                             null, addon.wrapper,
+                                             oldAddon ? oldAddon.wrapper : null,
+                                             false);
+    AddonManagerPrivate.callAddonListeners("onInstalled", addon.wrapper);
+
+    // Notify providers that a new theme has been enabled.
+    if (isTheme(addon.type))
+      AddonManagerPrivate.notifyAddonChanged(addon.id, addon.type, false);
+
+    return addon.wrapper;
+  },
+
+  /**
+   * Uninstalls an add-on, immediately if possible or marks it as pending
+   * uninstall if not.
+   *
+   * @param  aAddon
+   *         The DBAddonInternal to uninstall
+   * @param  aForcePending
+   *         Force this addon into the pending uninstall state (used
+   *         e.g. while the add-on manager is open and offering an
+   *         "undo" button)
+   * @throws if the addon cannot be uninstalled because it is in an install
+   *         location that does not allow it
+   */
+  async uninstallAddon(aAddon, aForcePending) {
+    if (!(aAddon.inDatabase))
+      throw new Error("Cannot uninstall addon " + aAddon.id + " because it is not installed");
+
+    if (aAddon._installLocation.locked)
+      throw new Error("Cannot uninstall addon " + aAddon.id
+          + " from locked install location " + aAddon._installLocation.name);
+
+    if (aForcePending && aAddon.pendingUninstall)
+      throw new Error("Add-on is already marked to be uninstalled");
+
+    aAddon._hasResourceCache.clear();
+
+    if (aAddon._updateCheck) {
+      logger.debug("Cancel in-progress update check for " + aAddon.id);
+      aAddon._updateCheck.cancel();
+    }
+
+    let wasPending = aAddon.pendingUninstall;
+
+    if (aForcePending) {
+      // We create an empty directory in the staging directory to indicate
+      // that an uninstall is necessary on next startup. Temporary add-ons are
+      // automatically uninstalled on shutdown anyway so there is no need to
+      // do this for them.
+      if (aAddon._installLocation.name != KEY_APP_TEMPORARY) {
+        let stage = getFile(aAddon.id, aAddon._installLocation.getStagingDir());
+        if (!stage.exists())
+          stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+      }
+
+      XPIDatabase.setAddonProperties(aAddon, {
+        pendingUninstall: true
+      });
+      Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
+      let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
+      if (xpiState) {
+        xpiState.enabled = false;
+        XPIStates.save();
+      } else {
+        logger.warn("Can't find XPI state while uninstalling ${id} from ${location}", aAddon);
+      }
+    }
+
+    // If the add-on is not visible then there is no need to notify listeners.
+    if (!aAddon.visible)
+      return;
+
+    let wrapper = aAddon.wrapper;
+
+    // If the add-on wasn't already pending uninstall then notify listeners.
+    if (!wasPending) {
+      AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper,
+                                             !!aForcePending);
+    }
+
+    let reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
+    let callUpdate = false;
+    let existingAddon = XPIStates.findAddon(aAddon.id, loc =>
+      loc.name != aAddon._installLocation.name);
+    if (existingAddon) {
+      reason = newVersionReason(aAddon.version, existingAddon.version);
+      callUpdate = isWebExtension(aAddon.type) && isWebExtension(existingAddon.type);
+    }
+
+    if (!aForcePending) {
+      if (aAddon.bootstrap) {
+        if (aAddon.active) {
+          XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
+                                          reason);
+        }
+
+        if (!callUpdate) {
+          XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, "uninstall",
+                                          reason);
+        }
+        XPIStates.disableAddon(aAddon.id);
+        XPIProvider.unloadBootstrapScope(aAddon.id);
+        flushChromeCaches();
+      }
+      aAddon._installLocation.uninstallAddon(aAddon.id);
+      XPIDatabase.removeAddonMetadata(aAddon);
+      XPIStates.removeAddon(aAddon.location, aAddon.id);
+      AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
+
+      if (existingAddon) {
+        let existing = await XPIDatabase.getAddonInLocation(aAddon.id, existingAddon.location.name);
+        XPIDatabase.makeAddonVisible(existing);
+
+        let wrappedAddon = existing.wrapper;
+        AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false);
+
+        if (!existing.disabled) {
+          XPIDatabase.updateAddonActive(existing, true);
+        }
+
+        if (aAddon.bootstrap) {
+          let method = callUpdate ? "update" : "install";
+          XPIProvider.callBootstrapMethod(existing, existing._sourceBundle,
+                                          method, reason);
+
+          if (existing.active) {
+            XPIProvider.callBootstrapMethod(existing, existing._sourceBundle,
+                                            "startup", reason);
+          } else {
+            XPIProvider.unloadBootstrapScope(existing.id);
+          }
+        }
+
+        AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon);
+      }
+    } else if (aAddon.bootstrap && aAddon.active) {
+      XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown", reason);
+      XPIStates.disableAddon(aAddon.id);
+      XPIProvider.unloadBootstrapScope(aAddon.id);
+      XPIDatabase.updateAddonActive(aAddon, false);
+    }
+
+    // Notify any other providers that a new theme has been enabled
+    if (isTheme(aAddon.type) && aAddon.active)
+      AddonManagerPrivate.notifyAddonChanged(null, aAddon.type);
+  },
+
+  /**
+   * Cancels the pending uninstall of an add-on.
+   *
+   * @param  aAddon
+   *         The DBAddonInternal to cancel uninstall for
+   */
+  cancelUninstallAddon(aAddon) {
+    if (!(aAddon.inDatabase))
+      throw new Error("Can only cancel uninstall for installed addons.");
+    if (!aAddon.pendingUninstall)
+      throw new Error("Add-on is not marked to be uninstalled");
+
+    if (aAddon._installLocation.name != KEY_APP_TEMPORARY)
+      aAddon._installLocation.cleanStagingDir([aAddon.id]);
+
+    XPIDatabase.setAddonProperties(aAddon, {
+      pendingUninstall: false
+    });
+
+    if (!aAddon.visible)
+      return;
+
+    XPIStates.getAddon(aAddon.location, aAddon.id).syncWithDB(aAddon);
+    XPIStates.save();
+
+    Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
+
+    // TODO hide hidden add-ons (bug 557710)
+    let wrapper = aAddon.wrapper;
+    AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
+
+    if (aAddon.bootstrap && !aAddon.disabled) {
+      XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup",
+                                      BOOTSTRAP_REASONS.ADDON_INSTALL);
+      XPIDatabase.updateAddonActive(aAddon, true);
+    }
+
+    // Notify any other providers that this theme is now enabled again.
+    if (isTheme(aAddon.type) && aAddon.active)
+      AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false);
+  },
+
   MutableDirectoryInstallLocation,
   SystemAddonInstallLocation,
 };
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -18,70 +18,54 @@ XPCOMUtils.defineLazyModuleGetters(this,
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   Extension: "resource://gre/modules/Extension.jsm",
   Langpack: "resource://gre/modules/Extension.jsm",
   LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
   FileUtils: "resource://gre/modules/FileUtils.jsm",
   PermissionsUtils: "resource://gre/modules/PermissionsUtils.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   ConsoleAPI: "resource://gre/modules/Console.jsm",
-  ProductAddonChecker: "resource://gre/modules/addons/ProductAddonChecker.jsm",
-  UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
   JSONFile: "resource://gre/modules/JSONFile.jsm",
   LegacyExtensionsUtils: "resource://gre/modules/LegacyExtensionsUtils.jsm",
   setTimeout: "resource://gre/modules/Timer.jsm",
   clearTimeout: "resource://gre/modules/Timer.jsm",
 
-  DownloadAddonInstall: "resource://gre/modules/addons/XPIInstall.jsm",
-  LocalAddonInstall: "resource://gre/modules/addons/XPIInstall.jsm",
   UpdateChecker: "resource://gre/modules/addons/XPIInstall.jsm",
   XPIInstall: "resource://gre/modules/addons/XPIInstall.jsm",
-  loadManifestFromFile: "resource://gre/modules/addons/XPIInstall.jsm",
   verifyBundleSignedState: "resource://gre/modules/addons/XPIInstall.jsm",
 });
 
 const {nsIBlocklistService} = Ci;
 
-XPCOMUtils.defineLazyServiceGetters(this, {
-  AddonPolicyService: ["@mozilla.org/addons/policy-service;1", "nsIAddonPolicyService"],
-  aomStartup: ["@mozilla.org/addons/addon-manager-startup;1", "amIAddonManagerStartup"],
-});
-
-XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => {
-  return new TextDecoder();
-});
+XPCOMUtils.defineLazyServiceGetter(this, "aomStartup",
+                                   "@mozilla.org/addons/addon-manager-startup;1",
+                                   "amIAddonManagerStartup");
 
 Cu.importGlobalProperties(["URL"]);
 
 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
                                        "initWithPath");
 
 const PREF_DB_SCHEMA                  = "extensions.databaseSchema";
 const PREF_XPI_STATE                  = "extensions.xpiState";
 const PREF_BLOCKLIST_ITEM_URL         = "extensions.blocklist.itemURL";
 const PREF_BOOTSTRAP_ADDONS           = "extensions.bootstrappedAddons";
 const PREF_PENDING_OPERATIONS         = "extensions.pendingOperations";
 const PREF_EM_EXTENSION_FORMAT        = "extensions.";
 const PREF_EM_ENABLED_SCOPES          = "extensions.enabledScopes";
 const PREF_EM_STARTUP_SCAN_SCOPES     = "extensions.startupScanScopes";
 const PREF_EM_SHOW_MISMATCH_UI        = "extensions.showMismatchUI";
-const PREF_XPI_ENABLED                = "xpinstall.enabled";
-const PREF_XPI_WHITELIST_REQUIRED     = "xpinstall.whitelist.required";
-const PREF_XPI_DIRECT_WHITELISTED     = "xpinstall.whitelist.directRequest";
-const PREF_XPI_FILE_WHITELISTED       = "xpinstall.whitelist.fileRequest";
 // xpinstall.signatures.required only supported in dev builds
 const PREF_XPI_SIGNATURES_REQUIRED    = "xpinstall.signatures.required";
 const PREF_XPI_SIGNATURES_DEV_ROOT    = "xpinstall.signatures.dev-root";
 const PREF_LANGPACK_SIGNATURES        = "extensions.langpacks.signatures.required";
 const PREF_XPI_PERMISSIONS_BRANCH     = "xpinstall.";
-const PREF_INSTALL_REQUIRESECUREORIGIN = "extensions.install.requireSecureOrigin";
 const PREF_INSTALL_DISTRO_ADDONS      = "extensions.installDistroAddons";
 const PREF_BRANCH_INSTALLED_ADDON     = "extensions.installedDistroAddon.";
 const PREF_SYSTEM_ADDON_SET           = "extensions.systemAddonSet";
-const PREF_SYSTEM_ADDON_UPDATE_URL    = "extensions.systemAddon.update.url";
 const PREF_ALLOW_LEGACY               = "extensions.legacy.enabled";
 
 const PREF_EM_MIN_COMPAT_APP_VERSION      = "extensions.minCompatibleAppVersion";
 const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
 
 const PREF_EM_LAST_APP_BUILD_ID       = "extensions.lastAppBuildId";
 
 // Specify a list of valid built-in add-ons to load.
@@ -438,38 +422,16 @@ function findMatchingStaticBlocklistItem
         return item;
       }
     }
   }
   return null;
 }
 
 /**
- * Determine the reason to pass to an extension's bootstrap methods when
- * switch between versions.
- *
- * @param {string} oldVersion The version of the existing extension instance.
- * @param {string} newVersion The version of the extension being installed.
- *
- * @return {BOOSTRAP_REASONS.ADDON_UPGRADE|BOOSTRAP_REASONS.ADDON_DOWNGRADE}
- */
-function newVersionReason(oldVersion, newVersion) {
-  return Services.vc.compare(oldVersion, newVersion) <= 0 ?
-         BOOTSTRAP_REASONS.ADDON_UPGRADE :
-         BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
-}
-
-/**
- * Converts an iterable of addon objects into a map with the add-on's ID as key.
- */
-function addonMap(addons) {
-  return new Map(addons.map(a => [a.id, a]));
-}
-
-/**
  * Helper function that determines whether an addon of a certain type is a
  * WebExtension.
  *
  * @param  {String} type
  * @return {Boolean}
  */
 function isWebExtension(type) {
   return type == "webextension" || type == "webextension-theme";
@@ -1837,17 +1799,17 @@ var XPIProvider = {
             let reason = BOOTSTRAP_REASONS.APP_SHUTDOWN;
             if (addon.disable) {
               reason = BOOTSTRAP_REASONS.ADDON_DISABLE;
             } else if (addon.location.name == KEY_APP_TEMPORARY) {
               reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
               let existing = XPIStates.findAddon(addon.id, loc =>
                 loc.name != TemporaryInstallLocation.name);
               if (existing) {
-                reason = newVersionReason(addon.version, existing.version);
+                reason = XPIInstall.newVersionReason(addon.version, existing.version);
               }
             }
             XPIProvider.callBootstrapMethod(addon, addon.file,
                                             "shutdown", reason);
           }
           Services.obs.removeObserver(this, "quit-application-granted");
         }
       }, "quit-application-granted");
@@ -1966,17 +1928,17 @@ var XPIProvider = {
       for (let [id, addon] of tempLocation.entries()) {
         tempLocation.delete(id);
 
         let reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
 
         let existing = XPIStates.findAddon(id, loc => loc != tempLocation);
         let callUpdate = false;
         if (existing) {
-          reason = newVersionReason(addon.version, existing.version);
+          reason = XPIInstall.newVersionReason(addon.version, existing.version);
           callUpdate = (isWebExtension(addon.type) && isWebExtension(existing.type));
         }
 
         this.callBootstrapMethod(addon, addon.file, "shutdown", reason);
         if (!callUpdate) {
           this.callBootstrapMethod(addon, addon.file, "uninstall", reason);
         }
         this.unloadBootstrapScope(id);
@@ -2140,143 +2102,16 @@ var XPIProvider = {
     clearTimeout(timer);
     if (window) {
       window.close();
     }
 
     return started;
   },
 
-  async updateSystemAddons() {
-    let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
-    if (!systemAddonLocation)
-      return;
-
-    // Don't do anything in safe mode
-    if (Services.appinfo.inSafeMode)
-      return;
-
-    // Download the list of system add-ons
-    let url = Services.prefs.getStringPref(PREF_SYSTEM_ADDON_UPDATE_URL, null);
-    if (!url) {
-      await systemAddonLocation.cleanDirectories();
-      return;
-    }
-
-    url = await UpdateUtils.formatUpdateURL(url);
-
-    logger.info(`Starting system add-on update check from ${url}.`);
-    let res = await ProductAddonChecker.getProductAddonList(url);
-
-    // If there was no list then do nothing.
-    if (!res || !res.gmpAddons) {
-      logger.info("No system add-ons list was returned.");
-      await systemAddonLocation.cleanDirectories();
-      return;
-    }
-
-    let addonList = new Map(
-      res.gmpAddons.map(spec => [spec.id, { spec, path: null, addon: null }]));
-
-    let setMatches = (wanted, existing) => {
-      if (wanted.size != existing.size)
-        return false;
-
-      for (let [id, addon] of existing) {
-        let wantedInfo = wanted.get(id);
-
-        if (!wantedInfo)
-          return false;
-        if (wantedInfo.spec.version != addon.version)
-          return false;
-      }
-
-      return true;
-    };
-
-    // If this matches the current set in the profile location then do nothing.
-    let updatedAddons = addonMap(await XPIDatabase.getAddonsInLocation(KEY_APP_SYSTEM_ADDONS));
-    if (setMatches(addonList, updatedAddons)) {
-      logger.info("Retaining existing updated system add-ons.");
-      await systemAddonLocation.cleanDirectories();
-      return;
-    }
-
-    // If this matches the current set in the default location then reset the
-    // updated set.
-    let defaultAddons = addonMap(await XPIDatabase.getAddonsInLocation(KEY_APP_SYSTEM_DEFAULTS));
-    if (setMatches(addonList, defaultAddons)) {
-      logger.info("Resetting system add-ons.");
-      systemAddonLocation.resetAddonSet();
-      await systemAddonLocation.cleanDirectories();
-      return;
-    }
-
-    // Download all the add-ons
-    async function downloadAddon(item) {
-      try {
-        let sourceAddon = updatedAddons.get(item.spec.id);
-        if (sourceAddon && sourceAddon.version == item.spec.version) {
-          // Copying the file to a temporary location has some benefits. If the
-          // file is locked and cannot be read then we'll fall back to
-          // downloading a fresh copy. It also means we don't have to remember
-          // whether to delete the temporary copy later.
-          try {
-            let path = OS.Path.join(OS.Constants.Path.tmpDir, "tmpaddon");
-            let unique = await OS.File.openUnique(path);
-            unique.file.close();
-            await OS.File.copy(sourceAddon._sourceBundle.path, unique.path);
-            // Make sure to update file modification times so this is detected
-            // as a new add-on.
-            await OS.File.setDates(unique.path);
-            item.path = unique.path;
-          } catch (e) {
-            logger.warn(`Failed make temporary copy of ${sourceAddon._sourceBundle.path}.`, e);
-          }
-        }
-        if (!item.path) {
-          item.path = await ProductAddonChecker.downloadAddon(item.spec);
-        }
-        item.addon = await loadManifestFromFile(nsIFile(item.path), systemAddonLocation);
-      } catch (e) {
-        logger.error(`Failed to download system add-on ${item.spec.id}`, e);
-      }
-    }
-    await Promise.all(Array.from(addonList.values()).map(downloadAddon));
-
-    // The download promises all resolve regardless, now check if they all
-    // succeeded
-    let validateAddon = (item) => {
-      if (item.spec.id != item.addon.id) {
-        logger.warn(`Downloaded system add-on expected to be ${item.spec.id} but was ${item.addon.id}.`);
-        return false;
-      }
-
-      if (item.spec.version != item.addon.version) {
-        logger.warn(`Expected system add-on ${item.spec.id} to be version ${item.spec.version} but was ${item.addon.version}.`);
-        return false;
-      }
-
-      if (!systemAddonLocation.isValidAddon(item.addon))
-        return false;
-
-      return true;
-    };
-
-    if (!Array.from(addonList.values()).every(item => item.path && item.addon && validateAddon(item))) {
-      throw new Error("Rejecting updated system add-on set that either could not " +
-                      "be downloaded or contained unusable add-ons.");
-    }
-
-    // Install into the install location
-    logger.info("Installing new system add-on set");
-    await systemAddonLocation.installAddonSet(Array.from(addonList.values())
-      .map(a => a.addon));
-  },
-
   /**
    * Verifies that all installed add-ons are still correctly signed.
    */
   async verifySignatures() {
     try {
       let addons = await XPIDatabase.getAddonList(a => true);
 
       let changes = {
@@ -2401,17 +2236,17 @@ var XPIProvider = {
             var file = existingAddon.file;
             if (file.exists()) {
               oldBootstrap = existingAddon;
 
               // We'll be replacing a currently active bootstrapped add-on so
               // call its uninstall method
               let newVersion = addon.version;
               let oldVersion = existingAddon;
-              let uninstallReason = newVersionReason(oldVersion, newVersion);
+              let uninstallReason = XPIInstall.newVersionReason(oldVersion, newVersion);
 
               this.callBootstrapMethod(existingAddon,
                                        file, "uninstall", uninstallReason,
                                        { newVersion });
               this.unloadBootstrapScope(id);
               XPIInstall.flushChromeCaches();
             }
           } catch (e) {
@@ -2704,297 +2539,22 @@ var XPIProvider = {
    * @param  aMimetype
    *         The mimetype to check for
    * @return true if the mimetype is application/x-xpinstall
    */
   supportsMimetype(aMimetype) {
     return aMimetype == "application/x-xpinstall";
   },
 
-  /**
-   * Called to test whether installing XPI add-ons is enabled.
-   *
-   * @return true if installing is enabled
-   */
-  isInstallEnabled() {
-    // Default to enabled if the preference does not exist
-    return Services.prefs.getBoolPref(PREF_XPI_ENABLED, true);
-  },
-
-  /**
-   * Called to test whether installing XPI add-ons by direct URL requests is
-   * whitelisted.
-   *
-   * @return true if installing by direct requests is whitelisted
-   */
-  isDirectRequestWhitelisted() {
-    // Default to whitelisted if the preference does not exist.
-    return Services.prefs.getBoolPref(PREF_XPI_DIRECT_WHITELISTED, true);
-  },
-
-  /**
-   * Called to test whether installing XPI add-ons from file referrers is
-   * whitelisted.
-   *
-   * @return true if installing from file referrers is whitelisted
-   */
-  isFileRequestWhitelisted() {
-    // Default to whitelisted if the preference does not exist.
-    return Services.prefs.getBoolPref(PREF_XPI_FILE_WHITELISTED, true);
-  },
-
-  /**
-   * Called to test whether installing XPI add-ons from a URI is allowed.
-   *
-   * @param  aInstallingPrincipal
-   *         The nsIPrincipal that initiated the install
-   * @return true if installing is allowed
-   */
-  isInstallAllowed(aInstallingPrincipal) {
-    if (!this.isInstallEnabled())
-      return false;
-
-    let uri = aInstallingPrincipal.URI;
-
-    // Direct requests without a referrer are either whitelisted or blocked.
-    if (!uri)
-      return this.isDirectRequestWhitelisted();
-
-    // Local referrers can be whitelisted.
-    if (this.isFileRequestWhitelisted() &&
-        (uri.schemeIs("chrome") || uri.schemeIs("file")))
-      return true;
-
-    this.importPermissions();
-
-    let permission = Services.perms.testPermissionFromPrincipal(aInstallingPrincipal, XPI_PERMISSION);
-    if (permission == Ci.nsIPermissionManager.DENY_ACTION)
-      return false;
-
-    let requireWhitelist = Services.prefs.getBoolPref(PREF_XPI_WHITELIST_REQUIRED, true);
-    if (requireWhitelist && (permission != Ci.nsIPermissionManager.ALLOW_ACTION))
-      return false;
-
-    let requireSecureOrigin = Services.prefs.getBoolPref(PREF_INSTALL_REQUIRESECUREORIGIN, true);
-    let safeSchemes = ["https", "chrome", "file"];
-    if (requireSecureOrigin && !safeSchemes.includes(uri.scheme))
-      return false;
-
-    return true;
-  },
-
   // Identify temporary install IDs.
   isTemporaryInstallID(id) {
     return id.endsWith(TEMPORARY_ADDON_SUFFIX);
   },
 
   /**
-   * Called to get an AddonInstall to download and install an add-on from a URL.
-   *
-   * @param  aUrl
-   *         The URL to be installed
-   * @param  aHash
-   *         A hash for the install
-   * @param  aName
-   *         A name for the install
-   * @param  aIcons
-   *         Icon URLs for the install
-   * @param  aVersion
-   *         A version for the install
-   * @param  aBrowser
-   *         The browser performing the install
-   */
-  async getInstallForURL(aUrl, aHash, aName, aIcons, aVersion, aBrowser) {
-    let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
-    let url = Services.io.newURI(aUrl);
-
-    let options = {
-      hash: aHash,
-      browser: aBrowser,
-      name: aName,
-      icons: aIcons,
-      version: aVersion,
-    };
-
-    if (url instanceof Ci.nsIFileURL) {
-      let install = new LocalAddonInstall(location, url, options);
-      await install.init();
-      return install.wrapper;
-    }
-
-    let install = new DownloadAddonInstall(location, url, options);
-    return install.wrapper;
-  },
-
-  /**
-   * Called to get an AddonInstall to install an add-on from a local file.
-   *
-   * @param  aFile
-   *         The file to be installed
-   */
-  async getInstallForFile(aFile) {
-    let install = await XPIInstall.createLocalInstall(aFile);
-    return install ? install.wrapper : null;
-  },
-
-  /**
-   * Temporarily installs add-on from a local XPI file or directory.
-   * As this is intended for development, the signature is not checked and
-   * the add-on does not persist on application restart.
-   *
-   * @param aFile
-   *        An nsIFile for the unpacked add-on directory or XPI file.
-   *
-   * @return See installAddonFromLocation return value.
-   */
-  installTemporaryAddon(aFile) {
-    return this.installAddonFromLocation(aFile, TemporaryInstallLocation);
-  },
-
-  /**
-   * Permanently installs add-on from a local XPI file or directory.
-   * The signature is checked but the add-on persist on application restart.
-   *
-   * @param aFile
-   *        An nsIFile for the unpacked add-on directory or XPI file.
-   *
-   * @return See installAddonFromLocation return value.
-   */
-  async installAddonFromSources(aFile) {
-    let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
-    return this.installAddonFromLocation(aFile, location, "proxy");
-  },
-
-  /**
-   * Installs add-on from a local XPI file or directory.
-   *
-   * @param aFile
-   *        An nsIFile for the unpacked add-on directory or XPI file.
-   * @param aInstallLocation
-   *        Define a custom install location object to use for the install.
-   * @param aInstallAction
-   *        Optional action mode to use when installing the addon
-   *        (see MutableDirectoryInstallLocation.installAddon)
-   *
-   * @return a Promise that resolves to an Addon object on success, or rejects
-   *         if the add-on is not a valid restartless add-on or if the
-   *         same ID is already installed.
-   */
-  async installAddonFromLocation(aFile, aInstallLocation, aInstallAction) {
-    if (aFile.exists() && aFile.isFile()) {
-      XPIInstall.flushJarCache(aFile);
-    }
-    let addon = await loadManifestFromFile(aFile, aInstallLocation);
-
-    aInstallLocation.installAddon({ id: addon.id, source: aFile, action: aInstallAction });
-
-    if (addon.appDisabled) {
-      let message = `Add-on ${addon.id} is not compatible with application version.`;
-
-      let app = addon.matchingTargetApplication;
-      if (app) {
-        if (app.minVersion) {
-          message += ` add-on minVersion: ${app.minVersion}.`;
-        }
-        if (app.maxVersion) {
-          message += ` add-on maxVersion: ${app.maxVersion}.`;
-        }
-      }
-      throw new Error(message);
-    }
-
-    if (!addon.bootstrap) {
-      throw new Error("Only restartless (bootstrap) add-ons"
-                    + " can be installed from sources:", addon.id);
-    }
-    let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL;
-    let oldAddon = await XPIDatabase.getVisibleAddonForID(addon.id);
-    let callUpdate = false;
-
-    let extraParams = {};
-    extraParams.temporarilyInstalled = aInstallLocation === TemporaryInstallLocation;
-    if (oldAddon) {
-      if (!oldAddon.bootstrap) {
-        logger.warn("Non-restartless Add-on is already installed", addon.id);
-        throw new Error("Non-restartless add-on with ID "
-                        + oldAddon.id + " is already installed");
-      } else {
-        logger.warn("Addon with ID " + oldAddon.id + " already installed,"
-                    + " older version will be disabled");
-
-        addon.installDate = oldAddon.installDate;
-
-        let existingAddonID = oldAddon.id;
-        let existingAddon = oldAddon._sourceBundle;
-
-        // We'll be replacing a currently active bootstrapped add-on so
-        // call its uninstall method
-        let newVersion = addon.version;
-        let oldVersion = oldAddon.version;
-
-        installReason = newVersionReason(oldVersion, newVersion);
-        let uninstallReason = installReason;
-
-        extraParams.newVersion = newVersion;
-        extraParams.oldVersion = oldVersion;
-
-        callUpdate = isWebExtension(oldAddon.type) && isWebExtension(addon.type);
-
-        if (oldAddon.active) {
-          XPIProvider.callBootstrapMethod(oldAddon, existingAddon,
-                                          "shutdown", uninstallReason,
-                                          extraParams);
-        }
-
-        if (!callUpdate) {
-          this.callBootstrapMethod(oldAddon, existingAddon,
-                                   "uninstall", uninstallReason, extraParams);
-        }
-        this.unloadBootstrapScope(existingAddonID);
-        XPIInstall.flushChromeCaches();
-      }
-    } else {
-      addon.installDate = Date.now();
-    }
-
-    let file = addon._sourceBundle;
-
-    let method = callUpdate ? "update" : "install";
-    XPIProvider.callBootstrapMethod(addon, file, method, installReason, extraParams);
-    addon.state = AddonManager.STATE_INSTALLED;
-    logger.debug("Install of temporary addon in " + aFile.path + " completed.");
-    addon.visible = true;
-    addon.enabled = true;
-    addon.active = true;
-    // WebExtension themes are installed as disabled, fix that here.
-    addon.userDisabled = false;
-
-    addon = XPIDatabase.addAddonMetadata(addon, file.path);
-
-    XPIStates.addAddon(addon);
-    XPIDatabase.saveChanges();
-    XPIStates.save();
-
-    AddonManagerPrivate.callAddonListeners("onInstalling", addon.wrapper,
-                                           false);
-    XPIProvider.callBootstrapMethod(addon, file, "startup", installReason, extraParams);
-    AddonManagerPrivate.callInstallListeners("onExternalInstall",
-                                             null, addon.wrapper,
-                                             oldAddon ? oldAddon.wrapper : null,
-                                             false);
-    AddonManagerPrivate.callAddonListeners("onInstalled", addon.wrapper);
-
-    // Notify providers that a new theme has been enabled.
-    if (isTheme(addon.type))
-      AddonManagerPrivate.notifyAddonChanged(addon.id, addon.type, false);
-
-    return addon.wrapper;
-  },
-
-  /**
    * Sets startupData for the given addon.  The provided data will be stored
    * in addonsStartup.json so it is available early during browser startup.
    * Note that this file is read synchronously at startup, so startupData
    * should be used with care.
    *
    * @param {string} aID
    *         The id of the addon to save startup data for.
    * @param {any} aData
@@ -3671,196 +3231,27 @@ var XPIProvider = {
         }
       } else if (isDisabled && !aBecauseSelecting) {
         AddonManagerPrivate.notifyAddonChanged(null, "theme");
       }
     }
 
     return isDisabled;
   },
-
-  /**
-   * Uninstalls an add-on, immediately if possible or marks it as pending
-   * uninstall if not.
-   *
-   * @param  aAddon
-   *         The DBAddonInternal to uninstall
-   * @param  aForcePending
-   *         Force this addon into the pending uninstall state (used
-   *         e.g. while the add-on manager is open and offering an
-   *         "undo" button)
-   * @throws if the addon cannot be uninstalled because it is in an install
-   *         location that does not allow it
-   */
-  async uninstallAddon(aAddon, aForcePending) {
-    if (!(aAddon.inDatabase))
-      throw new Error("Cannot uninstall addon " + aAddon.id + " because it is not installed");
-
-    if (aAddon._installLocation.locked)
-      throw new Error("Cannot uninstall addon " + aAddon.id
-          + " from locked install location " + aAddon._installLocation.name);
-
-    if (aForcePending && aAddon.pendingUninstall)
-      throw new Error("Add-on is already marked to be uninstalled");
-
-    aAddon._hasResourceCache.clear();
-
-    if (aAddon._updateCheck) {
-      logger.debug("Cancel in-progress update check for " + aAddon.id);
-      aAddon._updateCheck.cancel();
-    }
-
-    let wasPending = aAddon.pendingUninstall;
-
-    if (aForcePending) {
-      // We create an empty directory in the staging directory to indicate
-      // that an uninstall is necessary on next startup. Temporary add-ons are
-      // automatically uninstalled on shutdown anyway so there is no need to
-      // do this for them.
-      if (aAddon._installLocation.name != KEY_APP_TEMPORARY) {
-        let stage = getFile(aAddon.id, aAddon._installLocation.getStagingDir());
-        if (!stage.exists())
-          stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-      }
-
-      XPIDatabase.setAddonProperties(aAddon, {
-        pendingUninstall: true
-      });
-      Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
-      let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
-      if (xpiState) {
-        xpiState.enabled = false;
-        XPIStates.save();
-      } else {
-        logger.warn("Can't find XPI state while uninstalling ${id} from ${location}", aAddon);
-      }
-    }
-
-    // If the add-on is not visible then there is no need to notify listeners.
-    if (!aAddon.visible)
-      return;
-
-    let wrapper = aAddon.wrapper;
-
-    // If the add-on wasn't already pending uninstall then notify listeners.
-    if (!wasPending) {
-      AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper,
-                                             !!aForcePending);
-    }
-
-    let reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
-    let callUpdate = false;
-    let existingAddon = XPIStates.findAddon(aAddon.id, loc =>
-      loc.name != aAddon._installLocation.name);
-    if (existingAddon) {
-      reason = newVersionReason(aAddon.version, existingAddon.version);
-      callUpdate = isWebExtension(aAddon.type) && isWebExtension(existingAddon.type);
-    }
-
-    if (!aForcePending) {
-      if (aAddon.bootstrap) {
-        if (aAddon.active) {
-          this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
-                                   reason);
-        }
-
-        if (!callUpdate) {
-          this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "uninstall",
-                                   reason);
-        }
-        XPIStates.disableAddon(aAddon.id);
-        this.unloadBootstrapScope(aAddon.id);
-        XPIInstall.flushChromeCaches();
-      }
-      aAddon._installLocation.uninstallAddon(aAddon.id);
-      XPIDatabase.removeAddonMetadata(aAddon);
-      XPIStates.removeAddon(aAddon.location, aAddon.id);
-      AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
-
-      if (existingAddon) {
-        let existing = await XPIDatabase.getAddonInLocation(aAddon.id, existingAddon.location.name);
-        XPIDatabase.makeAddonVisible(existing);
-
-        let wrappedAddon = existing.wrapper;
-        AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false);
-
-        if (!existing.disabled) {
-          XPIDatabase.updateAddonActive(existing, true);
-        }
-
-        if (aAddon.bootstrap) {
-          let method = callUpdate ? "update" : "install";
-          XPIProvider.callBootstrapMethod(existing, existing._sourceBundle,
-                                          method, reason);
-
-          if (existing.active) {
-            XPIProvider.callBootstrapMethod(existing, existing._sourceBundle,
-                                            "startup", reason);
-          } else {
-            XPIProvider.unloadBootstrapScope(existing.id);
-          }
-        }
-
-        AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon);
-      }
-    } else if (aAddon.bootstrap && aAddon.active) {
-      this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown", reason);
-      XPIStates.disableAddon(aAddon.id);
-      this.unloadBootstrapScope(aAddon.id);
-      XPIDatabase.updateAddonActive(aAddon, false);
-    }
-
-    // Notify any other providers that a new theme has been enabled
-    if (isTheme(aAddon.type) && aAddon.active)
-      AddonManagerPrivate.notifyAddonChanged(null, aAddon.type);
-  },
-
-  /**
-   * Cancels the pending uninstall of an add-on.
-   *
-   * @param  aAddon
-   *         The DBAddonInternal to cancel uninstall for
-   */
-  cancelUninstallAddon(aAddon) {
-    if (!(aAddon.inDatabase))
-      throw new Error("Can only cancel uninstall for installed addons.");
-    if (!aAddon.pendingUninstall)
-      throw new Error("Add-on is not marked to be uninstalled");
-
-    if (aAddon._installLocation.name != KEY_APP_TEMPORARY)
-      aAddon._installLocation.cleanStagingDir([aAddon.id]);
-
-    XPIDatabase.setAddonProperties(aAddon, {
-      pendingUninstall: false
-    });
-
-    if (!aAddon.visible)
-      return;
-
-    XPIStates.getAddon(aAddon.location, aAddon.id).syncWithDB(aAddon);
-    XPIStates.save();
-
-    Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
-
-    // TODO hide hidden add-ons (bug 557710)
-    let wrapper = aAddon.wrapper;
-    AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
-
-    if (aAddon.bootstrap && !aAddon.disabled) {
-      this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup",
-                               BOOTSTRAP_REASONS.ADDON_INSTALL);
-      XPIDatabase.updateAddonActive(aAddon, true);
-    }
-
-    // Notify any other providers that this theme is now enabled again.
-    if (isTheme(aAddon.type) && aAddon.active)
-      AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false);
-  }
 };
 
+for (let meth of ["cancelUninstallAddon", "getInstallForFile",
+                  "getInstallForURL", "installAddonFromLocation",
+                  "installAddonFromSources", "installTemporaryAddon",
+                  "isInstallAllowed", "uninstallAddon", "updateSystemAddons"]) {
+  XPIProvider[meth] = function() {
+    return XPIInstall[meth](...arguments);
+  };
+}
+
 // Maps instances of AddonInternal to AddonWrapper
 const wrapperMap = new WeakMap();
 let addonFor = wrapper => wrapperMap.get(wrapper);
 
 /**
  * The AddonInternal is an internal only representation of add-ons. It may
  * have come from the database (see DBAddonInternal in XPIProviderUtils.jsm)
  * or an install manifest.
@@ -5238,21 +4629,21 @@ class SystemAddonInstallLocation extends
    */
   isActive() {
     return this._directory != null;
   }
 }
 
 forwardInstallMethods(SystemAddonInstallLocation,
                       ["cleanDirectories", "cleanStagingDir", "getStagingDir",
-                        "getTrashDir", "installAddon", "installAddon",
-                        "installAddonSet", "isValid", "isValidAddon",
-                        "releaseStagingDir", "requestStagingDir",
-                        "resetAddonSet", "resumeAddonSet", "uninstallAddon",
-                        "uninstallAddon"]);
+                       "getTrashDir", "installAddon", "installAddon",
+                       "installAddonSet", "isValid", "isValidAddon",
+                       "releaseStagingDir", "requestStagingDir",
+                       "resetAddonSet", "resumeAddonSet", "uninstallAddon",
+                       "uninstallAddon"]);
 
 /** An object which identifies an install location for temporary add-ons.
  */
 const TemporaryInstallLocation = { locked: false, name: KEY_APP_TEMPORARY,
   scope: AddonManager.SCOPE_TEMPORARY,
   getAddonLocations: () => [], isLinkedAddon: () => false, installAddon:
     () => {}, uninstallAddon: (aAddon) => {}, getStagingDir: () => {},
 };
@@ -5358,18 +4749,20 @@ var XPIInternal = {
   BOOTSTRAP_REASONS,
   KEY_APP_SYSTEM_ADDONS,
   KEY_APP_SYSTEM_DEFAULTS,
   KEY_APP_TEMPORARY,
   PREF_BRANCH_INSTALLED_ADDON,
   PREF_SYSTEM_ADDON_SET,
   SIGNED_TYPES,
   SystemAddonInstallLocation,
+  TemporaryInstallLocation,
   TEMPORARY_ADDON_SUFFIX,
   TOOLKIT_ID,
+  XPI_PERMISSION,
   XPIStates,
   awaitPromise,
   getExternalType,
   isTheme,
   isUsableAddon,
   isWebExtension,
   mustSign,
   recordAddonTelemetry,