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


Quelle  AddonManager.sys.mjs   Sprache: unbekannt

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// Cannot use Services.appinfo here, or else xpcshell-tests will blow up, as
// most tests later register different nsIAppInfo implementations, which
// wouldn't be reflected in Services.appinfo anymore, as the lazy getter
// underlying it would have been initialized if we used it here.
if ("@mozilla.org/xre/app-info;1" in Cc) {
  // eslint-disable-next-line mozilla/use-services
  let runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
  if (runtime.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
    // Refuse to run in child processes.
    throw new Error("You cannot use the AddonManager in child processes!");
  }
}

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

const MOZ_COMPATIBILITY_NIGHTLY = ![
  "aurora",
  "beta",
  "release",
  "esr",
].includes(AppConstants.MOZ_UPDATE_CHANNEL);

const INTL_LOCALES_CHANGED = "intl:app-locales-changed";
const XPIPROVIDER_BLOCKLIST_ATTENTION_UPDATED =
  "xpi-provider:blocklist-attention-updated";

const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion";
const PREF_EM_UPDATE_ENABLED = "extensions.update.enabled";
const PREF_EM_LAST_APP_VERSION = "extensions.lastAppVersion";
const PREF_EM_LAST_PLATFORM_VERSION = "extensions.lastPlatformVersion";
const PREF_EM_AUTOUPDATE_DEFAULT = "extensions.update.autoUpdateDefault";
const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility";
const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
const PREF_SYS_ADDON_UPDATE_ENABLED = "extensions.systemAddon.update.enabled";
const PREF_REMOTESETTINGS_DISABLED = "extensions.remoteSettings.disabled";
const PREF_USE_REMOTE = "extensions.webextensions.remote";

const PREF_MIN_WEBEXT_PLATFORM_VERSION =
  "extensions.webExtensionsMinPlatformVersion";
const PREF_WEBAPI_TESTING = "extensions.webapi.testing";
const PREF_EM_POSTDOWNLOAD_THIRD_PARTY =
  "extensions.postDownloadThirdPartyPrompt";

const UPDATE_REQUEST_VERSION = 2;

const BRANCH_REGEXP = /^([^\.]+\.[0-9]+[a-z]*).*/gi;
const PREF_EM_CHECK_COMPATIBILITY_BASE = "extensions.checkCompatibility";
var PREF_EM_CHECK_COMPATIBILITY = MOZ_COMPATIBILITY_NIGHTLY
  ? PREF_EM_CHECK_COMPATIBILITY_BASE + ".nightly"
  : undefined;

const WEBAPI_INSTALL_HOSTS =
  AppConstants.MOZ_APP_NAME !== "thunderbird"
    ? ["addons.mozilla.org"]
    : ["addons.thunderbird.net"];
const WEBAPI_TEST_INSTALL_HOSTS =
  AppConstants.MOZ_APP_NAME !== "thunderbird"
    ? ["addons.allizom.org", "addons-dev.allizom.org", "example.com"]
    : ["addons-stage.thunderbird.net", "example.com"];

const AMO_ATTRIBUTION_ALLOWED_SOURCES = ["amo", "disco", "rtamo"];
const AMO_ATTRIBUTION_DATA_KEYS = [
  "utm_campaign",
  "utm_content",
  "utm_medium",
  "utm_source",
];
const AMO_ATTRIBUTION_DATA_MAX_LENGTH = 40;

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

// This global is overridden by xpcshell tests, and therefore cannot be
// a const.
import { AsyncShutdown as realAsyncShutdown } from "resource://gre/modules/AsyncShutdown.sys.mjs";

var AsyncShutdown = realAsyncShutdown;

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs",
  AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
  Extension: "resource://gre/modules/Extension.sys.mjs",
  RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
  TelemetryTimestamps: "resource://gre/modules/TelemetryTimestamps.sys.mjs",
  isGatedPermissionType:
    "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs",
  isKnownPublicSuffix:
    "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs",
  isPrincipalInSitePermissionsBlocklist:
    "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs",
});

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "WEBEXT_POSTDOWNLOAD_THIRD_PARTY",
  PREF_EM_POSTDOWNLOAD_THIRD_PARTY,
  false
);

// Initialize the WebExtension process script service as early as possible,
// since it needs to be able to track things like new frameLoader globals that
// are created before other framework code has been initialized.
Services.ppmm.loadProcessScript(
  "resource://gre/modules/extensionProcessScriptLoader.js",
  true
);

const INTEGER = /^[1-9]\d*$/;

const CATEGORY_PROVIDER_MODULE = "addon-provider-module";

import { Log } from "resource://gre/modules/Log.sys.mjs";
// Configure a logger at the parent 'addons' level to format
// messages for all the modules under addons.*
const PARENT_LOGGER_ID = "addons";
var parentLogger = Log.repository.getLogger(PARENT_LOGGER_ID);
parentLogger.level = Log.Level.Warn;
var formatter = new Log.BasicFormatter();
// Set parent logger (and its children) to append to
// the Javascript section of the Browser Console
parentLogger.addAppender(new Log.ConsoleAppender(formatter));

// Create a new logger (child of 'addons' logger)
// for use by the Addons Manager
const LOGGER_ID = "addons.manager";
var logger = Log.repository.getLogger(LOGGER_ID);

// Provide the ability to enable/disable logging
// messages at runtime.
// If the "extensions.logging.enabled" preference is
// missing or 'false', messages at the WARNING and higher
// severity should be logged to the JS console and standard error.
// If "extensions.logging.enabled" is set to 'true', messages
// at DEBUG and higher should go to JS console and standard error.
const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";

const UNNAMED_PROVIDER = "<unnamed-provider>";
function providerName(aProvider) {
  return aProvider.name || UNNAMED_PROVIDER;
}

// A reference to XPIProvider. This should only be used to access properties or
// methods that are independent of XPIProvider startup.
var gXPIProvider;

/**
 * Preference listener which listens for a change in the
 * "extensions.logging.enabled" preference and changes the logging level of the
 * parent 'addons' level logger accordingly.
 */
var PrefObserver = {
  init() {
    Services.prefs.addObserver(PREF_LOGGING_ENABLED, this);
    Services.obs.addObserver(this, "xpcom-shutdown");
    this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED);
  },

  observe(aSubject, aTopic) {
    if (aTopic == "xpcom-shutdown") {
      Services.prefs.removeObserver(PREF_LOGGING_ENABLED, this);
      Services.obs.removeObserver(this, "xpcom-shutdown");
    } else if (aTopic == NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) {
      let debugLogEnabled = Services.prefs.getBoolPref(
        PREF_LOGGING_ENABLED,
        false
      );
      if (debugLogEnabled) {
        parentLogger.level = Log.Level.Debug;
      } else {
        parentLogger.level = Log.Level.Warn;
      }
    }
  },
};

PrefObserver.init();

/**
 * Calls a callback method consuming any thrown exception. Any parameters after
 * the callback parameter will be passed to the callback.
 *
 * @param  aCallback
 *         The callback method to call
 */
function safeCall(aCallback, ...aArgs) {
  try {
    aCallback.apply(null, aArgs);
  } catch (e) {
    logger.warn("Exception calling callback", e);
  }
}

/**
 * Report an exception thrown by a provider API method.
 */
function reportProviderError(aProvider, aMethod, aError) {
  let method = `provider ${providerName(aProvider)}.${aMethod}`;
  AddonManagerPrivate.recordException("AMI", method, aError);
  logger.error("Exception calling " + method, aError);
}

/**
 * Calls a method on a provider if it exists and consumes any thrown exception.
 * Any parameters after the aDefault parameter are passed to the provider's method.
 *
 * @param  aProvider
 *         The provider to call
 * @param  aMethod
 *         The method name to call
 * @param  aDefault
 *         A default return value if the provider does not implement the named
 *         method or throws an error.
 * @return the return value from the provider, or aDefault if the provider does not
 *         implement method or throws an error
 */
function callProvider(aProvider, aMethod, aDefault, ...aArgs) {
  if (!(aMethod in aProvider)) {
    return aDefault;
  }

  try {
    return aProvider[aMethod].apply(aProvider, aArgs);
  } catch (e) {
    reportProviderError(aProvider, aMethod, e);
    return aDefault;
  }
}

/**
 * Calls a method on a provider if it exists and consumes any thrown exception.
 * Parameters after aMethod are passed to aProvider.aMethod().
 * If the provider does not implement the method, or the method throws, calls
 * the callback with 'undefined'.
 *
 * @param  aProvider
 *         The provider to call
 * @param  aMethod
 *         The method name to call
 */
async function promiseCallProvider(aProvider, aMethod, ...aArgs) {
  if (!(aMethod in aProvider)) {
    return undefined;
  }
  try {
    return aProvider[aMethod].apply(aProvider, aArgs);
  } catch (e) {
    reportProviderError(aProvider, aMethod, e);
    return undefined;
  }
}

/**
 * Gets the currently selected locale for display.
 * @return  the selected locale or "en-US" if none is selected
 */
function getLocale() {
  return Services.locale.requestedLocale || "en-US";
}

const WEB_EXPOSED_ADDON_PROPERTIES = [
  "id",
  "version",
  "type",
  "name",
  "description",
  "isActive",
];

function webAPIForAddon(addon) {
  if (!addon) {
    return null;
  }

  // These web-exposed Addon properties (see AddonManager.webidl)
  // just come directly from an Addon object.
  let result = {};
  for (let prop of WEB_EXPOSED_ADDON_PROPERTIES) {
    result[prop] = addon[prop];
  }

  // These properties are computed.
  result.isEnabled = !addon.userDisabled;
  result.canUninstall = Boolean(
    addon.permissions & AddonManager.PERM_CAN_UNINSTALL
  );

  return result;
}

/**
 * Listens for a browser changing origin and cancels the installs that were
 * started by it.
 */
function BrowserListener(aBrowser, aInstallingPrincipal, aInstall) {
  this.browser = aBrowser;
  this.messageManager = this.browser.messageManager;
  this.principal = aInstallingPrincipal;
  this.install = aInstall;

  aBrowser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
  Services.obs.addObserver(this, "message-manager-close", true);

  aInstall.addListener(this);

  this.registered = true;
}

BrowserListener.prototype = {
  browser: null,
  install: null,
  registered: false,

  unregister() {
    if (!this.registered) {
      return;
    }
    this.registered = false;

    Services.obs.removeObserver(this, "message-manager-close");
    // The browser may have already been detached
    if (this.browser.removeProgressListener) {
      this.browser.removeProgressListener(this);
    }

    this.install.removeListener(this);
    this.install = null;
  },

  cancelInstall() {
    try {
      this.install.cancel();
    } catch (e) {
      // install may have already failed or been cancelled, ignore these
    }
  },

  observe(subject) {
    if (subject != this.messageManager) {
      return;
    }

    // The browser's message manager has closed and so the browser is
    // going away, cancel the install
    this.cancelInstall();
  },

  onLocationChange() {
    if (
      this.browser.contentPrincipal &&
      this.principal.subsumes(this.browser.contentPrincipal)
    ) {
      return;
    }

    // The browser has navigated to a new origin so cancel the install
    this.cancelInstall();
  },

  onDownloadCancelled() {
    this.unregister();
  },

  onDownloadFailed() {
    this.unregister();
  },

  onInstallFailed() {
    this.unregister();
  },

  onInstallEnded() {
    this.unregister();
  },

  QueryInterface: ChromeUtils.generateQI([
    "nsISupportsWeakReference",
    "nsIWebProgressListener",
    "nsIObserver",
  ]),
};

/**
 * This represents an author of an add-on (e.g. creator or developer)
 *
 * @param  aName
 *         The name of the author
 * @param  aURL
 *         The URL of the author's profile page
 */
function AddonAuthor(aName, aURL) {
  this.name = aName;
  this.url = aURL;
}

AddonAuthor.prototype = {
  name: null,
  url: null,

  // Returns the author's name, defaulting to the empty string
  toString() {
    return this.name || "";
  },
};

/**
 * This represents an screenshot for an add-on
 *
 * @param  aURL
 *         The URL to the full version of the screenshot
 * @param  aWidth
 *         The width in pixels of the screenshot
 * @param  aHeight
 *         The height in pixels of the screenshot
 * @param  aThumbnailURL
 *         The URL to the thumbnail version of the screenshot
 * @param  aThumbnailWidth
 *         The width in pixels of the thumbnail version of the screenshot
 * @param  aThumbnailHeight
 *         The height in pixels of the thumbnail version of the screenshot
 * @param  aCaption
 *         The caption of the screenshot
 */
function AddonScreenshot(
  aURL,
  aWidth,
  aHeight,
  aThumbnailURL,
  aThumbnailWidth,
  aThumbnailHeight,
  aCaption
) {
  this.url = aURL;
  if (aWidth) {
    this.width = aWidth;
  }
  if (aHeight) {
    this.height = aHeight;
  }
  if (aThumbnailURL) {
    this.thumbnailURL = aThumbnailURL;
  }
  if (aThumbnailWidth) {
    this.thumbnailWidth = aThumbnailWidth;
  }
  if (aThumbnailHeight) {
    this.thumbnailHeight = aThumbnailHeight;
  }
  if (aCaption) {
    this.caption = aCaption;
  }
}

AddonScreenshot.prototype = {
  url: null,
  width: null,
  height: null,
  thumbnailURL: null,
  thumbnailWidth: null,
  thumbnailHeight: null,
  caption: null,

  // Returns the screenshot URL, defaulting to the empty string
  toString() {
    return this.url || "";
  },
};

var gStarted = false;
var gStartedPromise = Promise.withResolvers();
var gStartupComplete = false;
var gCheckCompatibility = true;
var gStrictCompatibility = true;
var gCheckUpdateSecurityDefault = true;
var gCheckUpdateSecurity = gCheckUpdateSecurityDefault;
var gUpdateEnabled = true;
var gAutoUpdateDefault = true;
var gWebExtensionsMinPlatformVersion = "";
var gFinalShutdownBarrier = null;
var gBeforeShutdownBarrier = null;
var gRepoShutdownState = "";
var gShutdownInProgress = false;
var gBrowserUpdated = null;

export var AMTelemetry;
export var AMRemoteSettings;
export var AMBrowserExtensionsImport;

/**
 * This is the real manager, kept here rather than in AddonManager to keep its
 * contents hidden from API users.
 * @class
 * @lends AddonManager
 */
var AddonManagerInternal = {
  managerListeners: new Set(),
  installListeners: new Set(),
  addonListeners: new Set(),
  pendingProviders: new Set(),
  providers: new Set(),
  providerShutdowns: new Map(),
  typesByProvider: new Map(),
  startupChanges: {},
  // Store telemetry details per addon provider
  telemetryDetails: {},
  upgradeListeners: new Map(),
  externalExtensionLoaders: new Map(),

  recordTimestamp(name, value) {
    lazy.TelemetryTimestamps.add(name, value);
  },

  /**
   * Start up a provider, and register its shutdown hook if it has one
   *
   * @param {string} aProvider - An add-on provider.
   * @param {boolean} aAppChanged - Whether or not the app version has changed since last session.
   * @param {string} aOldAppVersion - Previous application version, if changed.
   * @param {string} aOldPlatformVersion - Previous platform version, if changed.
   *
   * @private
   */
  _startProvider(aProvider, aAppChanged, aOldAppVersion, aOldPlatformVersion) {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    logger.debug(`Starting provider: ${providerName(aProvider)}`);
    callProvider(
      aProvider,
      "startup",
      null,
      aAppChanged,
      aOldAppVersion,
      aOldPlatformVersion
    );
    if ("shutdown" in aProvider) {
      let name = providerName(aProvider);
      let AMProviderShutdown = () => {
        // If the provider has been unregistered, it will have been removed from
        // this.providers. If it hasn't been unregistered, then this is a normal
        // shutdown - and we move it to this.pendingProviders in case we're
        // running in a test that will start AddonManager again.
        if (this.providers.has(aProvider)) {
          this.providers.delete(aProvider);
          this.pendingProviders.add(aProvider);
        }

        return new Promise(resolve => {
          logger.debug("Calling shutdown blocker for " + name);
          resolve(aProvider.shutdown());
        }).catch(err => {
          logger.warn("Failure during shutdown of " + name, err);
          AddonManagerPrivate.recordException(
            "AMI",
            "Async shutdown of " + name,
            err
          );
        });
      };
      logger.debug("Registering shutdown blocker for " + name);
      this.providerShutdowns.set(aProvider, AMProviderShutdown);
      AddonManagerPrivate.finalShutdown.addBlocker(name, AMProviderShutdown);
    }

    this.pendingProviders.delete(aProvider);
    this.providers.add(aProvider);
    logger.debug(`Provider finished startup: ${providerName(aProvider)}`);
  },

  _getProviderByName(aName) {
    for (let provider of this.providers) {
      if (providerName(provider) == aName) {
        return provider;
      }
    }
    return undefined;
  },

  /**
   * Initializes the AddonManager, loading any known providers and initializing
   * them.
   */
  startup() {
    try {
      if (gStarted) {
        return;
      }

      this.recordTimestamp("AMI_startup_begin");

      // Enable the AMRemoteSettings client.
      AMRemoteSettings.init();

      // clear this for xpcshell test restarts
      for (let provider in this.telemetryDetails) {
        delete this.telemetryDetails[provider];
      }

      let appChanged = undefined;

      let oldAppVersion = null;
      try {
        oldAppVersion = Services.prefs.getCharPref(PREF_EM_LAST_APP_VERSION);
        appChanged = Services.appinfo.version != oldAppVersion;
      } catch (e) {}

      gBrowserUpdated = appChanged;

      let oldPlatformVersion = Services.prefs.getCharPref(
        PREF_EM_LAST_PLATFORM_VERSION,
        ""
      );

      if (appChanged !== false) {
        logger.debug("Application has been upgraded");
        Services.prefs.setCharPref(
          PREF_EM_LAST_APP_VERSION,
          Services.appinfo.version
        );
        Services.prefs.setCharPref(
          PREF_EM_LAST_PLATFORM_VERSION,
          Services.appinfo.platformVersion
        );
        Services.prefs.setIntPref(
          PREF_BLOCKLIST_PINGCOUNTVERSION,
          appChanged === undefined ? 0 : -1
        );
      }

      if (!MOZ_COMPATIBILITY_NIGHTLY) {
        PREF_EM_CHECK_COMPATIBILITY =
          PREF_EM_CHECK_COMPATIBILITY_BASE +
          "." +
          Services.appinfo.version.replace(BRANCH_REGEXP, "$1");
      }

      gCheckCompatibility = Services.prefs.getBoolPref(
        PREF_EM_CHECK_COMPATIBILITY,
        gCheckCompatibility
      );
      Services.prefs.addObserver(PREF_EM_CHECK_COMPATIBILITY, this);

      gStrictCompatibility = Services.prefs.getBoolPref(
        PREF_EM_STRICT_COMPATIBILITY,
        gStrictCompatibility
      );
      Services.prefs.addObserver(PREF_EM_STRICT_COMPATIBILITY, this);

      let defaultBranch = Services.prefs.getDefaultBranch("");
      gCheckUpdateSecurityDefault = defaultBranch.getBoolPref(
        PREF_EM_CHECK_UPDATE_SECURITY,
        gCheckUpdateSecurityDefault
      );

      gCheckUpdateSecurity = Services.prefs.getBoolPref(
        PREF_EM_CHECK_UPDATE_SECURITY,
        gCheckUpdateSecurity
      );
      Services.prefs.addObserver(PREF_EM_CHECK_UPDATE_SECURITY, this);

      gUpdateEnabled = Services.prefs.getBoolPref(
        PREF_EM_UPDATE_ENABLED,
        gUpdateEnabled
      );
      Services.prefs.addObserver(PREF_EM_UPDATE_ENABLED, this);

      gAutoUpdateDefault = Services.prefs.getBoolPref(
        PREF_EM_AUTOUPDATE_DEFAULT,
        gAutoUpdateDefault
      );
      Services.prefs.addObserver(PREF_EM_AUTOUPDATE_DEFAULT, this);

      gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(
        PREF_MIN_WEBEXT_PLATFORM_VERSION,
        gWebExtensionsMinPlatformVersion
      );
      Services.prefs.addObserver(PREF_MIN_WEBEXT_PLATFORM_VERSION, this);

      // Watch for changes to PREF_REMOTESETTINGS_DISABLED.
      Services.prefs.addObserver(PREF_REMOTESETTINGS_DISABLED, this);

      // Watch for language changes, refresh the addon cache when it changes.
      Services.obs.addObserver(this, INTL_LOCALES_CHANGED);

      // Watch for changes in the `AMBrowserExtensionsImport` singleton.
      Services.obs.addObserver(this, AMBrowserExtensionsImport.TOPIC_CANCELLED);
      Services.obs.addObserver(this, AMBrowserExtensionsImport.TOPIC_COMPLETE);
      Services.obs.addObserver(this, AMBrowserExtensionsImport.TOPIC_PENDING);

      // Watch for blocklist attention updates.
      Services.obs.addObserver(this, XPIPROVIDER_BLOCKLIST_ATTENTION_UPDATED);

      // Ensure all default providers have had a chance to register themselves.
      const { XPIExports } = ChromeUtils.importESModule(
        "resource://gre/modules/addons/XPIExports.sys.mjs"
      );
      gXPIProvider = XPIExports.XPIProvider;
      gXPIProvider.registerProvider();

      // Load any providers registered in the category manager
      for (let { entry, value: url } of Services.catMan.enumerateCategory(
        CATEGORY_PROVIDER_MODULE
      )) {
        try {
          ChromeUtils.importESModule(url);
          logger.debug(`Loaded provider scope for ${url}`);
        } catch (e) {
          AddonManagerPrivate.recordException(
            "AMI",
            "provider " + url + " load failed",
            e
          );
          logger.error(
            "Exception loading provider " +
              entry +
              ' from category "' +
              url +
              '"',
            e
          );
        }
      }

      // Register our shutdown handler with the AsyncShutdown manager
      gBeforeShutdownBarrier = new AsyncShutdown.Barrier(
        "AddonManager: Waiting to start provider shutdown."
      );
      gFinalShutdownBarrier = new AsyncShutdown.Barrier(
        "AddonManager: Waiting for providers to shut down."
      );
      AsyncShutdown.profileBeforeChange.addBlocker(
        "AddonManager: shutting down.",
        this.shutdownManager.bind(this),
        { fetchState: this.shutdownState.bind(this) }
      );

      // Once we start calling providers we must allow all normal methods to work.
      gStarted = true;

      for (let provider of this.pendingProviders) {
        this._startProvider(
          provider,
          appChanged,
          oldAppVersion,
          oldPlatformVersion
        );
      }

      // If this is a new profile just pretend that there were no changes
      if (appChanged === undefined) {
        for (let type in this.startupChanges) {
          delete this.startupChanges[type];
        }
      }

      gStartupComplete = true;
      gStartedPromise.resolve();
      this.recordTimestamp("AMI_startup_end");
    } catch (e) {
      logger.error("startup failed", e);
      AddonManagerPrivate.recordException("AMI", "startup failed", e);
      gStartedPromise.reject("startup failed");
    }

    // Disable the quarantined domains feature if the system add-on has been
    // disabled in a previous version.
    if (
      Services.prefs.getBoolPref(
        "extensions.webextensions.addons-restricted-domains@mozilla.com.disabled",
        false
      )
    ) {
      Services.prefs.setBoolPref(
        "extensions.quarantinedDomains.enabled",
        false
      );
      logger.debug(
        "Disabled quarantined domains because the system add-on was disabled"
      );
    }

    Glean.extensions.useRemotePolicy.set(
      WebExtensionPolicy.useRemoteWebExtensions
    );
    Glean.extensions.useRemotePref.set(
      Services.prefs.getBoolPref(PREF_USE_REMOTE)
    );
    Services.prefs.addObserver(PREF_USE_REMOTE, this);

    logger.debug("Completed startup sequence");
    this.callManagerListeners("onStartup");
  },

  /**
   * Registers a new AddonProvider.
   *
   * @param {string} aProvider -The provider to register
   * @param {string[]} [aTypes] - An optional array of add-on types
   */
  registerProvider(aProvider, aTypes) {
    if (!aProvider || typeof aProvider != "object") {
      throw Components.Exception(
        "aProvider must be specified",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (aTypes && !Array.isArray(aTypes)) {
      throw Components.Exception(
        "aTypes must be an array or null",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    this.pendingProviders.add(aProvider);

    if (aTypes) {
      this.typesByProvider.set(aProvider, new Set(aTypes));
    }

    // If we're registering after startup call this provider's startup.
    if (gStarted) {
      this._startProvider(aProvider);
    }
  },

  /**
   * Unregisters an AddonProvider.
   *
   * @param  aProvider
   *         The provider to unregister
   * @return Whatever the provider's 'shutdown' method returns (if anything).
   *         For providers that have async shutdown methods returning Promises,
   *         the caller should wait for that Promise to resolve.
   */
  unregisterProvider(aProvider) {
    if (!aProvider || typeof aProvider != "object") {
      throw Components.Exception(
        "aProvider must be specified",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    this.providers.delete(aProvider);
    // The test harness will unregister XPIProvider *after* shutdown, which is
    // after the provider will have been moved from providers to
    // pendingProviders.
    this.pendingProviders.delete(aProvider);

    this.typesByProvider.delete(aProvider);

    // If we're unregistering after startup but before shutting down,
    // remove the blocker for this provider's shutdown and call it.
    // If we're already shutting down, just let gFinalShutdownBarrier
    // call it to avoid races.
    if (gStarted && !gShutdownInProgress) {
      logger.debug(
        "Unregistering shutdown blocker for " + providerName(aProvider)
      );
      let shutter = this.providerShutdowns.get(aProvider);
      if (shutter) {
        this.providerShutdowns.delete(aProvider);
        gFinalShutdownBarrier.client.removeBlocker(shutter);
        return shutter();
      }
    }
    return undefined;
  },

  /**
   * Mark a provider as safe to access via AddonManager APIs, before its
   * startup has completed.
   *
   * Normally a provider isn't marked as safe until after its (synchronous)
   * startup() method has returned. Until a provider has been marked safe,
   * it won't be used by any of the AddonManager APIs. markProviderSafe()
   * allows a provider to mark itself as safe during its startup; this can be
   * useful if the provider wants to perform tasks that block startup, which
   * happen after its required initialization tasks and therefore when the
   * provider is in a safe state.
   *
   * @param aProvider Provider object to mark safe
   */
  markProviderSafe(aProvider) {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    if (!aProvider || typeof aProvider != "object") {
      throw Components.Exception(
        "aProvider must be specified",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (!this.pendingProviders.has(aProvider)) {
      return;
    }

    this.pendingProviders.delete(aProvider);
    this.providers.add(aProvider);
  },

  /**
   * Calls a method on all registered providers if it exists and consumes any
   * thrown exception. Return values are ignored. Any parameters after the
   * method parameter are passed to the provider's method.
   * WARNING: Do not use for asynchronous calls; callProviders() does not
   * invoke callbacks if provider methods throw synchronous exceptions.
   *
   * @param  aMethod
   *         The method name to call
   */
  callProviders(aMethod, ...aArgs) {
    if (!aMethod || typeof aMethod != "string") {
      throw Components.Exception(
        "aMethod must be a non-empty string",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    let providers = [...this.providers];
    for (let provider of providers) {
      try {
        if (aMethod in provider) {
          provider[aMethod].apply(provider, aArgs);
        }
      } catch (e) {
        reportProviderError(provider, aMethod, e);
      }
    }
  },

  /**
   * Report the current state of asynchronous shutdown
   */
  shutdownState() {
    let state = [];
    for (let barrier of [gBeforeShutdownBarrier, gFinalShutdownBarrier]) {
      if (barrier) {
        state.push({ name: barrier.client.name, state: barrier.state });
      }
    }
    state.push({
      name: "AddonRepository: async shutdown",
      state: gRepoShutdownState,
    });
    return state;
  },

  /**
   * Shuts down the addon manager and all registered providers, this must clean
   * up everything in order for automated tests to fake restarts.
   * @return Promise{null} that resolves when all providers and dependent modules
   *                       have finished shutting down
   */
  async shutdownManager() {
    logger.debug("before shutdown");
    try {
      await gBeforeShutdownBarrier.wait();
    } catch (e) {
      Cu.reportError(e);
    }

    logger.debug("shutdown");
    this.callManagerListeners("onShutdown");

    if (!gStartupComplete) {
      gStartedPromise.reject("shutting down");
    }

    gRepoShutdownState = "pending";
    gShutdownInProgress = true;

    // Clean up listeners
    Services.prefs.removeObserver(PREF_EM_CHECK_COMPATIBILITY, this);
    Services.prefs.removeObserver(PREF_EM_STRICT_COMPATIBILITY, this);
    Services.prefs.removeObserver(PREF_EM_CHECK_UPDATE_SECURITY, this);
    Services.prefs.removeObserver(PREF_EM_UPDATE_ENABLED, this);
    Services.prefs.removeObserver(PREF_EM_AUTOUPDATE_DEFAULT, this);
    Services.prefs.removeObserver(PREF_REMOTESETTINGS_DISABLED, this);

    Services.obs.removeObserver(this, INTL_LOCALES_CHANGED);

    Services.obs.removeObserver(
      this,
      AMBrowserExtensionsImport.TOPIC_CANCELLED
    );
    Services.obs.removeObserver(this, AMBrowserExtensionsImport.TOPIC_COMPLETE);
    Services.obs.removeObserver(this, AMBrowserExtensionsImport.TOPIC_PENDING);
    Services.obs.removeObserver(this, XPIPROVIDER_BLOCKLIST_ATTENTION_UPDATED);

    AMRemoteSettings.shutdown();

    let savedError = null;
    // Only shut down providers if they've been started.
    if (gStarted) {
      try {
        await gFinalShutdownBarrier.wait();
      } catch (err) {
        savedError = err;
        logger.error("Failure during wait for shutdown barrier", err);
        AddonManagerPrivate.recordException(
          "AMI",
          "Async shutdown of AddonManager providers",
          err
        );
      }
    }
    gXPIProvider = null;

    // Shut down AddonRepository after providers (if any).
    try {
      gRepoShutdownState = "in progress";
      await lazy.AddonRepository.shutdown();
      gRepoShutdownState = "done";
    } catch (err) {
      savedError = err;
      logger.error("Failure during AddonRepository shutdown", err);
      AddonManagerPrivate.recordException(
        "AMI",
        "Async shutdown of AddonRepository",
        err
      );
    }

    logger.debug("Async provider shutdown done");
    this.managerListeners.clear();
    this.installListeners.clear();
    this.addonListeners.clear();
    this.providerShutdowns.clear();
    for (let type in this.startupChanges) {
      delete this.startupChanges[type];
    }
    gStarted = false;
    gStartedPromise = Promise.withResolvers();
    gStartupComplete = false;
    gFinalShutdownBarrier = null;
    gBeforeShutdownBarrier = null;
    gShutdownInProgress = false;
    if (savedError) {
      throw savedError;
    }
  },

  /**
   * Notified when a preference we're interested in has changed.
   */
  observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case INTL_LOCALES_CHANGED: {
        // Asynchronously fetch and update the addons cache.
        lazy.AddonRepository.backgroundUpdateCheck();
        return;
      }

      case AMBrowserExtensionsImport.TOPIC_CANCELLED:
      case AMBrowserExtensionsImport.TOPIC_COMPLETE:
      case AMBrowserExtensionsImport.TOPIC_PENDING:
        this.callManagerListeners("onBrowserExtensionsImportChanged");
        return;

      case XPIPROVIDER_BLOCKLIST_ATTENTION_UPDATED:
        this.callManagerListeners("onBlocklistAttentionUpdated");
        return;
    }

    switch (aData) {
      case PREF_EM_CHECK_COMPATIBILITY: {
        let oldValue = gCheckCompatibility;
        gCheckCompatibility = Services.prefs.getBoolPref(
          PREF_EM_CHECK_COMPATIBILITY,
          true
        );

        this.callManagerListeners("onCompatibilityModeChanged");

        if (gCheckCompatibility != oldValue) {
          this.updateAddonAppDisabledStates();
        }

        break;
      }
      case PREF_EM_STRICT_COMPATIBILITY: {
        let oldValue = gStrictCompatibility;
        gStrictCompatibility = Services.prefs.getBoolPref(
          PREF_EM_STRICT_COMPATIBILITY,
          true
        );

        this.callManagerListeners("onCompatibilityModeChanged");

        if (gStrictCompatibility != oldValue) {
          this.updateAddonAppDisabledStates();
        }

        break;
      }
      case PREF_EM_CHECK_UPDATE_SECURITY: {
        let oldValue = gCheckUpdateSecurity;
        gCheckUpdateSecurity = Services.prefs.getBoolPref(
          PREF_EM_CHECK_UPDATE_SECURITY,
          true
        );

        this.callManagerListeners("onCheckUpdateSecurityChanged");

        if (gCheckUpdateSecurity != oldValue) {
          this.updateAddonAppDisabledStates();
        }

        break;
      }
      case PREF_EM_UPDATE_ENABLED: {
        gUpdateEnabled = Services.prefs.getBoolPref(
          PREF_EM_UPDATE_ENABLED,
          true
        );

        this.callManagerListeners("onUpdateModeChanged");
        break;
      }
      case PREF_EM_AUTOUPDATE_DEFAULT: {
        gAutoUpdateDefault = Services.prefs.getBoolPref(
          PREF_EM_AUTOUPDATE_DEFAULT,
          true
        );

        this.callManagerListeners("onUpdateModeChanged");
        break;
      }
      case PREF_MIN_WEBEXT_PLATFORM_VERSION: {
        gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(
          PREF_MIN_WEBEXT_PLATFORM_VERSION
        );
        break;
      }
      case PREF_REMOTESETTINGS_DISABLED: {
        if (Services.prefs.getBoolPref(PREF_REMOTESETTINGS_DISABLED, false)) {
          AMRemoteSettings.shutdown();
        } else {
          AMRemoteSettings.init();
        }
        break;
      }
      case PREF_USE_REMOTE: {
        Glean.extensions.useRemotePref.set(
          Services.prefs.getBoolPref(PREF_USE_REMOTE)
        );
        break;
      }
    }
  },

  /**
   * Replaces %...% strings in an addon url (update and updateInfo) with
   * appropriate values.
   *
   * @param  aAddon
   *         The Addon representing the add-on
   * @param  aUri
   *         The string representation of the URI to escape
   * @param  aAppVersion
   *         The optional application version to use for %APP_VERSION%
   * @return The appropriately escaped URI.
   */
  escapeAddonURI(aAddon, aUri, aAppVersion) {
    if (!aAddon || typeof aAddon != "object") {
      throw Components.Exception(
        "aAddon must be an Addon object",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (!aUri || typeof aUri != "string") {
      throw Components.Exception(
        "aUri must be a non-empty string",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (aAppVersion && typeof aAppVersion != "string") {
      throw Components.Exception(
        "aAppVersion must be a string or null",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    var addonStatus =
      aAddon.userDisabled || aAddon.softDisabled
        ? "userDisabled"
        : "userEnabled";

    if (!aAddon.isCompatible) {
      addonStatus += ",incompatible";
    }

    let { blocklistState } = aAddon;
    if (blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
      addonStatus += ",blocklisted";
    }
    if (blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
      addonStatus += ",softblocked";
    }

    let params = new Map(
      Object.entries({
        ITEM_ID: aAddon.id,
        ITEM_VERSION: aAddon.version,
        ITEM_STATUS: addonStatus,
        APP_ID: Services.appinfo.ID,
        APP_VERSION: aAppVersion ? aAppVersion : Services.appinfo.version,
        REQ_VERSION: UPDATE_REQUEST_VERSION,
        APP_OS: Services.appinfo.OS,
        APP_ABI: Services.appinfo.XPCOMABI,
        APP_LOCALE: getLocale(),
        CURRENT_APP_VERSION: Services.appinfo.version,
      })
    );

    let uri = aUri.replace(/%([A-Z_]+)%/g, (m0, m1) => params.get(m1) || m0);

    // escape() does not properly encode + symbols in any embedded FVF strings.
    return uri.replace(/\+/g, "%2B");
  },

  _updatePromptHandler(info) {
    let oldPerms = info.existingAddon.userPermissions;
    if (!oldPerms) {
      // Updating from a legacy add-on, just let it proceed
      return Promise.resolve();
    }

    let newPerms = info.addon.userPermissions;

    let difference = lazy.Extension.comparePermissions(oldPerms, newPerms);

    // If there are no new permissions, just go ahead with the update
    if (!difference.origins.length && !difference.permissions.length) {
      return Promise.resolve();
    }

    return new Promise((resolve, reject) => {
      let subject = {
        wrappedJSObject: {
          addon: info.addon,
          permissions: difference,
          resolve,
          reject,
          // Reference to the related AddonInstall object (used in AMTelemetry to
          // link the recorded event to the other events from the same install flow).
          install: info.install,
        },
      };
      Services.obs.notifyObservers(subject, "webextension-update-permissions");
    });
  },

  // Returns true if System Addons should be updated
  systemUpdateEnabled() {
    if (!Services.prefs.getBoolPref(PREF_SYS_ADDON_UPDATE_ENABLED)) {
      return false;
    }
    if (Services.policies && !Services.policies.isAllowed("SysAddonUpdate")) {
      return false;
    }
    return true;
  },

  /**
   * Performs a background update check by starting an update for all add-ons
   * that can be updated.
   * @return Promise{null} Resolves when the background update check is complete
   *                       (the resulting addon installations may still be in progress).
   */
  backgroundUpdateCheck() {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    let buPromise = (async () => {
      logger.debug("Background update check beginning");

      Services.obs.notifyObservers(null, "addons-background-update-start");

      if (this.updateEnabled) {
        // Keep track of all the async add-on updates happening in parallel
        let updates = [];

        let allAddons = await this.getAllAddons();

        // Repopulate repository cache first, to ensure compatibility overrides
        // are up to date before checking for addon updates.
        await lazy.AddonRepository.backgroundUpdateCheck();

        for (let addon of allAddons) {
          // Check all add-ons for updates so that any compatibility updates will
          // be applied

          if (!(addon.permissions & AddonManager.PERM_CAN_UPGRADE)) {
            continue;
          }

          updates.push(
            new Promise(resolve => {
              addon.findUpdates(
                {
                  onUpdateAvailable(aAddon, aInstall) {
                    // Start installing updates when the add-on can be updated and
                    // background updates should be applied.
                    logger.debug("Found update for add-on ${id}", aAddon);
                    if (AddonManager.shouldAutoUpdate(aAddon)) {
                      // XXX we really should resolve when this install is done,
                      // not when update-available check completes, no?
                      logger.debug(`Starting upgrade install of ${aAddon.id}`);
                      aInstall.promptHandler = (...args) =>
                        AddonManagerInternal._updatePromptHandler(...args);
                      aInstall.install();
                    }
                  },

                  onUpdateFinished: aAddon => {
                    logger.debug("onUpdateFinished for ${id}", aAddon);
                    resolve();
                  },
                },
                AddonManager.UPDATE_WHEN_PERIODIC_UPDATE
              );
            })
          );
        }
        Services.obs.notifyObservers(
          null,
          "addons-background-updates-found",
          updates.length
        );
        await Promise.all(updates);
      }

      if (AddonManagerInternal.systemUpdateEnabled()) {
        try {
          await AddonManagerInternal._getProviderByName(
            "XPIProvider"
          ).updateSystemAddons();
        } catch (e) {
          logger.warn("Failed to update system addons", e);
        }
      }

      logger.debug("Background update check complete");
      Services.obs.notifyObservers(null, "addons-background-update-complete");
    })();
    // Fork the promise chain so we can log the error and let our caller see it too.
    buPromise.catch(e => logger.warn("Error in background update", e));
    return buPromise;
  },

  /**
   * Adds a add-on to the list of detected changes for this startup. If
   * addStartupChange is called multiple times for the same add-on in the same
   * startup then only the most recent change will be remembered.
   *
   * @param  aType
   *         The type of change as a string. Providers can define their own
   *         types of changes or use the existing defined STARTUP_CHANGE_*
   *         constants
   * @param  aID
   *         The ID of the add-on
   */
  addStartupChange(aType, aID) {
    if (!aType || typeof aType != "string") {
      throw Components.Exception(
        "aType must be a non-empty string",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (!aID || typeof aID != "string") {
      throw Components.Exception(
        "aID must be a non-empty string",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (gStartupComplete) {
      return;
    }
    logger.debug("Registering startup change '" + aType + "' for " + aID);

    // Ensure that an ID is only listed in one type of change
    for (let type in this.startupChanges) {
      this.removeStartupChange(type, aID);
    }

    if (!(aType in this.startupChanges)) {
      this.startupChanges[aType] = [];
    }
    this.startupChanges[aType].push(aID);
  },

  /**
   * Removes a startup change for an add-on.
   *
   * @param  aType
   *         The type of change
   * @param  aID
   *         The ID of the add-on
   */
  removeStartupChange(aType, aID) {
    if (!aType || typeof aType != "string") {
      throw Components.Exception(
        "aType must be a non-empty string",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (!aID || typeof aID != "string") {
      throw Components.Exception(
        "aID must be a non-empty string",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (gStartupComplete) {
      return;
    }

    if (!(aType in this.startupChanges)) {
      return;
    }

    this.startupChanges[aType] = this.startupChanges[aType].filter(
      aItem => aItem != aID
    );
  },

  /**
   * Calls all registered AddonManagerListeners with an event. Any parameters
   * after the method parameter are passed to the listener.
   *
   * @param  aMethod
   *         The method on the listeners to call
   */
  callManagerListeners(aMethod, ...aArgs) {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    if (!aMethod || typeof aMethod != "string") {
      throw Components.Exception(
        "aMethod must be a non-empty string",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    let managerListeners = new Set(this.managerListeners);
    for (let listener of managerListeners) {
      try {
        if (aMethod in listener) {
          listener[aMethod].apply(listener, aArgs);
        }
      } catch (e) {
        logger.warn(
          "AddonManagerListener threw exception when calling " + aMethod,
          e
        );
      }
    }
  },

  /**
   * Calls all registered InstallListeners with an event. Any parameters after
   * the extraListeners parameter are passed to the listener.
   *
   * @param  aMethod
   *         The method on the listeners to call
   * @param  aExtraListeners
   *         An optional array of extra InstallListeners to also call
   * @return false if any of the listeners returned false, true otherwise
   */
  callInstallListeners(aMethod, aExtraListeners, ...aArgs) {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    if (!aMethod || typeof aMethod != "string") {
      throw Components.Exception(
        "aMethod must be a non-empty string",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (aExtraListeners && !Array.isArray(aExtraListeners)) {
      throw Components.Exception(
        "aExtraListeners must be an array or null",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    let result = true;
    let listeners;
    if (aExtraListeners) {
      listeners = new Set(
        aExtraListeners.concat(Array.from(this.installListeners))
      );
    } else {
      listeners = new Set(this.installListeners);
    }

    for (let listener of listeners) {
      try {
        if (aMethod in listener) {
          if (listener[aMethod].apply(listener, aArgs) === false) {
            result = false;
          }
        }
      } catch (e) {
        logger.warn(
          "InstallListener threw exception when calling " + aMethod,
          e
        );
      }
    }
    return result;
  },

  /**
   * Calls all registered AddonListeners with an event. Any parameters after
   * the method parameter are passed to the listener.
   *
   * @param  aMethod
   *         The method on the listeners to call
   */
  callAddonListeners(aMethod, ...aArgs) {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    if (!aMethod || typeof aMethod != "string") {
      throw Components.Exception(
        "aMethod must be a non-empty string",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    let addonListeners = new Set(this.addonListeners);
    for (let listener of addonListeners) {
      try {
        if (aMethod in listener) {
          listener[aMethod].apply(listener, aArgs);
        }
      } catch (e) {
        logger.warn("AddonListener threw exception when calling " + aMethod, e);
      }
    }
  },

  /**
   * Notifies all providers that an add-on has been enabled when that type of
   * add-on only supports a single add-on being enabled at a time. This allows
   * the providers to disable theirs if necessary.
   *
   * @param  aID
   *         The ID of the enabled add-on
   * @param  aType
   *         The type of the enabled add-on
   * @param  aPendingRestart
   *         A boolean indicating if the change will only take place the next
   *         time the application is restarted
   */
  async notifyAddonChanged(aID, aType, aPendingRestart) {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    if (aID && typeof aID != "string") {
      throw Components.Exception(
        "aID must be a string or null",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (!aType || typeof aType != "string") {
      throw Components.Exception(
        "aType must be a non-empty string",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    // Temporary hack until bug 520124 lands.
    // We can get here during synchronous startup, at which point it's
    // considered unsafe (and therefore disallowed by AddonManager.sys.mjs) to
    // access providers that haven't been initialized yet. Since this is when
    // XPIProvider is starting up, XPIProvider can't access itself via APIs
    // going through AddonManager.sys.mjs. Thankfully, this is the only use
    // of this API, and we know it's safe to use this API with both
    // providers; so we have this hack to allow bypassing the normal
    // safetey guard.
    // The notifyAddonChanged/addonChanged API will be unneeded and therefore
    // removed by bug 520124, so this is a temporary quick'n'dirty hack.
    let providers = [...this.providers, ...this.pendingProviders];
    for (let provider of providers) {
      let result = callProvider(
        provider,
        "addonChanged",
        null,
        aID,
        aType,
        aPendingRestart
      );
      if (result) {
        await result;
      }
    }
  },

  /**
   * Notifies all providers they need to update the appDisabled property for
   * their add-ons in response to an application change such as a blocklist
   * update.
   */
  updateAddonAppDisabledStates() {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    this.callProviders("updateAddonAppDisabledStates");
  },

  /**
   * Notifies all providers that the repository has updated its data for
   * installed add-ons.
   */
  updateAddonRepositoryData() {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    return (async () => {
      for (let provider of this.providers) {
        await promiseCallProvider(provider, "updateAddonRepositoryData");
      }

      // only tests should care about this
      Services.obs.notifyObservers(null, "TEST:addon-repository-data-updated");
    })();
  },

  /**
   * Asynchronously gets an AddonInstall for a URL.
   *
   * @param  aUrl
   *         The string represenation of the URL where the add-on is located
   * @param  {Object} [aOptions = {}]
   *         Additional options for this install
   * @param  {string} [aOptions.hash]
   *         An optional hash of the add-on
   * @param  {string} [aOptions.name]
   *         An optional placeholder name while the add-on is being downloaded
   * @param  {string|Object} [aOptions.icons]
   *         Optional placeholder icons while the add-on is being downloaded
   * @param  {string} [aOptions.version]
   *         An optional placeholder version while the add-on is being downloaded
   * @param  {XULElement} [aOptions.browser]
   *         An optional <browser> element for download permissions prompts.
   * @param  {nsIPrincipal} [aOptions.triggeringPrincipal]
   *         The principal which is attempting to install the add-on.
   * @param  {Object} [aOptions.telemetryInfo]
   *         An optional object which provides details about the installation source
   *         included in the addon manager telemetry events.
   * @throws if aUrl is not specified or if an optional argument of
   *         an improper type is passed.
   */
  async getInstallForURL(aUrl, aOptions = {}) {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    if (!aUrl || typeof aUrl != "string") {
      throw Components.Exception(
        "aURL must be a non-empty string",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (aOptions.hash && typeof aOptions.hash != "string") {
      throw Components.Exception(
        "hash must be a string or null",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (aOptions.name && typeof aOptions.name != "string") {
      throw Components.Exception(
        "name must be a string or null",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (aOptions.icons) {
      if (typeof aOptions.icons == "string") {
        aOptions.icons = { 32: aOptions.icons };
      } else if (typeof aOptions.icons != "object") {
        throw Components.Exception(
          "icons must be a string, an object or null",
          Cr.NS_ERROR_INVALID_ARG
        );
      }
    } else {
      aOptions.icons = {};
    }

    if (aOptions.version && typeof aOptions.version != "string") {
      throw Components.Exception(
        "version must be a string or null",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (aOptions.browser && !Element.isInstance(aOptions.browser)) {
      throw Components.Exception(
        "aOptions.browser must be an Element or null",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    for (let provider of this.providers) {
      let install = await promiseCallProvider(
        provider,
        "getInstallForURL",
        aUrl,
        aOptions
      );
      if (install) {
        return install;
      }
    }

    return null;
  },

  /**
   * Asynchronously gets an AddonInstall for an nsIFile.
   *
   * @param  aFile
   *         The nsIFile where the add-on is located
   * @param  aMimetype
   *         An optional mimetype hint for the add-on
   * @param  aTelemetryInfo
   *         An optional object which provides details about the installation source
   *         included in the addon manager telemetry events.
   * @param  aUseSystemLocation
   *         If true the addon is installed into the system profile location.
   * @throws if the aFile or aCallback arguments are not specified
   */
  getInstallForFile(aFile, aMimetype, aTelemetryInfo, aUseSystemLocation) {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    if (!(aFile instanceof Ci.nsIFile)) {
      throw Components.Exception(
        "aFile must be a nsIFile",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (aMimetype && typeof aMimetype != "string") {
      throw Components.Exception(
        "aMimetype must be a string or null",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    return (async () => {
      for (let provider of this.providers) {
        let install = await promiseCallProvider(
          provider,
          "getInstallForFile",
          aFile,
          aTelemetryInfo,
          aUseSystemLocation
        );

        if (install) {
          return install;
        }
      }

      return null;
    })();
  },

  /**
   * Get a SitePermsAddonInstall instance.
   *
   * @param  {Element} aBrowser: The optional browser element that started the install
   * @param {nsIPrincipal} aInstallingPrincipal
   * @param {String} aSitePerm
   * @returns {Promise<SitePermsAddonInstall|null>} The promise will resolve with null if there
   *         are no provider with a getSitePermsAddonInstallForWebpage method. In practice,
   *         this should only be the case when SitePermsAddonProvider is not enabled,
   *         i.e. when dom.sitepermsaddon-provider.enabled is false.
   * @throws {Components.Exception} Will throw an error if:
   *         - the AddonManager is not initialized
   *         - `aInstallingPrincipal` is not a nsIPrincipal
   *         - `aInstallingPrincipal` scheme is not https
   *         - `aInstallingPrincipal` is a public etld
   *         - `aInstallingPrincipal` is a plain ip address
   *         - `aInstallingPrincipal` is in the blocklist
   *         - `aSitePerm` is not a gated permission
   *         - `aBrowser` is not null and not an element
   */
  async getSitePermsAddonInstallForWebpage(
    aBrowser,
    aInstallingPrincipal,
    aSitePerm
  ) {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    if (
      !aInstallingPrincipal ||
      !(aInstallingPrincipal instanceof Ci.nsIPrincipal)
    ) {
      throw Components.Exception(
        "aInstallingPrincipal must be a nsIPrincipal",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (aBrowser && !Element.isInstance(aBrowser)) {
      throw Components.Exception(
        "aBrowser must be an Element, or null",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (lazy.isPrincipalInSitePermissionsBlocklist(aInstallingPrincipal)) {
      throw Components.Exception(
        `SitePermsAddons can't be installed`,
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    // Block install from null principal.
    // /!\ We need to do this check before checking if this is a remote origin iframe,
    // otherwise isThirdPartyPrincipal might throw.
    if (aInstallingPrincipal.isNullPrincipal) {
      throw Components.Exception(
        `SitePermsAddons can't be installed from sandboxed subframes`,
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    // Block install from remote origin iframe
    if (
      aBrowser &&
      aBrowser.contentPrincipal.isThirdPartyPrincipal(aInstallingPrincipal)
    ) {
      throw Components.Exception(
        `SitePermsAddons can't be installed from cross origin subframes`,
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (aInstallingPrincipal.isIpAddress) {
      throw Components.Exception(
        `SitePermsAddons install disallowed when the host is an IP address`,
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    // Gated APIs should probably not be available on non-secure origins,
    // but let's double check here.
    if (aInstallingPrincipal.scheme !== "https") {
      throw Components.Exception(
        `SitePermsAddons can only be installed from secure origins`,
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    // Install origin cannot be on a known etld (e.g. github.io).
    if (lazy.isKnownPublicSuffix(aInstallingPrincipal.siteOriginNoSuffix)) {
      throw Components.Exception(
        `SitePermsAddon can't be installed from public eTLDs ${aInstallingPrincipal.siteOriginNoSuffix}`,
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (!lazy.isGatedPermissionType(aSitePerm)) {
      throw Components.Exception(
        `"${aSitePerm}" is not a gated permission`,
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    for (let provider of this.providers) {
      let install = await promiseCallProvider(
        provider,
        "getSitePermsAddonInstallForWebpage",
        aInstallingPrincipal,
        aSitePerm
      );
      if (install) {
        return install;
      }
    }

    return null;
  },

  /**
   * Uninstall an addon from the system profile location.
   *
   * @param {string} aID
   *         The ID of the addon to remove.
   * @returns A promise that resolves when the addon is uninstalled.
   */
  uninstallSystemProfileAddon(aID) {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }
    return AddonManagerInternal._getProviderByName(
      "XPIProvider"
    ).uninstallSystemProfileAddon(aID);
  },

  /**
   * Asynchronously gets all current AddonInstalls optionally limiting to a list
   * of types.
   *
   * @param  aTypes
   *         An optional array of types to retrieve. Each type is a string name
   * @throws If the aCallback argument is not specified
   */
  getInstallsByTypes(aTypes) {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    if (aTypes && !Array.isArray(aTypes)) {
      throw Components.Exception(
        "aTypes must be an array or null",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    return (async () => {
      let installs = [];

      for (let provider of this.providers) {
        let providerInstalls = await promiseCallProvider(
          provider,
          "getInstallsByTypes",
          aTypes
        );

        if (providerInstalls) {
          installs.push(...providerInstalls);
        }
      }

      return installs;
    })();
  },

  /**
   * Asynchronously gets all current AddonInstalls.
   */
  getAllInstalls() {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    return this.getInstallsByTypes(null);
  },

  /**
   * Checks whether installation is enabled for a particular mimetype.
   *
   * @param  aMimetype
   *         The mimetype to check
   * @return true if installation is enabled for the mimetype
   */
  isInstallEnabled(aMimetype) {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    if (!aMimetype || typeof aMimetype != "string") {
      throw Components.Exception(
        "aMimetype must be a non-empty string",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    let providers = [...this.providers];
    for (let provider of providers) {
      if (
        callProvider(provider, "supportsMimetype", false, aMimetype) &&
        callProvider(provider, "isInstallEnabled")
      ) {
        return true;
      }
    }
    return false;
  },

  /**
   * Checks whether a particular source is allowed to install add-ons of a
   * given mimetype.
   *
   * @param  aMimetype
   *         The mimetype of the add-on
   * @param  aInstallingPrincipal
   *         The nsIPrincipal that initiated the install
   * @return true if the source is allowed to install this mimetype
   */
  isInstallAllowed(aMimetype, aInstallingPrincipal) {
    if (!gStarted) {
      throw Components.Exception(
        "AddonManager is not initialized",
        Cr.NS_ERROR_NOT_INITIALIZED
      );
    }

    if (!aMimetype || typeof aMimetype != "string") {
      throw Components.Exception(
        "aMimetype must be a non-empty string",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (
      !aInstallingPrincipal ||
      !(aInstallingPrincipal instanceof Ci.nsIPrincipal)
    ) {
      throw Components.Exception(
        "aInstallingPrincipal must be a nsIPrincipal",
        Cr.NS_ERROR_INVALID_ARG
      );
    }

    if (
      this.isInstallAllowedByPolicy(
        aInstallingPrincipal,
        null,
        true /* explicit */
      )
    ) {
      return true;
    }

    let providers = [...this.providers];
    for (let provider of providers) {
      if (
        callProvider(provider, "supportsMimetype", false, aMimetype) &&
        callProvider(provider, "isInstallAllowed", null, aInstallingPrincipal)
      ) {
        return true;
      }
    }
    return false;
  },

  /**
   * Checks whether a particular source is allowed to install add-ons based
   * on policy.
   *
   * @param  aInstallingPrincipal
   *         The nsIPrincipal that initiated the install
   * @param  aInstall
   *         The AddonInstall to be installed
   * @param  explicit
   *         If this is set, we only return true if the source is explicitly
   *         blocked via policy.
   *
   * @return boolean
   *         By default, returns true if the source is blocked by policy
   *         or there is no policy.
   *         If explicit is set, only returns true of the source is
   *         blocked by policy, false otherwise. This is needed for
   *         handling inverse cases.
   */
  isInstallAllowedByPolicy(aInstallingPrincipal, aInstall, explicit) {
    if (Services.policies) {
      let extensionSettings = Services.policies.getExtensionSettings("*");
      if (extensionSettings && extensionSettings.install_sources) {
        if (
          (!aInstall ||
            Services.policies.allowedInstallSource(aInstall.sourceURI)) &&
          (!aInstallingPrincipal ||
            !aInstallingPrincipal.URI ||
            Services.policies.allowedInstallSource(aInstallingPrincipal.URI))
        ) {
          return true;
        }
        return false;
      }
    }
    return !explicit;
  },

  installNotifyObservers(
    aTopic,
    aBrowser,
    aUri,
    aInstall,
    aInstallFn,
    aCancelFn
  ) {
    let info = {
      wrappedJSObject: {
        browser: aBrowser,
        originatingURI: aUri,
        installs: [aInstall],
        install: aInstallFn,
        cancel: aCancelFn,
      },
    };
    Services.obs.notifyObservers(info, aTopic);
  },

  startInstall(browser, url, install) {
    this.installNotifyObservers("addon-install-started", browser, url, install);

    // Local installs may already be in a failed state in which case
    // we won't get any further events, detect those cases now.
    if (
      install.state == AddonManager.STATE_DOWNLOADED &&
      (install.addon.appDisabled ||
        install.addon.blocklistState !=
          Ci.nsIBlocklistService.STATE_NOT_BLOCKED)
    ) {
      install.cancel();
      this.installNotifyObservers(
        "addon-install-failed",
        browser,
        url,
        install
      );
      return;
    }

    let self = this;
    let listener = {
      onDownloadCancelled() {
        install.removeListener(listener);
      },

      onDownloadFailed() {
--> --------------------

--> maximum size reached

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

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

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge