Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/toolkit/mozapps/extensions/internal/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 95 kB image not shown  

Quelle  XPIProvider.sys.mjs   Sprache: unbekannt

 
/* 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/. */

/**
 * This file contains most of the logic required to load and run
 * extensions at startup. Anything which is not required immediately at
 * startup should go in XPIInstall.sys.mjs or XPIDatabase.sys.mjs if at all
 * possible, in order to minimize the impact on startup performance.
 */

/**
 * @typedef {number} integer
 */

/* eslint "valid-jsdoc": [2, {requireReturn: false, requireReturnDescription: false, prefer: {return: "returns"}}] */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

import { XPIExports } from "resource://gre/modules/addons/XPIExports.sys.mjs";
import {
  AddonManager,
  AddonManagerPrivate,
} from "resource://gre/modules/AddonManager.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs",
  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
  Dictionary: "resource://gre/modules/Extension.sys.mjs",
  Extension: "resource://gre/modules/Extension.sys.mjs",
  ExtensionData: "resource://gre/modules/Extension.sys.mjs",
  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
  JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
  Langpack: "resource://gre/modules/Extension.sys.mjs",
  TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs",
});

XPCOMUtils.defineLazyServiceGetters(lazy, {
  aomStartup: [
    "@mozilla.org/addons/addon-manager-startup;1",
    "amIAddonManagerStartup",
  ],
  resProto: [
    "@mozilla.org/network/protocol;1?name=resource",
    "nsISubstitutingProtocolHandler",
  ],
  spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"],
  timerManager: [
    "@mozilla.org/updates/timer-manager;1",
    "nsIUpdateTimerManager",
  ],
});

const nsIFile = Components.Constructor(
  "@mozilla.org/file/local;1",
  "nsIFile",
  "initWithPath"
);
const FileInputStream = Components.Constructor(
  "@mozilla.org/network/file-input-stream;1",
  "nsIFileInputStream",
  "init"
);

const PREF_DB_SCHEMA = "extensions.databaseSchema";
const PREF_PENDING_OPERATIONS = "extensions.pendingOperations";
const PREF_EM_ENABLED_SCOPES = "extensions.enabledScopes";
const PREF_EM_STARTUP_SCAN_SCOPES = "extensions.startupScanScopes";
// xpinstall.signatures.required only supported in dev builds
const PREF_XPI_SIGNATURES_REQUIRED = "xpinstall.signatures.required";
const PREF_LANGPACK_SIGNATURES = "extensions.langpacks.signatures.required";
const PREF_INSTALL_DISTRO_ADDONS = "extensions.installDistroAddons";
const PREF_BRANCH_INSTALLED_ADDON = "extensions.installedDistroAddon.";
const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet";

const PREF_EM_LAST_APP_BUILD_ID = "extensions.lastAppBuildId";

// Specify a list of valid built-in add-ons to load.
const BUILT_IN_ADDONS_URI = "chrome://browser/content/built_in_addons.json";

const DIR_EXTENSIONS = "extensions";
const DIR_SYSTEM_ADDONS = "features";
const DIR_APP_SYSTEM_PROFILE = "system-extensions";
const DIR_STAGE = "staged";
const DIR_TRASH = "trash";

const FILE_XPI_STATES = "addonStartup.json.lz4";

const KEY_PROFILEDIR = "ProfD";
const KEY_ADDON_APP_DIR = "XREAddonAppDir";
const KEY_APP_DISTRIBUTION = "XREAppDist";
const KEY_APP_FEATURES = "XREAppFeat";

const KEY_APP_PROFILE = "app-profile";
const KEY_APP_SYSTEM_PROFILE = "app-system-profile";
const KEY_APP_SYSTEM_ADDONS = "app-system-addons";
const KEY_APP_SYSTEM_DEFAULTS = "app-system-defaults";
const KEY_APP_BUILTINS = "app-builtin";
const KEY_APP_GLOBAL = "app-global";
const KEY_APP_SYSTEM_LOCAL = "app-system-local";
const KEY_APP_SYSTEM_SHARE = "app-system-share";
const KEY_APP_SYSTEM_USER = "app-system-user";
const KEY_APP_TEMPORARY = "app-temporary";

const TEMPORARY_ADDON_SUFFIX = "@temporary-addon";

const STARTUP_MTIME_SCOPES = [
  KEY_APP_GLOBAL,
  KEY_APP_SYSTEM_LOCAL,
  KEY_APP_SYSTEM_SHARE,
  KEY_APP_SYSTEM_USER,
];

const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions";
const XPI_PERMISSION = "install";

const XPI_SIGNATURE_CHECK_PERIOD = 24 * 60 * 60;

const DB_SCHEMA = 37;

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "enabledScopesPref",
  PREF_EM_ENABLED_SCOPES,
  AddonManager.SCOPE_ALL
);

Object.defineProperty(lazy, "enabledScopes", {
  get() {
    // The profile location is always enabled
    return lazy.enabledScopesPref | AddonManager.SCOPE_PROFILE;
  },
});

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]));
    }
  }

  return result.join("");
}

const BOOTSTRAP_REASONS = {
  APP_STARTUP: 1,
  APP_SHUTDOWN: 2,
  ADDON_ENABLE: 3,
  ADDON_DISABLE: 4,
  ADDON_INSTALL: 5,
  ADDON_UNINSTALL: 6,
  ADDON_UPGRADE: 7,
  ADDON_DOWNGRADE: 8,
};

// All addonTypes supported by the XPIProvider. These values can be passed to
// AddonManager.getAddonsByTypes in order to get XPIProvider.getAddonsByTypes
// to return only supported add-ons. Without these, it is possible for
// AddonManager.getAddonsByTypes to return addons from other providers, or even
// add-on types that are no longer supported by XPIProvider.
const ALL_XPI_TYPES = new Set(["dictionary", "extension", "locale", "theme"]);

/**
 * 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;

import { Log } from "resource://gre/modules/Log.sys.mjs";

const LOGGER_ID = "addons.xpi";

// Create a new logger for use by all objects in this Addons XPI Provider module
// (Requires AddonManager.sys.mjs)
var logger = Log.repository.getLogger(LOGGER_ID);

/**
 * Spins the event loop until the given promise resolves, and then eiter returns
 * its success value or throws its rejection value.
 *
 * @param {Promise} promise
 *        The promise to await.
 * @returns {any}
 *        The promise's resolution value, if any.
 */
function awaitPromise(promise) {
  let success = undefined;
  let result = null;

  promise.then(
    val => {
      success = true;
      result = val;
    },
    val => {
      success = false;
      result = val;
    }
  );

  Services.tm.spinEventLoopUntil(
    "XPIProvider.sys.mjs:awaitPromise",
    () => success !== undefined
  );

  if (!success) {
    throw result;
  }
  return result;
}

/**
 * Returns a nsIFile instance for the given path, relative to the given
 * base file, if provided.
 *
 * @param {string} path
 *        The (possibly relative) path of the file.
 * @param {nsIFile} [base]
 *        An optional file to use as a base path if `path` is relative.
 * @returns {nsIFile}
 */
function getFile(path, base = null) {
  // First try for an absolute path, as we get in the case of proxy
  // files. Ideally we would try a relative path first, but on Windows,
  // paths which begin with a drive letter are valid as relative paths,
  // and treated as such.
  try {
    return new nsIFile(path);
  } catch (e) {
    // Ignore invalid relative paths. The only other error we should see
    // here is EOM, and either way, any errors that we care about should
    // be re-thrown below.
  }

  // If the path isn't absolute, we must have a base path.
  let file = base.clone();
  file.appendRelativePath(path);
  return file;
}

/**
 * Returns true if the given file, based on its name, should be treated
 * as an XPI. If the file does not have an appropriate extension, it is
 * assumed to be an unpacked add-on.
 *
 * @param {string} filename
 *        The filename to check.
 * @param {boolean} [strict = false]
 *        If true, this file is in a location maintained by the browser, and
 *        must have a strict, lower-case ".xpi" extension.
 * @returns {boolean}
 *        True if the file is an XPI.
 */
function isXPI(filename, strict) {
  if (strict) {
    return filename.endsWith(".xpi");
  }
  let ext = filename.slice(-4).toLowerCase();
  return ext === ".xpi" || ext === ".zip";
}

/**
 * Returns the extension expected ID for a given file in an extension install
 * directory.
 *
 * @param {nsIFile} file
 *        The extension XPI file or unpacked directory.
 * @returns {AddonId?}
 *        The add-on ID, if valid, or null otherwise.
 */
function getExpectedID(file) {
  let { leafName } = file;
  let id = isXPI(leafName, true) ? leafName.slice(0, -4) : leafName;
  if (gIDTest.test(id)) {
    return id;
  }
  return null;
}

/**
 * Evaluates whether an add-on is allowed to run in safe mode.
 *
 * @param {AddonInternal} aAddon
 *        The add-on to check
 * @returns {boolean}
 *        True if the add-on should run in safe mode
 */
function canRunInSafeMode(aAddon) {
  let location = aAddon.location || null;
  if (!location) {
    return false;
  }

  // Even though the updated system add-ons aren't generally run in safe mode we
  // include them here so their uninstall functions get called when switching
  // back to the default set.

  // TODO product should make the call about temporary add-ons running
  // in safe mode. assuming for now that they are.
  return location.isTemporary || location.isSystem || location.isBuiltin;
}

/**
 * Gets an nsIURI for a file within another file, either a directory or an XPI
 * file. If aFile is a directory then this will return a file: URI, if it is an
 * XPI file then it will return a jar: URI.
 *
 * @param {nsIFile} aFile
 *        The file containing the resources, must be either a directory or an
 *        XPI file
 * @param {string} aPath
 *        The path to find the resource at, "/" separated. If aPath is empty
 *        then the uri to the root of the contained files will be returned
 * @returns {nsIURI}
 *        An nsIURI pointing at the resource
 */
function getURIForResourceInFile(aFile, aPath) {
  if (!isXPI(aFile.leafName)) {
    let resource = aFile.clone();
    if (aPath) {
      aPath.split("/").forEach(part => resource.append(part));
    }

    return Services.io.newFileURI(resource);
  }

  return buildJarURI(aFile, aPath);
}

/**
 * Creates a jar: URI for a file inside a ZIP file.
 *
 * @param {nsIFile} aJarfile
 *        The ZIP file as an nsIFile
 * @param {string} aPath
 *        The path inside the ZIP file
 * @returns {nsIURI}
 *        An nsIURI for the file
 */
function buildJarURI(aJarfile, aPath) {
  let uri = Services.io.newFileURI(aJarfile);
  uri = "jar:" + uri.spec + "!/" + aPath;
  return Services.io.newURI(uri);
}

function maybeResolveURI(uri) {
  if (uri.schemeIs("resource")) {
    return Services.io.newURI(lazy.resProto.resolveURI(uri));
  }
  return uri;
}

/**
 * Iterates over the entries in a given directory.
 *
 * Fails silently if the given directory does not exist.
 *
 * @param {nsIFile} aDir
 *        Directory to iterate.
 */
function* iterDirectory(aDir) {
  let dirEnum;
  try {
    dirEnum = aDir.directoryEntries;
    let file;
    while ((file = dirEnum.nextFile)) {
      yield file;
    }
  } catch (e) {
    if (aDir.exists()) {
      logger.warn(`Can't iterate directory ${aDir.path}`, e);
    }
  } finally {
    if (dirEnum) {
      dirEnum.close();
    }
  }
}

/**
 * Migrate data about an addon to match the change made in bug 857456
 * in which "webextension-foo" types were converted to "foo" and the
 * "loader" property was added to distinguish different addon types.
 *
 * @param {Object} addon  The addon info to migrate.
 * @returns {boolean} True if the addon data was converted, false if not.
 */
function migrateAddonLoader(addon) {
  if (addon.hasOwnProperty("loader")) {
    return false;
  }

  switch (addon.type) {
    case "extension":
    case "dictionary":
    case "locale":
    case "theme":
      addon.loader = "bootstrap";
      break;

    case "webextension":
      addon.type = "extension";
      addon.loader = null;
      break;

    case "webextension-dictionary":
      addon.type = "dictionary";
      addon.loader = null;
      break;

    case "webextension-langpack":
      addon.type = "locale";
      addon.loader = null;
      break;

    case "webextension-theme":
      addon.type = "theme";
      addon.loader = null;
      break;

    default:
      logger.warn(`Not converting unknown addon type ${addon.type}`);
  }
  return true;
}

/**
 * The on-disk state of an individual XPI, created from an Object
 * as stored in the addonStartup.json file.
 */
const JSON_FIELDS = Object.freeze([
  "blocklistState",
  "dependencies",
  "enabled",
  "file",
  "loader",
  "lastModifiedTime",
  "path",
  "recommendationState",
  "rootURI",
  "runInSafeMode",
  "signedState",
  "signedDate",
  "startupData",
  "telemetryKey",
  "type",
  "version",
]);

class XPIState {
  constructor(location, id, saved = {}) {
    this.location = location;
    this.id = id;

    // Set default values.
    this.type = "extension";

    for (let prop of JSON_FIELDS) {
      if (prop in saved) {
        this[prop] = saved[prop];
      }
    }

    // Builds prior to be 1512436 did not include the rootURI property.
    // If we're updating from such a build, add that property now.
    if (!("rootURI" in this) && this.file) {
      this.rootURI = getURIForResourceInFile(this.file, "").spec;
    }

    if (!this.telemetryKey) {
      this.telemetryKey = this.getTelemetryKey();
    }

    if (
      saved.currentModifiedTime &&
      saved.currentModifiedTime != this.lastModifiedTime
    ) {
      this.lastModifiedTime = saved.currentModifiedTime;
    } else if (saved.currentModifiedTime === null) {
      this.missing = true;
    }
  }

  // Compatibility shim getters for legacy callers in XPIDatabase.sys.mjs.
  get mtime() {
    return this.lastModifiedTime;
  }
  get active() {
    return this.enabled;
  }

  /**
   * @property {string} path
   *        The full on-disk path of the add-on.
   */
  get path() {
    return this.file && this.file.path;
  }
  set path(path) {
    this.file = path ? getFile(path, this.location.dir) : null;
  }

  /**
   * @property {string} relativePath
   *        The path to the add-on relative to its parent location, or
   *        the full path if its parent location has no on-disk path.
   */
  get relativePath() {
    if (this.location.dir && this.location.dir.contains(this.file)) {
      let path = this.file.getRelativePath(this.location.dir);
      if (AppConstants.platform == "win") {
        path = path.replace(/\//g, "\\");
      }
      return path;
    }
    return this.path;
  }

  /**
   * Returns a JSON-compatible representation of this add-on's state
   * data, to be saved to addonStartup.json.
   *
   * @returns {Object}
   */
  toJSON() {
    let json = {
      blocklistState: this.blocklistState,
      dependencies: this.dependencies,
      enabled: this.enabled,
      lastModifiedTime: this.lastModifiedTime,
      loader: this.loader,
      path: this.relativePath,
      recommendationState: this.recommendationState,
      rootURI: this.rootURI,
      runInSafeMode: this.runInSafeMode,
      signedState: this.signedState,
      signedDate: this.signedDate,
      telemetryKey: this.telemetryKey,
      version: this.version,
    };
    if (this.type != "extension") {
      json.type = this.type;
    }
    if (this.startupData) {
      json.startupData = this.startupData;
    }
    return json;
  }

  get isWebExtension() {
    return this.loader == null;
  }

  get isPrivileged() {
    return lazy.ExtensionData.getIsPrivileged({
      signedState: this.signedState,
      builtIn: this.location.isBuiltin,
      temporarilyInstalled: this.location.isTemporary,
    });
  }

  /**
   * Update the last modified time for an add-on on disk.
   *
   * @param {nsIFile} aFile
   *        The location of the add-on.
   * @returns {boolean}
   *       True if the time stamp has changed.
   */
  getModTime(aFile) {
    let mtime = 0;
    try {
      // Clone the file object so we always get the actual mtime, rather
      // than whatever value it may have cached.
      mtime = aFile.clone().lastModifiedTime;
    } catch (e) {
      logger.warn("Can't get modified time of ${path}", aFile, e);
    }

    let changed = mtime != this.lastModifiedTime;
    this.lastModifiedTime = mtime;
    return changed;
  }

  /**
   * Returns a string key by which to identify this add-on in telemetry
   * and crash reports.
   *
   * @returns {string}
   */
  getTelemetryKey() {
    return encoded`${this.id}:${this.version}`;
  }

  get resolvedRootURI() {
    return maybeResolveURI(Services.io.newURI(this.rootURI));
  }

  /**
   * Update the XPIState to match an XPIDatabase entry; if 'enabled' is changed to true,
   * update the last-modified time. This should probably be made async, but for now we
   * don't want to maintain parallel sync and async versions of the scan.
   *
   * Caller is responsible for doing XPIStates.save() if necessary.
   *
   * @param {DBAddonInternal} aDBAddon
   *        The DBAddonInternal for this add-on.
   * @param {boolean} [aUpdated = false]
   *        The add-on was updated, so we must record new modified time.
   */
  syncWithDB(aDBAddon, aUpdated = false) {
    logger.debug("Updating XPIState for " + JSON.stringify(aDBAddon));
    // If the add-on changes from disabled to enabled, we should re-check the modified time.
    // If this is a newly found add-on, it won't have an 'enabled' field but we
    // did a full recursive scan in that case, so we don't need to do it again.
    // We don't use aDBAddon.active here because it's not updated until after restart.
    let mustGetMod = aDBAddon.visible && !aDBAddon.disabled && !this.enabled;

    this.enabled = aDBAddon.visible && !aDBAddon.disabled;

    this.version = aDBAddon.version;
    this.type = aDBAddon.type;
    this.loader = aDBAddon.loader;

    if (aDBAddon.startupData) {
      this.startupData = aDBAddon.startupData;
    }

    this.telemetryKey = this.getTelemetryKey();

    this.dependencies = aDBAddon.dependencies;
    this.runInSafeMode = canRunInSafeMode(aDBAddon);
    this.signedState = aDBAddon.signedState;
    this.signedDate = aDBAddon.signedDate;
    this.file = aDBAddon._sourceBundle;
    this.rootURI = aDBAddon.rootURI;
    this.recommendationState = aDBAddon.recommendationState;
    this.blocklistState = aDBAddon.blocklistState;

    if ((aUpdated || mustGetMod) && this.file) {
      this.getModTime(this.file);
      if (this.lastModifiedTime != aDBAddon.updateDate) {
        aDBAddon.updateDate = this.lastModifiedTime;
        if (XPIExports.XPIDatabase.initialized) {
          XPIExports.XPIDatabase.saveChanges();
        }
      }
    }
  }
}

/**
 * Manages the state data for add-ons in a given install location.
 *
 * @param {string} name
 *        The name of the install location (e.g., "app-profile").
 * @param {string | nsIFile | null} path
 *        The on-disk path of the install location. May be null for some
 *        locations which do not map to a specific on-disk path.
 * @param {integer} scope
 *        The scope of add-ons installed in this location.
 * @param {object} [saved]
 *        The persisted JSON state data to restore.
 */
class XPIStateLocation extends Map {
  constructor(name, path, scope, saved) {
    super();

    this.name = name;
    this.scope = scope;
    if (path instanceof Ci.nsIFile) {
      this.dir = path;
      this.path = path.path;
    } else {
      this.path = path;
      this.dir = this.path && new nsIFile(this.path);
    }
    this.staged = {};
    this.changed = false;

    if (saved) {
      this.restore(saved);
    }

    this._installer = undefined;
  }

  hasPrecedence(otherLocation) {
    let locations = Array.from(XPIStates.locations());
    return locations.indexOf(this) <= locations.indexOf(otherLocation);
  }

  get installer() {
    if (this._installer === undefined) {
      this._installer = this.makeInstaller();
    }
    return this._installer;
  }

  makeInstaller() {
    return null;
  }

  restore(saved) {
    if (!this.path && saved.path) {
      this.path = saved.path;
      this.dir = new nsIFile(this.path);
    }
    this.staged = saved.staged || {};
    this.changed = saved.changed || false;

    for (let [id, data] of Object.entries(saved.addons || {})) {
      let xpiState = this._addState(id, data);

      // Make a note that this state was restored from saved data. But
      // only if this location hasn't moved since the last startup,
      // since that causes problems for new system add-on bundles.
      if (!this.path || this.path == saved.path) {
        xpiState.wasRestored = true;
      }
    }
  }

  /**
   * Returns a JSON-compatible representation of this location's state
   * data, to be saved to addonStartup.json.
   *
   * @returns {Object}
   */
  toJSON() {
    let json = {
      addons: {},
      staged: this.staged,
    };

    if (this.path) {
      json.path = this.path;
    }

    if (STARTUP_MTIME_SCOPES.includes(this.name)) {
      json.checkStartupModifications = true;
    }

    for (let [id, addon] of this.entries()) {
      json.addons[id] = addon;
    }
    return json;
  }

  get hasStaged() {
    for (let key in this.staged) {
      return true;
    }
    return false;
  }

  _addState(addonId, saved) {
    let xpiState = new XPIState(this, addonId, saved);
    this.set(addonId, xpiState);
    return xpiState;
  }

  /**
   * Adds state data for the given DB add-on to the DB.
   *
   * @param {DBAddon} addon
   *        The DBAddon to add.
   */
  addAddon(addon) {
    logger.debug(
      "XPIStates adding add-on ${id} in ${location}: ${path}",
      addon
    );

    XPIProvider.persistStartupData(addon);

    let xpiState = this._addState(addon.id, { file: addon._sourceBundle });
    xpiState.syncWithDB(addon, true);

    XPIProvider.addTelemetry(addon.id, { location: this.name });
  }

  /**
   * Remove the XPIState for an add-on and save the new state.
   *
   * @param {string} aId
   *        The ID of the add-on.
   */
  removeAddon(aId) {
    if (this.has(aId)) {
      this.delete(aId);
      XPIStates.save();
    }
  }

  /**
   * Adds stub state data for the local file to the DB.
   *
   * @param {string} addonId
   *        The ID of the add-on represented by the given file.
   * @param {nsIFile} file
   *        The local file or directory containing the add-on.
   * @returns {XPIState}
   */
  addFile(addonId, file) {
    let xpiState = this._addState(addonId, {
      enabled: false,
      file: file.clone(),
    });
    xpiState.getModTime(xpiState.file);
    return xpiState;
  }

  /**
   * Adds metadata for a staged install which should be performed after
   * the next restart.
   *
   * @param {string} addonId
   *        The ID of the staged install. The leaf name of the XPI
   *        within the location's staging directory must correspond to
   *        this ID.
   * @param {object} metadata
   *        The JSON metadata of the parsed install, to be used during
   *        the next startup.
   */
  stageAddon(addonId, metadata) {
    this.staged[addonId] = metadata;
    XPIStates.save();
  }

  /**
   * Removes staged install metadata for the given add-on ID.
   *
   * @param {string} addonId
   *        The ID of the staged install.
   */
  unstageAddon(addonId) {
    if (addonId in this.staged) {
      delete this.staged[addonId];
      XPIStates.save();
    }
  }

  *getStagedAddons() {
    for (let [id, metadata] of Object.entries(this.staged)) {
      yield [id, metadata];
    }
  }

  /**
   * Returns true if the given addon was installed in this location by a text
   * file pointing to its real path.
   *
   * @param {string} aId
   *        The ID of the addon
   * @returns {boolean}
   */
  isLinkedAddon(aId) {
    if (!this.dir) {
      return true;
    }
    return this.has(aId) && !this.dir.contains(this.get(aId).file);
  }

  get isTemporary() {
    return false;
  }

  get isSystem() {
    return false;
  }

  get isBuiltin() {
    return false;
  }

  get hidden() {
    return this.isBuiltin;
  }

  // If this property is false, it does not implement readAddons()
  // interface.  This is used for the temporary and built-in locations
  // that do not correspond to a physical location that can be scanned.
  get enumerable() {
    return true;
  }
}

class TemporaryLocation extends XPIStateLocation {
  /**
   * @param {string} name
   *        The string identifier for the install location.
   */
  constructor(name) {
    super(name, null, AddonManager.SCOPE_TEMPORARY);
    this.locked = false;
  }

  makeInstaller() {
    // Installs are a no-op. We only register that add-ons exist, and
    // run them from their current location.
    return {
      installAddon() {},
      uninstallAddon() {},
    };
  }

  toJSON() {
    return {};
  }

  get isTemporary() {
    return true;
  }

  get enumerable() {
    return false;
  }
}

var TemporaryInstallLocation = new TemporaryLocation(KEY_APP_TEMPORARY);

/**
 * A "location" for addons installed from assets packged into the app.
 */
var BuiltInLocation = new (class _BuiltInLocation extends XPIStateLocation {
  constructor() {
    super(KEY_APP_BUILTINS, null, AddonManager.SCOPE_APPLICATION);
    this.locked = false;
  }

  // The installer object is responsible for moving files around on disk
  // when (un)installing an addon.  Since this location handles only addons
  // that are embedded within the browser, these are no-ops.
  makeInstaller() {
    return {
      installAddon() {},
      uninstallAddon() {},
    };
  }

  get hidden() {
    return false;
  }

  get isBuiltin() {
    return true;
  }

  get enumerable() {
    return false;
  }

  // Builtin addons are never linked, return false
  // here for correct behavior elsewhere.
  isLinkedAddon(/* aId */) {
    return false;
  }
})();

/**
 * An object which identifies a directory install location for add-ons. The
 * location consists of a directory which contains the add-ons installed in the
 * location.
 *
 */
class DirectoryLocation extends XPIStateLocation {
  /**
   * Each add-on installed in the location is either a directory containing the
   * add-on's files or a text file containing an absolute path to the directory
   * containing the add-ons files. The directory or text file must have the same
   * name as the add-on's ID.
   *
   * @param {string} name
   *        The string identifier for the install location.
   * @param {nsIFile} dir
   *        The directory for the install location.
   * @param {integer} scope
   *        The scope of add-ons installed in this location.
   * @param {boolean} [locked = true]
   *        If false, the location accepts new add-on installs.
   * @param {boolean} [system = false]
   *        If true, the location is a system addon location.
   */
  constructor(name, dir, scope, locked = true, system = false) {
    super(name, dir, scope);
    this.locked = locked;
    this._isSystem = system;
  }

  makeInstaller() {
    if (this.locked) {
      return null;
    }
    return new XPIExports.XPIInstall.DirectoryInstaller(this);
  }

  /**
   * Reads a single-line file containing the path to a directory, and
   * returns an nsIFile pointing to that directory, if successful.
   *
   * @param {nsIFile} aFile
   *        The file containing the directory path
   * @returns {nsIFile?}
   *        An nsIFile object representing the linked directory, or null
   *        on error.
   */
  _readLinkFile(aFile) {
    let linkedDirectory;
    if (aFile.isSymlink()) {
      linkedDirectory = aFile.clone();
      try {
        linkedDirectory.normalize();
      } catch (e) {
        logger.warn(
          `Symbolic link ${aFile.path} points to a path ` +
            `which does not exist`
        );
        return null;
      }
    } else {
      let fis = new FileInputStream(aFile, -1, -1, false);
      let line = {};
      fis.QueryInterface(Ci.nsILineInputStream).readLine(line);
      fis.close();

      if (line.value) {
        linkedDirectory = Cc["@mozilla.org/file/local;1"].createInstance(
          Ci.nsIFile
        );
        try {
          linkedDirectory.initWithPath(line.value);
        } catch (e) {
          linkedDirectory.setRelativeDescriptor(aFile.parent, line.value);
        }
      }
    }

    if (linkedDirectory) {
      if (!linkedDirectory.exists()) {
        logger.warn(
          `File pointer ${aFile.path} points to ${linkedDirectory.path} ` +
            "which does not exist"
        );
        return null;
      }

      if (!linkedDirectory.isDirectory()) {
        logger.warn(
          `File pointer ${aFile.path} points to ${linkedDirectory.path} ` +
            "which is not a directory"
        );
        return null;
      }

      return linkedDirectory;
    }

    logger.warn(`File pointer ${aFile.path} does not contain a path`);
    return null;
  }

  /**
   * Finds all the add-ons installed in this location.
   *
   * @returns {Map<AddonID, nsIFile>}
   *        A map of add-ons present in this location.
   */
  readAddons() {
    let addons = new Map();

    if (!this.dir) {
      return addons;
    }

    // Use a snapshot of the directory contents to avoid possible issues with
    // iterating over a directory while removing files from it (the YAFFS2
    // embedded filesystem has this issue, see bug 772238).
    for (let entry of Array.from(iterDirectory(this.dir))) {
      let id = getExpectedID(entry);
      if (!id) {
        if (![DIR_STAGE, DIR_TRASH].includes(entry.leafName)) {
          logger.debug(
            "Ignoring file: name is not a valid add-on ID: ${}",
            entry.path
          );
        }
        continue;
      }

      if (id == entry.leafName && (entry.isFile() || entry.isSymlink())) {
        let newEntry = this._readLinkFile(entry);
        if (!newEntry) {
          logger.debug(`Deleting stale pointer file ${entry.path}`);
          try {
            entry.remove(true);
          } catch (e) {
            logger.warn(`Failed to remove stale pointer file ${entry.path}`, e);
            // Failing to remove the stale pointer file is ignorable
          }
          continue;
        }

        entry = newEntry;
      }

      addons.set(id, entry);
    }
    return addons;
  }

  get isSystem() {
    return this._isSystem;
  }
}

/**
 * An object which identifies a built-in install location for add-ons, such
 * as default system add-ons.
 *
 * This location should point either to a XPI, or a directory in a local build.
 */
class SystemAddonDefaults extends DirectoryLocation {
  /**
   * Read the manifest of allowed add-ons and build a mapping between ID and URI
   * for each.
   *
   * @returns {Map<AddonID, nsIFile>}
   *        A map of add-ons present in this location.
   */
  readAddons() {
    let addons = new Map();

    let manifest = XPIProvider.builtInAddons;

    if (!("system" in manifest)) {
      logger.debug("No list of valid system add-ons found.");
      return addons;
    }

    for (let id of manifest.system) {
      let file = this.dir.clone();
      file.append(`${id}.xpi`);

      // Only attempt to load unpacked directory if unofficial build.
      if (!AppConstants.MOZILLA_OFFICIAL && !file.exists()) {
        file = this.dir.clone();
        file.append(`${id}`);
      }

      addons.set(id, file);
    }

    return addons;
  }

  get isSystem() {
    return true;
  }

  get isBuiltin() {
    return true;
  }
}

/**
 * An object which identifies a directory install location for system add-ons
 * updates.
 */
class SystemAddonLocation extends DirectoryLocation {
  /**
   * The location consists of a directory which contains the add-ons installed.
   *
   * @param {string} name
   *        The string identifier for the install location.
   * @param {nsIFile} dir
   *        The directory for the install location.
   * @param {integer} scope
   *        The scope of add-ons installed in this location.
   * @param {boolean} resetSet
   *        True to throw away the current add-on set
   */
  constructor(name, dir, scope, resetSet) {
    let addonSet = SystemAddonLocation._loadAddonSet();
    let directory = null;

    // The system add-on update directory is stored in a pref.
    // Therefore, this is looked up before calling the
    // constructor on the superclass.
    if (addonSet.directory) {
      directory = getFile(addonSet.directory, dir);
      logger.info(`SystemAddonLocation scanning directory ${directory.path}`);
    } else {
      logger.info("SystemAddonLocation directory is missing");
    }

    super(name, directory, scope, false);

    this._addonSet = addonSet;
    this._baseDir = dir;

    if (resetSet) {
      this.installer.resetAddonSet();
    }
  }

  makeInstaller() {
    if (this.locked) {
      return null;
    }
    return new XPIExports.XPIInstall.SystemAddonInstaller(this);
  }

  /**
   * Reads the current set of system add-ons
   *
   * @returns {Object}
   */
  static _loadAddonSet() {
    try {
      let setStr = Services.prefs.getStringPref(PREF_SYSTEM_ADDON_SET, null);
      if (setStr) {
        let addonSet = JSON.parse(setStr);
        if (typeof addonSet == "object" && addonSet.schema == 1) {
          return addonSet;
        }
      }
    } catch (e) {
      logger.error("Malformed system add-on set, resetting.");
    }

    return { schema: 1, addons: {} };
  }

  readAddons() {
    // Updated system add-ons are ignored in safe mode
    if (Services.appinfo.inSafeMode) {
      return new Map();
    }

    let addons = super.readAddons();

    // Strip out any unexpected add-ons from the list
    for (let id of addons.keys()) {
      if (!(id in this._addonSet.addons)) {
        addons.delete(id);
      }
    }

    return addons;
  }

  /**
   * Tests whether updated system add-ons are expected.
   *
   * @returns {boolean}
   */
  isActive() {
    return this.dir != null;
  }

  get isSystem() {
    return true;
  }

  get isBuiltin() {
    return true;
  }
}

/**
 * An object that identifies a registry install location for add-ons. The location
 * consists of a registry key which contains string values mapping ID to the
 * path where an add-on is installed
 *
 */
class WinRegLocation extends XPIStateLocation {
  /**
   * @param {string} name
   *        The string identifier for the install location.
   * @param {integer} rootKey
   *        The root key (one of the ROOT_KEY_ values from nsIWindowsRegKey).
   * @param {integer} scope
   *        The scope of add-ons installed in this location.
   */
  constructor(name, rootKey, scope) {
    super(name, undefined, scope);

    this.locked = true;
    this._rootKey = rootKey;
  }

  /**
   * Retrieves the path of this Application's data key in the registry.
   */
  get _appKeyPath() {
    let appVendor = Services.appinfo.vendor;
    let appName = Services.appinfo.name;

    // XXX Thunderbird doesn't specify a vendor string
    if (appVendor == "" && AppConstants.MOZ_APP_NAME == "thunderbird") {
      appVendor = "Mozilla";
    }

    return `SOFTWARE\\${appVendor}\\${appName}`;
  }

  /**
   * Read the registry and build a mapping between ID and path for each
   * installed add-on.
   *
   * @returns {Map<AddonID, nsIFile>}
   *        A map of add-ons in this location.
   */
  readAddons() {
    let addons = new Map();

    let path = `${this._appKeyPath}\\Extensions`;
    let key = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
      Ci.nsIWindowsRegKey
    );

    // Reading the registry may throw an exception, and that's ok.  In error
    // cases, we just leave ourselves in the empty state.
    try {
      key.open(this._rootKey, path, Ci.nsIWindowsRegKey.ACCESS_READ);
    } catch (e) {
      return addons;
    }

    try {
      let count = key.valueCount;
      for (let i = 0; i < count; ++i) {
        let id = key.getValueName(i);
        let file = new nsIFile(key.readStringValue(id));
        if (!file.exists()) {
          logger.warn(`Ignoring missing add-on in ${file.path}`);
          continue;
        }

        addons.set(id, file);
      }
    } finally {
      key.close();
    }

    return addons;
  }
}

/**
 * Keeps track of the state of XPI add-ons on the file system.
 */
var XPIStates = {
  // Map(location-name -> XPIStateLocation)
  db: new Map(),

  _jsonFile: null,

  /**
   * @property {Map<string, XPIState>} sideLoadedAddons
   *        A map of new add-ons detected during install location
   *        directory scans. Keys are add-on IDs, values are XPIState
   *        objects corresponding to those add-ons.
   */
  sideLoadedAddons: new Map(),

  get size() {
    let count = 0;
    for (let location of this.locations()) {
      count += location.size;
    }
    return count;
  },

  /**
   * Load extension state data from addonStartup.json.
   *
   * @returns {Object}
   */
  loadExtensionState() {
    let state;
    try {
      state = lazy.aomStartup.readStartupData();
    } catch (e) {
      logger.warn("Error parsing extensions state: ${error}", { error: e });
    }

    // When upgrading from a build prior to bug 857456, convert startup
    // metadata.
    let done = false;
    for (let location of Object.values(state || {})) {
      for (let data of Object.values(location.addons || {})) {
        if (!migrateAddonLoader(data)) {
          done = true;
          break;
        }
      }
      if (done) {
        break;
      }
    }

    logger.debug("Loaded add-on state: ${}", state);
    return state || {};
  },

  /**
   * Walk through all install locations, highest priority first,
   * comparing the on-disk state of extensions to what is stored in prefs.
   *
   * @param {boolean} [ignoreSideloads = true]
   *        If true, ignore changes in scopes where we don't accept
   *        side-loads.
   *
   * @returns {boolean}
   *        True if anything has changed.
   */
  scanForChanges(ignoreSideloads = true) {
    let oldState = this.initialStateData || this.loadExtensionState();
    // We're called twice, do not restore the second time as new data
    // may have been inserted since the first call.
    let shouldRestoreLocationData = !this.initialStateData;
    this.initialStateData = oldState;

    let changed = false;
    let oldLocations = new Set(Object.keys(oldState));

    let startupScanScopes;
    if (
      Services.appinfo.appBuildID ==
      Services.prefs.getCharPref(PREF_EM_LAST_APP_BUILD_ID, "")
    ) {
      startupScanScopes = Services.prefs.getIntPref(
        PREF_EM_STARTUP_SCAN_SCOPES,
        0
      );
    } else {
      // If the build id has changed, we need to do a full scan on first startup.
      Services.prefs.setCharPref(
        PREF_EM_LAST_APP_BUILD_ID,
        Services.appinfo.appBuildID
      );
      startupScanScopes = AddonManager.SCOPE_ALL;
    }

    for (let loc of XPIStates.locations()) {
      oldLocations.delete(loc.name);

      if (shouldRestoreLocationData && oldState[loc.name]) {
        loc.restore(oldState[loc.name]);
      }
      changed = changed || loc.changed;

      // Don't bother checking scopes where we don't accept side-loads.
      if (ignoreSideloads && !(loc.scope & startupScanScopes)) {
        continue;
      }

      if (!loc.enumerable) {
        continue;
      }

      // Don't bother scanning scopes where we don't have addons installed if they
      // do not allow sideloading new addons.  Once we have an addon in one of those
      // locations, we need to check the location for changes (updates/deletions).
      if (!loc.size && !(loc.scope & lazy.AddonSettings.SCOPES_SIDELOAD)) {
        continue;
      }

      let knownIds = new Set(loc.keys());
      for (let [id, file] of loc.readAddons()) {
        knownIds.delete(id);

        let xpiState = loc.get(id);
        if (!xpiState) {
          // If the location is not supported for sideloading, skip new
          // addons.  We handle this here so changes for existing sideloads
          // will function.
          if (
            !loc.isSystem &&
            !(loc.scope & lazy.AddonSettings.SCOPES_SIDELOAD)
          ) {
            continue;
          }
          logger.debug("New add-on ${id} in ${loc}", { id, loc: loc.name });

          changed = true;
          xpiState = loc.addFile(id, file);
          if (!loc.isSystem) {
            this.sideLoadedAddons.set(id, xpiState);
          }
        } else {
          let addonChanged =
            xpiState.getModTime(file) || file.path != xpiState.path;
          xpiState.file = file.clone();

          if (addonChanged) {
            changed = true;
            logger.debug("Changed add-on ${id} in ${loc}", {
              id,
              loc: loc.name,
            });
          } else {
            logger.debug("Existing add-on ${id} in ${loc}", {
              id,
              loc: loc.name,
            });
          }
        }
        XPIProvider.addTelemetry(id, { location: loc.name });
      }

      // Anything left behind in oldState was removed from the file system.
      for (let id of knownIds) {
        loc.delete(id);
        changed = true;
      }
    }

    // If there's anything left in oldState, an install location that held add-ons
    // was removed from the browser configuration.
    changed = changed || oldLocations.size > 0;

    logger.debug("scanForChanges changed: ${rv}, state: ${state}", {
      rv: changed,
      state: this.db,
    });
    return changed;
  },

  locations() {
    return this.db.values();
  },

  /**
   * @param {string} name
   *        The location name.
   * @param {XPIStateLocation} location
   *        The location object.
   */
  addLocation(name, location) {
    if (this.db.has(name)) {
      throw new Error(`Trying to add duplicate location: ${name}`);
    }
    this.db.set(name, location);
  },

  /**
   * Get the Map of XPI states for a particular location.
   *
   * @param {string} name
   *        The name of the install location.
   *
   * @returns {XPIStateLocation?}
   *        (id -> XPIState) or null if there are no add-ons in the location.
   */
  getLocation(name) {
    return this.db.get(name);
  },

  /**
   * Get the XPI state for a specific add-on in a location.
   * If the state is not in our cache, return null.
   *
   * @param {string} aLocation
   *        The name of the location where the add-on is installed.
   * @param {string} aId
   *        The add-on ID
   *
   * @returns {XPIState?}
   *        The XPIState entry for the add-on, or null.
   */
  getAddon(aLocation, aId) {
    let location = this.db.get(aLocation);
    return location && location.get(aId);
  },

  /**
   * Find the highest priority location of an add-on by ID and return the
   * XPIState.
   * @param {string} aId
   *        The add-on IDa
   * @param {function} aFilter
   *        An optional filter to apply to install locations.  If provided,
   *        addons in locations that do not match the filter are not considered.
   *
   * @returns {XPIState?}
   */
  findAddon(aId, aFilter = () => true) {
    // Fortunately the Map iterator returns in order of insertion, which is
    // also our highest -> lowest priority order.
    for (let location of this.locations()) {
      if (!aFilter(location)) {
        continue;
      }
      if (location.has(aId)) {
        return location.get(aId);
      }
    }
    return undefined;
  },

  /**
   * Iterates over the list of all enabled add-ons in any location.
   */
  *enabledAddons() {
    for (let location of this.locations()) {
      for (let entry of location.values()) {
        if (entry.enabled) {
          yield entry;
        }
      }
    }
  },

  /**
   * Add a new XPIState for an add-on and synchronize it with the DBAddonInternal.
   *
   * @param {DBAddonInternal} aAddon
   *        The add-on to add.
   */
  addAddon(aAddon) {
    aAddon.location.addAddon(aAddon);
  },

  /**
   * Save the current state of installed add-ons.
   */
  save() {
    if (!this._jsonFile) {
      this._jsonFile = new lazy.JSONFile({
        path: PathUtils.join(
          Services.dirsvc.get("ProfD", Ci.nsIFile).path,
          FILE_XPI_STATES
        ),
        finalizeAt: AddonManagerPrivate.finalShutdown,
        compression: "lz4",
      });
      this._jsonFile.data = this;
    }

    this._jsonFile.saveSoon();
  },

  toJSON() {
    let data = {};
    for (let [key, loc] of this.db.entries()) {
      if (!loc.isTemporary && (loc.size || loc.hasStaged)) {
        data[key] = loc;
      }
    }
    return data;
  },

  /**
   * Remove the XPIState for an add-on and save the new state.
   *
   * @param {string} aLocation
   *        The name of the add-on location.
   * @param {string} aId
   *        The ID of the add-on.
   *
   */
  removeAddon(aLocation, aId) {
    logger.debug(`Removing XPIState for ${aLocation}: ${aId}`);
    let location = this.db.get(aLocation);
    if (location) {
      location.removeAddon(aId);
      this.save();
    }
  },

  /**
   * Disable the XPIState for an add-on.
   *
   * @param {string} aId
   *        The ID of the add-on.
   */
  disableAddon(aId) {
    logger.debug(`Disabling XPIState for ${aId}`);
    let state = this.findAddon(aId);
    if (state) {
      state.enabled = false;
    }
  },
};

/**
 * A helper class to manage the lifetime of and interaction with
 * bootstrap scopes for an add-on.
 *
 * @param {Object} addon
 *        The add-on which owns this scope. Should be either an
 *        AddonInternal or XPIState object.
 */
class BootstrapScope {
  constructor(addon) {
    if (!addon.id || !addon.version || !addon.type) {
      throw new Error("Addon must include an id, version, and type");
    }

    this.addon = addon;
    this.instanceID = null;
    this.scope = null;
    this.started = false;
  }

  /**
   * Returns a BootstrapScope object for the given add-on. If an active
   * scope exists, it is returned. Otherwise a new one is created.
   *
   * @param {Object} addon
   *        The add-on which owns this scope, as accepted by the
   *        constructor.
   * @returns {BootstrapScope}
   */
  static get(addon) {
    let scope = XPIProvider.activeAddons.get(addon.id);
    if (!scope) {
      scope = new this(addon);
    }
    return scope;
  }

  get file() {
    return this.addon.file || this.addon._sourceBundle;
  }

  get runInSafeMode() {
    return "runInSafeMode" in this.addon
      ? this.addon.runInSafeMode
      : canRunInSafeMode(this.addon);
  }

  /**
   * Returns state information for use by an AsyncShutdown blocker. If
   * the wrapped bootstrap scope has a fetchState method, it is called,
   * and its result returned. If not, returns null.
   *
   * @returns {Object|null}
   */
  fetchState() {
    if (this.scope && this.scope.fetchState) {
      return this.scope.fetchState();
    }
    return null;
  }

  /**
   * Calls a bootstrap method for an add-on.
   *
   * @param {string} aMethod
   *        The name of the bootstrap method to call
   * @param {integer} aReason
   *        The reason flag to pass to the bootstrap's startup method
   * @param {Object} [aExtraParams = {}]
   *        An object of additional key/value pairs to pass to the method in
   *        the params argument
   * @returns {any}
   *        The return value of the bootstrap method.
   */
  async callBootstrapMethod(aMethod, aReason, aExtraParams = {}) {
    let { addon, runInSafeMode } = this;
    if (
      Services.appinfo.inSafeMode &&
      !runInSafeMode &&
      aMethod !== "uninstall"
    ) {
      return null;
    }

    try {
      if (!this.scope) {
        this.loadBootstrapScope(aReason);
      }

      if (aMethod == "startup" || aMethod == "shutdown") {
        aExtraParams.instanceID = this.instanceID;
      }

      let method = undefined;
      let { scope } = this;
      try {
        method = scope[aMethod];
      } catch (e) {
        // An exception will be caught if the expected method is not defined.
        // That will be logged below.
      }

      if (aMethod == "startup") {
        this.started = true;
      } else if (aMethod == "shutdown") {
        this.started = false;

        // Extensions are automatically deinitialized in the correct order at shutdown.
        if (aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
          this._pendingDisable = true;
          for (let addon of XPIProvider.getDependentAddons(this.addon)) {
            if (addon.active) {
              await XPIExports.XPIDatabase.updateAddonDisabledState(addon);
            }
          }
        }
      }

      // NOTE: Make sure the properties meant to be consistently passed to
      // the bootstrap startup method to be part of the XPIStates JSON_FIELDS
      // and to have been propagated from the db properties stored in the DB
      // to the startupCache XPIStates by the syncWithDB method (because of
      // browser startup the properties for the already installed addons
      // are going to be retrieved from the XPIStates before the addonDB
      // has been fully loaded).
      let params = {
        id: addon.id,
        version: addon.version,
        type: addon.type,
        resourceURI: addon.resolvedRootURI,
        signedState: addon.signedState,
        temporarilyInstalled: addon.location.isTemporary,
        builtIn: addon.location.isBuiltin,
        isSystem: addon.location.isSystem,
        isPrivileged: addon.isPrivileged,
        locationHidden: addon.location.hidden,
        recommendationState: addon.recommendationState,
        blocklistState: addon.blocklistState,
      };

      if (aMethod == "startup" && addon.startupData) {
        params.startupData = addon.startupData;
      }

      Object.assign(params, aExtraParams);

      let result;
      if (!method) {
        logger.warn(
          `Add-on ${addon.id} is missing bootstrap method ${aMethod}`
        );
      } else {
        logger.debug(
          `Calling bootstrap method ${aMethod} on ${addon.id} version ${addon.version}`
        );

        this._beforeCallBootstrapMethod(aMethod, params, aReason);

        try {
          result = await method.call(scope, params, aReason);
        } catch (e) {
          logger.warn(
            `Exception running bootstrap method ${aMethod} on ${addon.id}`,
            e
          );
        }
      }
      return result;
    } finally {
      // Extensions are automatically initialized in the correct order at startup.
      if (aMethod == "startup" && aReason != BOOTSTRAP_REASONS.APP_STARTUP) {
        for (let addon of XPIProvider.getDependentAddons(this.addon)) {
          XPIExports.XPIDatabase.updateAddonDisabledState(addon);
        }
      }
    }
  }

  // No-op method to be overridden by tests.
  _beforeCallBootstrapMethod() {}

  /**
   * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason
   * values as constants in the scope.
   *
   * @param {integer?} [aReason]
   *        The reason this bootstrap is being loaded, as passed to a
   *        bootstrap method.
   */
  loadBootstrapScope(aReason) {
    this.instanceID = Symbol(this.addon.id);
    this._pendingDisable = false;

    XPIProvider.activeAddons.set(this.addon.id, this);

    // Mark the add-on as active for the crash reporter before loading.
    // But not at app startup, since we'll already have added all of our
    // annotations before starting any loads.
    if (aReason !== BOOTSTRAP_REASONS.APP_STARTUP) {
      XPIProvider.addAddonsToCrashReporter();
    }

    logger.debug(`Loading bootstrap scope from ${this.addon.rootURI}`);

    if (this.addon.isWebExtension) {
      switch (this.addon.type) {
        case "extension":
        case "theme":
          this.scope = lazy.Extension.getBootstrapScope();
          break;

        case "locale":
          this.scope = lazy.Langpack.getBootstrapScope();
          break;

        case "dictionary":
          this.scope = lazy.Dictionary.getBootstrapScope();
          break;

        default:
          throw new Error(`Unknown webextension type ${this.addon.type}`);
      }
    } else {
      let loader = AddonManagerPrivate.externalExtensionLoaders.get(
        this.addon.loader
      );
      if (!loader) {
        throw new Error(`Cannot find loader for ${this.addon.loader}`);
      }

      this.scope = loader.loadScope(this.addon);
    }
  }

  /**
   * Unloads a bootstrap scope by dropping all references to it and then
   * updating the list of active add-ons with the crash reporter.
   */
  unloadBootstrapScope() {
    XPIProvider.activeAddons.delete(this.addon.id);
    XPIProvider.addAddonsToCrashReporter();

    this.scope = null;
    this.startupPromise = null;
    this.instanceID = null;
  }

  /**
   * Calls the bootstrap scope's startup method, with the given reason
   * and extra parameters.
   *
   * @param {integer} reason
   *        The reason code for the startup call.
   * @param {Object} [aExtraParams]
   *        Optional extra parameters to pass to the bootstrap method.
   * @returns {Promise}
   *        Resolves when the startup method has run to completion, rejects
   *        if called late during shutdown.
   */
  async startup(reason, aExtraParams) {
    if (this.shutdownPromise) {
      await this.shutdownPromise;
    }

    if (
      Services.startup.isInOrBeyondShutdownPhase(
        Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
      )
    ) {
      let err = new Error(
        `XPIProvider can't start bootstrap scope for ${this.addon.id} after shutdown was already granted`
      );
      logger.warn("BoostrapScope startup failure: ${error}", { error: err });
      this.startupPromise = Promise.reject(err);
    } else {
      this.startupPromise = this.callBootstrapMethod(
        "startup",
        reason,
        aExtraParams
      );
    }

    return this.startupPromise;
  }

  /**
   * Calls the bootstrap scope's shutdown method, with the given reason
   * and extra parameters.
   *
   * @param {integer} reason
   *        The reason code for the shutdown call.
   * @param {Object} [aExtraParams]
   *        Optional extra parameters to pass to the bootstrap method.
   */
  async shutdown(reason, aExtraParams) {
    this.shutdownPromise = this._shutdown(reason, aExtraParams);
    await this.shutdownPromise;
    this.shutdownPromise = null;
  }

  async _shutdown(reason, aExtraParams) {
    await this.startupPromise;
    return this.callBootstrapMethod("shutdown", reason, aExtraParams);
  }

  /**
   * If the add-on is already running, calls its "shutdown" method, and
   * unloads its bootstrap scope.
   *
   * @param {integer} reason
   *        The reason code for the shutdown call.
   * @param {Object} [aExtraParams]
   *        Optional extra parameters to pass to the bootstrap method.
   */
  async disable() {
    if (this.started) {
      await this.shutdown(BOOTSTRAP_REASONS.ADDON_DISABLE);
      // If we disable and re-enable very quickly, it's possible that
      // the next startup() method will be called immediately after this
      // shutdown method finishes. This almost never happens outside of
      // tests. In tests, alas...
      if (!this.started) {
        this.unloadBootstrapScope();
      }
    }
  }

  /**
   * Calls the bootstrap scope's install method, and optionally its
   * startup method.
   *
   * @param {integer} reason
   *        The reason code for the calls.
   * @param {boolean} [startup = false]
   *        If true, and the add-on is active, calls its startup method
   *        after its install method.
   * @param {Object} [extraArgs]
   *        Optional extra parameters to pass to the bootstrap method.
   * @returns {Promise}
   *        Resolves when the startup method has run to completion, if
   *        startup is required.
   */
  install(reason = BOOTSTRAP_REASONS.ADDON_INSTALL, startup, extraArgs) {
    return this._install(reason, false, startup, extraArgs);
  }

  async _install(reason, callUpdate, startup, extraArgs) {
    if (callUpdate) {
      await this.callBootstrapMethod("update", reason, extraArgs);
    } else {
      this.callBootstrapMethod("install", reason, extraArgs);
    }

    if (startup && this.addon.active) {
      await this.startup(reason, extraArgs);
    } else if (this.addon.disabled) {
      this.unloadBootstrapScope();
    }
  }

  /**
   * Calls the bootstrap scope's uninstall method, and unloads its
   * bootstrap scope. If the extension is already running, its shutdown
   * method is called before its uninstall method.
   *
   * @param {integer} reason
   *        The reason code for the calls.
   * @param {Object} [extraArgs]
   *        Optional extra parameters to pass to the bootstrap method.
   * @returns {Promise}
   *        Resolves when the shutdown method has run to completion, if
   *        shutdown is required, and the uninstall method has been
   *        called.
   */
  uninstall(reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL, extraArgs) {
    return this._uninstall(reason, false, extraArgs);
  }

  async _uninstall(reason, callUpdate, extraArgs) {
    if (this.started) {
      await this.shutdown(reason, extraArgs);
    }
    if (!callUpdate) {
      this.callBootstrapMethod("uninstall", reason, extraArgs);
    }
    this.unloadBootstrapScope();

    if (this.file) {
      XPIExports.XPIInstall.flushJarCache(this.file);
    }
  }

  /**
   * Calls the appropriate sequence of shutdown, uninstall, update,
   * startup, and install methods for updating the current scope's
   * add-on to the given new add-on, depending on the current state of
   * the scope.
   *
   * @param {XPIState} newAddon
   *        The new add-on which is being installed, as expected by the
   *        constructor.
   * @param {boolean} [startup = false]
   *        If true, and the new add-on is enabled, calls its startup
   *        method as its final operation.
   * @param {function} [updateCallback]
   *        An optional callback function to call between uninstalling
   *        the old add-on and installing the new one. This callback
   *        should update any database state which is necessary for the
   *        startup of the new add-on.
   * @returns {Promise}
   *        Resolves when all required bootstrap callbacks have
   *        completed.
   */
  async update(newAddon, startup = false, updateCallback) {
    let reason = XPIExports.XPIInstall.newVersionReason(
      this.addon.version,
      newAddon.version
    );

    let callUpdate = this.addon.isWebExtension && newAddon.isWebExtension;

    // BootstrapScope gets either an XPIState instance or an AddonInternal
    // instance, when we update, we need the latter to access permissions
    // from the manifest.
    let existingAddon = this.addon;

    let extraArgs = {
      oldVersion: existingAddon.version,
      newVersion: newAddon.version,
    };

    // If we're updating an extension, we may need to read data to
    // calculate permission changes.
    if (callUpdate && existingAddon.type === "extension") {
      if (this.addon instanceof XPIState) {
        // The existing addon will be cached in the database.
        existingAddon = await XPIExports.XPIDatabase.getAddonByID(
          this.addon.id
        );
      }

      if (newAddon instanceof XPIState) {
        newAddon = await XPIExports.XPIInstall.loadManifestFromFile(
          newAddon.file,
          newAddon.location
        );
      }

      Object.assign(extraArgs, {
        userPermissions: newAddon.userPermissions,
        optionalPermissions: newAddon.optionalPermissions,
        oldPermissions: existingAddon.userPermissions,
        oldOptionalPermissions: existingAddon.optionalPermissions,
      });
    }

    await this._uninstall(reason, callUpdate, extraArgs);

    if (updateCallback) {
      await updateCallback();
    }

    this.addon = newAddon;
    return this._install(reason, callUpdate, startup, extraArgs);
  }
}

let resolveDBReady;
let dbReadyPromise = new Promise(resolve => {
  resolveDBReady = resolve;
});
let resolveProviderReady;
let providerReadyPromise = new Promise(resolve => {
  resolveProviderReady = resolve;
});

export var XPIProvider = {
  get name() {
    return "XPIProvider";
  },

  BOOTSTRAP_REASONS: Object.freeze(BOOTSTRAP_REASONS),

  // A Map of active addons to their bootstrapScope by ID
  activeAddons: new Map(),
  // Per-addon telemetry information
  _telemetryDetails: {},
  // Have we started shutting down bootstrap add-ons?
  _closing: false,

  // Promises awaited by the XPIProvider before resolving providerReadyPromise,
  // (pushed into the array by XPIProvider maybeInstallBuiltinAddon and startup
  // methods).
  startupPromises: [],

  // Array of the bootstrap startup promises for the enabled addons being
  // initiated during the XPIProvider startup.
  //
  // NOTE: XPIProvider will wait for these promises (and the startupPromises one)
  // to have settled before allowing the application to proceed with shutting down
  // (see quitApplicationGranted blocker at the end of the XPIProvider.startup).
  enabledAddonsStartupPromises: [],

  databaseReady: Promise.all([dbReadyPromise, providerReadyPromise]),

  registerProvider() {
    AddonManagerPrivate.registerProvider(this, Array.from(ALL_XPI_TYPES));
  },

  // Check if the XPIDatabase has been loaded (without actually
  // triggering unwanted imports or I/O)
  get isDBLoaded() {
    // Make sure we don't touch the XPIDatabase getter before it's
    // actually loaded, and force an early load.
    return (
      (Object.getOwnPropertyDescriptor(XPIExports, "XPIDatabase").value &&
        XPIExports.XPIDatabase.initialized) ||
      false
    );
  },

  /**
   * Returns true if the add-on with the given ID is currently active,
   * without forcing the add-ons database to load.
   *
   * @param {string} addonId
   *        The ID of the add-on to check.
   * @returns {boolean}
   */
  addonIsActive(addonId) {
    let state = XPIStates.findAddon(addonId);
    return state && state.enabled;
  },

  /**
   * Returns an array of the add-on values in `enabledAddons`,
   * sorted so that all of an add-on's dependencies appear in the array
   * before itself.
   *
   * @returns {Array<object>}
   *   A sorted array of add-on objects. Each value is a copy of the
   *   corresponding value in the `enabledAddons` object, with an
   *   additional `id` property, which corresponds to the key in that
   *   object, which is the same as the add-ons ID.
   */
  sortBootstrappedAddons() {
    function compare(a, b) {
      if (a === b) {
        return 0;
      }
      return a < b ? -1 : 1;
    }

    // Sort the list so that ordering is deterministic.
    let list = Array.from(XPIStates.enabledAddons());
    list.sort((a, b) => compare(a.id, b.id));

    let addons = {};
    for (let entry of list) {
      addons[entry.id] = entry;
    }

    let res = new Set();
    let seen = new Set();

    let add = addon => {
      seen.add(addon.id);

      for (let id of addon.dependencies || []) {
        if (id in addons && !seen.has(id)) {
          add(addons[id]);
        }
      }

      res.add(addon.id);
    };

    Object.values(addons).forEach(add);

    return Array.from(res, id => addons[id]);
  },

  /*
   * Adds metadata to the telemetry payload for the given add-on.
   */
  addTelemetry(aId, aPayload) {
    if (!this._telemetryDetails[aId]) {
      this._telemetryDetails[aId] = {};
    }
    Object.assign(this._telemetryDetails[aId], aPayload);
  },

  setupInstallLocations(aAppChanged) {
    function DirectoryLoc(aName, aScope, aKey, aPaths, aLocked, aIsSystem) {
      try {
        var dir = lazy.FileUtils.getDir(aKey, aPaths);
      } catch (e) {
        return null;
      }
      return new DirectoryLocation(aName, dir, aScope, aLocked, aIsSystem);
    }

    function SystemDefaultsLoc(name, scope, key, paths) {
      try {
        var dir = lazy.FileUtils.getDir(key, paths);
      } catch (e) {
        return null;
      }
      return new SystemAddonDefaults(name, dir, scope);
    }

    function SystemLoc(aName, aScope, aKey, aPaths) {
      try {
        var dir = lazy.FileUtils.getDir(aKey, aPaths);
      } catch (e) {
        return null;
      }
      return new SystemAddonLocation(aName, dir, aScope, aAppChanged !== false);
    }

    function RegistryLoc(aName, aScope, aKey) {
      if ("nsIWindowsRegKey" in Ci) {
        return new WinRegLocation(aName, Ci.nsIWindowsRegKey[aKey], aScope);
      }
    }

    // These must be in order of priority, highest to lowest,
    // for processFileChanges etc. to work
    let locations = [
      [() => TemporaryInstallLocation, TemporaryInstallLocation.name, null],

      [
        DirectoryLoc,
        KEY_APP_PROFILE,
        AddonManager.SCOPE_PROFILE,
        KEY_PROFILEDIR,
        [DIR_EXTENSIONS],
        false,
      ],

      [
        DirectoryLoc,
        KEY_APP_SYSTEM_PROFILE,
        AddonManager.SCOPE_APPLICATION,
        KEY_PROFILEDIR,
        [DIR_APP_SYSTEM_PROFILE],
        false,
        true,
      ],

      [
        SystemLoc,
        KEY_APP_SYSTEM_ADDONS,
        AddonManager.SCOPE_PROFILE,
        KEY_PROFILEDIR,
        [DIR_SYSTEM_ADDONS],
      ],

      [
        SystemDefaultsLoc,
        KEY_APP_SYSTEM_DEFAULTS,
        AddonManager.SCOPE_PROFILE,
        KEY_APP_FEATURES,
        [],
      ],

--> --------------------

--> maximum size reached

--> --------------------

[ zur Elbe Produktseite wechseln0.53Quellennavigators  Analyse erneut starten  ]