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 150 kB image not shown  

Quelle  XPIInstall.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 install extensions.
 * In general, we try to avoid loading it until extension installation
 * or update is required. Please keep that in mind when deciding whether
 * to add code here or elsewhere.
 */

/**
 * @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 {
  computeSha256HashAsString,
  getHashStringForCrypto,
  hasStrongSignature,
} from "resource://gre/modules/addons/crypto-utils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import {
  AddonManager,
  AddonManagerPrivate,
} from "resource://gre/modules/AddonManager.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
  AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs",
  CertUtils: "resource://gre/modules/CertUtils.sys.mjs",
  ExtensionData: "resource://gre/modules/Extension.sys.mjs",
  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
  ProductAddonChecker:
    "resource://gre/modules/addons/ProductAddonChecker.sys.mjs",
  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
  UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "IconDetails", () => {
  return ChromeUtils.importESModule(
    "resource://gre/modules/ExtensionParent.sys.mjs"
  ).ExtensionParent.IconDetails;
});

const { nsIBlocklistService } = Ci;

const nsIFile = Components.Constructor(
  "@mozilla.org/file/local;1",
  "nsIFile",
  "initWithPath"
);

const BinaryOutputStream = Components.Constructor(
  "@mozilla.org/binaryoutputstream;1",
  "nsIBinaryOutputStream",
  "setOutputStream"
);
const CryptoHash = Components.Constructor(
  "@mozilla.org/security/hash;1",
  "nsICryptoHash",
  "initWithString"
);
const FileInputStream = Components.Constructor(
  "@mozilla.org/network/file-input-stream;1",
  "nsIFileInputStream",
  "init"
);
const FileOutputStream = Components.Constructor(
  "@mozilla.org/network/file-output-stream;1",
  "nsIFileOutputStream",
  "init"
);
const ZipReader = Components.Constructor(
  "@mozilla.org/libjar/zip-reader;1",
  "nsIZipReader",
  "open"
);

XPCOMUtils.defineLazyServiceGetters(lazy, {
  gCertDB: ["@mozilla.org/security/x509certdb;1", "nsIX509CertDB"],
});

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";
const PREF_XPI_WEAK_SIGNATURES_ALLOWED =
  "xpinstall.signatures.weakSignaturesTemporarilyAllowed";

const PREF_SELECTED_THEME = "extensions.activeThemeID";

const TOOLKIT_ID = "toolkit@mozilla.org";

ChromeUtils.defineLazyGetter(lazy, "MOZ_UNSIGNED_SCOPES", () => {
  let result = 0;
  if (AppConstants.MOZ_UNSIGNED_APP_SCOPE) {
    result |= AddonManager.SCOPE_APPLICATION;
  }
  if (AppConstants.MOZ_UNSIGNED_SYSTEM_SCOPE) {
    result |= AddonManager.SCOPE_SYSTEM;
  }
  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;
}

/**
 * Sends local and remote notifications to flush a JAR file cache entry
 *
 * @param {nsIFile} aJarFile
 *        The ZIP/XPI/JAR file as a nsIFile
 */
function flushJarCache(aJarFile) {
  Services.obs.notifyObservers(aJarFile, "flush-cache-entry");
  Services.ppmm.broadcastAsyncMessage(MSG_JAR_FLUSH, {
    path: aJarFile.path,
  });
}

const PREF_EM_UPDATE_BACKGROUND_URL = "extensions.update.background.url";
const PREF_EM_UPDATE_URL = "extensions.update.url";
const PREF_XPI_SIGNATURES_DEV_ROOT = "xpinstall.signatures.dev-root";

const KEY_TEMPDIR = "TmpD";

// This is a random number array that can be used as "salt" when generating
// an automatic ID based on the directory path of an add-on. It will prevent
// someone from creating an ID for a permanent add-on that could be replaced
// by a temporary add-on (because that would be confusing, I guess).
const TEMP_INSTALL_ID_GEN_SESSION = new Uint8Array(
  Float64Array.of(Math.random()).buffer
);

const MSG_JAR_FLUSH = "Extension:FlushJarCache";

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

// Stores the ID of the theme which was selected during the last session,
// if any. When installing a new built-in theme with this ID, it will be
// automatically enabled.
let lastSelectedTheme = null;

function getJarURI(file, path = "") {
  if (file instanceof Ci.nsIFile) {
    file = Services.io.newFileURI(file);
  }
  if (file instanceof Ci.nsIURI) {
    file = file.spec;
  }
  return Services.io.newURI(`jar:${file}!/${path}`);
}

let DirPackage;
let XPIPackage;
class Package {
  static get(file) {
    if (file.isFile()) {
      return new XPIPackage(file);
    }
    return new DirPackage(file);
  }

  constructor(file, rootURI) {
    this.file = file;
    this.filePath = file.path;
    this.rootURI = rootURI;
  }

  close() {}

  async readString(...path) {
    let buffer = await this.readBinary(...path);
    return new TextDecoder().decode(buffer);
  }

  async verifySignedState(addonId, addonType, addonLocation) {
    if (!shouldVerifySignedState(addonType, addonLocation)) {
      return {
        signedState: AddonManager.SIGNEDSTATE_NOT_REQUIRED,
        cert: null,
      };
    }

    let root = Ci.nsIX509CertDB.AddonsPublicRoot;
    if (
      (!AppConstants.MOZ_REQUIRE_SIGNING ||
        // Allow mochitests to switch to dev-root on all channels.
        Cu.isInAutomation ||
        // Allow xpcshell tests to switch to dev-root on all channels,
        // included tests where "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer"
        // pref is set to false and Cu.isInAutomation is going to be false (e.g. test_signed_langpack.js).
        // TODO(Bug 1598804): we should be able to remove the following checks once Cu.isAutomation is fixed.
        (Services.env.exists("XPCSHELL_TEST_PROFILE_DIR") &&
          Services.appinfo.name === "XPCShell")) &&
      Services.prefs.getBoolPref(PREF_XPI_SIGNATURES_DEV_ROOT, false)
    ) {
      root = Ci.nsIX509CertDB.AddonsStageRoot;
    }

    return this.verifySignedStateForRoot(addonId, root);
  }

  flushCache() {}
}

DirPackage = class DirPackage extends Package {
  constructor(file) {
    super(file, Services.io.newFileURI(file));
  }

  hasResource(...path) {
    return IOUtils.exists(PathUtils.join(this.filePath, ...path));
  }

  async iterDirectory(path, callback) {
    let fullPath = PathUtils.join(this.filePath, ...path);

    let children = await IOUtils.getChildren(fullPath);
    for (let path of children) {
      let { type } = await IOUtils.stat(path);
      callback({
        isDir: type == "directory",
        name: PathUtils.filename(path),
        path,
      });
    }
  }

  iterFiles(callback, path = []) {
    return this.iterDirectory(path, async entry => {
      let entryPath = [...path, entry.name];
      if (entry.isDir) {
        callback({
          path: entryPath.join("/"),
          isDir: true,
        });
        await this.iterFiles(callback, entryPath);
      } else {
        callback({
          path: entryPath.join("/"),
          isDir: false,
        });
      }
    });
  }

  readBinary(...path) {
    return IOUtils.read(PathUtils.join(this.filePath, ...path));
  }

  async verifySignedStateForRoot() {
    return { signedState: AddonManager.SIGNEDSTATE_UNKNOWN, cert: null };
  }
};

XPIPackage = class XPIPackage extends Package {
  constructor(file) {
    super(file, getJarURI(file));

    this.zipReader = new ZipReader(file);
  }

  close() {
    this.zipReader.close();
    this.zipReader = null;
    this.flushCache();
  }

  async hasResource(...path) {
    return this.zipReader.hasEntry(path.join("/"));
  }

  async iterFiles(callback) {
    for (let path of this.zipReader.findEntries("*")) {
      let entry = this.zipReader.getEntry(path);
      callback({
        path,
        isDir: entry.isDirectory,
      });
    }
  }

  async readBinary(...path) {
    let response = await fetch(this.rootURI.resolve(path.join("/")));
    return response.arrayBuffer();
  }

  verifySignedStateForRoot(addonId, root) {
    return new Promise(resolve => {
      let callback = {
        openSignedAppFileFinished(aRv, aZipReader, aSignatureInfos) {
          // aSignatureInfos is an array of nsIAppSignatureInfo.
          // In the future, this code can iterate through the array to
          // determine if one of the verified signatures used a satisfactory
          // algorithm and signing certificate.
          // For now, any verified signature is acceptable.
          let cert;
          if (aRv == Cr.NS_OK && aSignatureInfos.length) {
            cert = aSignatureInfos[0].signerCert;
          }
          if (aZipReader) {
            aZipReader.close();
          }
          resolve({
            cert,
            signedState: getSignedStatus(aRv, cert, addonId),
            signedTypes: aSignatureInfos?.map(
              signatureInfo => signatureInfo.signatureAlgorithm
            ),
          });
        },
      };
      // This allows the certificate DB to get the raw JS callback object so the
      // test code can pass through objects that XPConnect would reject.
      callback.wrappedJSObject = callback;

      lazy.gCertDB.openSignedAppFileAsync(root, this.file, callback);
    });
  }

  flushCache() {
    flushJarCache(this.file);
  }
};

/**
 * Return an object that implements enough of the Package interface
 * to allow loadManifest() to work for a built-in addon (ie, one loaded
 * from a resource: url)
 *
 * @param {nsIURL} baseURL The URL for the root of the add-on.
 * @returns {object}
 */
function builtinPackage(baseURL) {
  return {
    rootURI: baseURL,
    filePath: baseURL.spec,
    file: null,
    verifySignedState() {
      return {
        signedState: AddonManager.SIGNEDSTATE_NOT_REQUIRED,
        cert: null,
      };
    },
    async hasResource(path) {
      try {
        let response = await fetch(this.rootURI.resolve(path));
        return response.ok;
      } catch (e) {
        return false;
      }
    },
  };
}

/**
 * Determine the reason to pass to an extension's bootstrap methods when
 * switch between versions.
 *
 * @param {string} oldVersion The version of the existing extension instance.
 * @param {string} newVersion The version of the extension being installed.
 *
 * @returns {integer}
 *        BOOSTRAP_REASONS.ADDON_UPGRADE or BOOSTRAP_REASONS.ADDON_DOWNGRADE
 */
function newVersionReason(oldVersion, newVersion) {
  return Services.vc.compare(oldVersion, newVersion) <= 0
    ? XPIExports.XPIInternal.BOOTSTRAP_REASONS.ADDON_UPGRADE
    : XPIExports.XPIInternal.BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
}

// Behaves like Promise.all except waits for all promises to resolve/reject
// before resolving/rejecting itself
function waitForAllPromises(promises) {
  return new Promise((resolve, reject) => {
    let shouldReject = false;
    let rejectValue = null;

    let newPromises = promises.map(p =>
      p.catch(value => {
        shouldReject = true;
        rejectValue = value;
      })
    );
    Promise.all(newPromises).then(results =>
      shouldReject ? reject(rejectValue) : resolve(results)
    );
  });
}

/**
 * Reads an AddonInternal object from a webextension manifest.json
 *
 * @param {Package} aPackage
 *        The install package for the add-on
 * @param {XPIStateLocation} aLocation
 *        The install location the add-on is installed in, or will be
 *        installed to.
 * @returns {{ addon: AddonInternal, verifiedSignedState: object}}
 * @throws if the install manifest in the stream is corrupt or could not
 *         be read
 */
async function loadManifestFromWebManifest(aPackage, aLocation) {
  let verifiedSignedState;
  const temporarilyInstalled = aLocation.isTemporary;
  let extension = await lazy.ExtensionData.constructAsync({
    rootURI: XPIExports.XPIInternal.maybeResolveURI(aPackage.rootURI),
    temporarilyInstalled,
    async checkPrivileged(type, id) {
      verifiedSignedState = await aPackage.verifySignedState(
        id,
        type,
        aLocation
      );
      return lazy.ExtensionData.getIsPrivileged({
        signedState: verifiedSignedState.signedState,
        builtIn: aLocation.isBuiltin,
        temporarilyInstalled,
      });
    },
  });

  let manifest = await extension.loadManifest();

  // Read the list of available locales, and pre-load messages for
  // all locales.
  let locales = !extension.errors.length
    ? await extension.initAllLocales()
    : null;

  if (extension.errors.length) {
    let error = new Error("Extension is invalid");
    // Add detailed errors on the error object so that the front end can display them
    // if needed (eg in about:debugging).
    error.additionalErrors = extension.errors;
    throw error;
  }

  // Internally, we use the `applications` key but it is because we assign the value
  // of `browser_specific_settings` to `applications` in `ExtensionData.parseManifest()`.
  // Yet, as of MV3, only `browser_specific_settings` is accepted in manifest.json files.
  let bss = manifest.applications?.gecko || {};

  // A * is illegal in strict_min_version
  if (bss.strict_min_version?.split(".").some(part => part == "*")) {
    throw new Error("The use of '*' in strict_min_version is invalid");
  }

  let addon = new XPIExports.AddonInternal();
  addon.id = bss.id;
  addon.version = manifest.version;
  addon.manifestVersion = manifest.manifest_version;
  addon.name = manifest.name;
  addon.type = extension.type;
  addon.loader = null;
  addon.strictCompatibility = true;
  addon.internalName = null;
  addon.updateURL = bss.update_url;
  addon.installOrigins = manifest.install_origins;
  addon.optionsBrowserStyle = true;
  addon.optionsURL = null;
  addon.optionsType = null;
  addon.aboutURL = null;
  addon.dependencies = Object.freeze(Array.from(extension.dependencies));
  addon.startupData = extension.startupData;
  addon.hidden = extension.isPrivileged && manifest.hidden;
  addon.incognito = manifest.incognito;

  if (addon.type === "theme" && (await aPackage.hasResource("preview.png"))) {
    addon.previewImage = "preview.png";
  }

  const { optionsPageProperties } = extension;
  if (optionsPageProperties) {
    // Store just the relative path here, the AddonWrapper getURL
    // wrapper maps this to a full URL.
    addon.optionsURL = optionsPageProperties.page;
    if (optionsPageProperties.open_in_tab) {
      addon.optionsType = AddonManager.OPTIONS_TYPE_TAB;
    } else {
      addon.optionsType = AddonManager.OPTIONS_TYPE_INLINE_BROWSER;
    }

    addon.optionsBrowserStyle = optionsPageProperties.browser_style;
  }

  // WebExtensions don't use iconURLs
  addon.iconURL = null;
  addon.icons = manifest.icons || {};
  addon.userPermissions = extension.getRequiredPermissions();
  addon.optionalPermissions = extension.manifestOptionalPermissions;
  addon.requestedPermissions = extension.getRequestedPermissions();
  addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;

  function getLocale(aLocale) {
    // Use the raw manifest, here, since we need values with their
    // localization placeholders still in place.
    let rawManifest = extension.rawManifest;

    // As a convenience, allow author to be set if its a string bug 1313567.
    let creator =
      typeof rawManifest.author === "string" ? rawManifest.author : null;
    let homepageURL = rawManifest.homepage_url;

    // Allow developer to override creator and homepage_url.
    if (rawManifest.developer) {
      if (rawManifest.developer.name) {
        creator = rawManifest.developer.name;
      }
      if (rawManifest.developer.url) {
        homepageURL = rawManifest.developer.url;
      }
    }

    let result = {
      name: extension.localize(rawManifest.name, aLocale),
      description: extension.localize(rawManifest.description, aLocale),
      creator: extension.localize(creator, aLocale),
      homepageURL: extension.localize(homepageURL, aLocale),

      developers: null,
      translators: null,
      contributors: null,
      locales: [aLocale],
    };
    if (result.name.length > lazy.ExtensionData.EXT_NAME_MAX_LEN) {
      // See comment at EXT_NAME_MAX_LEN in Extension.sys.mjs.
      logger.warn(`Truncating add-on name ${addon.id} for locale ${aLocale}`);
      result.name = result.name.slice(0, lazy.ExtensionData.EXT_NAME_MAX_LEN);
    }
    return result;
  }

  addon.defaultLocale = getLocale(extension.defaultLocale);
  addon.locales = Array.from(locales.keys(), getLocale);

  delete addon.defaultLocale.locales;

  addon.targetApplications = [
    {
      id: TOOLKIT_ID,
      minVersion: bss.strict_min_version,
      maxVersion: bss.strict_max_version,
    },
  ];

  addon.adminInstallOnly = bss.admin_install_only;

  addon.targetPlatforms = [];
  // Themes are disabled by default, except when they're installed from a web page.
  addon.userDisabled = extension.type === "theme";
  addon.softDisabled =
    addon.blocklistState == nsIBlocklistService.STATE_SOFTBLOCKED;

  return { addon, verifiedSignedState };
}

async function readRecommendationStates(aPackage, aAddonID) {
  let recommendationData;
  try {
    recommendationData = await aPackage.readString(
      "mozilla-recommendation.json"
    );
  } catch (e) {
    // Ignore I/O errors.
    return null;
  }

  try {
    recommendationData = JSON.parse(recommendationData);
  } catch (e) {
    logger.warn("Failed to parse recommendation", e);
  }

  if (recommendationData) {
    let { addon_id, states, validity } = recommendationData;

    if (addon_id === aAddonID && Array.isArray(states) && validity) {
      let validNotAfter = Date.parse(validity.not_after);
      let validNotBefore = Date.parse(validity.not_before);
      if (validNotAfter && validNotBefore) {
        return {
          validNotAfter,
          validNotBefore,
          states,
        };
      }
    }
    logger.warn(
      `Invalid recommendation for ${aAddonID}: ${JSON.stringify(
        recommendationData
      )}`
    );
  }

  return null;
}

function defineSyncGUID(aAddon) {
  // Define .syncGUID as a lazy property which is also settable
  Object.defineProperty(aAddon, "syncGUID", {
    get: () => {
      aAddon.syncGUID = Services.uuid.generateUUID().toString();
      return aAddon.syncGUID;
    },
    set: val => {
      delete aAddon.syncGUID;
      aAddon.syncGUID = val;
    },
    configurable: true,
    enumerable: true,
  });
}

// Generate a unique ID based on the path to this temporary add-on location.
function generateTemporaryInstallID(aFile) {
  const hasher = CryptoHash("sha1");
  const data = new TextEncoder().encode(aFile.path);
  // Make it so this ID cannot be guessed.
  const sess = TEMP_INSTALL_ID_GEN_SESSION;
  hasher.update(sess, sess.length);
  hasher.update(data, data.length);
  let id = `${getHashStringForCrypto(hasher)}${
    XPIExports.XPIInternal.TEMPORARY_ADDON_SUFFIX
  }`;
  logger.info(`Generated temp id ${id} (${sess.join("")}) for ${aFile.path}`);
  return id;
}

var loadManifest = async function (aPackage, aLocation, aOldAddon) {
  let addon;
  let verifiedSignedState;
  if (await aPackage.hasResource("manifest.json")) {
    ({ addon, verifiedSignedState } = await loadManifestFromWebManifest(
      aPackage,
      aLocation
    ));
  } else {
    // TODO bug 1674799: Remove this unused branch.
    for (let loader of AddonManagerPrivate.externalExtensionLoaders.values()) {
      if (await aPackage.hasResource(loader.manifestFile)) {
        addon = await loader.loadManifest(aPackage);
        addon.loader = loader.name;
        verifiedSignedState = await aPackage.verifySignedState(
          addon.id,
          addon.type,
          aLocation
        );
        break;
      }
    }
  }

  if (!addon) {
    throw new Error(
      `File ${aPackage.filePath} does not contain a valid manifest`
    );
  }

  addon._sourceBundle = aPackage.file;
  addon.rootURI = aPackage.rootURI.spec;
  addon.location = aLocation;

  let { cert, signedState, signedTypes } = verifiedSignedState;
  addon.signedState = signedState;
  addon.signedDate = cert?.validity?.notBefore / 1000 || null;
  // An array of the algorithms used by the signatures found in the signed XPI files,
  // as an array of integers (see nsIAppSignatureInfo_SignatureAlgorithm enum defined
  // in nsIX509CertDB.idl).
  addon.signedTypes = signedTypes;

  if (!addon.id) {
    if (cert) {
      addon.id = cert.commonName;
      if (!gIDTest.test(addon.id)) {
        throw new Error(`Extension is signed with an invalid id (${addon.id})`);
      }
    }
    if (!addon.id && aLocation.isTemporary) {
      addon.id = generateTemporaryInstallID(aPackage.file);
    }
  }

  addon.propagateDisabledState(aOldAddon);
  if (!aLocation.isSystem && !aLocation.isBuiltin) {
    if (addon.type === "extension" && !aLocation.isTemporary) {
      addon.recommendationState = await readRecommendationStates(
        aPackage,
        addon.id
      );
    }

    await addon.updateBlocklistState();
    addon.appDisabled = !XPIExports.XPIDatabase.isUsableAddon(addon);

    // Always report when there is an attempt to install a blocked add-on.
    // (transitions from STATE_BLOCKED to STATE_NOT_BLOCKED are checked
    //  in the individual AddonInstall subclasses).
    if (addon.blocklistState > nsIBlocklistService.STATE_NOT_BLOCKED) {
      addon.recordAddonBlockChangeTelemetry(
        aOldAddon ? "addon_update" : "addon_install"
      );
    }
  }

  defineSyncGUID(addon);

  return addon;
};

/**
 * Loads an add-on's manifest from the given file or directory.
 *
 * @param {nsIFile} aFile
 *        The file to load the manifest from.
 * @param {XPIStateLocation} aLocation
 *        The install location the add-on is installed in, or will be
 *        installed to.
 * @param {AddonInternal?} aOldAddon
 *        The currently-installed add-on with the same ID, if one exist.
 *        This is used to migrate user settings like the add-on's
 *        disabled state.
 * @returns {AddonInternal}
 *        The parsed Addon object for the file's manifest.
 */
var loadManifestFromFile = async function (aFile, aLocation, aOldAddon) {
  let pkg = Package.get(aFile);
  try {
    let addon = await loadManifest(pkg, aLocation, aOldAddon);
    return addon;
  } finally {
    pkg.close();
  }
};

/*
 * A synchronous method for loading an add-on's manifest. Do not use
 * this.
 */
function syncLoadManifest(state, location, oldAddon) {
  if (location.name == "app-builtin") {
    let pkg = builtinPackage(Services.io.newURI(state.rootURI));
    return XPIExports.XPIInternal.awaitPromise(
      loadManifest(pkg, location, oldAddon)
    );
  }

  let file = new nsIFile(state.path);
  let pkg = Package.get(file);
  return XPIExports.XPIInternal.awaitPromise(
    (async () => {
      try {
        let addon = await loadManifest(pkg, location, oldAddon);
        addon.rootURI = XPIExports.XPIInternal.getURIForResourceInFile(
          file,
          ""
        ).spec;
        return addon;
      } finally {
        pkg.close();
      }
    })()
  );
}

/**
 * Creates and returns a new unique temporary file. The caller should delete
 * the file when it is no longer needed.
 *
 * @returns {nsIFile}
 *       An nsIFile that points to a randomly named, initially empty file in
 *       the OS temporary files directory
 */
function getTemporaryFile() {
  let file = lazy.FileUtils.getDir(KEY_TEMPDIR, []);
  let random = Math.round(Math.random() * 36 ** 3).toString(36);
  file.append(`tmp-${random}.xpi`);
  file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, lazy.FileUtils.PERMS_FILE);
  return file;
}

function getHashForFile(file, algorithm) {
  let crypto = CryptoHash(algorithm);
  let fis = new FileInputStream(file, -1, -1, false);
  try {
    crypto.updateFromStream(fis, file.fileSize);
  } finally {
    fis.close();
  }
  return getHashStringForCrypto(crypto);
}

/**
 * Returns the signedState for a given return code and certificate by verifying
 * it against the expected ID.
 *
 * @param {nsresult} aRv
 *        The result code returned by the signature checker for the
 *        signature check operation.
 * @param {nsIX509Cert?} aCert
 *        The certificate the add-on was signed with, if a valid
 *        certificate exists.
 * @param {string?} aAddonID
 *        The expected ID of the add-on. If passed, this must match the
 *        ID in the certificate's CN field.
 * @returns {number}
 *        A SIGNEDSTATE result code constant, as defined on the
 *        AddonManager class.
 */
function getSignedStatus(aRv, aCert, aAddonID) {
  let expectedCommonName = aAddonID;
  if (aAddonID && aAddonID.length > 64) {
    expectedCommonName = computeSha256HashAsString(aAddonID);
  }

  switch (aRv) {
    case Cr.NS_OK:
      if (expectedCommonName && expectedCommonName != aCert.commonName) {
        return AddonManager.SIGNEDSTATE_BROKEN;
      }

      if (aCert.organizationalUnit == "Mozilla Components") {
        return AddonManager.SIGNEDSTATE_SYSTEM;
      }

      if (aCert.organizationalUnit == "Mozilla Extensions") {
        return AddonManager.SIGNEDSTATE_PRIVILEGED;
      }

      return /preliminary/i.test(aCert.organizationalUnit)
        ? AddonManager.SIGNEDSTATE_PRELIMINARY
        : AddonManager.SIGNEDSTATE_SIGNED;
    case Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED:
      return AddonManager.SIGNEDSTATE_MISSING;
    case Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID:
    case Cr.NS_ERROR_SIGNED_JAR_ENTRY_INVALID:
    case Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING:
    case Cr.NS_ERROR_SIGNED_JAR_ENTRY_TOO_LARGE:
    case Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY:
    case Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY:
      return AddonManager.SIGNEDSTATE_BROKEN;
    default:
      // Any other error indicates that either the add-on isn't signed or it
      // is signed by a signature that doesn't chain to the trusted root.
      logger.warn(`Failed to verify signature for ${aAddonID}: ${aRv}`);
      return AddonManager.SIGNEDSTATE_UNKNOWN;
  }
}

function shouldVerifySignedState(aAddonType, aLocation) {
  // TODO when KEY_APP_SYSTEM_DEFAULTS and KEY_APP_SYSTEM_ADDONS locations
  // are removed, we need to reorganize the logic here.  At that point we
  // should:
  //   if builtin or MOZ_UNSIGNED_SCOPES return false
  //   if system return true
  //   return SIGNED_TYPES.has(type)

  // We don't care about signatures for default system add-ons
  if (aLocation.name == XPIExports.XPIInternal.KEY_APP_SYSTEM_DEFAULTS) {
    return false;
  }

  // Updated system add-ons should always have their signature checked
  if (aLocation.isSystem) {
    return true;
  }

  if (aLocation.isBuiltin || aLocation.scope & lazy.MOZ_UNSIGNED_SCOPES) {
    return false;
  }

  // Otherwise only check signatures if the add-on is one of the signed
  // types.
  return XPIExports.XPIDatabase.SIGNED_TYPES.has(aAddonType);
}

/**
 * Verifies that a bundle's contents are all correctly signed by an
 * AMO-issued certificate
 *
 * @param {nsIFile} aBundle
 *        The nsIFile for the bundle to check, either a directory or zip file.
 * @param {AddonInternal} aAddon
 *        The add-on object to verify.
 * @returns {Promise<{ signedState: number, signedTypes: Array<number>}>?}
 *        A Promise that resolves to object including a signedState property set to
 *        an AddonManager.SIGNEDSTATE_* constant and a signedTypes property set to
 *        either an array of Ci.nsIAppSignatureInfo SignatureAlgorithm enum values
 *        or undefined if the file wasn't signed.
 */
export var verifyBundleSignedState = async function (aBundle, aAddon) {
  let pkg = Package.get(aBundle);
  try {
    let { signedState, signedTypes } = await pkg.verifySignedState(
      aAddon.id,
      aAddon.type,
      aAddon.location
    );
    return { signedState, signedTypes };
  } finally {
    pkg.close();
  }
};

/**
 * Replaces %...% strings in an addon url (update and updateInfo) with
 * appropriate values.
 *
 * @param {AddonInternal} aAddon
 *        The AddonInternal representing the add-on
 * @param {string} aUri
 *        The URI to escape
 * @param {integer?} aUpdateType
 *        An optional number representing the type of update, only applicable
 *        when creating a url for retrieving an update manifest
 * @param {string?} aAppVersion
 *        The optional application version to use for %APP_VERSION%
 * @returns {string}
 *       The appropriately escaped URI.
 */
function escapeAddonURI(aAddon, aUri, aUpdateType, aAppVersion) {
  let uri = AddonManager.escapeAddonURI(aAddon, aUri, aAppVersion);

  // If there is an updateType then replace the UPDATE_TYPE string
  if (aUpdateType) {
    uri = uri.replace(/%UPDATE_TYPE%/g, aUpdateType);
  }

  // If this add-on has compatibility information for either the current
  // application or toolkit then replace the ITEM_MAXAPPVERSION with the
  // maxVersion
  let app = aAddon.matchingTargetApplication;
  if (app) {
    var maxVersion = app.maxVersion;
  } else {
    maxVersion = "";
  }
  uri = uri.replace(/%ITEM_MAXAPPVERSION%/g, maxVersion);

  let compatMode = "normal";
  if (!AddonManager.checkCompatibility) {
    compatMode = "ignore";
  } else if (AddonManager.strictCompatibility) {
    compatMode = "strict";
  }
  uri = uri.replace(/%COMPATIBILITY_MODE%/g, compatMode);

  return uri;
}

/**
 * Converts an iterable of addon objects into a map with the add-on's ID as key.
 *
 * @param {sequence<AddonInternal>} addons
 *        A sequence of AddonInternal objects.
 *
 * @returns {Map<string, AddonInternal>}
 */
function addonMap(addons) {
  return new Map(addons.map(a => [a.id, a]));
}

async function removeAsync(aFile) {
  await IOUtils.remove(aFile.path, { ignoreAbsent: true, recursive: true });
}

/**
 * Recursively removes a directory or file fixing permissions when necessary.
 *
 * @param {nsIFile} aFile
 *        The nsIFile to remove
 */
function recursiveRemove(aFile) {
  let isDir = null;

  try {
    isDir = aFile.isDirectory();
  } catch (e) {
    // If the file has already gone away then don't worry about it, this can
    // happen on OSX where the resource fork is automatically moved with the
    // data fork for the file. See bug 733436.
    if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
      return;
    }

    throw e;
  }

  setFilePermissions(
    aFile,
    isDir ? lazy.FileUtils.PERMS_DIRECTORY : lazy.FileUtils.PERMS_FILE
  );

  try {
    aFile.remove(true);
    return;
  } catch (e) {
    if (!aFile.isDirectory() || aFile.isSymlink()) {
      logger.error("Failed to remove file " + aFile.path, e);
      throw e;
    }
  }

  // Use a snapshot of the directory contents to avoid possible issues with
  // iterating over a directory while removing files from it (the YAFFS2
  // embedded filesystem has this issue, see bug 772238), and to remove
  // normal files before their resource forks on OSX (see bug 733436).
  let entries = Array.from(XPIExports.XPIInternal.iterDirectory(aFile));
  entries.forEach(recursiveRemove);

  try {
    aFile.remove(true);
  } catch (e) {
    logger.error("Failed to remove empty directory " + aFile.path, e);
    throw e;
  }
}

/**
 * Sets permissions on a file
 *
 * @param {nsIFile} aFile
 *        The file or directory to operate on.
 * @param {integer} aPermissions
 *        The permissions to set
 */
function setFilePermissions(aFile, aPermissions) {
  try {
    aFile.permissions = aPermissions;
  } catch (e) {
    logger.warn(
      "Failed to set permissions " +
        aPermissions.toString(8) +
        " on " +
        aFile.path,
      e
    );
  }
}

/**
 * Write a given string to a file
 *
 * @param {nsIFile} file
 *        The nsIFile instance to write into
 * @param {string} string
 *        The string to write
 */
function writeStringToFile(file, string) {
  let fileStream = new FileOutputStream(
    file,
    lazy.FileUtils.MODE_WRONLY |
      lazy.FileUtils.MODE_CREATE |
      lazy.FileUtils.MODE_TRUNCATE,
    lazy.FileUtils.PERMS_FILE,
    0
  );

  try {
    let binStream = new BinaryOutputStream(fileStream);

    binStream.writeByteArray(new TextEncoder().encode(string));
  } finally {
    fileStream.close();
  }
}

/**
 * A safe way to install a file or the contents of a directory to a new
 * directory. The file or directory is moved or copied recursively and if
 * anything fails an attempt is made to rollback the entire operation. The
 * operation may also be rolled back to its original state after it has
 * completed by calling the rollback method.
 *
 * Operations can be chained. Calling move or copy multiple times will remember
 * the whole set and if one fails all of the operations will be rolled back.
 */
function SafeInstallOperation() {
  this._installedFiles = [];
  this._createdDirs = [];
}

SafeInstallOperation.prototype = {
  _installedFiles: null,
  _createdDirs: null,

  _installFile(aFile, aTargetDirectory, aCopy) {
    let oldFile = aCopy ? null : aFile.clone();
    let newFile = aFile.clone();
    try {
      if (aCopy) {
        newFile.copyTo(aTargetDirectory, null);
        // copyTo does not update the nsIFile with the new.
        newFile = getFile(aFile.leafName, aTargetDirectory);
        // Windows roaming profiles won't properly sync directories if a new file
        // has an older lastModifiedTime than a previous file, so update.
        newFile.lastModifiedTime = Date.now();
      } else {
        newFile.moveTo(aTargetDirectory, null);
      }
    } catch (e) {
      logger.error(
        "Failed to " +
          (aCopy ? "copy" : "move") +
          " file " +
          aFile.path +
          " to " +
          aTargetDirectory.path,
        e
      );
      throw e;
    }
    this._installedFiles.push({ oldFile, newFile });
  },

  /**
   * Moves a file or directory into a new directory. If an error occurs then all
   * files that have been moved will be moved back to their original location.
   *
   * @param {nsIFile} aFile
   *        The file or directory to be moved.
   * @param {nsIFile} aTargetDirectory
   *        The directory to move into, this is expected to be an empty
   *        directory.
   */
  moveUnder(aFile, aTargetDirectory) {
    try {
      this._installFile(aFile, aTargetDirectory, false);
    } catch (e) {
      this.rollback();
      throw e;
    }
  },

  /**
   * Renames a file to a new location.  If an error occurs then all
   * files that have been moved will be moved back to their original location.
   *
   * @param {nsIFile} aOldLocation
   *        The old location of the file.
   * @param {nsIFile} aNewLocation
   *        The new location of the file.
   */
  moveTo(aOldLocation, aNewLocation) {
    try {
      let oldFile = aOldLocation.clone(),
        newFile = aNewLocation.clone();
      oldFile.moveTo(newFile.parent, newFile.leafName);
      this._installedFiles.push({ oldFile, newFile, isMoveTo: true });
    } catch (e) {
      this.rollback();
      throw e;
    }
  },

  /**
   * Copies a file or directory into a new directory. If an error occurs then
   * all new files that have been created will be removed.
   *
   * @param {nsIFile} aFile
   *        The file or directory to be copied.
   * @param {nsIFile} aTargetDirectory
   *        The directory to copy into, this is expected to be an empty
   *        directory.
   */
  copy(aFile, aTargetDirectory) {
    try {
      this._installFile(aFile, aTargetDirectory, true);
    } catch (e) {
      this.rollback();
      throw e;
    }
  },

  /**
   * Rolls back all the moves that this operation performed. If an exception
   * occurs here then both old and new directories are left in an indeterminate
   * state
   */
  rollback() {
    while (this._installedFiles.length) {
      let move = this._installedFiles.pop();
      if (move.isMoveTo) {
        move.newFile.moveTo(move.oldDir.parent, move.oldDir.leafName);
      } else if (move.newFile.isDirectory() && !move.newFile.isSymlink()) {
        let oldDir = getFile(move.oldFile.leafName, move.oldFile.parent);
        oldDir.create(
          Ci.nsIFile.DIRECTORY_TYPE,
          lazy.FileUtils.PERMS_DIRECTORY
        );
      } else if (!move.oldFile) {
        // No old file means this was a copied file
        move.newFile.remove(true);
      } else {
        move.newFile.moveTo(move.oldFile.parent, null);
      }
    }

    while (this._createdDirs.length) {
      recursiveRemove(this._createdDirs.pop());
    }
  },
};

// A hash algorithm if the caller of AddonInstall did not specify one.
const DEFAULT_HASH_ALGO = "sha256";

/**
 * Base class for objects that manage the installation of an addon.
 * This class isn't instantiated directly, see the derived classes below.
 */
class AddonInstall {
  /**
   * Instantiates an AddonInstall.
   *
   * @param {XPIStateLocation} installLocation
   *        The install location the add-on will be installed into
   * @param {nsIURL} url
   *        The nsIURL to get the add-on from. If this is an nsIFileURL then
   *        the add-on will not need to be downloaded
   * @param {Object} [options = {}]
   *        Additional options for the install
   * @param {string} [options.hash]
   *        An optional hash for the add-on
   * @param {AddonInternal} [options.existingAddon]
   *        The add-on this install will update if known
   * @param {string} [options.name]
   *        An optional name for the add-on
   * @param {string} [options.type]
   *        An optional type for the add-on
   * @param {object} [options.icons]
   *        Optional icons for the add-on
   * @param {string} [options.version]
   *        The expected version for the add-on.
   *        Required for updates, i.e. when existingAddon is set.
   * @param {Object?} [options.telemetryInfo]
   *        An optional object which provides details about the installation source
   *        included in the addon manager telemetry events.
   * @param {boolean} [options.isUserRequestedUpdate]
   *        An optional boolean, true if the install object is related to a user triggered update.
   * @param {nsIURL} [options.releaseNotesURI]
   *        An optional nsIURL that release notes where release notes can be retrieved.
   * @param {function(string) : Promise<void>} [options.promptHandler]
   *        A callback to prompt the user before installing.
   */
  constructor(installLocation, url, options = {}) {
    this.wrapper = new AddonInstallWrapper(this);
    this.location = installLocation;
    this.sourceURI = url;

    if (options.hash) {
      let hashSplit = options.hash.toLowerCase().split(":");
      this.originalHash = {
        algorithm: hashSplit[0],
        data: hashSplit[1],
      };
    }
    this.hash = this.originalHash;
    this.fileHash = null;
    this.existingAddon = options.existingAddon || null;
    this.promptHandler = options.promptHandler || (() => Promise.resolve());
    this.releaseNotesURI = options.releaseNotesURI || null;

    this._startupPromise = null;

    this._installPromise = new Promise(resolve => {
      this._resolveInstallPromise = resolve;
    });
    // Ignore uncaught rejections for this promise, since they're
    // handled by install listeners.
    this._installPromise.catch(() => {});

    this.listeners = [];
    this.icons = options.icons || {};
    this.error = 0;

    this.progress = 0;
    this.maxProgress = -1;

    // Giving each instance of AddonInstall a reference to the logger.
    this.logger = logger;

    this.name = options.name || null;
    this.type = options.type || null;
    this.version = options.version || null;
    this.isUserRequestedUpdate = options.isUserRequestedUpdate;
    this.installTelemetryInfo = null;

    if (options.telemetryInfo) {
      this.installTelemetryInfo = options.telemetryInfo;
    } else if (this.existingAddon) {
      // Inherits the installTelemetryInfo on updates (so that the source of the original
      // installation telemetry data is being preserved across the extension updates).
      this.installTelemetryInfo = this.existingAddon.installTelemetryInfo;
      this.existingAddon._updateInstall = this;
    }

    this.file = null;
    this.ownsTempFile = null;

    this.addon = null;
    this.state = null;

    // Backing up currently active theme id when a new install flow is about to start
    // (this will then be propagated to the AddonInternal object if the new addon
    // being installed is a static theme and it doesn't have the same addon id of
    // the theme already active, and eventually used to rollback to the previously
    // active theme through the Undo button available in the theme post-install dialog).
    this.initialActiveThemeID = Services.prefs.getCharPref(
      "extensions.activeThemeID",
      lazy.AddonSettings.DEFAULT_THEME_ID
    );

    XPIInstall.installs.add(this);
  }

  /**
   * Called when we are finished with this install and are ready to remove
   * any external references to it.
   */
  _cleanup() {
    XPIInstall.installs.delete(this);
    if (this.addon && this.addon._install) {
      if (this.addon._install === this) {
        this.addon._install = null;
      } else {
        Cu.reportError(new Error("AddonInstall mismatch"));
      }
    }
    if (this.existingAddon && this.existingAddon._updateInstall) {
      if (this.existingAddon._updateInstall === this) {
        this.existingAddon._updateInstall = null;
      } else {
        Cu.reportError(new Error("AddonInstall existingAddon mismatch"));
      }
    }
  }

  /**
   * Starts installation of this add-on from whatever state it is currently at
   * if possible.
   *
   * Note this method is overridden to handle additional state in
   * the subclassses below.
   *
   * @returns {Promise<Addon>}
   * @throws if installation cannot proceed from the current state
   */
  install() {
    switch (this.state) {
      case AddonManager.STATE_DOWNLOADED:
        this.checkPrompt();
        break;
      case AddonManager.STATE_PROMPTS_DONE:
        this.checkForBlockers();
        break;
      case AddonManager.STATE_READY:
        this.startInstall();
        break;
      case AddonManager.STATE_POSTPONED:
        logger.debug(`Postponing install of ${this.addon.id}`);
        break;
      case AddonManager.STATE_DOWNLOADING:
      case AddonManager.STATE_CHECKING_UPDATE:
      case AddonManager.STATE_INSTALLING:
        // Installation is already running
        break;
      default:
        throw new Error("Cannot start installing from this state");
    }
    return this._installPromise;
  }

  continuePostponedInstall() {
    if (this.state !== AddonManager.STATE_POSTPONED) {
      throw new Error("AddonInstall not in postponed state");
    }

    // Force the postponed install to continue.
    logger.info(`${this.addon.id} has resumed a previously postponed upgrade`);
    this.state = AddonManager.STATE_READY;
    this.install();
  }

  /**
   * Called during XPIProvider shutdown so that we can do any necessary
   * pre-shutdown cleanup.
   */
  onShutdown() {
    switch (this.state) {
      case AddonManager.STATE_POSTPONED:
        this.removeTemporaryFile();
        break;
    }
  }

  /**
   * Cancels installation of this add-on.
   *
   * Note this method is overridden to handle additional state in
   * the subclass DownloadAddonInstall.
   *
   * @throws if installation cannot be cancelled from the current state
   */
  cancel() {
    switch (this.state) {
      case AddonManager.STATE_AVAILABLE:
      case AddonManager.STATE_DOWNLOADED:
        logger.debug("Cancelling download of " + this.sourceURI.spec);
        this.state = AddonManager.STATE_CANCELLED;
        this._cleanup();
        this._callInstallListeners("onDownloadCancelled");
        this.removeTemporaryFile();
        break;
      case AddonManager.STATE_POSTPONED:
        logger.debug(`Cancelling postponed install of ${this.addon.id}`);
        this.state = AddonManager.STATE_CANCELLED;
        this._cleanup();
        this._callInstallListeners(
          "onInstallCancelled",
          /* aCancelledByUser */ false
        );
        this.removeTemporaryFile();

        let stagingDir = this.location.installer.getStagingDir();
        let stagedAddon = stagingDir.clone();

        this.unstageInstall(stagedAddon);
        break;
      default:
        throw new Error(
          "Cannot cancel install of " +
            this.sourceURI.spec +
            " from this state (" +
            this.state +
            ")"
        );
    }
  }

  /**
   * Adds an InstallListener for this instance if the listener is not already
   * registered.
   *
   * @param {InstallListener} aListener
   *        The InstallListener to add
   */
  addListener(aListener) {
    if (
      !this.listeners.some(function (i) {
        return i == aListener;
      })
    ) {
      this.listeners.push(aListener);
    }
  }

  /**
   * Removes an InstallListener for this instance if it is registered.
   *
   * @param {InstallListener} aListener
   *        The InstallListener to remove
   */
  removeListener(aListener) {
    this.listeners = this.listeners.filter(function (i) {
      return i != aListener;
    });
  }

  /**
   * Removes the temporary file owned by this AddonInstall if there is one.
   */
  removeTemporaryFile() {
    // Only proceed if this AddonInstall owns its XPI file
    if (!this.ownsTempFile) {
      this.logger.debug(
        `removeTemporaryFile: ${this.sourceURI.spec} does not own temp file`
      );
      return;
    }

    try {
      this.logger.debug(
        `removeTemporaryFile: ${this.sourceURI.spec} removing temp file ` +
          this.file.path
      );
      flushJarCache(this.file);
      this.file.remove(true);
      this.ownsTempFile = false;
    } catch (e) {
      this.logger.warn(
        `Failed to remove temporary file ${this.file.path} for addon ` +
          this.sourceURI.spec,
        e
      );
    }
  }

  _setFileHash(calculatedHash) {
    this.fileHash = {
      algorithm: this.hash ? this.hash.algorithm : DEFAULT_HASH_ALGO,
      data: calculatedHash,
    };

    if (this.hash && calculatedHash != this.hash.data) {
      return false;
    }
    return true;
  }

  /**
   * Updates the addon metadata that has to be propagated across restarts.
   */
  updatePersistedMetadata() {
    this.addon.sourceURI = this.sourceURI.spec;

    if (this.releaseNotesURI) {
      this.addon.releaseNotesURI = this.releaseNotesURI.spec;
    }

    if (this.installTelemetryInfo) {
      this.addon.installTelemetryInfo = this.installTelemetryInfo;
    }
  }

  /**
   * Called after the add-on is a local file and the signature and install
   * manifest can be read.
   *
   * @param {nsIFile} file
   *        The file from which to load the manifest.
   * @returns {Promise<void>}
   */
  async loadManifest(file) {
    let pkg;
    try {
      pkg = Package.get(file);
    } catch (e) {
      return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
    }

    try {
      try {
        this.addon = await loadManifest(pkg, this.location, this.existingAddon);
        // Set the install.name property to the addon name if it is not set yet,
        // install.name is expected to be set to the addon name and used to
        // fill the addon name in the fluent strings when reporting install
        // errors.
        this.name = this.name ?? this.addon.name;
      } catch (e) {
        return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
      }

      if (!this.addon.id) {
        let msg = `Cannot find id for addon ${file.path}.`;
        if (Services.prefs.getBoolPref(PREF_XPI_SIGNATURES_DEV_ROOT, false)) {
          msg += ` Preference ${PREF_XPI_SIGNATURES_DEV_ROOT} is set.`;
        }

        return Promise.reject([
          AddonManager.ERROR_CORRUPT_FILE,
          new Error(msg),
        ]);
      }

      if (
        AppConstants.platform == "android" &&
        this.addon.type !== "extension"
      ) {
        return Promise.reject([
          AddonManager.ERROR_UNSUPPORTED_ADDON_TYPE,
          `Unsupported add-on type: ${this.addon.type}`,
        ]);
      }

      if (this.existingAddon) {
        // Check various conditions related to upgrades
        if (this.addon.id != this.existingAddon.id) {
          return Promise.reject([
            AddonManager.ERROR_INCORRECT_ID,
            `Refusing to upgrade addon ${this.existingAddon.id} to different ID ${this.addon.id}`,
          ]);
        }

        if (this.existingAddon.isWebExtension && !this.addon.isWebExtension) {
          // This condition is never met on regular Firefox builds.
          // Remove it along with externalExtensionLoaders (bug 1674799).
          return Promise.reject([
            AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
            "WebExtensions may not be updated to other extension types",
          ]);
        }
        if (this.existingAddon.type != this.addon.type) {
          return Promise.reject([
            AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
            `Refusing to change addon type from ${this.existingAddon.type} to ${this.addon.type}`,
          ]);
        }

        if (this.version !== this.addon.version) {
          return Promise.reject([
            AddonManager.ERROR_UNEXPECTED_ADDON_VERSION,
            `Expected addon version ${this.version} instead of ${this.addon.version}`,
          ]);
        }
      }

      if (XPIExports.XPIDatabase.mustSign(this.addon.type)) {
        if (this.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
          // This add-on isn't properly signed by a signature that chains to the
          // trusted root.
          let state = this.addon.signedState;
          this.addon = null;

          if (state == AddonManager.SIGNEDSTATE_MISSING) {
            return Promise.reject([
              AddonManager.ERROR_SIGNEDSTATE_REQUIRED,
              "signature is required but missing",
            ]);
          }

          return Promise.reject([
            AddonManager.ERROR_CORRUPT_FILE,
            "signature verification failed",
          ]);
        }

        if (
          this.addon.adminInstallOnly &&
          !this.addon.wrapper.isInstalledByEnterprisePolicy
        ) {
          return Promise.reject([
            AddonManager.ERROR_ADMIN_INSTALL_ONLY,
            "This addon can only be installed through Enterprise Policies",
          ]);
        }

        // Restrict install for signed extension only signed with weak signature algorithms, unless the
        // restriction is explicitly disabled through prefs or enterprise policies.
        if (
          !XPIInstall.isWeakSignatureInstallAllowed() &&
          this.addon.signedDate &&
          !hasStrongSignature(this.addon)
        ) {
          const addonAllowedByPolicies =
            Services.policies?.getExtensionSettings(
              this.addon.id
            )?.temporarily_allow_weak_signatures;

          const globallyAllowedByPolicies =
            Services.policies?.getExtensionSettings(
              "*"
            )?.temporarily_allow_weak_signatures;

          const allowedByPolicies =
            (globallyAllowedByPolicies &&
              (addonAllowedByPolicies || addonAllowedByPolicies == null)) ||
            addonAllowedByPolicies;

          if (
            !allowedByPolicies &&
            (!this.existingAddon || hasStrongSignature(this.existingAddon))
          ) {
            // Reject if it is a new install or installing over an existing addon including
            // strong cryptographic signatures.
            return Promise.reject([
              AddonManager.ERROR_CORRUPT_FILE,
              "install rejected due to the package not including a strong cryptographic signature",
            ]);
          }

          // Still allow installs using weak signatures to install if either:
          // - it is explicitly allowed through Enterprise Policies Settings
          // - or there is an existing addon with a weak signature.
          logger.warn(
            allowedByPolicies
              ? `Allow weak signature install for ${this.addon.id} XPI due to Enterprise Policies`
              : `Allow weak signature install over existing "${this.existingAddon.id}" XPI`
          );
        }
      }
    } finally {
      pkg.close();
    }

    this.updatePersistedMetadata();

    this.addon._install = this;
    this.name = this.addon.selectedLocale.name;
    this.type = this.addon.type;
    this.version = this.addon.version;

    // Setting the iconURL to something inside the XPI locks the XPI and
    // makes it impossible to delete on Windows.

    // Try to load from the existing cache first
    let repoAddon = await lazy.AddonRepository.getCachedAddonByID(
      this.addon.id
    );

    // It wasn't there so try to re-download it
    if (!repoAddon) {
      try {
        [repoAddon] = await lazy.AddonRepository.cacheAddons([this.addon.id]);
      } catch (err) {
        logger.debug(
          `Error getting metadata for ${this.addon.id}: ${err.message}`
        );
      }
    }

    this.addon._repositoryAddon = repoAddon;
    this.name = this.name || this.addon._repositoryAddon.name;
    this.addon.appDisabled = !XPIExports.XPIDatabase.isUsableAddon(this.addon);
    return undefined;
  }

  getIcon(desiredSize = 64) {
    if (!this.addon.icons || !this.file) {
      return null;
    }

    let { icon } = lazy.IconDetails.getPreferredIcon(
      this.addon.icons,
      null,
      desiredSize
    );
    if (icon.startsWith("chrome://")) {
      return icon;
    }
    return getJarURI(this.file, icon).spec;
  }

  /**
   * This method should be called when the XPI is ready to be installed,
   * i.e., when a download finishes or when a local file has been verified.
   * It should only be called from install() when the install is in
   * STATE_DOWNLOADED (which actually means that the file is available
   * and has been verified).
   */
  checkPrompt() {
    (async () => {
      if (this.promptHandler) {
        let info = {
          existingAddon: this.existingAddon ? this.existingAddon.wrapper : null,
          addon: this.addon.wrapper,
          icon: this.getIcon(),
          // Used in AMTelemetry to detect the install flow related to this prompt.
          install: this.wrapper,
        };

        try {
          await this.promptHandler(info);
        } catch (err) {
          if (this.error < 0) {
            logger.info(`Install of ${this.addon.id} failed ${this.error}`);
            this.state = AddonManager.STATE_INSTALL_FAILED;
            this._cleanup();
            // In some cases onOperationCancelled is called during failures
            // to install/uninstall/enable/disable addons.  We may need to
            // do that here in the future.
            this._callInstallListeners("onInstallFailed");
            this.removeTemporaryFile();
          } else {
            logger.info(`Install of ${this.addon.id} cancelled by user`);
            this.state = AddonManager.STATE_CANCELLED;
            this._cleanup();
            this._callInstallListeners(
              "onInstallCancelled",
              /* aCancelledByUser */ true
            );
          }
          return;
        }
      }
      this.state = AddonManager.STATE_PROMPTS_DONE;
      this.install();
    })();
  }

  /**
   * This method should be called when we have the XPI and any needed
   * permissions prompts have been completed.  If there are any upgrade
   * listeners, they are invoked and the install moves into STATE_POSTPONED.
   * Otherwise, the install moves into STATE_INSTALLING
   */
  checkForBlockers() {
    // If an upgrade listener is registered for this add-on, pass control
    // over the upgrade to the add-on.
    if (AddonManagerPrivate.hasUpgradeListener(this.addon.id)) {
      logger.info(
        `add-on ${this.addon.id} has an upgrade listener, postponing upgrade until restart`
      );
      let resumeFn = () => {
        this.continuePostponedInstall();
      };
      this.postpone(resumeFn);
      return;
    }

    this.state = AddonManager.STATE_READY;
    this.install();
  }

  /**
   * Installs the add-on into the install location.
   */
  async startInstall() {
    this.state = AddonManager.STATE_INSTALLING;
    if (!this._callInstallListeners("onInstallStarted")) {
      this.state = AddonManager.STATE_DOWNLOADED;
      this.removeTemporaryFile();
      this._cleanup();
      this._callInstallListeners(
        "onInstallCancelled",
        /* aCancelledByUser */ false
      );
      return;
    }

    // Reinstall existing user-disabled addon (of the same installed version).
    // If addon is marked to be uninstalled - don't reinstall it.
    if (
      this.existingAddon &&
      this.existingAddon.location === this.location &&
      this.existingAddon.version === this.addon.version &&
      this.existingAddon.userDisabled &&
      !this.existingAddon.pendingUninstall
    ) {
      await XPIExports.XPIDatabase.updateAddonDisabledState(
        this.existingAddon,
        {
          userDisabled: false,
        }
      );
      this.state = AddonManager.STATE_INSTALLED;
      this._callInstallListeners("onInstallEnded", this.existingAddon.wrapper);
      this._cleanup();
      return;
    }

    let isSameLocation = this.existingAddon?.location == this.location;
    let willActivate =
      isSameLocation ||
      !this.existingAddon ||
      this.location.hasPrecedence(this.existingAddon.location);

    logger.debug(
      "Starting install of " + this.addon.id + " from " + this.sourceURI.spec
    );

    if (
      this.addon.type === "theme" &&
      this.addon.id !== this.initialActiveThemeID
    ) {
      // Propagate the theme ID that was active when a new theme install flow has
      // started, in case we need to restore it (on user clicking Undo in the post-install
      // theme dialog).
      this.addon.previousActiveThemeID = this.initialActiveThemeID;
    }

    AddonManagerPrivate.callAddonListeners(
      "onInstalling",
      this.addon.wrapper,
      false
    );

    let stagedAddon = this.location.installer.getStagingDir();

    try {
      await this.location.installer.requestStagingDir();

      // remove any previously staged files
      await this.unstageInstall(stagedAddon);

      stagedAddon.append(`${this.addon.id}.xpi`);

      await this.stageInstall(false, stagedAddon, isSameLocation);

      this._cleanup();

      let install = async () => {
        // Mark this instance of the addon as inactive if it is being
        // superseded by an addon in a different location.
        if (
          willActivate &&
          this.existingAddon &&
          this.existingAddon.active &&
          !isSameLocation
        ) {
          XPIExports.XPIDatabase.updateAddonActive(this.existingAddon, false);
        }

        // Install the new add-on into its final location
        let file = await this.location.installer.installAddon({
          id: this.addon.id,
          source: stagedAddon,
        });

        // Update the metadata in the database
        this.addon.sourceBundle = file;
        // If this addon will be the active addon, make it visible.
        this.addon.visible = willActivate;

        if (isSameLocation) {
          this.addon = XPIExports.XPIDatabase.updateAddonMetadata(
            this.existingAddon,
            this.addon,
            file.path
          );
          let state = this.location.get(this.addon.id);
          if (state) {
            state.syncWithDB(this.addon, true);
          } else {
            logger.warn(
              "Unexpected missing XPI state for add-on ${id}",
              this.addon
            );
          }
        } else {
          this.addon.active = this.addon.visible && !this.addon.disabled;
          this.addon = XPIExports.XPIDatabase.addToDatabase(
            this.addon,
            file.path
          );
          XPIExports.XPIInternal.XPIStates.addAddon(this.addon);
          this.addon.installDate = this.addon.updateDate;
          XPIExports.XPIDatabase.saveChanges();
        }
        XPIExports.XPIInternal.XPIStates.save();

        AddonManagerPrivate.callAddonListeners(
          "onInstalled",
          this.addon.wrapper
        );

        logger.debug(`Install of ${this.sourceURI.spec} completed.`);
        this.state = AddonManager.STATE_INSTALLED;
        this._callInstallListeners("onInstallEnded", this.addon.wrapper);

        XPIExports.XPIDatabase.recordAddonTelemetry(this.addon);

        // Notify providers that a new theme has been enabled.
        if (this.addon.type === "theme" && this.addon.active) {
          AddonManagerPrivate.notifyAddonChanged(
            this.addon.id,
            this.addon.type
          );
        }

        // Clear the colorways builtins migrated to a non-builtin themes
        // form the list of the retained themes.
        if (
          this.existingAddon?.isBuiltinColorwayTheme &&
          !this.addon.isBuiltin &&
          XPIExports.BuiltInThemesHelpers.isColorwayMigrationEnabled
        ) {
          XPIExports.BuiltInThemesHelpers.unretainMigratedColorwayTheme(
            this.addon.id
          );
        }
      };

      this._startupPromise = (async () => {
        if (!willActivate) {
          await install();
        } else if (this.existingAddon) {
          await XPIExports.XPIInternal.BootstrapScope.get(
            this.existingAddon
          ).update(this.addon, !this.addon.disabled, install);

          if (this.addon.disabled) {
            flushJarCache(this.file);
          }
        } else {
          await install();
          await XPIExports.XPIInternal.BootstrapScope.get(this.addon).install(
            undefined,
            true
          );
        }
      })();

      await this._startupPromise;
    } catch (e) {
      logger.warn(
        `Failed to install ${this.file.path} from ${this.sourceURI.spec} to ${stagedAddon.path}`,
        e
      );

      if (stagedAddon.exists()) {
        recursiveRemove(stagedAddon);
      }
      this.state = AddonManager.STATE_INSTALL_FAILED;
      this.error = AddonManager.ERROR_FILE_ACCESS;
      this._cleanup();
      AddonManagerPrivate.callAddonListeners(
        "onOperationCancelled",
        this.addon.wrapper
      );
      this._callInstallListeners("onInstallFailed");
    } finally {
      this.removeTemporaryFile();
      this.location.installer.releaseStagingDir();
    }
  }

  /**
   * Stages an add-on for install.
   *
   * @param {boolean} restartRequired
   *        If true, the final installation will be deferred until the
   *        next app startup.
   * @param {nsIFile} stagedAddon
   *        The file where the add-on should be staged.
   * @param {boolean} isSameLocation
   *        True if this installation is an upgrade for an existing
   *        add-on in the same location.
   * @throws if the file cannot be staged.
   */
  async stageInstall(restartRequired, stagedAddon, isSameLocation) {
    logger.debug(`Addon ${this.addon.id} will be installed as a packed xpi`);
    stagedAddon.leafName = `${this.addon.id}.xpi`;

    try {
      await IOUtils.copy(this.file.path, stagedAddon.path);

      let calculatedHash = getHashForFile(stagedAddon, this.fileHash.algorithm);
      if (calculatedHash != this.fileHash.data) {
        logger.warn(
          `Staged file hash (${calculatedHash}) did not match initial hash (${this.fileHash.data})`
        );
--> --------------------

--> maximum size reached

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

[ Dauer der Verarbeitung: 0.46 Sekunden  (vorverarbeitet)  ]