Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  XPIDatabase.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 maintain the
 * extensions database, including querying and modifying extension
 * metadata. In general, we try to avoid loading it during startup when
 * at all possible. Please keep that in mind when deciding whether to
 * add code here or elsewhere.
 */

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

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

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

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
  AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
  AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
  AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs",
  Blocklist: "resource://gre/modules/Blocklist.sys.mjs",
  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
  ExtensionData: "resource://gre/modules/Extension.sys.mjs",
  ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs",
  ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
  PermissionsUtils: "resource://gre/modules/PermissionsUtils.sys.mjs",
  QuarantinedDomains: "resource://gre/modules/ExtensionPermissions.sys.mjs",
  ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
});

// WARNING: BuiltInThemes.sys.mjs may be provided by the host application (e.g.
// Firefox), or it might not exist at all. Use with caution, as we don't
// want things to completely fail if that module can't be loaded.
ChromeUtils.defineLazyGetter(lazy, "BuiltInThemes", () => {
  try {
    let { BuiltInThemes } = ChromeUtils.importESModule(
      "resource:///modules/BuiltInThemes.sys.mjs"
    );
    return BuiltInThemes;
  } catch (e) {
    Cu.reportError(`Unable to load BuiltInThemes.sys.mjs: ${e}`);
  }
  return undefined;
});

// A set of helpers to account from a single place that in some builds
// (e.g. GeckoView and Thunderbird) the BuiltInThemes module may either
// not be bundled at all or not be exposing the same methods provided
// by the module as defined in Firefox Desktop.
export const BuiltInThemesHelpers = {
  getLocalizedColorwayGroupName(addonId) {
    return lazy.BuiltInThemes?.getLocalizedColorwayGroupName?.(addonId);
  },

  getLocalizedColorwayDescription(addonId) {
    return lazy.BuiltInThemes?.getLocalizedColorwayGroupDescription?.(addonId);
  },

  isActiveTheme(addonId) {
    return lazy.BuiltInThemes?.isActiveTheme?.(addonId);
  },

  isRetainedExpiredTheme(addonId) {
    return lazy.BuiltInThemes?.isRetainedExpiredTheme?.(addonId);
  },

  themeIsExpired(addonId) {
    return lazy.BuiltInThemes?.themeIsExpired?.(addonId);
  },

  // Helper function called form XPInstall.sys.mjs to remove from the retained
  // themes list the built-in colorways theme that have been migrated to a non
  // built-in.
  unretainMigratedColorwayTheme(addonId) {
    lazy.BuiltInThemes?.unretainMigratedColorwayTheme?.(addonId);
  },
};

XPCOMUtils.defineLazyPreferenceGetter(
  BuiltInThemesHelpers,
  "isColorwayMigrationEnabled",
  "browser.theme.colorway-migration",
  false
);

// A temporary hidden pref just meant to be used as a last resort, in case
// we need to force-disable the "per-addon quarantined domains user controls"
// feature during the beta cycle, e.g. if unexpected issues are caught late and
// it shouldn't  ride the train.
//
// TODO(Bug 1839616): remove this pref after the user controls features have been
// released.
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "isQuarantineUIDisabled",
  "extensions.quarantinedDomains.uiDisabled",
  false
);

const { nsIBlocklistService } = Ci;

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

const LOGGER_ID = "addons.xpi-utils";

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

// Create a new logger for use by the Addons XPI Provider Utils
// (Requires AddonManager.sys.mjs)
var logger = Log.repository.getLogger(LOGGER_ID);

const FILE_JSON_DB = "extensions.json";

const PREF_DB_SCHEMA = "extensions.databaseSchema";
const PREF_EM_AUTO_DISABLED_SCOPES = "extensions.autoDisableScopes";
const PREF_PENDING_OPERATIONS = "extensions.pendingOperations";
const PREF_XPI_PERMISSIONS_BRANCH = "xpinstall.";
const PREF_XPI_SIGNATURES_DEV_ROOT = "xpinstall.signatures.dev-root";

const TOOLKIT_ID = "toolkit@mozilla.org";

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

const DEFAULT_THEME_ID = "default-theme@mozilla.org";

// Properties to cache and reload when an addon installation is pending
const PENDING_INSTALL_METADATA = [
  "syncGUID",
  "targetApplications",
  "userDisabled",
  "softDisabled",
  "embedderDisabled",
  "sourceURI",
  "releaseNotesURI",
  "installDate",
  "updateDate",
  "applyBackgroundUpdates",
  "installTelemetryInfo",
];

// Properties to save in JSON file
const PROP_JSON_FIELDS = [
  "id",
  "syncGUID",
  "version",
  "type",
  "loader",
  "updateURL",
  "installOrigins",
  "manifestVersion",
  "optionsURL",
  "optionsType",
  "optionsBrowserStyle",
  "aboutURL",
  "defaultLocale",
  "visible",
  "active",
  "userDisabled",
  "appDisabled",
  "embedderDisabled",
  "pendingUninstall",
  "installDate",
  "updateDate",
  "applyBackgroundUpdates",
  "path",
  "skinnable",
  "sourceURI",
  "releaseNotesURI",
  "softDisabled",
  "foreignInstall",
  "strictCompatibility",
  "locales",
  "targetApplications",
  "targetPlatforms",
  "signedState",
  "signedTypes",
  "signedDate",
  "seen",
  "dependencies",
  "incognito",
  "userPermissions",
  "optionalPermissions",
  "requestedPermissions",
  "icons",
  "iconURL",
  "blocklistAttentionDismissed",
  "blocklistState",
  "blocklistURL",
  "startupData",
  "previewImage",
  "hidden",
  "installTelemetryInfo",
  "recommendationState",
  "rootURI",
];

const SIGNED_TYPES = new Set(["extension", "locale", "theme"]);

// Time to wait before async save of XPI JSON database, in milliseconds
const ASYNC_SAVE_DELAY_MS = 20;

const l10n = new Localization(["browser/appExtensionFields.ftl"], true);

/**
 * Schedules an idle task, and returns a promise which resolves to an
 * IdleDeadline when an idle slice is available. The caller should
 * perform all of its idle work in the same micro-task, before the
 * deadline is reached.
 *
 * @returns {Promise<IdleDeadline>}
 */
function promiseIdleSlice() {
  return new Promise(resolve => {
    ChromeUtils.idleDispatch(resolve);
  });
}

let arrayForEach = Function.call.bind(Array.prototype.forEach);

/**
 * Loops over the given array, in the same way as Array forEach, but
 * splitting the work among idle tasks.
 *
 * @param {Array} array
 *        The array to loop over.
 * @param {function} func
 *        The function to call on each array element.
 * @param {integer} [taskTimeMS = 5]
 *        The minimum time to allocate to each task. If less time than
 *        this is available in a given idle slice, and there are more
 *        elements to loop over, they will be deferred until the next
 *        idle slice.
 */
async function idleForEach(array, func, taskTimeMS = 5) {
  let deadline;
  for (let i = 0; i < array.length; i++) {
    if (!deadline || deadline.timeRemaining() < taskTimeMS) {
      deadline = await promiseIdleSlice();
    }
    func(array[i], i);
  }
}

/**
 * Asynchronously fill in the _repositoryAddon field for one addon
 *
 * @param {AddonInternal} aAddon
 *        The add-on to annotate.
 * @returns {AddonInternal}
 *        The annotated add-on.
 */
async function getRepositoryAddon(aAddon) {
  if (aAddon) {
    aAddon._repositoryAddon = await lazy.AddonRepository.getCachedAddonByID(
      aAddon.id
    );
  }
  return aAddon;
}

/**
 * Copies properties from one object to another. If no target object is passed
 * a new object will be created and returned.
 *
 * @param {object} aObject
 *        An object to copy from
 * @param {string[]} aProperties
 *        An array of properties to be copied
 * @param {object?} [aTarget]
 *        An optional target object to copy the properties to
 * @returns {Object}
 *        The object that the properties were copied onto
 */
function copyProperties(aObject, aProperties, aTarget) {
  if (!aTarget) {
    aTarget = {};
  }
  aProperties.forEach(function (aProp) {
    if (aProp in aObject) {
      aTarget[aProp] = aObject[aProp];
    }
  });
  return aTarget;
}

// Maps instances of AddonInternal to AddonWrapper
const wrapperMap = new WeakMap();
let addonFor = wrapper => wrapperMap.get(wrapper);

const EMPTY_ARRAY = Object.freeze([]);

let AddonWrapper;

/**
 * The AddonInternal is an internal only representation of add-ons. It
 * may have come from the database or an extension manifest.
 */
export class AddonInternal {
  constructor(addonData) {
    this._wrapper = null;
    this._selectedLocale = null;
    this.active = false;
    this.visible = false;
    this.userDisabled = false;
    this.appDisabled = false;
    this.softDisabled = false;
    this.embedderDisabled = false;
    this.blocklistAttentionDismissed = false;
    this.blocklistState = nsIBlocklistService.STATE_NOT_BLOCKED;
    this.blocklistURL = null;
    this.sourceURI = null;
    this.releaseNotesURI = null;
    this.foreignInstall = false;
    this.seen = true;
    this.skinnable = false;
    this.startupData = null;
    this._hidden = false;
    this.installTelemetryInfo = null;
    this.rootURI = null;
    this._updateInstall = null;
    this.recommendationState = null;

    this.inDatabase = false;

    /**
     * @property {Array<string>} dependencies
     *   An array of bootstrapped add-on IDs on which this add-on depends.
     *   The add-on will remain appDisabled if any of the dependent
     *   add-ons is not installed and enabled.
     */
    this.dependencies = EMPTY_ARRAY;

    if (addonData) {
      copyProperties(addonData, PROP_JSON_FIELDS, this);
      this.location = addonData.location;

      if (!this.dependencies) {
        this.dependencies = [];
      }
      Object.freeze(this.dependencies);

      if (this.location) {
        this.addedToDatabase();
      }

      this.sourceBundle = addonData._sourceBundle;
    }
  }

  get sourceBundle() {
    return this._sourceBundle;
  }

  set sourceBundle(file) {
    this._sourceBundle = file;
    if (file) {
      this.rootURI = XPIExports.XPIInternal.getURIForResourceInFile(
        file,
        ""
      ).spec;
    }
  }

  get wrapper() {
    if (!this._wrapper) {
      this._wrapper = new AddonWrapper(this);
    }
    return this._wrapper;
  }

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

  get isBuiltinColorwayTheme() {
    return (
      this.type === "theme" &&
      this.location.isBuiltin &&
      this.id.endsWith("-colorway@mozilla.org")
    );
  }

  /**
   * Validate a list of origins are contained in the installOrigins array (defined in manifest.json).
   *
   * SitePermission addons are a special case, where the triggering install site may be a subdomain
   * of a valid xpi origin.
   *
   * @param {Object}  origins             Object containing URIs related to install.
   * @params {nsIURI} origins.installFrom The nsIURI of the website that has triggered the install flow.
   * @params {nsIURI} origins.source      The nsIURI where the xpi is hosted.
   * @returns {boolean}
   */
  validInstallOrigins({ installFrom, source }) {
    if (
      !Services.prefs.getBoolPref("extensions.install_origins.enabled", true)
    ) {
      return true;
    }

    let { installOrigins, manifestVersion } = this;
    if (!installOrigins) {
      // Install origins are mandatory in MV3 and optional
      // in MV2.  Old addons need to keep installing per the
      // old install flow.
      return manifestVersion < 3;
    }
    // An empty install_origins prevents any install from 3rd party websites.
    if (!installOrigins.length) {
      return false;
    }

    for (const [name, uri] of Object.entries({ installFrom, source })) {
      if (!installOrigins.includes(new URL(uri.spec).origin)) {
        logger.warn(
          `Addon ${this.id} Installation not allowed, ${name} "${uri.spec}" is not included in the Addon install_origins`
        );
        return false;
      }
    }
    return true;
  }

  addedToDatabase() {
    this._key = `${this.location.name}:${this.id}`;
    this.inDatabase = true;
  }

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

  get selectedLocale() {
    if (this._selectedLocale) {
      return this._selectedLocale;
    }

    /**
     * this.locales is a list of objects that have property `locales`.
     * It's value is an array of locale codes.
     *
     * First, we reduce this nested structure to a flat list of locale codes.
     */
    const locales = [].concat(...this.locales.map(loc => loc.locales));

    let requestedLocales = Services.locale.requestedLocales;

    /**
     * If en-US is not in the list, add it as the last fallback.
     */
    if (!requestedLocales.includes("en-US")) {
      requestedLocales.push("en-US");
    }

    /**
     * Then we negotiate best locale code matching the app locales.
     */
    let bestLocale = Services.locale.negotiateLanguages(
      requestedLocales,
      locales,
      "und",
      Services.locale.langNegStrategyLookup
    )[0];

    /**
     * If no match has been found, we'll assign the default locale as
     * the selected one.
     */
    if (bestLocale === "und") {
      this._selectedLocale = this.defaultLocale;
    } else {
      /**
       * Otherwise, we'll go through all locale entries looking for the one
       * that has the best match in it's locales list.
       */
      this._selectedLocale = this.locales.find(loc =>
        loc.locales.includes(bestLocale)
      );
    }

    return this._selectedLocale;
  }

  get providesUpdatesSecurely() {
    return !this.updateURL || this.updateURL.startsWith("https:");
  }

  get isCorrectlySigned() {
    switch (this.location.name) {
      case KEY_APP_SYSTEM_PROFILE:
        // Add-ons installed via Normandy must be signed by the system
        // key or the "Mozilla Extensions" key.
        return [
          lazy.AddonManager.SIGNEDSTATE_SYSTEM,
          lazy.AddonManager.SIGNEDSTATE_PRIVILEGED,
        ].includes(this.signedState);
      case KEY_APP_SYSTEM_ADDONS:
        // System add-ons must be signed by the system key.
        return this.signedState == lazy.AddonManager.SIGNEDSTATE_SYSTEM;

      case KEY_APP_SYSTEM_DEFAULTS:
      case KEY_APP_BUILTINS:
      case KEY_APP_TEMPORARY:
        // Temporary and built-in add-ons do not require signing.
        return true;

      case KEY_APP_SYSTEM_SHARE:
      case KEY_APP_SYSTEM_LOCAL:
        // On UNIX platforms except OSX, an additional location for system
        // add-ons exists in /usr/{lib,share}/mozilla/extensions. Add-ons
        // installed there do not require signing.
        if (Services.appinfo.OS != "Darwin") {
          return true;
        }
        break;
    }

    if (this.signedState === lazy.AddonManager.SIGNEDSTATE_NOT_REQUIRED) {
      return true;
    }
    return this.signedState > lazy.AddonManager.SIGNEDSTATE_MISSING;
  }

  get isCompatible() {
    return this.isCompatibleWith();
  }

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

  get hidden() {
    return (
      this.location.hidden ||
      // The hidden flag is intended to only be used for features that are part
      // of the application. Temporary add-ons should not be hidden.
      (this._hidden && this.isPrivileged && !this.location.isTemporary) ||
      false
    );
  }

  set hidden(val) {
    this._hidden = val;
  }

  get disabled() {
    return (
      this.userDisabled ||
      this.appDisabled ||
      this.softDisabled ||
      this.embedderDisabled
    );
  }

  get isPlatformCompatible() {
    if (!this.targetPlatforms.length) {
      return true;
    }

    let matchedOS = false;

    // If any targetPlatform matches the OS and contains an ABI then we will
    // only match a targetPlatform that contains both the current OS and ABI
    let needsABI = false;

    // Some platforms do not specify an ABI, test against null in that case.
    let abi = null;
    try {
      abi = Services.appinfo.XPCOMABI;
    } catch (e) {}

    // Something is causing errors in here
    try {
      for (let platform of this.targetPlatforms) {
        if (platform.os == Services.appinfo.OS) {
          if (platform.abi) {
            needsABI = true;
            if (platform.abi === abi) {
              return true;
            }
          } else {
            matchedOS = true;
          }
        }
      }
    } catch (e) {
      let message =
        "Problem with addon " +
        this.id +
        " targetPlatforms " +
        JSON.stringify(this.targetPlatforms);
      logger.error(message, e);
      lazy.AddonManagerPrivate.recordException("XPI", message, e);
      // don't trust this add-on
      return false;
    }

    return matchedOS && !needsABI;
  }

  isCompatibleWith(aAppVersion, aPlatformVersion) {
    let app = this.matchingTargetApplication;
    if (!app) {
      return false;
    }

    // set reasonable defaults for minVersion and maxVersion
    let minVersion = app.minVersion || "0";
    let maxVersion = app.maxVersion || "*";

    if (!aAppVersion) {
      aAppVersion = Services.appinfo.version;
    }
    if (!aPlatformVersion) {
      aPlatformVersion = Services.appinfo.platformVersion;
    }

    let version;
    if (app.id == Services.appinfo.ID) {
      version = aAppVersion;
    } else if (app.id == TOOLKIT_ID) {
      version = aPlatformVersion;
    }

    // Only extensions and dictionaries can be compatible by default; themes
    // and language packs always use strict compatibility checking.
    // Dictionaries are compatible by default unless requested by the dictinary.
    if (
      !this.strictCompatibility &&
      (!lazy.AddonManager.strictCompatibility || this.type == "dictionary")
    ) {
      return Services.vc.compare(version, minVersion) >= 0;
    }

    return (
      Services.vc.compare(version, minVersion) >= 0 &&
      Services.vc.compare(version, maxVersion) <= 0
    );
  }

  get matchingTargetApplication() {
    let app = null;
    for (let targetApp of this.targetApplications) {
      if (targetApp.id == Services.appinfo.ID) {
        return targetApp;
      }
      if (targetApp.id == TOOLKIT_ID) {
        app = targetApp;
      }
    }
    return app;
  }

  updateBlocklistAttentionDismissed(val) {
    if (!this.inDatabase || this.blocklistAttentionDismissed === val) {
      return;
    }
    this.blocklistAttentionDismissed = val;
    XPIDatabase.maybeUpdateBlocklistAttentionAddonIdsSet(this);
    XPIDatabase.saveChanges();
  }

  async findBlocklistEntry() {
    return lazy.Blocklist.getAddonBlocklistEntry(this.wrapper);
  }

  async updateBlocklistState(options = {}) {
    if (this.location.isSystem || this.location.isBuiltin) {
      return;
    }

    let { applySoftBlock = true, updateDatabase = true } = options;

    let oldState = this.blocklistState;

    let entry = await this.findBlocklistEntry();
    let newState = entry ? entry.state : Services.blocklist.STATE_NOT_BLOCKED;

    // Clear the blocklistAttentionDismissed flag if the blocklist state
    // is changing.
    if (this.blocklistState !== newState) {
      this.updateBlocklistAttentionDismissed(false);
    }

    this.blocklistState = newState;
    this.blocklistURL = entry && entry.url;

    let userDisabled, softDisabled;
    // After a blocklist update, the blocklist service manually applies
    // new soft blocks after displaying a UI, in which cases we need to
    // skip updating it here.
    if (applySoftBlock && oldState != newState) {
      if (newState == Services.blocklist.STATE_SOFTBLOCKED) {
        if (this.type == "theme") {
          userDisabled = true;
        } else {
          softDisabled = !this.userDisabled;
        }
      } else {
        softDisabled = false;
      }
    }

    if (this.inDatabase && updateDatabase) {
      await XPIDatabase.updateAddonDisabledState(this, {
        userDisabled,
        softDisabled,
      });
      XPIDatabase.saveChanges();
    } else {
      this.appDisabled = !XPIDatabase.isUsableAddon(this);
      if (userDisabled !== undefined) {
        this.userDisabled = userDisabled;
      }
      if (softDisabled !== undefined) {
        this.softDisabled = softDisabled;
      }
    }

    if (oldState != newState) {
      lazy.AddonManagerPrivate.callAddonListeners(
        "onPropertyChanged",
        this.wrapper,
        ["blocklistState"]
      );
      if (this.active) {
        // Make sure to sync the XPIState with the blocklistState
        // set in the AddonDB if the addon is active.
        XPIDatabase.updateXPIStates(this);
      }
    }
  }

  recordAddonBlockChangeTelemetry(reason) {
    lazy.Blocklist.recordAddonBlockChangeTelemetry(this.wrapper, reason);
  }

  async setUserDisabled(val, allowSystemAddons = false) {
    if (val == (this.userDisabled || this.softDisabled)) {
      return;
    }

    if (this.inDatabase) {
      // System add-ons should not be user disabled, as there is no UI to
      // re-enable them.
      if (this.location.isSystem && !allowSystemAddons) {
        throw new Error(`Cannot disable system add-on ${this.id}`);
      }
      await XPIDatabase.updateAddonDisabledState(this, { userDisabled: val });
    } else {
      this.userDisabled = val;
      // When enabling remove the softDisabled flag
      if (!val) {
        this.softDisabled = false;
      }
    }
  }

  applyCompatibilityUpdate(aUpdate, aSyncCompatibility) {
    let wasCompatible = this.isCompatible;

    for (let targetApp of this.targetApplications) {
      for (let updateTarget of aUpdate.targetApplications) {
        if (
          targetApp.id == updateTarget.id &&
          (aSyncCompatibility ||
            Services.vc.compare(targetApp.maxVersion, updateTarget.maxVersion) <
              0)
        ) {
          targetApp.minVersion = updateTarget.minVersion;
          targetApp.maxVersion = updateTarget.maxVersion;

          if (this.inDatabase) {
            XPIDatabase.saveChanges();
          }
        }
      }
    }

    if (wasCompatible != this.isCompatible) {
      if (this.inDatabase) {
        XPIDatabase.updateAddonDisabledState(this);
      } else {
        this.appDisabled = !XPIDatabase.isUsableAddon(this);
      }
    }
  }

  toJSON() {
    let obj = copyProperties(this, PROP_JSON_FIELDS);
    obj.location = this.location.name;
    return obj;
  }

  /**
   * When an add-on install is pending its metadata will be cached in a file.
   * This method reads particular properties of that metadata that may be newer
   * than that in the extension manifest, like compatibility information.
   *
   * @param {Object} aObj
   *        A JS object containing the cached metadata
   */
  importMetadata(aObj) {
    for (let prop of PENDING_INSTALL_METADATA) {
      if (!(prop in aObj)) {
        continue;
      }

      this[prop] = aObj[prop];
    }

    // Compatibility info may have changed so update appDisabled
    this.appDisabled = !XPIDatabase.isUsableAddon(this);
  }

  permissions() {
    let permissions = 0;

    let settings = Services.policies?.getExtensionSettings(this.id) || {};
    // The permission to "toggle the private browsing access" is locked down
    // when the extension has opted out or it gets the permission automatically
    // on every extension startup (as system, privileged and builtin addons) or
    // when private browsing access as been set and locke dthrough enterprise
    // policy settings.
    if (
      this.type === "extension" &&
      this.incognito !== "not_allowed" &&
      this.signedState !== lazy.AddonManager.SIGNEDSTATE_PRIVILEGED &&
      this.signedState !== lazy.AddonManager.SIGNEDSTATE_SYSTEM &&
      !this.location.isBuiltin &&
      !("private_browsing" in settings)
    ) {
      // NOTE: This permission is computed even for addons not in the database because
      // it is being used in the first dialog part of the install flow, when the addon
      // may not be installed yet (and so also not in the database), to determine if
      // the private browsing permission toggle button should be shown.
      permissions |= lazy.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS;
    }

    // Add-ons that aren't installed cannot be modified in any way
    if (!this.inDatabase) {
      return permissions;
    }

    if (!this.appDisabled) {
      if (this.userDisabled || this.softDisabled) {
        permissions |= lazy.AddonManager.PERM_CAN_ENABLE;
      } else if (this.type != "theme" || this.id != DEFAULT_THEME_ID) {
        // We do not expose disabling the default theme.
        permissions |= lazy.AddonManager.PERM_CAN_DISABLE;
      }
    }

    // Add-ons that are in locked install locations, or are pending uninstall
    // cannot be uninstalled or upgraded.  One caveat is extensions sideloaded
    // from non-profile locations. Since Firefox 73(?), new sideloaded extensions
    // from outside the profile have not been installed so any such extensions
    // must be from an older profile. Users may uninstall such an extension which
    // removes the related state from this profile but leaves the actual file alone
    // (since it is outside this profile and may be in use in other profiles)
    let changesAllowed = !this.location.locked && !this.pendingUninstall;
    if (changesAllowed) {
      // System add-on upgrades are triggered through a different mechanism (see updateSystemAddons())
      // Builtin addons are only upgraded with Firefox (or app) updates.
      let isSystem = this.location.isSystem || this.location.isBuiltin;
      // Add-ons that are installed by a file link cannot be upgraded.
      if (!isSystem && !this.location.isLinkedAddon(this.id)) {
        permissions |= lazy.AddonManager.PERM_CAN_UPGRADE;
      }
      // Allow active and retained colorways builtin themes to be updated to
      // the same theme hosted on AMO (the PERM_CAN_UPGRADE permission will
      // ensure we will be asking AMO for an update, then the AMO addon xpi
      // will be installed in the profile location, overridden in the
      // `createUpdate` defined in `XPIInstall.sys.mjs` and called from
      // `UpdateChecker` `onUpdateCheckComplete` method).
      if (
        this.isBuiltinColorwayTheme &&
        BuiltInThemesHelpers.isColorwayMigrationEnabled &&
        BuiltInThemesHelpers.themeIsExpired(this.id) &&
        (BuiltInThemesHelpers.isActiveTheme(this.id) ||
          BuiltInThemesHelpers.isRetainedExpiredTheme(this.id))
      ) {
        permissions |= lazy.AddonManager.PERM_CAN_UPGRADE;
      }
    }

    // We allow uninstall of legacy sideloaded extensions, even when in locked locations,
    // but we do not remove the addon file in that case.
    let isLegacySideload =
      this.foreignInstall &&
      !(this.location.scope & lazy.AddonSettings.SCOPES_SIDELOAD);
    if (changesAllowed || isLegacySideload) {
      permissions |= lazy.AddonManager.PERM_API_CAN_UNINSTALL;
      if (!this.location.isBuiltin) {
        permissions |= lazy.AddonManager.PERM_CAN_UNINSTALL;
      }
    }

    if (Services.policies) {
      if (!Services.policies.isAllowed(`uninstall-extension:${this.id}`)) {
        permissions &= ~lazy.AddonManager.PERM_CAN_UNINSTALL;
      }
      if (!Services.policies.isAllowed(`disable-extension:${this.id}`)) {
        permissions &= ~lazy.AddonManager.PERM_CAN_DISABLE;
      }
      if (Services.policies.getExtensionSettings(this.id)?.updates_disabled) {
        permissions &= ~lazy.AddonManager.PERM_CAN_UPGRADE;
      }
    }

    return permissions;
  }

  propagateDisabledState(oldAddon) {
    if (oldAddon) {
      this.userDisabled = oldAddon.userDisabled;
      this.embedderDisabled = oldAddon.embedderDisabled;
      this.softDisabled = oldAddon.softDisabled;
      this.blocklistState = oldAddon.blocklistState;
    }
  }
}

/**
 * The AddonWrapper wraps an Addon to provide the data visible to consumers of
 * the public API.
 *
 * NOTE: Do not add any new logic here.  Add it to AddonInternal and expose
 * through defineAddonWrapperProperty after this class definition.
 *
 * @param {AddonInternal} aAddon
 *        The add-on object to wrap.
 */
AddonWrapper = class {
  constructor(aAddon) {
    wrapperMap.set(this, aAddon);
  }

  get __AddonInternal__() {
    return addonFor(this);
  }

  get quarantineIgnoredByApp() {
    return this.isPrivileged || !!this.recommendationStates?.length;
  }

  get quarantineIgnoredByUser() {
    // NOTE: confirm if this getter could be replaced by a
    // lazy preference getter and the addon wrapper to not be
    // kept around longer by the pref observer registered
    // internally by the lazy getter.
    return lazy.QuarantinedDomains.isUserAllowedAddonId(this.id);
  }

  set quarantineIgnoredByUser(val) {
    lazy.QuarantinedDomains.setUserAllowedAddonIdPref(this.id, !!val);
  }

  get canChangeQuarantineIgnored() {
    // Never show the quarantined domains user controls UI if the
    // quarantined domains feature is disabled.
    return (
      WebExtensionPolicy.quarantinedDomainsEnabled &&
      !lazy.isQuarantineUIDisabled &&
      this.type === "extension" &&
      !this.quarantineIgnoredByApp
    );
  }

  get previousActiveThemeID() {
    if (this.type === "theme") {
      return addonFor(this).previousActiveThemeID;
    }
    return null;
  }

  get seen() {
    return addonFor(this).seen;
  }

  markAsSeen() {
    addonFor(this).seen = true;
    XPIDatabase.saveChanges();
  }

  get installTelemetryInfo() {
    const addon = addonFor(this);
    if (!addon.installTelemetryInfo && addon.location) {
      if (addon.location.isSystem) {
        return { source: "system-addon" };
      }

      if (addon.location.isTemporary) {
        return { source: "temporary-addon" };
      }
    }

    return addon.installTelemetryInfo;
  }

  get temporarilyInstalled() {
    return addonFor(this).location.isTemporary;
  }

  get aboutURL() {
    return this.isActive ? addonFor(this).aboutURL : null;
  }

  get optionsURL() {
    if (!this.isActive) {
      return null;
    }

    let addon = addonFor(this);
    if (addon.optionsURL) {
      if (this.isWebExtension) {
        // The internal object's optionsURL property comes from the addons
        // DB and should be a relative URL.  However, extensions with
        // options pages installed before bug 1293721 was fixed got absolute
        // URLs in the addons db.  This code handles both cases.
        let policy = WebExtensionPolicy.getByID(addon.id);
        if (!policy) {
          return null;
        }
        let base = policy.getURL();
        return new URL(addon.optionsURL, base).href;
      }
      return addon.optionsURL;
    }

    return null;
  }

  get optionsType() {
    if (!this.isActive) {
      return null;
    }

    let addon = addonFor(this);
    let hasOptionsURL = !!this.optionsURL;

    if (addon.optionsType) {
      switch (parseInt(addon.optionsType, 10)) {
        case lazy.AddonManager.OPTIONS_TYPE_TAB:
        case lazy.AddonManager.OPTIONS_TYPE_INLINE_BROWSER:
          return hasOptionsURL ? addon.optionsType : null;
      }
      return null;
    }

    return null;
  }

  get optionsBrowserStyle() {
    let addon = addonFor(this);
    return addon.optionsBrowserStyle;
  }

  get incognito() {
    return addonFor(this).incognito;
  }

  async getBlocklistURL() {
    return addonFor(this).blocklistURL;
  }

  get iconURL() {
    return lazy.AddonManager.getPreferredIconURL(this, 48);
  }

  get icons() {
    let addon = addonFor(this);
    let icons = {};

    if (addon._repositoryAddon) {
      for (let size in addon._repositoryAddon.icons) {
        icons[size] = addon._repositoryAddon.icons[size];
      }
    }

    if (addon.icons) {
      for (let size in addon.icons) {
        let path = addon.icons[size].replace(/^\//, "");
        icons[size] = this.getResourceURI(path).spec;
      }
    }

    let canUseIconURLs = this.isActive;
    if (canUseIconURLs && addon.iconURL) {
      icons[32] = addon.iconURL;
      icons[48] = addon.iconURL;
    }

    Object.freeze(icons);
    return icons;
  }

  get screenshots() {
    let addon = addonFor(this);
    let repositoryAddon = addon._repositoryAddon;
    if (repositoryAddon && "screenshots" in repositoryAddon) {
      let repositoryScreenshots = repositoryAddon.screenshots;
      if (repositoryScreenshots && repositoryScreenshots.length) {
        return repositoryScreenshots;
      }
    }

    if (addon.previewImage) {
      let url = this.getResourceURI(addon.previewImage).spec;
      return [new lazy.AddonManagerPrivate.AddonScreenshot(url)];
    }

    return null;
  }

  get recommendationStates() {
    let addon = addonFor(this);
    let state = addon.recommendationState;
    if (
      state &&
      state.validNotBefore < addon.updateDate &&
      state.validNotAfter > addon.updateDate &&
      addon.isCorrectlySigned &&
      !this.temporarilyInstalled
    ) {
      return state.states;
    }
    return [];
  }

  // NOTE: this boolean getter doesn't return true for all recommendation
  // states at the moment. For the states actually supported on the autograph
  // side see:
  // https://github.com/mozilla-services/autograph/blob/8a34847a/autograph.yaml#L1456-L1460
  get isRecommended() {
    return this.recommendationStates.includes("recommended");
  }

  get canBypassThirdParyInstallPrompt() {
    // We only bypass if the extension is signed (to support distributions
    // that turn off the signing requirement) and has recommendation states,
    // or the extension is signed as privileged.
    return (
      this.signedState == lazy.AddonManager.SIGNEDSTATE_PRIVILEGED ||
      (this.signedState >= lazy.AddonManager.SIGNEDSTATE_SIGNED &&
        this.recommendationStates.length)
    );
  }

  get applyBackgroundUpdates() {
    return addonFor(this).applyBackgroundUpdates;
  }
  set applyBackgroundUpdates(val) {
    let addon = addonFor(this);
    if (
      val != lazy.AddonManager.AUTOUPDATE_DEFAULT &&
      val != lazy.AddonManager.AUTOUPDATE_DISABLE &&
      val != lazy.AddonManager.AUTOUPDATE_ENABLE
    ) {
      val = val
        ? lazy.AddonManager.AUTOUPDATE_DEFAULT
        : lazy.AddonManager.AUTOUPDATE_DISABLE;
    }

    if (val == addon.applyBackgroundUpdates) {
      return;
    }

    XPIDatabase.setAddonProperties(addon, {
      applyBackgroundUpdates: val,
    });
    lazy.AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, [
      "applyBackgroundUpdates",
    ]);
  }

  set syncGUID(val) {
    let addon = addonFor(this);
    if (addon.syncGUID == val) {
      return;
    }

    if (addon.inDatabase) {
      XPIDatabase.setAddonSyncGUID(addon, val);
    }

    addon.syncGUID = val;
  }

  get install() {
    let addon = addonFor(this);
    if (!("_install" in addon) || !addon._install) {
      return null;
    }
    return addon._install.wrapper;
  }

  get updateInstall() {
    let addon = addonFor(this);
    return addon._updateInstall ? addon._updateInstall.wrapper : null;
  }

  get pendingUpgrade() {
    let addon = addonFor(this);
    return addon.pendingUpgrade ? addon.pendingUpgrade.wrapper : null;
  }

  get scope() {
    let addon = addonFor(this);
    if (addon.location) {
      return addon.location.scope;
    }

    return lazy.AddonManager.SCOPE_PROFILE;
  }

  get pendingOperations() {
    let addon = addonFor(this);
    let pending = 0;
    if (!addon.inDatabase) {
      // Add-on is pending install if there is no associated install (shouldn't
      // happen here) or if the install is in the process of or has successfully
      // completed the install. If an add-on is pending install then we ignore
      // any other pending operations.
      if (
        !addon._install ||
        addon._install.state == lazy.AddonManager.STATE_INSTALLING ||
        addon._install.state == lazy.AddonManager.STATE_INSTALLED
      ) {
        return lazy.AddonManager.PENDING_INSTALL;
      }
    } else if (addon.pendingUninstall) {
      // If an add-on is pending uninstall then we ignore any other pending
      // operations
      return lazy.AddonManager.PENDING_UNINSTALL;
    }

    if (addon.active && addon.disabled) {
      pending |= lazy.AddonManager.PENDING_DISABLE;
    } else if (!addon.active && !addon.disabled) {
      pending |= lazy.AddonManager.PENDING_ENABLE;
    }

    if (addon.pendingUpgrade) {
      pending |= lazy.AddonManager.PENDING_UPGRADE;
    }

    return pending;
  }

  get operationsRequiringRestart() {
    return 0;
  }

  get isDebuggable() {
    return this.isActive;
  }

  get permissions() {
    return addonFor(this).permissions();
  }

  get isActive() {
    let addon = addonFor(this);
    if (!addon.active) {
      return false;
    }
    if (!Services.appinfo.inSafeMode) {
      return true;
    }
    return XPIExports.XPIInternal.canRunInSafeMode(addon);
  }

  get startupPromise() {
    let addon = addonFor(this);
    if (!this.isActive) {
      return null;
    }

    let activeAddon = XPIExports.XPIProvider.activeAddons.get(addon.id);
    if (activeAddon) {
      return activeAddon.startupPromise || null;
    }
    return null;
  }

  get blocklistAttentionDismissed() {
    let addon = addonFor(this);
    return addon.blocklistAttentionDismissed;
  }

  set blocklistAttentionDismissed(val) {
    let addon = addonFor(this);
    addon.updateBlocklistAttentionDismissed(val);
  }

  updateBlocklistState(applySoftBlock = true) {
    return addonFor(this).updateBlocklistState({ applySoftBlock });
  }

  get userDisabled() {
    let addon = addonFor(this);
    return addon.softDisabled || addon.userDisabled;
  }

  /**
   * Get the embedderDisabled property for this addon.
   *
   * This is intended for embedders of Gecko like GeckoView apps to control
   * which addons are usable on their app.
   *
   * @returns {boolean}
   */
  get embedderDisabled() {
    if (!lazy.AddonSettings.IS_EMBEDDED) {
      return undefined;
    }

    return addonFor(this).embedderDisabled;
  }

  /**
   * Set the embedderDisabled property for this addon.
   *
   * This is intended for embedders of Gecko like GeckoView apps to control
   * which addons are usable on their app.
   *
   * Embedders can disable addons for various reasons, e.g. the addon is not
   * compatible with their implementation of the WebExtension API.
   *
   * When an addon is embedderDisabled it will behave like it was appDisabled.
   *
   * @param {boolean} val
   *        whether this addon should be embedder disabled or not.
   */
  async setEmbedderDisabled(val) {
    if (!lazy.AddonSettings.IS_EMBEDDED) {
      throw new Error("Setting embedder disabled while not embedding.");
    }

    let addon = addonFor(this);
    if (addon.embedderDisabled == val) {
      return val;
    }

    if (addon.inDatabase) {
      await XPIDatabase.updateAddonDisabledState(addon, {
        embedderDisabled: val,
      });
    } else {
      addon.embedderDisabled = val;
    }

    return val;
  }

  enable(options = {}) {
    const { allowSystemAddons = false } = options;
    return addonFor(this).setUserDisabled(false, allowSystemAddons);
  }

  disable(options = {}) {
    const { allowSystemAddons = false } = options;
    return addonFor(this).setUserDisabled(true, allowSystemAddons);
  }

  async setSoftDisabled(val) {
    let addon = addonFor(this);
    if (val == addon.softDisabled) {
      return val;
    }

    if (addon.inDatabase) {
      // When softDisabling a theme just enable the active theme
      if (addon.type === "theme" && val && !addon.userDisabled) {
        if (addon.isWebExtension) {
          await XPIDatabase.updateAddonDisabledState(addon, {
            softDisabled: val,
          });
        }
      } else {
        await XPIDatabase.updateAddonDisabledState(addon, {
          softDisabled: val,
        });
      }
    } else if (!addon.userDisabled) {
      // Only set softDisabled if not already disabled
      addon.softDisabled = val;
    }

    return val;
  }

  get isPrivileged() {
    return addonFor(this).isPrivileged;
  }

  get hidden() {
    return addonFor(this).hidden;
  }

  get isSystem() {
    let addon = addonFor(this);
    return addon.location.isSystem;
  }

  get isBuiltin() {
    return addonFor(this).location.isBuiltin;
  }

  // Returns true if Firefox Sync should sync this addon. Only addons
  // in the profile install location are considered syncable.
  get isSyncable() {
    let addon = addonFor(this);
    return addon.location.name == KEY_APP_PROFILE;
  }

  /**
   * Returns true if the addon is configured to be installed
   * by enterprise policy.
   */
  get isInstalledByEnterprisePolicy() {
    const policySettings = Services.policies?.getExtensionSettings(this.id);
    return ["force_installed", "normal_installed"].includes(
      policySettings?.installation_mode
    );
  }

  /**
   * Required permissions that extension has access to based on its manifest.
   * In mv3 this doesn't include host_permissions.
   */
  get userPermissions() {
    return addonFor(this).userPermissions;
  }

  get optionalPermissions() {
    return addonFor(this).optionalPermissions;
  }

  /**
   * Additional permissions that extension is requesting in its manifest.
   * Currently this is host_permissions in MV3.
   */
  get requestedPermissions() {
    return addonFor(this).requestedPermissions;
  }

  /**
   * A helper that returns all permissions for the install prompt.
   */
  get installPermissions() {
    let required = this.userPermissions;
    if (!required) {
      return null;
    }
    let requested = this.requestedPermissions;
    // Currently this can't result in duplicates, but if logic of what goes
    // into these lists changes, make sure to check for dupes.
    let perms = {
      origins: required.origins.concat(requested?.origins ?? []),
      permissions: required.permissions.concat(requested?.permissions ?? []),
    };
    return perms;
  }

  get optionalOriginsNormalized() {
    const { permissions } = this.userPermissions ?? {};

    const priv = this.isPrivileged && permissions?.includes("mozillaAddons");
    const mps = new MatchPatternSet(this.optionalPermissions?.origins ?? [], {
      restrictSchemes: !priv,
      ignorePath: true,
    });

    let temp = [...lazy.ExtensionPermissions.tempOrigins.get(this.id)];
    let origins = [
      ...mps.patterns.map(matcher => matcher.pattern),
      ...temp.filter(o =>
        // Make sure origins are still in the current set of optional
        // permissions, which might have changed on extension update.
        mps.subsumes(new MatchPattern(o, { restrictSchemes: !priv }))
      ),
    ];

    // De-dup the normalized host permission patterns.
    return [...new Set(origins)];
  }

  isCompatibleWith(aAppVersion, aPlatformVersion) {
    return addonFor(this).isCompatibleWith(aAppVersion, aPlatformVersion);
  }

  async uninstall(alwaysAllowUndo) {
    let addon = addonFor(this);
    return XPIExports.XPIInstall.uninstallAddon(addon, alwaysAllowUndo);
  }

  cancelUninstall() {
    let addon = addonFor(this);
    XPIExports.XPIInstall.cancelUninstallAddon(addon);
  }

  findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
    new XPIExports.UpdateChecker(
      addonFor(this),
      aListener,
      aReason,
      aAppVersion,
      aPlatformVersion
    );
  }

  // Returns true if there was an update in progress, false if there was no update to cancel
  cancelUpdate() {
    let addon = addonFor(this);
    if (addon._updateCheck) {
      addon._updateCheck.cancel();
      return true;
    }
    return false;
  }

  /**
   * Reloads the add-on.
   *
   * For temporarily installed add-ons, this uninstalls and re-installs the
   * add-on. Otherwise, the addon is disabled and then re-enabled, and the cache
   * is flushed.
   */
  async reload() {
    const addon = addonFor(this);

    logger.debug(`reloading add-on ${addon.id}`);

    if (!this.temporarilyInstalled) {
      await XPIDatabase.updateAddonDisabledState(addon, { userDisabled: true });
      await XPIDatabase.updateAddonDisabledState(addon, {
        userDisabled: false,
      });
    } else {
      // This function supports re-installing an existing add-on.
      await lazy.AddonManager.installTemporaryAddon(addon._sourceBundle);
    }
  }

  /**
   * Returns a URI to the selected resource or to the add-on bundle if aPath
   * is null. URIs to the bundle will always be file: URIs. URIs to resources
   * will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is
   * still an XPI file.
   *
   * @param {string?} aPath
   *        The path in the add-on to get the URI for or null to get a URI to
   *        the file or directory the add-on is installed as.
   * @returns {nsIURI}
   */
  getResourceURI(aPath) {
    let addon = addonFor(this);
    let url = Services.io.newURI(addon.rootURI);
    if (aPath) {
      if (aPath.startsWith("/")) {
        throw new Error("getResourceURI() must receive a relative path");
      }
      url = Services.io.newURI(aPath, null, url);
    }
    return url;
  }
};

function chooseValue(aAddon, aObj, aProp) {
  let repositoryAddon = aAddon._repositoryAddon;
  let objValue = aObj[aProp];

  if (
    repositoryAddon &&
    aProp in repositoryAddon &&
    (aProp === "creator" || objValue == null)
  ) {
    return [repositoryAddon[aProp], true];
  }

  return [objValue, false];
}

function defineAddonWrapperProperty(name, getter) {
  Object.defineProperty(AddonWrapper.prototype, name, {
    get: getter,
    enumerable: true,
  });
}

[
  "id",
  "syncGUID",
  "version",
  "type",
  "isWebExtension",
  "isCompatible",
  "isPlatformCompatible",
  "providesUpdatesSecurely",
  "blocklistState",
  "appDisabled",
  "softDisabled",
  "skinnable",
  "foreignInstall",
  "strictCompatibility",
  "updateURL",
  "installOrigins",
  "manifestVersion",
  "validInstallOrigins",
  "dependencies",
  "signedState",
  "signedTypes",
  "isCorrectlySigned",
  "isBuiltinColorwayTheme",
].forEach(function (aProp) {
  defineAddonWrapperProperty(aProp, function () {
    let addon = addonFor(this);
    return aProp in addon ? addon[aProp] : undefined;
  });
});

[
  "fullDescription",
  "supportURL",
  "contributionURL",
  "averageRating",
  "reviewCount",
  "reviewURL",
  "weeklyDownloads",
  "amoListingURL",
].forEach(function (aProp) {
  defineAddonWrapperProperty(aProp, function () {
    let addon = addonFor(this);
    if (addon._repositoryAddon) {
      return addon._repositoryAddon[aProp];
    }

    return null;
  });
});

["installDate", "updateDate"].forEach(function (aProp) {
  defineAddonWrapperProperty(aProp, function () {
    let addon = addonFor(this);
    // installDate is always set, updateDate is sometimes missing.
    return new Date(addon[aProp] ?? addon.installDate);
  });
});

defineAddonWrapperProperty("signedDate", function () {
  let addon = addonFor(this);
  let { signedDate } = addon;
  if (signedDate != null) {
    return new Date(signedDate);
  }
  return null;
});

["sourceURI", "releaseNotesURI"].forEach(function (aProp) {
  defineAddonWrapperProperty(aProp, function () {
    let addon = addonFor(this);

    // Temporary Installed Addons do not have a "sourceURI",
    // But we can use the "_sourceBundle" as an alternative,
    // which points to the path of the addon xpi installed
    // or its source dir (if it has been installed from a
    // directory).
    if (aProp == "sourceURI" && this.temporarilyInstalled) {
      return Services.io.newFileURI(addon._sourceBundle);
    }

    let [target, fromRepo] = chooseValue(addon, addon, aProp);
    if (!target) {
      return null;
    }
    if (fromRepo) {
      return target;
    }
    return Services.io.newURI(target);
  });
});

// Add to this Map if you need to change an addon's Fluent ID. Keep it in sync
// with the list in browser_verify_l10n_strings.js
const updatedAddonFluentIds = new Map([
  ["extension-default-theme-name", "extension-default-theme-name-auto"],
]);

["name", "description", "creator", "homepageURL"].forEach(function (aProp) {
  defineAddonWrapperProperty(aProp, function () {
    let addon = addonFor(this);

    let formattedMessage;
    // We want to make sure that all built-in themes that are localizable can
    // actually localized, particularly those for thunderbird and desktop.
    if (
      (aProp === "name" || aProp === "description") &&
      addon.location.name === KEY_APP_BUILTINS &&
      addon.type === "theme"
    ) {
      // Built-in themes are localized with Fluent instead of the WebExtension API.
      let addonIdPrefix = addon.id.replace("@mozilla.org", "");
      const colorwaySuffix = "colorway";
      if (addonIdPrefix.endsWith(colorwaySuffix)) {
        // FIXME: Depending on BuiltInThemes here is sort of a hack. Bug 1733466
        // would provide a more generalized way of doing this.
        if (aProp == "description") {
          return BuiltInThemesHelpers.getLocalizedColorwayDescription(addon.id);
        }
        // Colorway collections are usually divided into and presented as
        // "groups". A group either contains closely related colorways, e.g.
        // stemming from the same base color but with different intensities, or
        // if the current collection doesn't have intensities, each colorway is
        // their own group. Colorway names combine the group name with an
        // intensity. Their ids have the format
        // {colorwayGroup}-{intensity}-colorway@mozilla.org or
        // {colorwayGroupName}-colorway@mozilla.org). L10n for colorway group
        // names is optional and falls back on the unlocalized name from the
        // theme's manifest. The intensity part, if present, must be localized.
        let localizedColorwayGroupName =
          BuiltInThemesHelpers.getLocalizedColorwayGroupName(addon.id);
        let [colorwayGroupName, intensity] = addonIdPrefix.split("-", 2);
        if (intensity == colorwaySuffix) {
          // This theme doesn't have an intensity.
          return localizedColorwayGroupName || addon.defaultLocale.name;
        }
        // We're not using toLocaleUpperCase because these color names are
        // always in English.
        colorwayGroupName =
          localizedColorwayGroupName ||
          colorwayGroupName[0].toUpperCase() + colorwayGroupName.slice(1);
        let defaultFluentId = `extension-colorways-${intensity}-name`;
        let fluentId =
          updatedAddonFluentIds.get(defaultFluentId) || defaultFluentId;
        [formattedMessage] = l10n.formatMessagesSync([
          {
            id: fluentId,
            args: {
              "colorway-name": colorwayGroupName,
            },
          },
        ]);
      } else {
        let defaultFluentId = `extension-${addonIdPrefix}-${aProp}`;
        let fluentId =
          updatedAddonFluentIds.get(defaultFluentId) || defaultFluentId;
        [formattedMessage] = l10n.formatMessagesSync([{ id: fluentId }]);
      }

      return formattedMessage.value;
    }

    let [result, usedRepository] = chooseValue(
      addon,
      addon.selectedLocale,
      aProp
    );

    if (result == null) {
      // Legacy add-ons may be partially localized. Fall back to the default
      // locale ensure that the result is a string where possible.
      [result, usedRepository] = chooseValue(addon, addon.defaultLocale, aProp);
    }

    if (result && !usedRepository && aProp == "creator") {
      return new lazy.AddonManagerPrivate.AddonAuthor(result);
    }

    return result;
  });
});

["developers", "translators", "contributors"].forEach(function (aProp) {
  defineAddonWrapperProperty(aProp, function () {
    let addon = addonFor(this);

    let [results, usedRepository] = chooseValue(
      addon,
      addon.selectedLocale,
      aProp
    );

    if (results && !usedRepository) {
      results = results.map(function (aResult) {
        return new lazy.AddonManagerPrivate.AddonAuthor(aResult);
      });
    }

    return results;
  });
});

/**
 * @typedef {Map<string, AddonInternal>} AddonDB
 */

/**
 * Internal interface: find an addon from an already loaded addonDB.
 *
 * @param {AddonDB} addonDB
 *        The add-on database.
 * @param {function(AddonInternal) : boolean} aFilter
 *        The filter predecate. The first add-on for which it returns
 *        true will be returned.
 * @returns {AddonInternal?}
 *        The first matching add-on, if one is found.
 */
function _findAddon(addonDB, aFilter) {
  for (let addon of addonDB.values()) {
    if (aFilter(addon)) {
      return addon;
    }
  }
  return null;
}

/**
 * Internal interface to get a filtered list of addons from a loaded addonDB
 *
 * @param {AddonDB} addonDB
 *        The add-on database.
 * @param {function(AddonInternal) : boolean} aFilter
 *        The filter predecate. Add-ons which match this predicate will
 *        be returned.
 * @returns {Array<AddonInternal>}
 *        The list of matching add-ons.
 */
function _filterDB(addonDB, aFilter) {
  return Array.from(addonDB.values()).filter(aFilter);
}

export const XPIDatabase = {
  // true if the database connection has been opened
  initialized: false,
  // The database file
  jsonFilePath: PathUtils.join(PathUtils.profileDir, FILE_JSON_DB),
  rebuildingDatabase: false,
  syncLoadingDB: false,
  // Add-ons from the database in locations which are no longer
  // supported.
  orphanedAddons: [],

  // Set of the add-on ids for all the add-ons of type extension that are appDisabled or softDisabled
  // through the blocklist, excluding the ones that the user has already explicitly dismissed before
  // (used for the blocklist attention dot and messagebar to be shown in the extensions button/panel).
  //
  // Set<addonId: string>
  blocklistAttentionAddonIdsSet: new Set(),

  _saveTask: null,

  // Saved error object if we fail to read an existing database
  _loadError: null,

  // Saved error object if we fail to save the database
  _saveError: null,

  // Error reported by our most recent attempt to read or write the database, if any
  get lastError() {
    if (this._loadError) {
      return this._loadError;
    }
    if (this._saveError) {
      return this._saveError;
    }
    return null;
  },

  async _saveNow() {
    try {
      await IOUtils.writeJSON(this.jsonFilePath, this, {
        tmpPath: `${this.jsonFilePath}.tmp`,
      });

      if (!this._schemaVersionSet) {
        // Update the XPIDB schema version preference the first time we
        // successfully save the database.
        logger.debug(
          "XPI Database saved, setting schema version preference to " +
            XPIExports.XPIInternal.DB_SCHEMA
        );
        Services.prefs.setIntPref(
          PREF_DB_SCHEMA,
          XPIExports.XPIInternal.DB_SCHEMA
        );
        this._schemaVersionSet = true;

        // Reading the DB worked once, so we don't need the load error
        this._loadError = null;
      }
    } catch (error) {
      logger.warn("Failed to save XPI database", error);
      this._saveError = error;

      if (!DOMException.isInstance(error) || error.name !== "AbortError") {
        throw error;
      }
    }
  },

  /**
   * Mark the current stored data dirty, and schedule a flush to disk
   */
  saveChanges() {
    if (!this.initialized) {
      throw new Error("Attempt to use XPI database when it is not initialized");
    }

    if (XPIExports.XPIProvider._closing) {
      // use an Error here so we get a stack trace.
      let err = new Error("XPI database modified after shutdown began");
      logger.warn(err);
      lazy.AddonManagerPrivate.recordSimpleMeasure(
        "XPIDB_late_stack",
        Log.stackTrace(err)
      );
    }

    if (!this._saveTask) {
      this._saveTask = new lazy.DeferredTask(
        () => this._saveNow(),
        ASYNC_SAVE_DELAY_MS
      );
    }

    this._saveTask.arm();
  },

  async finalize() {
    // handle the "in memory only" and "saveChanges never called" cases
    if (!this._saveTask) {
      return;
    }

    await this._saveTask.finalize();
  },

  /**
   * Converts the current internal state of the XPI addon database to
   * a JSON.stringify()-ready structure
   *
   * @returns {Object}
   */
  toJSON() {
    if (!this.addonDB) {
      // We never loaded the database?
      throw new Error("Attempt to save database without loading it first");
    }

    let toSave = {
      schemaVersion: XPIExports.XPIInternal.DB_SCHEMA,
      addons: Array.from(this.addonDB.values()).filter(
        addon => !addon.location.isTemporary
      ),
    };
    return toSave;
  },

  /**
   * Synchronously loads the database, by running the normal async load
   * operation with idle dispatch disabled, and spinning the event loop
   * until it finishes.
   *
   * @param {boolean} aRebuildOnError
   *        A boolean indicating whether add-on information should be loaded
   *        from the install locations if the database needs to be rebuilt.
   *        (if false, caller is XPIProvider.checkForChanges() which will rebuild)
   */
  syncLoadDB(aRebuildOnError) {
    let err = new Error("Synchronously loading the add-ons database");
    logger.debug(err.message);
    lazy.AddonManagerPrivate.recordSimpleMeasure(
      "XPIDB_sync_stack",
      Log.stackTrace(err)
    );
    try {
      this.syncLoadingDB = true;
      XPIExports.XPIInternal.awaitPromise(this.asyncLoadDB(aRebuildOnError));
    } finally {
      this.syncLoadingDB = false;
    }
  },

  _recordStartupError(reason) {
    lazy.AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", reason);
  },

  /**
   * Parse loaded data, reconstructing the database if the loaded data is not valid
   *
   * @param {object} aInputAddons
   *        The add-on JSON to parse.
   * @param {boolean} aRebuildOnError
   *        If true, synchronously reconstruct the database from installed add-ons
   */
  async parseDB(aInputAddons, aRebuildOnError) {
    try {
      let parseTimer = lazy.AddonManagerPrivate.simpleTimer("XPIDB_parseDB_MS");

      if (!("schemaVersion" in aInputAddons) || !("addons" in aInputAddons)) {
        let error = new Error("Bad JSON file contents");
        error.rebuildReason = "XPIDB_rebuildBadJSON_MS";
        throw error;
      }

      if (aInputAddons.schemaVersion <= 27) {
        // Types were translated in bug 857456.
        for (let addon of aInputAddons.addons) {
          XPIExports.XPIInternal.migrateAddonLoader(addon);
        }
      } else if (
        aInputAddons.schemaVersion != XPIExports.XPIInternal.DB_SCHEMA
      ) {
        // For now, we assume compatibility for JSON data with a
        // mismatched schema version, though we throw away any fields we
        // don't know about (bug 902956)
        this._recordStartupError(
          `schemaMismatch-${aInputAddons.schemaVersion}`
        );
        logger.debug(
          `JSON schema mismatch: expected ${XPIExports.XPIInternal.DB_SCHEMA}, actual ${aInputAddons.schemaVersion}`
        );
      }

      let forEach = this.syncLoadingDB ? arrayForEach : idleForEach;

      this.clearBlocklistAttentionAddonIdsSet();

      // If we got here, we probably have good data
      // Make AddonInternal instances from the loaded data and save them
      let addonDB = new Map();
      await forEach(aInputAddons.addons, loadedAddon => {
        if (loadedAddon.path) {
          try {
            loadedAddon._sourceBundle = new nsIFile(loadedAddon.path);
          } catch (e) {
            // We can fail here when the path is invalid, usually from the
            // wrong OS
            logger.warn(
              "Could not find source bundle for add-on " + loadedAddon.id,
              e
            );
          }
        }
        loadedAddon.location = XPIExports.XPIInternal.XPIStates.getLocation(
          loadedAddon.location
        );

        let newAddon = new AddonInternal(loadedAddon);
        if (loadedAddon.location) {
          addonDB.set(newAddon._key, newAddon);
          this.maybeUpdateBlocklistAttentionAddonIdsSet(newAddon);
        } else {
          this.orphanedAddons.push(newAddon);
        }
      });

      parseTimer.done();
      this.addonDB = addonDB;
      logger.debug("Successfully read XPI database");
      this.initialized = true;
    } catch (e) {
      if (e.name == "SyntaxError") {
        logger.error("Syntax error parsing saved XPI JSON data");
        this._recordStartupError("syntax");
      } else {
        logger.error("Failed to load XPI JSON data from profile", e);
        this._recordStartupError("other");
      }

      this.timeRebuildDatabase(
        e.rebuildReason || "XPIDB_rebuildReadFailed_MS",
        aRebuildOnError
      );
    }
  },

  async maybeIdleDispatch() {
    if (!this.syncLoadingDB) {
      await promiseIdleSlice();
    }
  },

  /**
   * Open and read the XPI database asynchronously, upgrading if
   * necessary. If any DB load operation fails, we need to
   * synchronously rebuild the DB from the installed extensions.
   *
   * @param {boolean} [aRebuildOnError = true]
   *        A boolean indicating whether add-on information should be loaded
   *        from the install locations if the database needs to be rebuilt.
   *        (if false, caller is XPIProvider.checkForChanges() which will rebuild)
   * @returns {Promise<AddonDB>}
   *        Resolves to the Map of loaded JSON data stored in
   *        this.addonDB; rejects in case of shutdown.
   */
  asyncLoadDB(aRebuildOnError = true) {
    // Already started (and possibly finished) loading
    if (this._dbPromise) {
      return this._dbPromise;
    }

    if (XPIExports.XPIProvider._closing) {
      // use an Error here so we get a stack trace.
      let err = new Error(
        "XPIDatabase.asyncLoadDB attempt after XPIProvider shutdown."
      );
      logger.warn("Fail to load AddonDB: ${error}", { error: err });
      lazy.AddonManagerPrivate.recordSimpleMeasure(
        "XPIDB_late_load",
        Log.stackTrace(err)
      );
      this._dbPromise = Promise.reject(err);

      XPIExports.XPIInternal.resolveDBReady(this._dbPromise);

      return this._dbPromise;
    }

    logger.debug(`Starting async load of XPI database ${this.jsonFilePath}`);
    this._dbPromise = (async () => {
      try {
        let json = await IOUtils.readJSON(this.jsonFilePath);

        logger.debug("Finished async read of XPI database, parsing...");
        await this.maybeIdleDispatch();
        await this.parseDB(json, true);
      } catch (error) {
        if (DOMException.isInstance(error) && error.name === "NotFoundError") {
          if (Services.prefs.getIntPref(PREF_DB_SCHEMA, 0)) {
            this._recordStartupError("dbMissing");
          }
        } else {
          logger.warn(
            `Extensions database ${this.jsonFilePath} exists but is not readable; rebuilding`,
            error
          );
          this._loadError = error;
        }
        this.timeRebuildDatabase(
          "XPIDB_rebuildUnreadableDB_MS",
          aRebuildOnError
        );
      }
      return this.addonDB;
    })();

    XPIExports.XPIInternal.resolveDBReady(this._dbPromise);

    return this._dbPromise;
  },

  timeRebuildDatabase(timerName, rebuildOnError) {
    lazy.AddonManagerPrivate.recordTiming(timerName, () => {
      return this.rebuildDatabase(rebuildOnError);
    });
  },

  /**
   * Rebuild the database from addon install directories.
   *
   * @param {boolean} aRebuildOnError
   *        A boolean indicating whether add-on information should be loaded
   *        from the install locations if the database needs to be rebuilt.
   *        (if false, caller is XPIProvider.checkForChanges() which will rebuild)
   */
  rebuildDatabase(aRebuildOnError) {
    this.addonDB = new Map();
    this.initialized = true;

    if (XPIExports.XPIInternal.XPIStates.size == 0) {
      // No extensions installed, so we're done
      logger.debug("Rebuilding XPI database with no extensions");
      return;
    }

    this.rebuildingDatabase = !!aRebuildOnError;

    if (aRebuildOnError) {
      logger.warn("Rebuilding add-ons database from installed extensions.");
      try {
        XPIDatabaseReconcile.processFileChanges({}, false);
      } catch (e) {
        logger.error(
--> --------------------

--> maximum size reached

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

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

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge