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

Quelle  Extension.sys.mjs   Sprache: unbekannt

 
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* 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 is the main entry point for extensions. When an extension
 * loads, its bootstrap.js file creates a Extension instance
 * and calls .startup() on it. It calls .shutdown() when the extension
 * unloads. Extension manages any extension-specific state in
 * the chrome process.
 *
 * TODO(rpl): we are current restricting the extensions to a single process
 * (set as the current default value of the "dom.ipc.processCount.extension"
 * preference), if we switch to use more than one extension process, we have to
 * be sure that all the browser's frameLoader are associated to the same process,
 * e.g. by enabling the `maychangeremoteness` attribute, and/or setting
 * `initialBrowsingContextGroupId` attribute to the correct value.
 *
 * At that point we are going to keep track of the existing browsers associated to
 * a webextension to ensure that they are all running in the same process (and we
 * are also going to do the same with the browser element provided to the
 * addon debugging Remote Debugging actor, e.g. because the addon has been
 * reloaded by the user, we have to  ensure that the new extension pages are going
 * to run in the same process of the existing addon debugging browser element).
 */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
import { ExtensionParent } from "resource://gre/modules/ExtensionParent.sys.mjs";
import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
import { Log } from "resource://gre/modules/Log.sys.mjs";

/** @type {Lazy} */
const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
  AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
  AddonSettings: "resource://gre/modules/addons/AddonSettings.sys.mjs",
  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
  E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
  ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs",
  ExtensionDNRStore: "resource://gre/modules/ExtensionDNRStore.sys.mjs",
  ExtensionMenus: "resource://gre/modules/ExtensionMenus.sys.mjs",
  ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
  ExtensionPreferencesManager:
    "resource://gre/modules/ExtensionPreferencesManager.sys.mjs",
  ExtensionProcessScript:
    "resource://gre/modules/ExtensionProcessScript.sys.mjs",
  ExtensionScriptingStore:
    "resource://gre/modules/ExtensionScriptingStore.sys.mjs",
  ExtensionStorage: "resource://gre/modules/ExtensionStorage.sys.mjs",
  ExtensionStorageIDB: "resource://gre/modules/ExtensionStorageIDB.sys.mjs",
  ExtensionUserScripts: "resource://gre/modules/ExtensionUserScripts.sys.mjs",
  ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.sys.mjs",
  LightweightThemeManager:
    "resource://gre/modules/LightweightThemeManager.sys.mjs",
  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
  SITEPERMS_ADDON_TYPE:
    "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs",
  Schemas: "resource://gre/modules/Schemas.sys.mjs",
  ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.sys.mjs",
  extensionStorageSync: "resource://gre/modules/ExtensionStorageSync.sys.mjs",
  PERMISSION_L10N: "resource://gre/modules/ExtensionPermissionMessages.sys.mjs",
  permissionToL10nId:
    "resource://gre/modules/ExtensionPermissionMessages.sys.mjs",
  QuarantinedDomains: "resource://gre/modules/ExtensionPermissions.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "resourceProtocol", () =>
  Services.io
    .getProtocolHandler("resource")
    .QueryInterface(Ci.nsIResProtocolHandler)
);

XPCOMUtils.defineLazyServiceGetters(lazy, {
  aomStartup: [
    "@mozilla.org/addons/addon-manager-startup;1",
    "amIAddonManagerStartup",
  ],
  spellCheck: ["@mozilla.org/spellchecker/engine;1", "mozISpellCheckingEngine"],
});

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "processCount",
  "dom.ipc.processCount.extension"
);

// Temporary pref to be turned on when ready.
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "userContextIsolation",
  "extensions.userContextIsolation.enabled",
  false
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "userContextIsolationDefaultRestricted",
  "extensions.userContextIsolation.defaults.restricted",
  "[]"
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "dnrEnabled",
  "extensions.dnr.enabled",
  true
);

// All functionality is gated by the "userScripts" permission, and forgetting
// about its existence is enough to hide all userScripts functionality.
// MV3 userScripts API in development (bug 1875475), off by default.
// Not to be confused with MV2 and extensions.webextensions.userScripts.enabled!
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "userScriptsMV3Enabled",
  "extensions.userScripts.mv3.enabled",
  false
);

// This pref modifies behavior for MV2.  MV3 is enabled regardless.
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "eventPagesEnabled",
  "extensions.eventPages.enabled"
);

// This pref is used to check if storage.sync is still the Kinto-based backend
// (GeckoView should be the only one still using it).
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "storageSyncOldKintoBackend",
  "webextensions.storage.sync.kinto",
  false
);

// Deprecation of browser_style, through .supported & .same_as_mv2 prefs:
// - true true  = warn only: deprecation message only (no behavioral changes).
// - true false = deprecate: default to false, even if default was true in MV2.
// - false      = remove: always use false, even when true is specified.
//                (if .same_as_mv2 is set, also warn if the default changed)
// Deprecation plan: https://bugzilla.mozilla.org/show_bug.cgi?id=1827910#c1
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "browserStyleMV3supported",
  "extensions.browser_style_mv3.supported",
  false
);
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "browserStyleMV3sameAsMV2",
  "extensions.browser_style_mv3.same_as_mv2",
  false
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "processCrashThreshold",
  "extensions.webextensions.crash.threshold",
  // The default number of times an extension process is allowed to crash
  // within a timeframe.
  5
);
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "processCrashTimeframe",
  "extensions.webextensions.crash.timeframe",
  // The default timeframe used to count crashes, in milliseconds.
  30 * 1000
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "installIncludesOrigins",
  "extensions.originControls.grantByDefault",
  false
);

var {
  GlobalManager,
  IconDetails,
  ParentAPIManager,
  StartupCache,
  apiManager: Management,
} = ExtensionParent;

export { Management };

const { getUniqueId, promiseTimeout } = ExtensionUtils;

const { EventEmitter, redefineGetter, updateAllowedOrigins } = ExtensionCommon;

ChromeUtils.defineLazyGetter(
  lazy,
  "LocaleData",
  () => ExtensionCommon.LocaleData
);

ChromeUtils.defineLazyGetter(lazy, "NO_PROMPT_PERMISSIONS", async () => {
  // Wait until all extension API schemas have been loaded and parsed.
  await Management.lazyInit();
  return new Set(
    lazy.Schemas.getPermissionNames([
      "PermissionNoPrompt",
      "OptionalPermissionNoPrompt",
      "PermissionPrivileged",
    ])
  );
});

const { sharedData } = Services.ppmm;

const PRIVATE_ALLOWED_PERMISSION = "internal:privateBrowsingAllowed";
const SVG_CONTEXT_PROPERTIES_PERMISSION =
  "internal:svgContextPropertiesAllowed";

// The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB
// storage used by the browser.storage.local API is not directly accessible from the extension code,
// it is defined and reserved as "userContextIdInternal.webextStorageLocal" in ContextualIdentityService.sys.mjs).
const WEBEXT_STORAGE_USER_CONTEXT_ID = -1 >>> 0;

// The maximum time to wait for extension child shutdown blockers to complete.
const CHILD_SHUTDOWN_TIMEOUT_MS = 8000;

// Permissions that are only available to privileged extensions.
const PRIVILEGED_PERMS = new Set([
  "activityLog",
  "mozillaAddons",
  "networkStatus",
  "normandyAddonStudy",
  "telemetry",
]);

const PRIVILEGED_PERMS_ANDROID_ONLY = new Set([
  "geckoViewAddons",
  "nativeMessagingFromContent",
  "nativeMessaging",
]);

const PRIVILEGED_PERMS_DESKTOP_ONLY = new Set(["normandyAddonStudy"]);

if (AppConstants.platform == "android") {
  for (const perm of PRIVILEGED_PERMS_ANDROID_ONLY) {
    PRIVILEGED_PERMS.add(perm);
  }
}

if (
  AppConstants.MOZ_APP_NAME != "firefox" ||
  AppConstants.platform == "android"
) {
  for (const perm of PRIVILEGED_PERMS_DESKTOP_ONLY) {
    PRIVILEGED_PERMS.delete(perm);
  }
}

// Permissions that are not available in manifest version 2.
const PERMS_NOT_IN_MV2 = new Set([
  // MV2 had a userScripts API, tied to "user_scripts" manifest key. In MV3 the
  // userScripts API availability is gated by the "userScripts" permission.
  "userScripts",
]);

// Message included in warnings and errors related to privileged permissions and
// privileged manifest properties. Provides a link to the firefox-source-docs.mozilla.org
// section related to developing and sign Privileged Add-ons.
const PRIVILEGED_ADDONS_DEVDOCS_MESSAGE =
  "See https://mzl.la/3NS9KJd for more details about how to develop a privileged add-on.";

const INSTALL_AND_UPDATE_STARTUP_REASONS = new Set([
  "ADDON_INSTALL",
  "ADDON_UPGRADE",
  "ADDON_DOWNGRADE",
]);

const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler";
const PERMISSION_KEY_DELIMITER = "^";

// These are used for manipulating jar entry paths, which always use Unix
// separators (originally copied from `ospath_unix.jsm` as part of the "OS.Path
// to PathUtils" migration).

/**
 * Return the final part of the path.
 * The final part of the path is everything after the last "/".
 */
function basename(path) {
  return path.slice(path.lastIndexOf("/") + 1);
}

/**
 * Return the directory part of the path.
 * The directory part of the path is everything before the last
 * "/". If the last few characters of this part are also "/",
 * they are ignored.
 *
 * If the path contains no directory, return ".".
 */
function dirname(path) {
  let index = path.lastIndexOf("/");
  if (index == -1) {
    return ".";
  }
  while (index >= 0 && path[index] == "/") {
    --index;
  }
  return path.slice(0, index + 1);
}

// Returns true if the extension is owned by Mozilla (is either privileged,
// using one of the @mozilla.com/@mozilla.org protected addon id suffixes).
//
// This method throws if the extension's startupReason is not one of the
// expected ones (either ADDON_INSTALL, ADDON_UPGRADE or ADDON_DOWNGRADE).
//
// TODO(Bug 1835787): Consider to remove the restriction based on the
// startupReason now that the recommendationState property is always
// included in the addonData with any of the startupReason.
function isMozillaExtension(extension) {
  const { addonData, id, isPrivileged, startupReason } = extension;

  if (!INSTALL_AND_UPDATE_STARTUP_REASONS.has(startupReason)) {
    throw new Error(
      `isMozillaExtension called with unexpected startupReason: ${startupReason}`
    );
  }

  if (isPrivileged) {
    return true;
  }

  if (id.endsWith("@mozilla.com") || id.endsWith("@mozilla.org")) {
    return true;
  }

  // This check is a subset of what is being checked in AddonWrapper's
  // recommendationStates (states expire dates for line extensions are
  // not considered important in determining that the extension is
  // provided by mozilla, and so they are omitted here on purpose).
  const isMozillaLineExtension =
    addonData.recommendationState?.states?.includes("line");
  const isSigned =
    addonData.signedState > lazy.AddonManager.SIGNEDSTATE_MISSING;

  return isSigned && isMozillaLineExtension;
}

/**
 * Classify an individual permission from a webextension manifest
 * as a host/origin permission, an api permission, or a regular permission.
 *
 * @param {string} perm  The permission string to classify
 * @param {boolean} restrictSchemes
 * @param {boolean} isPrivileged whether or not the webextension is privileged
 *
 * @returns {object}
 *          An object with exactly one of the following properties:
 *          "origin" to indicate this is a host/origin permission.
 *          "api" to indicate this is an api permission
 *                (as used for webextensions experiments).
 *          "permission" to indicate this is a regular permission.
 *          "invalid" to indicate that the given permission cannot be used.
 */
function classifyPermission(perm, restrictSchemes, isPrivileged) {
  let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
  if (!match) {
    try {
      let { pattern } = new MatchPattern(perm, {
        restrictSchemes,
        ignorePath: true,
      });
      return { origin: pattern };
    } catch (e) {
      return { invalid: perm };
    }
  } else if (match[1] == "experiments" && match[2]) {
    return { api: match[2] };
  } else if (!isPrivileged && PRIVILEGED_PERMS.has(match[1])) {
    return { invalid: perm, privileged: true };
  } else if (perm.startsWith("declarativeNetRequest") && !lazy.dnrEnabled) {
    return { invalid: perm };
  } else if (perm === "userScripts" && !lazy.userScriptsMV3Enabled) {
    return { invalid: perm };
  }
  return { permission: perm };
}

const LOGGER_ID_BASE = "addons.webextension.";
const UUID_MAP_PREF = "extensions.webextensions.uuids";
const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";

const COMMENT_REGEXP = new RegExp(
  String.raw`
    ^
    (
      (?:
        [^"\n] |
        " (?:[^"\\\n] | \\.)* "
      )*?
    )

    //.*
  `.replace(/\s+/g, ""),
  "gm"
);

// All moz-extension URIs use a machine-specific UUID rather than the
// extension's own ID in the host component. This makes it more
// difficult for web pages to detect whether a user has a given add-on
// installed (by trying to load a moz-extension URI referring to a
// web_accessible_resource from the extension). UUIDMap.get()
// returns the UUID for a given add-on ID.
var UUIDMap = {
  _read() {
    let pref = Services.prefs.getStringPref(UUID_MAP_PREF, "{}");
    try {
      return JSON.parse(pref);
    } catch (e) {
      Cu.reportError(`Error parsing ${UUID_MAP_PREF}.`);
      return {};
    }
  },

  _write(map) {
    Services.prefs.setStringPref(UUID_MAP_PREF, JSON.stringify(map));
  },

  get(id, create = true) {
    let map = this._read();

    if (id in map) {
      return map[id];
    }

    let uuid = null;
    if (create) {
      uuid = Services.uuid.generateUUID().number;
      uuid = uuid.slice(1, -1); // Strip { and } off the UUID.

      map[id] = uuid;
      this._write(map);
    }
    return uuid;
  },

  remove(id) {
    let map = this._read();
    delete map[id];
    this._write(map);
  },
};

function clearCacheForExtensionPrincipal(principal, clearAll = false) {
  if (!principal.schemeIs("moz-extension")) {
    return Promise.reject(new Error("Unexpected non extension principal"));
  }

  // TODO(Bug 1750053): replace the two specific flags with a "clear all caches one"
  // (along with covering the other kind of cached data with tests).
  const clearDataFlags = clearAll
    ? Ci.nsIClearDataService.CLEAR_ALL_CACHES
    : Ci.nsIClearDataService.CLEAR_IMAGE_CACHE |
      Ci.nsIClearDataService.CLEAR_CSS_CACHE |
      Ci.nsIClearDataService.CLEAR_JS_CACHE;

  return new Promise(resolve =>
    Services.clearData.deleteDataFromPrincipal(
      principal,
      false,
      clearDataFlags,
      () => resolve()
    )
  );
}

/**
 * Observer AddonManager events and translate them into extension events,
 * as well as handle any last cleanup after uninstalling an extension.
 */
var ExtensionAddonObserver = {
  initialized: false,

  init() {
    if (!this.initialized) {
      lazy.AddonManager.addAddonListener(this);
      this.initialized = true;
    }
  },

  // AddonTestUtils will call this as necessary.
  uninit() {
    if (this.initialized) {
      lazy.AddonManager.removeAddonListener(this);
      this.initialized = false;
    }
  },

  onEnabling(addon) {
    if (addon.type !== "extension") {
      return;
    }
    Management._callHandlers([addon.id], "enabling", "onEnabling");
  },

  onDisabled(addon) {
    if (addon.type !== "extension") {
      return;
    }
    if (Services.appinfo.inSafeMode) {
      // Ensure ExtensionPreferencesManager updates its data and
      // modules can run any disable logic they need to.  We only
      // handle safeMode here because there is a bunch of additional
      // logic that happens in Extension.shutdown when running in
      // normal mode.
      Management._callHandlers([addon.id], "disable", "onDisable");
    }
  },

  onUninstalling(addon) {
    let extension = GlobalManager.extensionMap.get(addon.id);
    if (extension) {
      // Let any other interested listeners respond
      // (e.g., display the uninstall URL)
      Management.emit("uninstalling", extension);
    }
  },

  onUninstalled(addon) {
    this.clearOnUninstall(addon.id);
  },

  /**
   * Clears persistent state from the add-on post install.
   *
   * @param {string} addonId The ID of the addon that has been uninstalled.
   */
  clearOnUninstall(addonId) {
    const tasks = [];
    function addShutdownBlocker(name, promise) {
      lazy.AsyncShutdown.profileChangeTeardown.addBlocker(name, promise);
      tasks.push({ name, promise });
    }
    function notifyUninstallTaskObservers() {
      Management.emit("cleanupAfterUninstall", addonId, tasks);
    }

    // Cleanup anything that is used by non-extension addon types
    // since only extensions have uuid's.
    addShutdownBlocker(
      `Clear ExtensionPermissions for ${addonId}`,
      lazy.ExtensionPermissions.removeAll(addonId)
    );

    lazy.QuarantinedDomains.clearUserPref(addonId);

    let uuid = UUIDMap.get(addonId, false);
    if (!uuid) {
      notifyUninstallTaskObservers();
      return;
    }

    let baseURI = Services.io.newURI(`moz-extension://${uuid}/`);
    let principal = Services.scriptSecurityManager.createContentPrincipal(
      baseURI,
      {}
    );

    // Clear all cached resources (e.g. CSS and images);
    addShutdownBlocker(
      `Clear cache for ${addonId}`,
      clearCacheForExtensionPrincipal(principal, /* clearAll */ true)
    );

    // Clear all the registered service workers for the extension
    // principal (the one that may have been registered through the
    // manifest.json file and the ones that may have been registered
    // from an extension page through the service worker API).
    //
    // Any stored data would be cleared below (if the pref
    // "extensions.webextensions.keepStorageOnUninstall has not been
    // explicitly set to true, which is usually only done in
    // tests and by some extensions developers for testing purpose).
    //
    // TODO: ServiceWorkerCleanUp may go away once Bug 1183245
    // is fixed, and so this may actually go away, replaced by
    // marking the registration as disabled or to be removed on
    // shutdown (where we do know if the extension is shutting
    // down because is being uninstalled) and then cleared from
    // the persisted serviceworker registration on the next
    // startup.
    addShutdownBlocker(
      `Clear ServiceWorkers for ${addonId}`,
      lazy.ServiceWorkerCleanUp.removeFromPrincipal(principal)
    );

    // Clear the persisted menus created with the menus/contextMenus API (if any).
    addShutdownBlocker(
      `Clear menus store for ${addonId}`,
      lazy.ExtensionMenus.clearPersistedMenusOnUninstall(addonId)
    );

    // Clear the persisted dynamic content scripts created with the scripting
    // API (if any).
    addShutdownBlocker(
      `Clear scripting store for ${addonId}`,
      lazy.ExtensionScriptingStore.clearOnUninstall(addonId)
    );

    // Clear MV3 userScripts API data, if any.
    addShutdownBlocker(
      `Clear user scripts for ${addonId}`,
      lazy.ExtensionUserScripts.clearOnUninstall(addonId)
    );

    // Clear the DNR API's rules data persisted on disk (if any).
    addShutdownBlocker(
      `Clear declarativeNetRequest store for ${addonId}`,
      lazy.ExtensionDNRStore.clearOnUninstall(uuid)
    );

    if (!Services.prefs.getBoolPref(LEAVE_STORAGE_PREF, false)) {
      // Clear browser.storage.local backends.
      addShutdownBlocker(
        `Clear Extension Storage ${addonId} (File Backend)`,
        lazy.ExtensionStorage.clear(addonId, { shouldNotifyListeners: false })
      );

      // Clear browser.storage.sync rust-based backend.
      // (storage.sync clearOnUninstall will resolve and log an error on the
      // browser console in case of unexpected failures).
      if (!lazy.storageSyncOldKintoBackend) {
        addShutdownBlocker(
          `Clear Extension StorageSync ${addonId}`,
          lazy.extensionStorageSync.clearOnUninstall(addonId)
        );
      }

      // Clear any IndexedDB and Cache API storage created by the extension.
      // If LSNG is enabled, this also clears localStorage.
      Services.qms.clearStoragesForPrincipal(principal);

      // Clear any storage.local data stored in the IDBBackend.
      let storagePrincipal =
        Services.scriptSecurityManager.createContentPrincipal(baseURI, {
          userContextId: WEBEXT_STORAGE_USER_CONTEXT_ID,
        });
      Services.qms.clearStoragesForPrincipal(storagePrincipal);

      lazy.ExtensionStorageIDB.clearMigratedExtensionPref(addonId);

      // If LSNG is not enabled, we need to clear localStorage explicitly using
      // the old API.
      if (!Services.domStorageManager.nextGenLocalStorageEnabled) {
        // Clear localStorage created by the extension
        let storage = Services.domStorageManager.getStorage(
          null,
          principal,
          principal
        );
        if (storage) {
          storage.clear();
        }
      }

      // Remove any permissions related to the unlimitedStorage permission
      // if we are also removing all the data stored by the extension.
      Services.perms.removeFromPrincipal(
        principal,
        "WebExtensions-unlimitedStorage"
      );
      Services.perms.removeFromPrincipal(principal, "persistent-storage");
    }

    // Clear any protocol handler permissions granted to this add-on.
    let permissions = Services.perms.getAllWithTypePrefix(
      PROTOCOL_HANDLER_OPEN_PERM_KEY + PERMISSION_KEY_DELIMITER
    );
    for (let perm of permissions) {
      if (perm.principal.equalsURI(baseURI)) {
        Services.perms.removePermission(perm);
      }
    }

    if (!Services.prefs.getBoolPref(LEAVE_UUID_PREF, false)) {
      // Clear the entry in the UUID map
      UUIDMap.remove(addonId);
    }

    notifyUninstallTaskObservers();
  },

  onPropertyChanged(addon, properties) {
    let extension = GlobalManager.extensionMap.get(addon.id);
    if (!extension) {
      return;
    }

    if (properties.includes("quarantineIgnoredByUser")) {
      extension.ignoreQuarantine = addon.quarantineIgnoredByUser;
      extension.policy.ignoreQuarantine = addon.quarantineIgnoredByUser;

      extension.setSharedData("", extension.serialize());
      Services.ppmm.sharedData.flush();

      extension.emit("update-ignore-quarantine");
      extension.broadcast("Extension:UpdateIgnoreQuarantine", {
        id: extension.id,
        ignoreQuarantine: addon.quarantineIgnoredByUser,
      });
    }

    if (properties.includes("blocklistState")) {
      extension.blocklistState = addon.blocklistState;
      extension.emit("update-blocklist-state");
    }
  },
};

ExtensionAddonObserver.init();

/**
 * Observer ExtensionProcess crashes and notify all the extensions
 * using a Management event named "extension-process-crash".
 */
export var ExtensionProcessCrashObserver = {
  initialized: false,

  // For Android apps we initially consider the app as always starting
  // in the background, then we expect to be setting it to foreground
  // when GeckoView LifecycleListener onResume method is called on the
  // Android app first startup. After the application has got on the
  // foreground for the first time then onPause/onResumed LifecycleListener
  // are called, the application-foreground/-background topics will be
  // notified to Gecko and this flag will be updated accordingly.
  _appInForeground: AppConstants.platform !== "android",
  _isAndroid: AppConstants.platform === "android",
  _processSpawningDisabled: false,

  // Technically there is at most one child extension process,
  // but we may need to adjust this assumption to account for more
  // than one if that ever changes in the future.
  currentProcessChildID: undefined,
  lastCrashedProcessChildID: undefined,
  QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),

  // Collect the timestamps of the crashes happened over the last
  // `processCrashTimeframe` milliseconds.
  lastCrashTimestamps: [],

  logger: Log.repository.getLogger("addons.process-crash-observer"),

  init() {
    if (!this.initialized) {
      Services.obs.addObserver(this, "ipc:content-created");
      Services.obs.addObserver(this, "process-type-set");
      Services.obs.addObserver(this, "ipc:content-shutdown");
      if (this._isAndroid) {
        Services.obs.addObserver(this, "geckoview-initial-foreground");
        Services.obs.addObserver(this, "application-foreground");
        Services.obs.addObserver(this, "application-background");
      }
      this.initialized = true;
    }
  },

  uninit() {
    if (this.initialized) {
      try {
        Services.obs.removeObserver(this, "ipc:content-created");
        Services.obs.removeObserver(this, "process-type-set");
        Services.obs.removeObserver(this, "ipc:content-shutdown");
        if (this._isAndroid) {
          Services.obs.removeObserver(this, "geckoview-initial-foreground");
          Services.obs.removeObserver(this, "application-foreground");
          Services.obs.removeObserver(this, "application-background");
        }
      } catch (err) {
        // Removing the observer may fail if they are not registered anymore,
        // this shouldn't happen in practice, but let's still log the error
        // in case it does.
        Cu.reportError(err);
      }
      this.initialized = false;
    }
  },

  observe(subject, topic, data) {
    let childID = data;
    switch (topic) {
      case "geckoview-initial-foreground":
        this._appInForeground = true;
        this.logger.debug(
          `Detected Android application moved in the foreground (geckoview-initial-foreground)`
        );
        break;
      case "application-foreground":
      // Intentional fall-through
      case "application-background":
        this._appInForeground = topic === "application-foreground";
        this.logger.debug(
          `Detected Android application moved in the ${
            this._appInForeground ? "foreground" : "background"
          }`
        );
        if (this._appInForeground) {
          Management.emit("application-foreground", {
            appInForeground: this._appInForeground,
            childID: this.currentProcessChildID,
            processSpawningDisabled: this.processSpawningDisabled,
          });
        }
        break;
      case "process-type-set":
      // Intentional fall-through
      case "ipc:content-created": {
        let pp = subject.QueryInterface(Ci.nsIDOMProcessParent);
        if (pp.remoteType === "extension") {
          this.currentProcessChildID = childID;
          Glean.extensions.processEvent[
            this.appInForeground ? "created_fg" : "created_bg"
          ].add(1);
        }
        break;
      }
      case "ipc:content-shutdown": {
        if (Services.startup.shuttingDown) {
          // The application is shutting down, don't bother
          // signaling process crashes anymore.
          return;
        }
        if (this.currentProcessChildID !== childID) {
          // Ignore non-extension child process shutdowns.
          return;
        }

        // At this point we are sure that the current extension
        // process is gone, and so even if the process did shutdown
        // cleanly instead of crashing, we can clear the property
        // that keeps track of the current extension process childID.
        this.currentProcessChildID = undefined;

        subject.QueryInterface(Ci.nsIPropertyBag2);
        if (!subject.get("abnormal")) {
          // Ignore non-abnormal child process shutdowns.
          return;
        }

        this.lastCrashedProcessChildID = childID;

        const now = Cu.now();
        // Filter crash timestamps older than processCrashTimeframe.
        this.lastCrashTimestamps = this.lastCrashTimestamps.filter(
          timestamp => now - timestamp < lazy.processCrashTimeframe
        );
        // Push the new timeframe.
        this.lastCrashTimestamps.push(now);
        // Set the flag that disable process spawning when we exceed the
        // `processCrashThreshold`.
        this._processSpawningDisabled =
          this.lastCrashTimestamps.length > lazy.processCrashThreshold;

        this.logger.debug(
          `Extension process crashed ${this.lastCrashTimestamps.length} times over the last ${lazy.processCrashTimeframe}ms`
        );

        const { appInForeground } = this;

        if (this.processSpawningDisabled) {
          if (appInForeground) {
            Glean.extensions.processEvent.crashed_over_threshold_fg.add(1);
          } else {
            Glean.extensions.processEvent.crashed_over_threshold_bg.add(1);
          }
          this.logger.warn(
            `Extension process respawning disabled because it crashed too often in the last ${lazy.processCrashTimeframe}ms (${this.lastCrashTimestamps.length} > ${lazy.processCrashThreshold}).`
          );
        }

        Glean.extensions.processEvent[
          appInForeground ? "crashed_fg" : "crashed_bg"
        ].add(1);
        Management.emit("extension-process-crash", {
          childID,
          processSpawningDisabled: this.processSpawningDisabled,
          appInForeground,
        });
        break;
      }
    }
  },

  enableProcessSpawning() {
    const crashCounter = this.lastCrashTimestamps.length;
    this.lastCrashTimestamps = [];
    this.logger.debug(`reset crash counter (was ${crashCounter})`);
    this._processSpawningDisabled = false;
    Management.emit("extension-enable-process-spawning");
  },

  get appInForeground() {
    // Only account for application in the background for
    // android builds.
    return this._isAndroid ? this._appInForeground : true;
  },

  get processSpawningDisabled() {
    return this._processSpawningDisabled;
  },
};

ExtensionProcessCrashObserver.init();

const manifestTypes = new Map([
  ["theme", "manifest.ThemeManifest"],
  ["locale", "manifest.WebExtensionLangpackManifest"],
  ["dictionary", "manifest.WebExtensionDictionaryManifest"],
  ["extension", "manifest.WebExtensionManifest"],
]);

/**
 * Represents the data contained in an extension, contained either
 * in a directory or a zip file, which may or may not be installed.
 * This class implements the functionality of the Extension class,
 * primarily related to manifest parsing and localization, which is
 * useful prior to extension installation or initialization.
 *
 * No functionality of this class is guaranteed to work before
 * `loadManifest` has been called, and completed.
 */
export class ExtensionData {
  /**
   * Note: These fields are only available and meant to be used on Extension
   * instances, declared here because methods from this class reference them.
   */
  /** @type {object} TODO: move to the Extension class, bug 1871094. */
  addonData;
  /** @type {nsIURI} */
  baseURI;
  /** @type {nsIPrincipal} */
  principal;
  /** @type {boolean} */
  temporarilyInstalled;

  constructor(rootURI, isPrivileged = false) {
    this.rootURI = rootURI;
    this.resourceURL = rootURI.spec;
    this.isPrivileged = isPrivileged;

    this.manifest = null;
    this.type = null;
    this.id = null;
    this.uuid = null;
    this.localeData = null;
    this.fluentL10n = null;
    this._promiseLocales = null;

    this.apiNames = new Set();
    this.dependencies = new Set();
    this.permissions = new Set();

    this.startupData = null;

    this.errors = [];
    this.warnings = [];
    this.eventPagesEnabled = lazy.eventPagesEnabled;
  }

  /**
   * A factory function that allows the construction of ExtensionData, with
   * the isPrivileged flag computed asynchronously.
   *
   * @param {object} options
   * @param {nsIURI} options.rootURI
   *  The URI pointing to the extension root.
   * @param {function(type, id): boolean} options.checkPrivileged
   *  An (async) function that takes the addon type and addon ID and returns
   *  whether the given add-on is privileged.
   * @param {boolean} options.temporarilyInstalled
   *  whether the given add-on is installed as temporary.
   * @returns {Promise<ExtensionData>}
   */
  static async constructAsync({
    rootURI,
    checkPrivileged,
    temporarilyInstalled,
  }) {
    let extension = new ExtensionData(rootURI);
    // checkPrivileged depends on the extension type and id.
    await extension.initializeAddonTypeAndID();
    let { type, id } = extension;
    extension.isPrivileged = await checkPrivileged(type, id);
    extension.temporarilyInstalled = temporarilyInstalled;
    return extension;
  }

  static getIsPrivileged({ signedState, builtIn, temporarilyInstalled }) {
    return (
      signedState === lazy.AddonManager.SIGNEDSTATE_PRIVILEGED ||
      signedState === lazy.AddonManager.SIGNEDSTATE_SYSTEM ||
      builtIn ||
      (lazy.AddonSettings.EXPERIMENTS_ENABLED && temporarilyInstalled)
    );
  }

  get builtinMessages() {
    return null;
  }

  get logger() {
    let id = this.id || "<unknown>";
    return Log.repository.getLogger(LOGGER_ID_BASE + id);
  }

  /**
   * Report an error about the extension's manifest file.
   *
   * @param {string} message The error message
   */
  manifestError(message) {
    this.packagingError(`Reading manifest: ${message}`);
  }

  /**
   * Report a warning about the extension's manifest file.
   *
   * @param {string} message The warning message
   */
  manifestWarning(message) {
    this.packagingWarning(`Reading manifest: ${message}`);
  }

  // Report an error about the extension's general packaging.
  packagingError(message) {
    this.errors.push(message);
    this.logError(message);
  }

  packagingWarning(message) {
    this.warnings.push(message);
    this.logWarning(message);
  }

  logWarning(message) {
    this._logMessage(message, "warn");
  }

  logError(message) {
    this._logMessage(message, "error");
  }

  _logMessage(message, severity) {
    this.logger[severity](`Loading extension '${this.id}': ${message}`);
  }

  ensureNoErrors() {
    if (this.errors.length) {
      // startup() repeatedly checks whether there are errors after parsing the
      // extension/manifest before proceeding with starting up.
      throw new Error(this.errors.join("\n"));
    }
  }

  /**
   * Returns the moz-extension: URL for the given path within this
   * extension.
   *
   * Must not be called unless either the `id` or `uuid` property has
   * already been set.
   *
   * @param {string} path The path portion of the URL.
   * @returns {string}
   */
  getURL(path = "") {
    if (!(this.id || this.uuid)) {
      throw new Error(
        "getURL may not be called before an `id` or `uuid` has been set"
      );
    }
    if (!this.uuid) {
      this.uuid = UUIDMap.get(this.id);
    }
    return `moz-extension://${this.uuid}/${path}`;
  }

  /**
   * Discovers the file names within a directory or JAR file.
   *
   * @param {string} path
   *   The path to the directory or jar file to look at.
   * @param {boolean} [directoriesOnly]
   *   If true, this will return only the directories present within the directory.
   * @returns {Promise<string[]>}
   *   An array of names of files/directories (only the name, not the path).
   */
  async _readDirectory(path, directoriesOnly = false) {
    if (this.rootURI instanceof Ci.nsIFileURL) {
      let uri = Services.io.newURI("./" + path, null, this.rootURI);
      let fullPath = uri.QueryInterface(Ci.nsIFileURL).file.path;

      let results = [];
      try {
        let children = await IOUtils.getChildren(fullPath);
        for (let child of children) {
          if (
            !directoriesOnly ||
            (await IOUtils.stat(child)).type == "directory"
          ) {
            results.push(PathUtils.filename(child));
          }
        }
      } catch (ex) {
        // Fall-through, return what we have.
      }
      return results;
    }

    let uri = this.rootURI.QueryInterface(Ci.nsIJARURI);

    // Append the sub-directory path to the base JAR URI and normalize the
    // result.
    let entry = `${uri.JAREntry}/${path}/`
      .replace(/\/\/+/g, "/")
      .replace(/^\//, "");
    uri = Services.io.newURI(`jar:${uri.JARFile.spec}!/${entry}`);

    let results = [];
    for (let name of lazy.aomStartup.enumerateJARSubtree(uri)) {
      if (!name.startsWith(entry)) {
        throw new Error("Unexpected ZipReader entry");
      }

      // The enumerator returns the full path of all entries.
      // Trim off the leading path, and filter out entries from
      // subdirectories.
      name = name.slice(entry.length);
      if (
        name &&
        !/\/./.test(name) &&
        (!directoriesOnly || name.endsWith("/"))
      ) {
        results.push(name.replace("/", ""));
      }
    }

    return results;
  }

  readJSON(path) {
    return new Promise((resolve, reject) => {
      let uri = this.rootURI.resolve(`./${path}`);

      lazy.NetUtil.asyncFetch(
        { uri, loadUsingSystemPrincipal: true },
        (inputStream, status) => {
          if (!Components.isSuccessCode(status)) {
            // Convert status code to a string
            let e = Components.Exception("", status);
            reject(new Error(`Error while loading '${uri}' (${e.name})`));
            return;
          }
          try {
            let text = lazy.NetUtil.readInputStreamToString(
              inputStream,
              inputStream.available(),
              { charset: "utf-8" }
            );

            text = text.replace(COMMENT_REGEXP, "$1");

            resolve(JSON.parse(text));
          } catch (e) {
            reject(e);
          }
        }
      );
    });
  }

  get restrictSchemes() {
    return !(this.isPrivileged && this.hasPermission("mozillaAddons"));
  }

  get optionsPageProperties() {
    let page = this.manifest.options_ui?.page ?? this.manifest.options_page;
    if (!page) {
      return null;
    }
    return {
      page,
      open_in_tab: this.manifest.options_ui
        ? (this.manifest.options_ui.open_in_tab ?? false)
        : true,
      // `options_ui.browser_style` is assigned the proper default value
      // (true for MV2 and false for MV3 when not explicitly set),
      // in `#parseBrowserStyleInManifest` (called when we are loading
      // and parse manifest data from the `parseManifest` method).
      browser_style: this.manifest.options_ui?.browser_style ?? false,
    };
  }

  /**
   * Given an array of host and permissions, generate a structured permissions object
   * that contains seperate host origins and permissions arrays.
   *
   * @param {Array} permissionsArray
   * @param {Array} [hostPermissions]
   * @returns {object} permissions object
   */
  permissionsObject(permissionsArray = [], hostPermissions = []) {
    let permissions = new Set();
    let origins = new Set();
    let { restrictSchemes, isPrivileged } = this;
    let isMV2 = this.manifestVersion === 2;

    for (let perm of permissionsArray.concat(hostPermissions)) {
      let type = classifyPermission(perm, restrictSchemes, isPrivileged);
      if (type.origin) {
        origins.add(perm);
      } else if (type.permission) {
        if (isMV2 && PERMS_NOT_IN_MV2.has(perm)) {
          // Skip, without warning (parseManifest warns if needed).
          continue;
        }
        permissions.add(perm);
      }
    }

    return {
      permissions,
      origins,
    };
  }

  /**
   * Returns an object representing any capabilities that the extension
   * has access to based on fixed properties in the manifest.  The result
   * includes the contents of the "permissions" property as well as other
   * capabilities that are derived from manifest fields that users should
   * be informed of (e.g., origins where content scripts are injected).
   *
   * For MV3 extensions with origin controls, this does not include origins.
   */
  getRequiredPermissions() {
    if (this.type !== "extension") {
      return null;
    }

    let { permissions } = this.permissionsObject(this.manifest.permissions);

    if (
      this.manifest.devtools_page &&
      !this.manifest.optional_permissions.includes("devtools")
    ) {
      permissions.add("devtools");
    }

    return {
      permissions: Array.from(permissions),
      origins: this.originControls ? [] : this.getManifestOrigins(),
    };
  }

  /**
   * @returns {string[]} all origins that are referenced in manifest via
   * permissions, host_permissions, or content_scripts keys.
   */
  getManifestOrigins() {
    if (this.type !== "extension") {
      return null;
    }

    let { origins } = this.permissionsObject(
      this.manifest.permissions,
      this.manifest.host_permissions
    );

    for (let entry of this.manifest.content_scripts || []) {
      for (let origin of entry.matches) {
        origins.add(origin);
      }
    }

    return Array.from(origins);
  }

  /**
   * @returns {MatchPatternSet} MatchPatternSet for only the origins that are
   * referenced in manifest via permissions, host_permissions, or content_scripts keys.
   */
  getManifestOriginsMatchPatternSet() {
    if (this.type !== "extension") {
      return null;
    }
    if (this._manifestOriginsMatchPatternSet) {
      return this._manifestOriginsMatchPatternSet;
    }
    this._manifestOriginsMatchPatternSet = new MatchPatternSet(
      this.getManifestOrigins(),
      {
        restrictSchemes: this.restrictSchemes,
        ignorePath: true,
      }
    );
    return this._manifestOriginsMatchPatternSet;
  }

  /**
   * Returns additional permissions that extensions is requesting based on its
   * manifest. For now, this is host_permissions (and content scripts) in mv3.
   */
  getRequestedPermissions() {
    if (this.type !== "extension") {
      return null;
    }
    if (this.originControls && lazy.installIncludesOrigins) {
      return { permissions: [], origins: this.getManifestOrigins() };
    }
    return { permissions: [], origins: [] };
  }

  /**
   * Returns optional permissions from the manifest, including host permissions
   * if originControls is true.
   */
  get manifestOptionalPermissions() {
    if (this.type !== "extension") {
      return null;
    }

    let { permissions, origins } = this.permissionsObject(
      this.manifest.optional_permissions,
      this.manifest.optional_host_permissions
    );
    if (this.originControls) {
      for (let origin of this.getManifestOrigins()) {
        origins.add(origin);
      }
    }

    return {
      permissions: Array.from(permissions),
      origins: Array.from(origins),
    };
  }

  /**
   * Returns an object representing all capabilities this extension has
   * access to, including fixed ones from the manifest as well as dynamically
   * granted permissions.
   */
  get activePermissions() {
    if (this.type !== "extension") {
      return null;
    }

    let result = {
      origins: this.allowedOrigins.patterns
        .map(matcher => matcher.pattern)
        // moz-extension://id/* is always added to allowedOrigins, but it
        // is not a valid host permission in the API. So, remove it.
        .filter(pattern => !pattern.startsWith("moz-extension:")),
      apis: [...this.apiNames],
    };

    const EXP_PATTERN = /^experiments\.\w+/;
    result.permissions = [...this.permissions].filter(
      p => !result.origins.includes(p) && !EXP_PATTERN.test(p)
    );
    return result;
  }

  // Returns whether the front end should prompt for this permission
  static async shouldPromptFor(permission) {
    return !(await lazy.NO_PROMPT_PERMISSIONS).has(permission);
  }

  // Compute the difference between two sets of permissions, suitable
  // for presenting to the user.
  static comparePermissions(oldPermissions, newPermissions) {
    let oldMatcher = new MatchPatternSet(oldPermissions.origins, {
      restrictSchemes: false,
    });
    return {
      // formatPermissionStrings ignores any scheme, so only look at the domain.
      origins: newPermissions.origins.filter(
        perm =>
          !oldMatcher.subsumesDomain(
            new MatchPattern(perm, { restrictSchemes: false })
          )
      ),
      permissions: newPermissions.permissions.filter(
        perm => !oldPermissions.permissions.includes(perm)
      ),
    };
  }

  // Return those permissions in oldPermissions that also exist in newPermissions.
  static intersectPermissions(oldPermissions, newPermissions) {
    let matcher = new MatchPatternSet(newPermissions.origins, {
      restrictSchemes: false,
    });

    return {
      origins: oldPermissions.origins.filter(perm =>
        matcher.subsumesDomain(
          new MatchPattern(perm, { restrictSchemes: false })
        )
      ),
      permissions: oldPermissions.permissions.filter(perm =>
        newPermissions.permissions.includes(perm)
      ),
    };
  }

  /**
   * When updating the addon, find and migrate permissions that have moved from required
   * to optional.  This also handles any updates required for permission removal.
   *
   * @param {string} id The id of the addon being updated
   * @param {object} oldPermissions
   * @param {object} oldOptionalPermissions
   * @param {object} newPermissions
   * @param {object} newOptionalPermissions
   */
  static async migratePermissions(
    id,
    oldPermissions,
    oldOptionalPermissions,
    newPermissions,
    newOptionalPermissions
  ) {
    let migrated = ExtensionData.intersectPermissions(
      oldPermissions,
      newOptionalPermissions
    );
    // If a permission is optional in this version and was mandatory in the previous
    // version, it was already accepted by the user at install time so add it to the
    // list of granted optional permissions now.
    await lazy.ExtensionPermissions.add(id, migrated);

    // Now we need to update ExtensionPreferencesManager, removing any settings
    // for old permissions that no longer exist.
    let permSet = new Set(
      newPermissions.permissions.concat(newOptionalPermissions.permissions)
    );
    let oldPerms = oldPermissions.permissions.concat(
      oldOptionalPermissions.permissions
    );

    let removed = oldPerms.filter(x => !permSet.has(x));
    // Force the removal here to ensure the settings are removed prior
    // to startup.  This will remove both required or optional permissions,
    // whereas the call from within ExtensionPermissions would only result
    // in a removal for optional permissions that were removed.
    await lazy.ExtensionPreferencesManager.removeSettingsForPermissions(
      id,
      removed
    );

    // Remove any optional permissions that have been removed from the manifest.
    await lazy.ExtensionPermissions.remove(id, {
      permissions: removed,
      origins: [],
    });
  }

  canUseAPIExperiment() {
    return (
      this.type == "extension" &&
      (this.isPrivileged ||
        // TODO(Bug 1771341): Allowing the "experiment_apis" property when only
        // AddonSettings.EXPERIMENTS_ENABLED is true is currently needed to allow,
        // while running under automation, the test harness extensions (like mochikit
        // and specialpowers) to use that privileged manifest property.
        lazy.AddonSettings.EXPERIMENTS_ENABLED)
    );
  }

  canUseThemeExperiment() {
    return (
      ["extension", "theme"].includes(this.type) &&
      (this.isPrivileged ||
        // "theme_experiment" MDN docs are currently explicitly mentioning this is expected
        // to be allowed also for non-signed extensions installed non-temporarily on builds
        // where the signature checks can be disabled).
        //
        // NOTE: be careful to don't regress "theme_experiment" (see Bug 1773076) while changing
        // AddonSettings.EXPERIMENTS_ENABLED (e.g. as part of fixing Bug 1771341).
        lazy.AddonSettings.EXPERIMENTS_ENABLED)
    );
  }

  get manifestVersion() {
    return this.manifest.manifest_version;
  }

  get workerBackground() {
    const background = this.manifest.background;

    const hasServiceWorker =
      background?.service_worker &&
      WebExtensionPolicy.backgroundServiceWorkerEnabled;
    if (!hasServiceWorker) {
      return false;
    }

    const hasDocument = background.scripts || background.page;
    if (!hasDocument) {
      return true;
    }

    // assurance: both "document" and "service_worker" environment specified in manifest

    for (let environment of background.preferred_environment || []) {
      if (environment === "document") {
        return false;
      }
      if (environment === "service_worker") {
        return true;
      }
    }

    // When not specified, prefer the the "document" environment
    // aka event page by default. This is consistent with Safari 18.

    return false;
  }

  get persistentBackground() {
    if (
      !this.manifest.background ||
      this.manifestVersion > 2 ||
      this.workerBackground
    ) {
      return false;
    }
    // V2 addons can only use event pages if the pref is also flipped and
    // persistent is explicilty set to false.
    return !this.eventPagesEnabled || this.manifest.background.persistent;
  }

  /**
   * backgroundState can be starting, running, suspending or stopped.
   * It is undefined if the extension has no background page.
   * See ext-backgroundPage.js for more details.
   *
   * @param {string} state starting, running, suspending or stopped
   */
  set backgroundState(state) {
    this._backgroundState = state;
  }

  get backgroundState() {
    return this._backgroundState;
  }

  async getExtensionVersionWithoutValidation() {
    return (await this.readJSON("manifest.json")).version;
  }

  /**
   * Load a locale and return a localized manifest.  The extension must
   * be initialized, and manifest parsed prior to calling.
   *
   * @param {string} locale to load, if necessary.
   * @returns {Promise<object>} normalized manifest.
   */
  async getLocalizedManifest(locale) {
    if (!this.type || !this.localeData) {
      throw new Error("The extension has not been initialized.");
    }
    // Upon update or reinstall, the Extension.manifest may be read from
    // StartupCache.manifest, however rawManifest is *not*.  We need the
    // raw manifest in order to get a localized manifest.
    if (!this.rawManifest) {
      this.rawManifest = await this.readJSON("manifest.json");
    }

    if (!this.localeData.has(locale)) {
      // Locales are not avialable until some additional
      // initialization is done.  We could just call initAllLocales,
      // but that is heavy handed, especially when we likely only
      // need one out of 20.
      let locales = await this.promiseLocales();
      if (locales.get(locale)) {
        await this.initLocale(locale);
      }
      if (!this.localeData.has(locale)) {
        throw new Error(`The extension does not contain the locale ${locale}`);
      }
    }
    let normalized = await this._getNormalizedManifest(locale);
    if (normalized.error) {
      throw new Error(normalized.error);
    }
    return normalized.value;
  }

  async _getNormalizedManifest(locale) {
    let manifestType = manifestTypes.get(this.type);

    let context = {
      url: this.baseURI && this.baseURI.spec,
      principal: this.principal,
      logError: error => {
        this.manifestWarning(error);
      },
      preprocessors: {},
      manifestVersion: this.manifestVersion,
      // We introduced this context param in Bug 1831417.
      ignoreUnrecognizedProperties: false,
    };

    if (this.fluentL10n || this.localeData) {
      context.preprocessors.localize = value => this.localize(value, locale);
    }

    return lazy.Schemas.normalize(this.rawManifest, manifestType, context);
  }

  #parseBrowserStyleInManifest(manifest, manifestKey, defaultValueInMV2) {
    const obj = manifest[manifestKey];
    if (!obj) {
      return;
    }
    const browserStyleIsVoid = obj.browser_style == null;
    obj.browser_style ??= defaultValueInMV2;
    if (this.manifestVersion < 3 || !obj.browser_style) {
      // MV2 (true or false), or MV3 (false set explicitly or default false).
      // No changes in observed behavior, return now to avoid logspam.
      return;
    }
    // Now there are two cases (MV3 only):
    // - browser_style was not specified, but defaults to true.
    // - browser_style was set to true by the extension.
    //
    // These will eventually be deprecated. For the deprecation plan, see
    // https://bugzilla.mozilla.org/show_bug.cgi?id=1827910#c1
    let warning;
    if (!lazy.browserStyleMV3supported) {
      obj.browser_style = false;
      if (browserStyleIsVoid && !lazy.browserStyleMV3sameAsMV2) {
        // defaultValueInMV2 is true, but there was no intent to use these
        // defaults. Don't warn.
        return;
      }
      warning = `"browser_style:true" is no longer supported in Manifest Version 3.`;
    } else {
      warning = `"browser_style:true" has been deprecated in Manifest Version 3 and will be unsupported in the near future.`;
    }
    if (browserStyleIsVoid) {
      warning += ` While "${manifestKey}.browser_style" was not explicitly specified in manifest.json, its default value was true.`;
      if (!lazy.browserStyleMV3sameAsMV2) {
        obj.browser_style = false;
        warning += ` The default value of "${manifestKey}.browser_style" has changed from true to false in Manifest Version 3.`;
      } else {
        warning += ` Its default will change to false in Manifest Version 3 starting from Firefox 115.`;
      }
    }

    this.manifestWarning(
      `Warning processing ${manifestKey}.browser_style: ${warning}`
    );
  }

  // AMO enforces a maximum length of 45 on the name since at least 2017, via
  // https://github.com/mozilla/addons-linter/blame/c4507688899aaafe29c522f1b1aec94b78b8a095/src/schema/updates/manifest.json#L111
  // added in https://github.com/mozilla/addons-linter/pull/1169
  // To avoid breaking add-ons that do not go through AMO (e.g. temporarily
  // loaded extensions), we enforce the limit by truncating and warning if
  // needed, instead enforcing a maxLength on "name" in schemas/manifest.json.
  //
  // We set the limit to 75, which is a safe limit that matches the CWS,
  // see https://bugzilla.mozilla.org/show_bug.cgi?id=1939087#c5
  static EXT_NAME_MAX_LEN = 75;

  async initializeAddonTypeAndID() {
    if (this.type) {
      // Already initialized.
      return;
    }
    this.rawManifest = await this.readJSON("manifest.json");
    let manifest = this.rawManifest;

    if (manifest.theme) {
      this.type = "theme";
    } else if (manifest.langpack_id) {
      this.type = "locale";
    } else if (manifest.dictionaries) {
      this.type = "dictionary";
    } else {
      this.type = "extension";
    }

    if (!this.id) {
      let bss =
        manifest.browser_specific_settings?.gecko ||
        manifest.applications?.gecko;
      let id = bss?.id;
      // This is a basic type check.
      // When parseManifest is called, the ID is validated more thoroughly
      // because the id is defined to be an ExtensionID type in
      // toolkit/components/extensions/schemas/manifest.json
      if (typeof id == "string") {
        this.id = id;
      }
    }
  }

  // eslint-disable-next-line complexity
  async parseManifest() {
    await Promise.all([this.initializeAddonTypeAndID(), Management.lazyInit()]);

    let manifest = this.rawManifest;
    this.manifest = manifest;

    if (manifest.default_locale) {
      await this.initLocale();
    }

    if (manifest.l10n_resources) {
      if (this.isPrivileged) {
        // TODO (Bug 1733466): For historical reasons fluent isn't being used to
        // localize manifest properties read from the add-on manager (e.g., author,
        // homepage, etc.), the changes introduced by Bug 1734987 does now ensure
        // that isPrivileged will be set while parsing the manifest and so this
        // can be now supported but requires some additional changes, being tracked
        // by Bug 1733466.
        if (this.constructor != ExtensionData) {
          this.fluentL10n = new Localization(manifest.l10n_resources, true);
        }
      } else if (this.temporarilyInstalled) {
        this.manifestError(
          `Using 'l10n_resources' requires a privileged add-on. ` +
            PRIVILEGED_ADDONS_DEVDOCS_MESSAGE
        );
      } else {
        // Warn but don't make this fatal.
        this.manifestWarning(
          "Ignoring l10n_resources in unprivileged extension"
        );
      }
    }

    let normalized = await this._getNormalizedManifest();
    if (normalized.error) {
      this.manifestError(normalized.error);
      return null;
    }

    manifest = normalized.value;

    const isMV2 = this.manifestVersion < 3;

    // `browser_specific_settings` is the recommended key to use in the
    // manifest, and the only possible choice in MV3+. For MV2 extensions, we
    // still allow `applications`, though. Because `applications` used to be
    // the only key in the distant past, most internal code is written using
    // applications. That's why we end up re-assigning `browser_specific_settings`
    // to `applications` below.
    //
    // Also, when a MV3+ extension specifies `applications`, the key isn't
    // recognized and therefore filtered out from the normalized manifest as
    // part of the JSONSchema normalization.
    if (manifest.browser_specific_settings?.gecko) {
      if (manifest.applications) {
        this.manifestWarning(
          `"applications" property ignored and overridden by "browser_specific_settings"`
        );
      }
      manifest.applications = manifest.browser_specific_settings;
    }

    // On Android, override the browser specific settings with those found in
    // `bss.gecko_android`, if any.
    //
    // It is also worth noting that the `gecko_android` key in `applications`
    // is marked as "unsupported" in the JSON schema.
    if (
      AppConstants.platform == "android" &&
      manifest.browser_specific_settings?.gecko_android
    ) {
      const { strict_min_version, strict_max_version } =
        manifest.browser_specific_settings.gecko_android;

      // When the manifest doesn't define `browser_specific_settings.gecko`, it
      // is still possible to reach this block but `manifest.applications`
      // won't be defined yet.
      if (!manifest?.applications) {
        manifest.applications = {
          // All properties should be optional in `gecko` so we omit them here.
          gecko: {},
        };
      }

      if (strict_min_version?.length) {
        manifest.applications.gecko.strict_min_version = strict_min_version;
      }

      if (strict_max_version?.length) {
        manifest.applications.gecko.strict_max_version = strict_max_version;
      }
    }

    if (manifest.name.length > ExtensionData.EXT_NAME_MAX_LEN) {
      // Truncate and warn - see comment in EXT_NAME_MAX_LEN.
      manifest.name = manifest.name.slice(0, ExtensionData.EXT_NAME_MAX_LEN);
      this.manifestWarning(
        `Warning processing "name": must be shorter than ${ExtensionData.EXT_NAME_MAX_LEN}`
      );
    }

    if (manifest.background) {
      const background = manifest.background;

      if (background.page && background.scripts) {
        // both page and scripts are specified, educate the author on the deterministic behaviour
        // Note: in Chrome and Safari, the precedence is inverted.
        this.manifestWarning(
          `Warning processing background: Both background.page and background.scripts specified. background.scripts will be ignored.`
        );
      }

      // take the presence of preferred_environment as clue the author knows what it is doing
      const hasPreference = Array.isArray(background.preferred_environment);
      if (!hasPreference && WebExtensionPolicy.backgroundServiceWorkerEnabled) {
        // both serviceWorker and document are specified, educate the author on the deterministic behaviour
        const documentType = background.page ? "page" : "scripts";
        this.manifestWarning(
          `Warning processing background: with both background.service_worker and background.${documentType}, only background.${documentType} will be loaded. This can be changed with background.preferred_environment.`
        );
      }

      if (
        this.manifestVersion < 3 &&
        !this.eventPagesEnabled &&
        !background.persistent
      ) {
        this.logWarning("Event pages are not currently supported.");
      }
    }

    if (
      this.isPrivileged &&
      manifest.hidden &&
      (manifest.action || manifest.browser_action || manifest.page_action)
    ) {
      this.manifestError(
        "Cannot use browser and/or page actions in hidden add-ons"
      );
    }

    // manifest.options_page opens the extension page in a new tab
    // and so we will not need to special handling browser_style.
    if (manifest.options_ui) {
      if (manifest.options_ui.open_in_tab) {
        // browser_style:true has no effect when open_in_tab is true.
        manifest.options_ui.browser_style = false;
      } else {
        this.#parseBrowserStyleInManifest(manifest, "options_ui", true);
      }
    }
    if (this.manifestVersion < 3) {
      this.#parseBrowserStyleInManifest(manifest, "browser_action", false);
    } else {
      this.#parseBrowserStyleInManifest(manifest, "action", false);
    }
    this.#parseBrowserStyleInManifest(manifest, "page_action", false);
    if (AppConstants.MOZ_BUILD_APP === "browser") {
      this.#parseBrowserStyleInManifest(manifest, "sidebar_action", true);
    }

    let apiNames = new Set();
    let dependencies = new Set();
    let originPermissions = new Set();
    let permissions = new Set();
    let webAccessibleResources = [];

    let schemaPromises = new Map();

    // Note: this.id and this.type were computed in initializeAddonTypeAndID.
    // The format of `this.id` was confirmed to be a valid extensionID by the
    // Schema validation as part of the _getNormalizedManifest() call.
    let result = {
      apiNames,
      dependencies,
      id: this.id,
      manifest,
      modules: null,
      // Whether to treat all origin permissions (including content scripts)
      // from the manifestas as optional, and enable users to control them.
      originControls: this.manifestVersion >= 3 && this.type === "extension",
      originPermissions,
      permissions,
      schemaURLs: null,
      type: this.type,
      webAccessibleResources,
    };

    if (this.type === "extension") {
      let { isPrivileged } = this;
      let restrictSchemes = !(
        isPrivileged && manifest.permissions.includes("mozillaAddons")
      );

      // Privileged and temporary extensions still get OriginControls, but
      // can have host permissions automatically granted during install.
      // For all other cases, ensure granted_host_permissions is false.
      if (!isPrivileged && !this.temporarilyInstalled) {
        manifest.granted_host_permissions = false;
      }

      let host_permissions = manifest.host_permissions ?? [];

      for (let perm of manifest.permissions.concat(host_permissions)) {
        if (perm === "geckoProfiler" && !isPrivileged) {
          const acceptedExtensions = Services.prefs.getStringPref(
            "extensions.geckoProfiler.acceptedExtensionIds",
            ""
          );
          if (!acceptedExtensions.split(",").includes(this.id)) {
            this.manifestError(
              "Only specific extensions are allowed to access the geckoProfiler."
            );
            continue;
          }
        }

        let type = classifyPermission(perm, restrictSchemes, isPrivileged);
        if (type.origin) {
          perm = type.origin;
          if (!result.originControls) {
            originPermissions.add(perm);
          }
        } else if (type.api) {
          apiNames.add(type.api);
        } else if (type.invalid) {
          // If EXPERIMENTS_ENABLED is not enabled prevent the install
          // to ensure developer awareness.
--> --------------------

--> maximum size reached

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

[ Dauer der Verarbeitung: 0.51 Sekunden  (vorverarbeitet)  ]