Bug 1363925: Part 8b - Move AddonInternal to XPIDatabase.jsm. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Sun, 22 Apr 2018 14:45:02 -0700
changeset 786321 295c3fc3ee8c153d358cc1e7c8b6f85925a03bd7
parent 786320 6c8467fbf0928b7d7f7b01c609c0edc52aedf3ad
child 786322 a61bc2c1054998ad0eb0219b74e8c4468c9eb8be
push id107433
push usermaglione.k@gmail.com
push dateSun, 22 Apr 2018 22:24:27 +0000
reviewersaswan
bugs1363925
milestone61.0a1
Bug 1363925: Part 8b - Move AddonInternal to XPIDatabase.jsm. r?aswan AddonInternal objects are only ever created after the database is loaded, so there's no reason to load that code beforehand. More importantly, creating the AddonWrapper class is expecially expensive, since most of their properties are created dynamically. We should avoid doing that at startup when at all possible. MozReview-Commit-ID: AaRVN12e1qM
toolkit/mozapps/extensions/internal/XPIDatabase.jsm
toolkit/mozapps/extensions/internal/XPIInstall.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -1,48 +1,62 @@
 /* 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";
 
 /* eslint "valid-jsdoc": [2, {requireReturn: false, requireReturnDescription: false, prefer: {return: "returns"}}] */
 
-var EXPORTED_SYMBOLS = ["XPIDatabase", "XPIDatabaseReconcile"];
+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 SIGNED_TYPES, BOOTSTRAP_REASONS, DB_SCHEMA,
-          AddonInternal, XPIProvider, XPIStates,
-          isUsableAddon, recordAddonTelemetry,
-          descriptorToPath */
+/* globals
+ *         BOOTSTRAP_REASONS,
+ *         DB_SCHEMA,
+ *         SIGNED_TYPES,
+ *         XPIProvider,
+ *         XPIStates,
+ *         descriptorToPath,
+ *         isTheme,
+ *         isUsableAddon,
+ *         isWebExtension,
+ *         recordAddonTelemetry,
+ */
 
 for (let sym of [
-  "AddonInternal",
   "BOOTSTRAP_REASONS",
   "DB_SCHEMA",
   "SIGNED_TYPES",
   "XPIProvider",
   "XPIStates",
   "descriptorToPath",
+  "isTheme",
   "isUsableAddon",
+  "isWebExtension",
   "recordAddonTelemetry",
 ]) {
   XPCOMUtils.defineLazyGetter(this, sym, () => XPIInternal[sym]);
 }
 
 ChromeUtils.import("resource://gre/modules/Log.jsm");
 const LOGGER_ID = "addons.xpi-utils";
 
@@ -53,25 +67,48 @@ const nsIFile = Components.Constructor("
 // (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 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",
                           "updateURL", "optionsURL",
                           "optionsType", "optionsBrowserStyle", "aboutURL",
                           "defaultLocale", "visible", "active", "userDisabled",
                           "appDisabled", "pendingUninstall", "installDate",
                           "updateDate", "applyBackgroundUpdates", "bootstrap", "path",
                           "skinnable", "size", "sourceURI", "releaseNotesURI",
@@ -80,16 +117,39 @@ const PROP_JSON_FIELDS = ["id", "syncGUI
                           "targetPlatforms", "signedState",
                           "seen", "dependencies", "hasEmbeddedWebExtension",
                           "userPermissions", "icons", "iconURL", "icon64URL",
                           "blocklistState", "blocklistURL", "startupData"];
 
 // 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.
  */
@@ -118,16 +178,1025 @@ function copyProperties(aObject, aProper
     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);
+
+/**
+ * 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.
+ */
+function AddonInternal() {
+  this._hasResourceCache = new Map();
+
+  XPCOMUtils.defineLazyGetter(this, "wrapper", () => {
+    return new AddonWrapper(this);
+  });
+}
+
+AddonInternal.prototype = {
+  _selectedLocale: null,
+  _hasResourceCache: null,
+  active: false,
+  visible: false,
+  userDisabled: false,
+  appDisabled: false,
+  softDisabled: false,
+  blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+  blocklistURL: null,
+  sourceURI: null,
+  releaseNotesURI: null,
+  foreignInstall: false,
+  seen: true,
+  skinnable: false,
+  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.
+   */
+  dependencies: Object.freeze([]),
+  hasEmbeddedWebExtension: false,
+
+  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) {
+      XPIProvider.updateAddonDisabledState(this, userDisabled, softDisabled);
+      XPIDatabase.saveChanges();
+    } else {
+      this.appDisabled = !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 = !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 = !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.
+ */
+function AddonWrapper(aAddon) {
+  wrapperMap.set(this, aAddon);
+}
+
+AddonWrapper.prototype = {
+  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}`);
+      }
+      XPIProvider.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))
+          XPIProvider.updateAddonDisabledState(addon, undefined, val);
+      } else {
+        XPIProvider.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;
+        XPIProvider.updateAddonDisabledState(addon, true);
+        Services.obs.notifyObservers(addonFile, "flush-cache-entry");
+        XPIProvider.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 {Object} aLoaded
  *        Addon data fields loaded from JSON or the addon manifest.
@@ -1022,16 +2091,35 @@ 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();
       }
     }
   },
+
+  /**
+   * 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
@@ -1404,17 +2492,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
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -15,46 +15,41 @@ var EXPORTED_SYMBOLS = [
 /* 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",
+
+  clearTimeout: "resource://gre/modules/Timer.jsm",
+  setTimeout: "resource://gre/modules/Timer.jsm",
+});
+
 XPCOMUtils.defineLazyGetter(this, "IconDetails", () => {
   return ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm", {}).ExtensionParent.IconDetails;
 });
-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, "XPIDatabase",
-                               "resource://gre/modules/addons/XPIDatabase.jsm");
-ChromeUtils.defineModuleGetter(this, "ZipUtils",
-                               "resource://gre/modules/ZipUtils.jsm");
-
-ChromeUtils.defineModuleGetter(this, "clearTimeout", "resource://gre/modules/Timer.jsm");
-ChromeUtils.defineModuleGetter(this, "setTimeout", "resource://gre/modules/Timer.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");
@@ -70,51 +65,45 @@ const RDFDataSource = Components.Constru
 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 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 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, XPI_PERMISSION, XPIStates, getExternalType, isTheme, isUsableAddon, isWebExtension, mustSign, recordAddonTelemetry */
+/* 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, isUsableAddon, isWebExtension, mustSign */
 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",
   "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
@@ -2039,17 +2028,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())
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -23,17 +23,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
   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",
   JSONFile: "resource://gre/modules/JSONFile.jsm",
   LegacyExtensionsUtils: "resource://gre/modules/LegacyExtensionsUtils.jsm",
 
-  UpdateChecker: "resource://gre/modules/addons/XPIInstall.jsm",
   XPIDatabase: "resource://gre/modules/addons/XPIDatabase.jsm",
   XPIDatabaseReconcile: "resource://gre/modules/addons/XPIDatabase.jsm",
   XPIInstall: "resource://gre/modules/addons/XPIInstall.jsm",
   verifyBundleSignedState: "resource://gre/modules/addons/XPIInstall.jsm",
 });
 
 const {nsIBlocklistService} = Ci;
 
@@ -43,20 +42,18 @@ XPCOMUtils.defineLazyServiceGetter(this,
 
 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";
 // 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.";
@@ -122,37 +119,16 @@ const XPI_PERMISSION                  = 
 const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
 const XPI_SIGNATURE_CHECK_PERIOD      = 24 * 60 * 60;
 
 XPCOMUtils.defineConstant(this, "DB_SCHEMA", 25);
 
 const NOTIFICATION_TOOLBOX_CONNECTION_CHANGE      = "toolbox-connection-change";
 
-// Properties that exist in the install manifest
-const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
-const PROP_LOCALE_MULTI  = ["developers", "translators", "contributors"];
-
-// 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"];
-
-// 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 encoded(strings, ...values) {
   let result = [];
 
   for (let [i, string] of strings.entries()) {
     result.push(string);
     if (i < values.length)
       result.push(encodeURIComponent(values[i]));
   }
@@ -216,21 +192,16 @@ function mustSign(aType) {
 // Keep track of where we are in startup for telemetry
 // event happened during XPIDatabase.startup()
 const XPI_STARTING = "XPIStarting";
 // event happened after startup() but before the final-ui-startup event
 const XPI_BEFORE_UI_STARTUP = "BeforeFinalUIStartup";
 // event happened after final-ui-startup
 const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup";
 
-const COMPATIBLE_BY_DEFAULT_TYPES = {
-  extension: true,
-  dictionary: true
-};
-
 var gGlobalScope = this;
 
 /**
  * Valid IDs fit this pattern.
  */
 var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i;
 
 ChromeUtils.import("resource://gre/modules/Log.jsm");
@@ -366,28 +337,16 @@ function descriptorToPath(descriptor, di
     let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
     file.persistentDescriptor = descriptor;
     return getRelativePath(file, dir);
   } catch (e) {
     return null;
   }
 }
 
-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;
-}
-
 /**
  * Helper function that determines whether an addon of a certain type is a
  * WebExtension.
  *
  * @param {string} type
  *        The add-on type to check.
  * @returns {boolean}
  */
@@ -655,35 +614,16 @@ function getDirectoryEntries(aDir, aSort
   } finally {
     if (dirEnum) {
       dirEnum.close();
     }
   }
 }
 
 /**
- * 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
- */
-function 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);
-  }
-}
-
-/**
  * The on-disk state of an individual XPI, created from an Object
  * as stored in the addonStartup.json file.
  */
 const JSON_FIELDS = Object.freeze([
   "bootstrapped",
   "changed",
   "dependencies",
   "enabled",
@@ -3061,1024 +3001,16 @@ for (let meth of ["cancelUninstallAddon"
                   "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 XPIDatabase.jsm)
- * or an install manifest.
- */
-function AddonInternal() {
-  this._hasResourceCache = new Map();
-
-  XPCOMUtils.defineLazyGetter(this, "wrapper", () => {
-    return new AddonWrapper(this);
-  });
-}
-
-AddonInternal.prototype = {
-  _selectedLocale: null,
-  _hasResourceCache: null,
-  active: false,
-  visible: false,
-  userDisabled: false,
-  appDisabled: false,
-  softDisabled: false,
-  blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
-  blocklistURL: null,
-  sourceURI: null,
-  releaseNotesURI: null,
-  foreignInstall: false,
-  seen: true,
-  skinnable: false,
-  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.
-   */
-  dependencies: Object.freeze([]),
-  hasEmbeddedWebExtension: false,
-
-  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) {
-      XPIProvider.updateAddonDisabledState(this, userDisabled, softDisabled);
-      XPIDatabase.saveChanges();
-    } else {
-      this.appDisabled = !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 = !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 = !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.
- */
-function AddonWrapper(aAddon) {
-  wrapperMap.set(this, aAddon);
-}
-
-AddonWrapper.prototype = {
-  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 getExternalType(addonFor(this).type);
-  },
-
-  get isWebExtension() {
-    return isWebExtension(addonFor(this).type);
-  },
-
-  get temporarilyInstalled() {
-    return addonFor(this)._installLocation == 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 && 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}`);
-      }
-      XPIProvider.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))
-          XPIProvider.updateAddonDisabledState(addon, undefined, val);
-      } else {
-        XPIProvider.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;
-        XPIProvider.updateAddonDisabledState(addon, true);
-        Services.obs.notifyObservers(addonFile, "flush-cache-entry");
-        XPIProvider.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 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;
-  });
-});
-
 function forwardInstallMethods(cls, methods) {
   for (let meth of methods) {
     cls.prototype[meth] = function() {
       return XPIInstall[cls.name].prototype[meth].apply(this, arguments);
     };
   }
 }
 
@@ -4575,41 +3507,41 @@ class WinRegInstallLocation extends Dire
    * @see DirectoryInstallLocation
    */
   isLinkedAddon(aId) {
     return true;
   }
 }
 
 var XPIInternal = {
-  AddonInternal,
   BOOTSTRAP_REASONS,
   DB_SCHEMA,
   KEY_APP_SYSTEM_ADDONS,
   KEY_APP_SYSTEM_DEFAULTS,
   KEY_APP_TEMPORARY,
   PREF_BRANCH_INSTALLED_ADDON,
   PREF_SYSTEM_ADDON_SET,
   SIGNED_TYPES,
   SystemAddonInstallLocation,
   TEMPORARY_ADDON_SUFFIX,
   TOOLKIT_ID,
   TemporaryInstallLocation,
   XPIProvider,
   XPIStates,
   XPI_PERMISSION,
   awaitPromise,
+  canRunInSafeMode,
   descriptorToPath,
   getExternalType,
+  getURIForResourceInFile,
   isDisabledLegacy,
   isTheme,
   isUsableAddon,
   isWebExtension,
   mustSign,
-  recordAddonTelemetry,
 };
 
 var addonTypes = [
   new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS,
                                     "type.extension.name",
                                     AddonManager.VIEW_TYPE_LIST, 4000,
                                     AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL),
   new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS,