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


SSL 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

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

[ Verzeichnis aufwärts0.72unsichere Verbindung  Übersetzung europäischer Sprachen durch Browser  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


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