1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5"use strict";
6
7// Cannot use Services.appinfo here, or else xpcshell-tests will blow up, as
8// most tests later register different nsIAppInfo implementations, which
9// wouldn't be reflected in Services.appinfo anymore, as the lazy getter
10// underlying it would have been initialized if we used it here.
11if ("@mozilla.org/xre/app-info;1" in Cc) {
12  // eslint-disable-next-line mozilla/use-services
13  let runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
14  if (runtime.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
15    // Refuse to run in child processes.
16    throw new Error("You cannot use the AddonManager in child processes!");
17  }
18}
19
20const { AppConstants } = ChromeUtils.import(
21  "resource://gre/modules/AppConstants.jsm"
22);
23
24const MOZ_COMPATIBILITY_NIGHTLY = ![
25  "aurora",
26  "beta",
27  "release",
28  "esr",
29].includes(AppConstants.MOZ_UPDATE_CHANNEL);
30
31const INTL_LOCALES_CHANGED = "intl:app-locales-changed";
32
33const PREF_AMO_ABUSEREPORT = "extensions.abuseReport.amWebAPI.enabled";
34const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion";
35const PREF_EM_UPDATE_ENABLED = "extensions.update.enabled";
36const PREF_EM_LAST_APP_VERSION = "extensions.lastAppVersion";
37const PREF_EM_LAST_PLATFORM_VERSION = "extensions.lastPlatformVersion";
38const PREF_EM_AUTOUPDATE_DEFAULT = "extensions.update.autoUpdateDefault";
39const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility";
40const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
41const PREF_SYS_ADDON_UPDATE_ENABLED = "extensions.systemAddon.update.enabled";
42
43const PREF_MIN_WEBEXT_PLATFORM_VERSION =
44  "extensions.webExtensionsMinPlatformVersion";
45const PREF_WEBAPI_TESTING = "extensions.webapi.testing";
46const PREF_EM_POSTDOWNLOAD_THIRD_PARTY =
47  "extensions.postDownloadThirdPartyPrompt";
48
49const UPDATE_REQUEST_VERSION = 2;
50
51const BRANCH_REGEXP = /^([^\.]+\.[0-9]+[a-z]*).*/gi;
52const PREF_EM_CHECK_COMPATIBILITY_BASE = "extensions.checkCompatibility";
53var PREF_EM_CHECK_COMPATIBILITY = MOZ_COMPATIBILITY_NIGHTLY
54  ? PREF_EM_CHECK_COMPATIBILITY_BASE + ".nightly"
55  : undefined;
56
57const WEBAPI_INSTALL_HOSTS = ["addons.mozilla.org"];
58const WEBAPI_TEST_INSTALL_HOSTS = [
59  "addons.allizom.org",
60  "addons-dev.allizom.org",
61  "example.com",
62];
63
64const AMO_ATTRIBUTION_ALLOWED_SOURCES = ["amo", "disco"];
65const AMO_ATTRIBUTION_DATA_KEYS = [
66  "utm_campaign",
67  "utm_content",
68  "utm_medium",
69  "utm_source",
70];
71const AMO_ATTRIBUTION_DATA_MAX_LENGTH = 40;
72
73const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
74const { XPCOMUtils } = ChromeUtils.import(
75  "resource://gre/modules/XPCOMUtils.jsm"
76);
77// This global is overridden by xpcshell tests, and therefore cannot be
78// a const.
79var { AsyncShutdown } = ChromeUtils.import(
80  "resource://gre/modules/AsyncShutdown.jsm"
81);
82const { PromiseUtils } = ChromeUtils.import(
83  "resource://gre/modules/PromiseUtils.jsm"
84);
85
86XPCOMUtils.defineLazyGlobalGetters(this, ["Element"]);
87
88XPCOMUtils.defineLazyModuleGetters(this, {
89  AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm",
90  AbuseReporter: "resource://gre/modules/AbuseReporter.jsm",
91  Extension: "resource://gre/modules/Extension.jsm",
92});
93
94XPCOMUtils.defineLazyPreferenceGetter(
95  this,
96  "WEBEXT_POSTDOWNLOAD_THIRD_PARTY",
97  PREF_EM_POSTDOWNLOAD_THIRD_PARTY,
98  false
99);
100
101// Initialize the WebExtension process script service as early as possible,
102// since it needs to be able to track things like new frameLoader globals that
103// are created before other framework code has been initialized.
104Services.ppmm.loadProcessScript(
105  "resource://gre/modules/extensionProcessScriptLoader.js",
106  true
107);
108
109const INTEGER = /^[1-9]\d*$/;
110
111var EXPORTED_SYMBOLS = ["AddonManager", "AddonManagerPrivate", "AMTelemetry"];
112
113const CATEGORY_PROVIDER_MODULE = "addon-provider-module";
114
115// A list of providers to load by default
116const DEFAULT_PROVIDERS = ["resource://gre/modules/addons/XPIProvider.jsm"];
117
118const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
119// Configure a logger at the parent 'addons' level to format
120// messages for all the modules under addons.*
121const PARENT_LOGGER_ID = "addons";
122var parentLogger = Log.repository.getLogger(PARENT_LOGGER_ID);
123parentLogger.level = Log.Level.Warn;
124var formatter = new Log.BasicFormatter();
125// Set parent logger (and its children) to append to
126// the Javascript section of the Browser Console
127parentLogger.addAppender(new Log.ConsoleAppender(formatter));
128
129// Create a new logger (child of 'addons' logger)
130// for use by the Addons Manager
131const LOGGER_ID = "addons.manager";
132var logger = Log.repository.getLogger(LOGGER_ID);
133
134// Provide the ability to enable/disable logging
135// messages at runtime.
136// If the "extensions.logging.enabled" preference is
137// missing or 'false', messages at the WARNING and higher
138// severity should be logged to the JS console and standard error.
139// If "extensions.logging.enabled" is set to 'true', messages
140// at DEBUG and higher should go to JS console and standard error.
141const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
142const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
143
144const UNNAMED_PROVIDER = "<unnamed-provider>";
145function providerName(aProvider) {
146  return aProvider.name || UNNAMED_PROVIDER;
147}
148
149/**
150 * Preference listener which listens for a change in the
151 * "extensions.logging.enabled" preference and changes the logging level of the
152 * parent 'addons' level logger accordingly.
153 */
154var PrefObserver = {
155  init() {
156    Services.prefs.addObserver(PREF_LOGGING_ENABLED, this);
157    Services.obs.addObserver(this, "xpcom-shutdown");
158    this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED);
159  },
160
161  observe(aSubject, aTopic, aData) {
162    if (aTopic == "xpcom-shutdown") {
163      Services.prefs.removeObserver(PREF_LOGGING_ENABLED, this);
164      Services.obs.removeObserver(this, "xpcom-shutdown");
165    } else if (aTopic == NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) {
166      let debugLogEnabled = Services.prefs.getBoolPref(
167        PREF_LOGGING_ENABLED,
168        false
169      );
170      if (debugLogEnabled) {
171        parentLogger.level = Log.Level.Debug;
172      } else {
173        parentLogger.level = Log.Level.Warn;
174      }
175    }
176  },
177};
178
179PrefObserver.init();
180
181/**
182 * Calls a callback method consuming any thrown exception. Any parameters after
183 * the callback parameter will be passed to the callback.
184 *
185 * @param  aCallback
186 *         The callback method to call
187 */
188function safeCall(aCallback, ...aArgs) {
189  try {
190    aCallback.apply(null, aArgs);
191  } catch (e) {
192    logger.warn("Exception calling callback", e);
193  }
194}
195
196/**
197 * Report an exception thrown by a provider API method.
198 */
199function reportProviderError(aProvider, aMethod, aError) {
200  let method = `provider ${providerName(aProvider)}.${aMethod}`;
201  AddonManagerPrivate.recordException("AMI", method, aError);
202  logger.error("Exception calling " + method, aError);
203}
204
205/**
206 * Calls a method on a provider if it exists and consumes any thrown exception.
207 * Any parameters after the aDefault parameter are passed to the provider's method.
208 *
209 * @param  aProvider
210 *         The provider to call
211 * @param  aMethod
212 *         The method name to call
213 * @param  aDefault
214 *         A default return value if the provider does not implement the named
215 *         method or throws an error.
216 * @return the return value from the provider, or aDefault if the provider does not
217 *         implement method or throws an error
218 */
219function callProvider(aProvider, aMethod, aDefault, ...aArgs) {
220  if (!(aMethod in aProvider)) {
221    return aDefault;
222  }
223
224  try {
225    return aProvider[aMethod].apply(aProvider, aArgs);
226  } catch (e) {
227    reportProviderError(aProvider, aMethod, e);
228    return aDefault;
229  }
230}
231
232/**
233 * Calls a method on a provider if it exists and consumes any thrown exception.
234 * Parameters after aMethod are passed to aProvider.aMethod().
235 * If the provider does not implement the method, or the method throws, calls
236 * the callback with 'undefined'.
237 *
238 * @param  aProvider
239 *         The provider to call
240 * @param  aMethod
241 *         The method name to call
242 */
243async function promiseCallProvider(aProvider, aMethod, ...aArgs) {
244  if (!(aMethod in aProvider)) {
245    return undefined;
246  }
247  try {
248    return aProvider[aMethod].apply(aProvider, aArgs);
249  } catch (e) {
250    reportProviderError(aProvider, aMethod, e);
251    return undefined;
252  }
253}
254
255/**
256 * Gets the currently selected locale for display.
257 * @return  the selected locale or "en-US" if none is selected
258 */
259function getLocale() {
260  return Services.locale.requestedLocale || "en-US";
261}
262
263const WEB_EXPOSED_ADDON_PROPERTIES = [
264  "id",
265  "version",
266  "type",
267  "name",
268  "description",
269  "isActive",
270];
271
272function webAPIForAddon(addon) {
273  if (!addon) {
274    return null;
275  }
276
277  // These web-exposed Addon properties (see AddonManager.webidl)
278  // just come directly from an Addon object.
279  let result = {};
280  for (let prop of WEB_EXPOSED_ADDON_PROPERTIES) {
281    result[prop] = addon[prop];
282  }
283
284  // These properties are computed.
285  result.isEnabled = !addon.userDisabled;
286  result.canUninstall = Boolean(
287    addon.permissions & AddonManager.PERM_CAN_UNINSTALL
288  );
289
290  return result;
291}
292
293/**
294 * Listens for a browser changing origin and cancels the installs that were
295 * started by it.
296 */
297function BrowserListener(aBrowser, aInstallingPrincipal, aInstall) {
298  this.browser = aBrowser;
299  this.messageManager = this.browser.messageManager;
300  this.principal = aInstallingPrincipal;
301  this.install = aInstall;
302
303  aBrowser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
304  Services.obs.addObserver(this, "message-manager-close", true);
305
306  aInstall.addListener(this);
307
308  this.registered = true;
309}
310
311BrowserListener.prototype = {
312  browser: null,
313  install: null,
314  registered: false,
315
316  unregister() {
317    if (!this.registered) {
318      return;
319    }
320    this.registered = false;
321
322    Services.obs.removeObserver(this, "message-manager-close");
323    // The browser may have already been detached
324    if (this.browser.removeProgressListener) {
325      this.browser.removeProgressListener(this);
326    }
327
328    this.install.removeListener(this);
329    this.install = null;
330  },
331
332  cancelInstall() {
333    try {
334      this.install.cancel();
335    } catch (e) {
336      // install may have already failed or been cancelled, ignore these
337    }
338  },
339
340  observe(subject, topic, data) {
341    if (subject != this.messageManager) {
342      return;
343    }
344
345    // The browser's message manager has closed and so the browser is
346    // going away, cancel the install
347    this.cancelInstall();
348  },
349
350  onLocationChange(webProgress, request, location) {
351    if (
352      this.browser.contentPrincipal &&
353      this.principal.subsumes(this.browser.contentPrincipal)
354    ) {
355      return;
356    }
357
358    // The browser has navigated to a new origin so cancel the install
359    this.cancelInstall();
360  },
361
362  onDownloadCancelled(install) {
363    this.unregister();
364  },
365
366  onDownloadFailed(install) {
367    this.unregister();
368  },
369
370  onInstallFailed(install) {
371    this.unregister();
372  },
373
374  onInstallEnded(install) {
375    this.unregister();
376  },
377
378  QueryInterface: ChromeUtils.generateQI([
379    "nsISupportsWeakReference",
380    "nsIWebProgressListener",
381    "nsIObserver",
382  ]),
383};
384
385/**
386 * This represents an author of an add-on (e.g. creator or developer)
387 *
388 * @param  aName
389 *         The name of the author
390 * @param  aURL
391 *         The URL of the author's profile page
392 */
393function AddonAuthor(aName, aURL) {
394  this.name = aName;
395  this.url = aURL;
396}
397
398AddonAuthor.prototype = {
399  name: null,
400  url: null,
401
402  // Returns the author's name, defaulting to the empty string
403  toString() {
404    return this.name || "";
405  },
406};
407
408/**
409 * This represents an screenshot for an add-on
410 *
411 * @param  aURL
412 *         The URL to the full version of the screenshot
413 * @param  aWidth
414 *         The width in pixels of the screenshot
415 * @param  aHeight
416 *         The height in pixels of the screenshot
417 * @param  aThumbnailURL
418 *         The URL to the thumbnail version of the screenshot
419 * @param  aThumbnailWidth
420 *         The width in pixels of the thumbnail version of the screenshot
421 * @param  aThumbnailHeight
422 *         The height in pixels of the thumbnail version of the screenshot
423 * @param  aCaption
424 *         The caption of the screenshot
425 */
426function AddonScreenshot(
427  aURL,
428  aWidth,
429  aHeight,
430  aThumbnailURL,
431  aThumbnailWidth,
432  aThumbnailHeight,
433  aCaption
434) {
435  this.url = aURL;
436  if (aWidth) {
437    this.width = aWidth;
438  }
439  if (aHeight) {
440    this.height = aHeight;
441  }
442  if (aThumbnailURL) {
443    this.thumbnailURL = aThumbnailURL;
444  }
445  if (aThumbnailWidth) {
446    this.thumbnailWidth = aThumbnailWidth;
447  }
448  if (aThumbnailHeight) {
449    this.thumbnailHeight = aThumbnailHeight;
450  }
451  if (aCaption) {
452    this.caption = aCaption;
453  }
454}
455
456AddonScreenshot.prototype = {
457  url: null,
458  width: null,
459  height: null,
460  thumbnailURL: null,
461  thumbnailWidth: null,
462  thumbnailHeight: null,
463  caption: null,
464
465  // Returns the screenshot URL, defaulting to the empty string
466  toString() {
467    return this.url || "";
468  },
469};
470
471var gStarted = false;
472var gStartedPromise = PromiseUtils.defer();
473var gStartupComplete = false;
474var gCheckCompatibility = true;
475var gStrictCompatibility = true;
476var gCheckUpdateSecurityDefault = true;
477var gCheckUpdateSecurity = gCheckUpdateSecurityDefault;
478var gUpdateEnabled = true;
479var gAutoUpdateDefault = true;
480var gWebExtensionsMinPlatformVersion = "";
481var gFinalShutdownBarrier = null;
482var gBeforeShutdownBarrier = null;
483var gRepoShutdownState = "";
484var gShutdownInProgress = false;
485var gBrowserUpdated = null;
486
487var AMTelemetry;
488
489/**
490 * This is the real manager, kept here rather than in AddonManager to keep its
491 * contents hidden from API users.
492 * @class
493 * @lends AddonManager
494 */
495var AddonManagerInternal = {
496  managerListeners: new Set(),
497  installListeners: new Set(),
498  addonListeners: new Set(),
499  pendingProviders: new Set(),
500  providers: new Set(),
501  providerShutdowns: new Map(),
502  typesByProvider: new Map(),
503  startupChanges: {},
504  // Store telemetry details per addon provider
505  telemetryDetails: {},
506  upgradeListeners: new Map(),
507  externalExtensionLoaders: new Map(),
508
509  recordTimestamp(name, value) {
510    this.TelemetryTimestamps.add(name, value);
511  },
512
513  /**
514   * Start up a provider, and register its shutdown hook if it has one
515   *
516   * @param {string} aProvider - An add-on provider.
517   * @param {boolean} aAppChanged - Whether or not the app version has changed since last session.
518   * @param {string} aOldAppVersion - Previous application version, if changed.
519   * @param {string} aOldPlatformVersion - Previous platform version, if changed.
520   *
521   * @private
522   */
523  _startProvider(aProvider, aAppChanged, aOldAppVersion, aOldPlatformVersion) {
524    if (!gStarted) {
525      throw Components.Exception(
526        "AddonManager is not initialized",
527        Cr.NS_ERROR_NOT_INITIALIZED
528      );
529    }
530
531    logger.debug(`Starting provider: ${providerName(aProvider)}`);
532    callProvider(
533      aProvider,
534      "startup",
535      null,
536      aAppChanged,
537      aOldAppVersion,
538      aOldPlatformVersion
539    );
540    if ("shutdown" in aProvider) {
541      let name = providerName(aProvider);
542      let AMProviderShutdown = () => {
543        // If the provider has been unregistered, it will have been removed from
544        // this.providers. If it hasn't been unregistered, then this is a normal
545        // shutdown - and we move it to this.pendingProviders in case we're
546        // running in a test that will start AddonManager again.
547        if (this.providers.has(aProvider)) {
548          this.providers.delete(aProvider);
549          this.pendingProviders.add(aProvider);
550        }
551
552        return new Promise((resolve, reject) => {
553          logger.debug("Calling shutdown blocker for " + name);
554          resolve(aProvider.shutdown());
555        }).catch(err => {
556          logger.warn("Failure during shutdown of " + name, err);
557          AddonManagerPrivate.recordException(
558            "AMI",
559            "Async shutdown of " + name,
560            err
561          );
562        });
563      };
564      logger.debug("Registering shutdown blocker for " + name);
565      this.providerShutdowns.set(aProvider, AMProviderShutdown);
566      AddonManagerPrivate.finalShutdown.addBlocker(name, AMProviderShutdown);
567    }
568
569    this.pendingProviders.delete(aProvider);
570    this.providers.add(aProvider);
571    logger.debug(`Provider finished startup: ${providerName(aProvider)}`);
572  },
573
574  _getProviderByName(aName) {
575    for (let provider of this.providers) {
576      if (providerName(provider) == aName) {
577        return provider;
578      }
579    }
580    return undefined;
581  },
582
583  /**
584   * Initializes the AddonManager, loading any known providers and initializing
585   * them.
586   */
587  startup() {
588    try {
589      if (gStarted) {
590        return;
591      }
592
593      this.recordTimestamp("AMI_startup_begin");
594
595      // Enable the addonsManager telemetry event category.
596      AMTelemetry.init();
597
598      // clear this for xpcshell test restarts
599      for (let provider in this.telemetryDetails) {
600        delete this.telemetryDetails[provider];
601      }
602
603      let appChanged = undefined;
604
605      let oldAppVersion = null;
606      try {
607        oldAppVersion = Services.prefs.getCharPref(PREF_EM_LAST_APP_VERSION);
608        appChanged = Services.appinfo.version != oldAppVersion;
609      } catch (e) {}
610
611      gBrowserUpdated = appChanged;
612
613      let oldPlatformVersion = Services.prefs.getCharPref(
614        PREF_EM_LAST_PLATFORM_VERSION,
615        ""
616      );
617
618      if (appChanged !== false) {
619        logger.debug("Application has been upgraded");
620        Services.prefs.setCharPref(
621          PREF_EM_LAST_APP_VERSION,
622          Services.appinfo.version
623        );
624        Services.prefs.setCharPref(
625          PREF_EM_LAST_PLATFORM_VERSION,
626          Services.appinfo.platformVersion
627        );
628        Services.prefs.setIntPref(
629          PREF_BLOCKLIST_PINGCOUNTVERSION,
630          appChanged === undefined ? 0 : -1
631        );
632      }
633
634      if (!MOZ_COMPATIBILITY_NIGHTLY) {
635        PREF_EM_CHECK_COMPATIBILITY =
636          PREF_EM_CHECK_COMPATIBILITY_BASE +
637          "." +
638          Services.appinfo.version.replace(BRANCH_REGEXP, "$1");
639      }
640
641      gCheckCompatibility = Services.prefs.getBoolPref(
642        PREF_EM_CHECK_COMPATIBILITY,
643        gCheckCompatibility
644      );
645      Services.prefs.addObserver(PREF_EM_CHECK_COMPATIBILITY, this);
646
647      gStrictCompatibility = Services.prefs.getBoolPref(
648        PREF_EM_STRICT_COMPATIBILITY,
649        gStrictCompatibility
650      );
651      Services.prefs.addObserver(PREF_EM_STRICT_COMPATIBILITY, this);
652
653      let defaultBranch = Services.prefs.getDefaultBranch("");
654      gCheckUpdateSecurityDefault = defaultBranch.getBoolPref(
655        PREF_EM_CHECK_UPDATE_SECURITY,
656        gCheckUpdateSecurityDefault
657      );
658
659      gCheckUpdateSecurity = Services.prefs.getBoolPref(
660        PREF_EM_CHECK_UPDATE_SECURITY,
661        gCheckUpdateSecurity
662      );
663      Services.prefs.addObserver(PREF_EM_CHECK_UPDATE_SECURITY, this);
664
665      gUpdateEnabled = Services.prefs.getBoolPref(
666        PREF_EM_UPDATE_ENABLED,
667        gUpdateEnabled
668      );
669      Services.prefs.addObserver(PREF_EM_UPDATE_ENABLED, this);
670
671      gAutoUpdateDefault = Services.prefs.getBoolPref(
672        PREF_EM_AUTOUPDATE_DEFAULT,
673        gAutoUpdateDefault
674      );
675      Services.prefs.addObserver(PREF_EM_AUTOUPDATE_DEFAULT, this);
676
677      gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(
678        PREF_MIN_WEBEXT_PLATFORM_VERSION,
679        gWebExtensionsMinPlatformVersion
680      );
681      Services.prefs.addObserver(PREF_MIN_WEBEXT_PLATFORM_VERSION, this);
682
683      // Watch for language changes, refresh the addon cache when it changes.
684      Services.obs.addObserver(this, INTL_LOCALES_CHANGED);
685
686      // Ensure all default providers have had a chance to register themselves
687      for (let url of DEFAULT_PROVIDERS) {
688        try {
689          let scope = {};
690          ChromeUtils.import(url, scope);
691          // Sanity check - make sure the provider exports a symbol that
692          // has a 'startup' method
693          let syms = Object.keys(scope);
694          if (syms.length < 1 || typeof scope[syms[0]].startup != "function") {
695            logger.warn("Provider " + url + " has no startup()");
696            AddonManagerPrivate.recordException(
697              "AMI",
698              "provider " + url,
699              "no startup()"
700            );
701          }
702          logger.debug(
703            "Loaded provider scope for " +
704              url +
705              ": " +
706              Object.keys(scope).toSource()
707          );
708        } catch (e) {
709          AddonManagerPrivate.recordException(
710            "AMI",
711            "provider " + url + " load failed",
712            e
713          );
714          logger.error('Exception loading default provider "' + url + '"', e);
715        }
716      }
717
718      // Load any providers registered in the category manager
719      for (let { entry, value: url } of Services.catMan.enumerateCategory(
720        CATEGORY_PROVIDER_MODULE
721      )) {
722        try {
723          ChromeUtils.import(url, {});
724          logger.debug(`Loaded provider scope for ${url}`);
725        } catch (e) {
726          AddonManagerPrivate.recordException(
727            "AMI",
728            "provider " + url + " load failed",
729            e
730          );
731          logger.error(
732            "Exception loading provider " +
733              entry +
734              ' from category "' +
735              url +
736              '"',
737            e
738          );
739        }
740      }
741
742      // Register our shutdown handler with the AsyncShutdown manager
743      gBeforeShutdownBarrier = new AsyncShutdown.Barrier(
744        "AddonManager: Waiting to start provider shutdown."
745      );
746      gFinalShutdownBarrier = new AsyncShutdown.Barrier(
747        "AddonManager: Waiting for providers to shut down."
748      );
749      AsyncShutdown.profileBeforeChange.addBlocker(
750        "AddonManager: shutting down.",
751        this.shutdownManager.bind(this),
752        { fetchState: this.shutdownState.bind(this) }
753      );
754
755      // Once we start calling providers we must allow all normal methods to work.
756      gStarted = true;
757
758      for (let provider of this.pendingProviders) {
759        this._startProvider(
760          provider,
761          appChanged,
762          oldAppVersion,
763          oldPlatformVersion
764        );
765      }
766
767      // If this is a new profile just pretend that there were no changes
768      if (appChanged === undefined) {
769        for (let type in this.startupChanges) {
770          delete this.startupChanges[type];
771        }
772      }
773
774      gStartupComplete = true;
775      gStartedPromise.resolve();
776      this.recordTimestamp("AMI_startup_end");
777    } catch (e) {
778      logger.error("startup failed", e);
779      AddonManagerPrivate.recordException("AMI", "startup failed", e);
780      gStartedPromise.reject("startup failed");
781    }
782
783    logger.debug("Completed startup sequence");
784    this.callManagerListeners("onStartup");
785  },
786
787  /**
788   * Registers a new AddonProvider.
789   *
790   * @param {string} aProvider -The provider to register
791   * @param {string[]} [aTypes] - An optional array of add-on types
792   */
793  registerProvider(aProvider, aTypes) {
794    if (!aProvider || typeof aProvider != "object") {
795      throw Components.Exception(
796        "aProvider must be specified",
797        Cr.NS_ERROR_INVALID_ARG
798      );
799    }
800
801    if (aTypes && !Array.isArray(aTypes)) {
802      throw Components.Exception(
803        "aTypes must be an array or null",
804        Cr.NS_ERROR_INVALID_ARG
805      );
806    }
807
808    this.pendingProviders.add(aProvider);
809
810    if (aTypes) {
811      this.typesByProvider.set(aProvider, new Set(aTypes));
812    }
813
814    // If we're registering after startup call this provider's startup.
815    if (gStarted) {
816      this._startProvider(aProvider);
817    }
818  },
819
820  /**
821   * Unregisters an AddonProvider.
822   *
823   * @param  aProvider
824   *         The provider to unregister
825   * @return Whatever the provider's 'shutdown' method returns (if anything).
826   *         For providers that have async shutdown methods returning Promises,
827   *         the caller should wait for that Promise to resolve.
828   */
829  unregisterProvider(aProvider) {
830    if (!aProvider || typeof aProvider != "object") {
831      throw Components.Exception(
832        "aProvider must be specified",
833        Cr.NS_ERROR_INVALID_ARG
834      );
835    }
836
837    this.providers.delete(aProvider);
838    // The test harness will unregister XPIProvider *after* shutdown, which is
839    // after the provider will have been moved from providers to
840    // pendingProviders.
841    this.pendingProviders.delete(aProvider);
842
843    this.typesByProvider.delete(aProvider);
844
845    // If we're unregistering after startup but before shutting down,
846    // remove the blocker for this provider's shutdown and call it.
847    // If we're already shutting down, just let gFinalShutdownBarrier
848    // call it to avoid races.
849    if (gStarted && !gShutdownInProgress) {
850      logger.debug(
851        "Unregistering shutdown blocker for " + providerName(aProvider)
852      );
853      let shutter = this.providerShutdowns.get(aProvider);
854      if (shutter) {
855        this.providerShutdowns.delete(aProvider);
856        gFinalShutdownBarrier.client.removeBlocker(shutter);
857        return shutter();
858      }
859    }
860    return undefined;
861  },
862
863  /**
864   * Mark a provider as safe to access via AddonManager APIs, before its
865   * startup has completed.
866   *
867   * Normally a provider isn't marked as safe until after its (synchronous)
868   * startup() method has returned. Until a provider has been marked safe,
869   * it won't be used by any of the AddonManager APIs. markProviderSafe()
870   * allows a provider to mark itself as safe during its startup; this can be
871   * useful if the provider wants to perform tasks that block startup, which
872   * happen after its required initialization tasks and therefore when the
873   * provider is in a safe state.
874   *
875   * @param aProvider Provider object to mark safe
876   */
877  markProviderSafe(aProvider) {
878    if (!gStarted) {
879      throw Components.Exception(
880        "AddonManager is not initialized",
881        Cr.NS_ERROR_NOT_INITIALIZED
882      );
883    }
884
885    if (!aProvider || typeof aProvider != "object") {
886      throw Components.Exception(
887        "aProvider must be specified",
888        Cr.NS_ERROR_INVALID_ARG
889      );
890    }
891
892    if (!this.pendingProviders.has(aProvider)) {
893      return;
894    }
895
896    this.pendingProviders.delete(aProvider);
897    this.providers.add(aProvider);
898  },
899
900  /**
901   * Calls a method on all registered providers if it exists and consumes any
902   * thrown exception. Return values are ignored. Any parameters after the
903   * method parameter are passed to the provider's method.
904   * WARNING: Do not use for asynchronous calls; callProviders() does not
905   * invoke callbacks if provider methods throw synchronous exceptions.
906   *
907   * @param  aMethod
908   *         The method name to call
909   */
910  callProviders(aMethod, ...aArgs) {
911    if (!aMethod || typeof aMethod != "string") {
912      throw Components.Exception(
913        "aMethod must be a non-empty string",
914        Cr.NS_ERROR_INVALID_ARG
915      );
916    }
917
918    let providers = [...this.providers];
919    for (let provider of providers) {
920      try {
921        if (aMethod in provider) {
922          provider[aMethod].apply(provider, aArgs);
923        }
924      } catch (e) {
925        reportProviderError(provider, aMethod, e);
926      }
927    }
928  },
929
930  /**
931   * Report the current state of asynchronous shutdown
932   */
933  shutdownState() {
934    let state = [];
935    for (let barrier of [gBeforeShutdownBarrier, gFinalShutdownBarrier]) {
936      if (barrier) {
937        state.push({ name: barrier.client.name, state: barrier.state });
938      }
939    }
940    state.push({
941      name: "AddonRepository: async shutdown",
942      state: gRepoShutdownState,
943    });
944    return state;
945  },
946
947  /**
948   * Shuts down the addon manager and all registered providers, this must clean
949   * up everything in order for automated tests to fake restarts.
950   * @return Promise{null} that resolves when all providers and dependent modules
951   *                       have finished shutting down
952   */
953  async shutdownManager() {
954    logger.debug("before shutdown");
955    try {
956      await gBeforeShutdownBarrier.wait();
957    } catch (e) {
958      Cu.reportError(e);
959    }
960
961    logger.debug("shutdown");
962    this.callManagerListeners("onShutdown");
963
964    if (!gStartupComplete) {
965      gStartedPromise.reject("shutting down");
966    }
967
968    gRepoShutdownState = "pending";
969    gShutdownInProgress = true;
970    // Clean up listeners
971    Services.prefs.removeObserver(PREF_EM_CHECK_COMPATIBILITY, this);
972    Services.prefs.removeObserver(PREF_EM_STRICT_COMPATIBILITY, this);
973    Services.prefs.removeObserver(PREF_EM_CHECK_UPDATE_SECURITY, this);
974    Services.prefs.removeObserver(PREF_EM_UPDATE_ENABLED, this);
975    Services.prefs.removeObserver(PREF_EM_AUTOUPDATE_DEFAULT, this);
976
977    Services.obs.removeObserver(this, INTL_LOCALES_CHANGED);
978
979    let savedError = null;
980    // Only shut down providers if they've been started.
981    if (gStarted) {
982      try {
983        await gFinalShutdownBarrier.wait();
984      } catch (err) {
985        savedError = err;
986        logger.error("Failure during wait for shutdown barrier", err);
987        AddonManagerPrivate.recordException(
988          "AMI",
989          "Async shutdown of AddonManager providers",
990          err
991        );
992      }
993    }
994
995    // Shut down AddonRepository after providers (if any).
996    try {
997      gRepoShutdownState = "in progress";
998      await AddonRepository.shutdown();
999      gRepoShutdownState = "done";
1000    } catch (err) {
1001      savedError = err;
1002      logger.error("Failure during AddonRepository shutdown", err);
1003      AddonManagerPrivate.recordException(
1004        "AMI",
1005        "Async shutdown of AddonRepository",
1006        err
1007      );
1008    }
1009
1010    logger.debug("Async provider shutdown done");
1011    this.managerListeners.clear();
1012    this.installListeners.clear();
1013    this.addonListeners.clear();
1014    this.providerShutdowns.clear();
1015    for (let type in this.startupChanges) {
1016      delete this.startupChanges[type];
1017    }
1018    gStarted = false;
1019    gStartedPromise = PromiseUtils.defer();
1020    gStartupComplete = false;
1021    gFinalShutdownBarrier = null;
1022    gBeforeShutdownBarrier = null;
1023    gShutdownInProgress = false;
1024    if (savedError) {
1025      throw savedError;
1026    }
1027  },
1028
1029  /**
1030   * Notified when a preference we're interested in has changed.
1031   */
1032  observe(aSubject, aTopic, aData) {
1033    switch (aTopic) {
1034      case INTL_LOCALES_CHANGED: {
1035        // Asynchronously fetch and update the addons cache.
1036        AddonRepository.backgroundUpdateCheck();
1037        return;
1038      }
1039    }
1040
1041    switch (aData) {
1042      case PREF_EM_CHECK_COMPATIBILITY: {
1043        let oldValue = gCheckCompatibility;
1044        gCheckCompatibility = Services.prefs.getBoolPref(
1045          PREF_EM_CHECK_COMPATIBILITY,
1046          true
1047        );
1048
1049        this.callManagerListeners("onCompatibilityModeChanged");
1050
1051        if (gCheckCompatibility != oldValue) {
1052          this.updateAddonAppDisabledStates();
1053        }
1054
1055        break;
1056      }
1057      case PREF_EM_STRICT_COMPATIBILITY: {
1058        let oldValue = gStrictCompatibility;
1059        gStrictCompatibility = Services.prefs.getBoolPref(
1060          PREF_EM_STRICT_COMPATIBILITY,
1061          true
1062        );
1063
1064        this.callManagerListeners("onCompatibilityModeChanged");
1065
1066        if (gStrictCompatibility != oldValue) {
1067          this.updateAddonAppDisabledStates();
1068        }
1069
1070        break;
1071      }
1072      case PREF_EM_CHECK_UPDATE_SECURITY: {
1073        let oldValue = gCheckUpdateSecurity;
1074        gCheckUpdateSecurity = Services.prefs.getBoolPref(
1075          PREF_EM_CHECK_UPDATE_SECURITY,
1076          true
1077        );
1078
1079        this.callManagerListeners("onCheckUpdateSecurityChanged");
1080
1081        if (gCheckUpdateSecurity != oldValue) {
1082          this.updateAddonAppDisabledStates();
1083        }
1084
1085        break;
1086      }
1087      case PREF_EM_UPDATE_ENABLED: {
1088        gUpdateEnabled = Services.prefs.getBoolPref(
1089          PREF_EM_UPDATE_ENABLED,
1090          true
1091        );
1092
1093        this.callManagerListeners("onUpdateModeChanged");
1094        break;
1095      }
1096      case PREF_EM_AUTOUPDATE_DEFAULT: {
1097        gAutoUpdateDefault = Services.prefs.getBoolPref(
1098          PREF_EM_AUTOUPDATE_DEFAULT,
1099          true
1100        );
1101
1102        this.callManagerListeners("onUpdateModeChanged");
1103        break;
1104      }
1105      case PREF_MIN_WEBEXT_PLATFORM_VERSION: {
1106        gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(
1107          PREF_MIN_WEBEXT_PLATFORM_VERSION
1108        );
1109        break;
1110      }
1111    }
1112  },
1113
1114  /**
1115   * Replaces %...% strings in an addon url (update and updateInfo) with
1116   * appropriate values.
1117   *
1118   * @param  aAddon
1119   *         The Addon representing the add-on
1120   * @param  aUri
1121   *         The string representation of the URI to escape
1122   * @param  aAppVersion
1123   *         The optional application version to use for %APP_VERSION%
1124   * @return The appropriately escaped URI.
1125   */
1126  escapeAddonURI(aAddon, aUri, aAppVersion) {
1127    if (!aAddon || typeof aAddon != "object") {
1128      throw Components.Exception(
1129        "aAddon must be an Addon object",
1130        Cr.NS_ERROR_INVALID_ARG
1131      );
1132    }
1133
1134    if (!aUri || typeof aUri != "string") {
1135      throw Components.Exception(
1136        "aUri must be a non-empty string",
1137        Cr.NS_ERROR_INVALID_ARG
1138      );
1139    }
1140
1141    if (aAppVersion && typeof aAppVersion != "string") {
1142      throw Components.Exception(
1143        "aAppVersion must be a string or null",
1144        Cr.NS_ERROR_INVALID_ARG
1145      );
1146    }
1147
1148    var addonStatus =
1149      aAddon.userDisabled || aAddon.softDisabled
1150        ? "userDisabled"
1151        : "userEnabled";
1152
1153    if (!aAddon.isCompatible) {
1154      addonStatus += ",incompatible";
1155    }
1156
1157    let { blocklistState } = aAddon;
1158    if (blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
1159      addonStatus += ",blocklisted";
1160    }
1161    if (blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
1162      addonStatus += ",softblocked";
1163    }
1164
1165    let params = new Map(
1166      Object.entries({
1167        ITEM_ID: aAddon.id,
1168        ITEM_VERSION: aAddon.version,
1169        ITEM_STATUS: addonStatus,
1170        APP_ID: Services.appinfo.ID,
1171        APP_VERSION: aAppVersion ? aAppVersion : Services.appinfo.version,
1172        REQ_VERSION: UPDATE_REQUEST_VERSION,
1173        APP_OS: Services.appinfo.OS,
1174        APP_ABI: Services.appinfo.XPCOMABI,
1175        APP_LOCALE: getLocale(),
1176        CURRENT_APP_VERSION: Services.appinfo.version,
1177      })
1178    );
1179
1180    let uri = aUri.replace(/%([A-Z_]+)%/g, (m0, m1) => params.get(m1) || m0);
1181
1182    // escape() does not properly encode + symbols in any embedded FVF strings.
1183    return uri.replace(/\+/g, "%2B");
1184  },
1185
1186  _updatePromptHandler(info) {
1187    let oldPerms = info.existingAddon.userPermissions;
1188    if (!oldPerms) {
1189      // Updating from a legacy add-on, just let it proceed
1190      return Promise.resolve();
1191    }
1192
1193    let newPerms = info.addon.userPermissions;
1194
1195    let difference = Extension.comparePermissions(oldPerms, newPerms);
1196
1197    // If there are no new permissions, just go ahead with the update
1198    if (!difference.origins.length && !difference.permissions.length) {
1199      return Promise.resolve();
1200    }
1201
1202    return new Promise((resolve, reject) => {
1203      let subject = {
1204        wrappedJSObject: {
1205          addon: info.addon,
1206          permissions: difference,
1207          resolve,
1208          reject,
1209          // Reference to the related AddonInstall object (used in AMTelemetry to
1210          // link the recorded event to the other events from the same install flow).
1211          install: info.install,
1212        },
1213      };
1214      Services.obs.notifyObservers(subject, "webextension-update-permissions");
1215    });
1216  },
1217
1218  // Returns true if System Addons should be updated
1219  systemUpdateEnabled() {
1220    if (!Services.prefs.getBoolPref(PREF_SYS_ADDON_UPDATE_ENABLED)) {
1221      return false;
1222    }
1223    if (Services.policies && !Services.policies.isAllowed("SysAddonUpdate")) {
1224      return false;
1225    }
1226    return true;
1227  },
1228
1229  /**
1230   * Performs a background update check by starting an update for all add-ons
1231   * that can be updated.
1232   * @return Promise{null} Resolves when the background update check is complete
1233   *                       (the resulting addon installations may still be in progress).
1234   */
1235  backgroundUpdateCheck() {
1236    if (!gStarted) {
1237      throw Components.Exception(
1238        "AddonManager is not initialized",
1239        Cr.NS_ERROR_NOT_INITIALIZED
1240      );
1241    }
1242
1243    let buPromise = (async () => {
1244      logger.debug("Background update check beginning");
1245
1246      Services.obs.notifyObservers(null, "addons-background-update-start");
1247
1248      if (this.updateEnabled) {
1249        // Keep track of all the async add-on updates happening in parallel
1250        let updates = [];
1251
1252        let allAddons = await this.getAllAddons();
1253
1254        // Repopulate repository cache first, to ensure compatibility overrides
1255        // are up to date before checking for addon updates.
1256        await AddonRepository.backgroundUpdateCheck();
1257
1258        for (let addon of allAddons) {
1259          // Check all add-ons for updates so that any compatibility updates will
1260          // be applied
1261
1262          if (!(addon.permissions & AddonManager.PERM_CAN_UPGRADE)) {
1263            continue;
1264          }
1265
1266          updates.push(
1267            new Promise((resolve, reject) => {
1268              addon.findUpdates(
1269                {
1270                  onUpdateAvailable(aAddon, aInstall) {
1271                    // Start installing updates when the add-on can be updated and
1272                    // background updates should be applied.
1273                    logger.debug("Found update for add-on ${id}", aAddon);
1274                    if (AddonManager.shouldAutoUpdate(aAddon)) {
1275                      // XXX we really should resolve when this install is done,
1276                      // not when update-available check completes, no?
1277                      logger.debug(`Starting upgrade install of ${aAddon.id}`);
1278                      aInstall.promptHandler = (...args) =>
1279                        AddonManagerInternal._updatePromptHandler(...args);
1280                      aInstall.install();
1281                    }
1282                  },
1283
1284                  onUpdateFinished: aAddon => {
1285                    logger.debug("onUpdateFinished for ${id}", aAddon);
1286                    resolve();
1287                  },
1288                },
1289                AddonManager.UPDATE_WHEN_PERIODIC_UPDATE
1290              );
1291            })
1292          );
1293        }
1294        await Promise.all(updates);
1295      }
1296
1297      if (AddonManagerInternal.systemUpdateEnabled()) {
1298        try {
1299          await AddonManagerInternal._getProviderByName(
1300            "XPIProvider"
1301          ).updateSystemAddons();
1302        } catch (e) {
1303          logger.warn("Failed to update system addons", e);
1304        }
1305      }
1306
1307      logger.debug("Background update check complete");
1308      Services.obs.notifyObservers(null, "addons-background-update-complete");
1309    })();
1310    // Fork the promise chain so we can log the error and let our caller see it too.
1311    buPromise.catch(e => logger.warn("Error in background update", e));
1312    return buPromise;
1313  },
1314
1315  /**
1316   * Adds a add-on to the list of detected changes for this startup. If
1317   * addStartupChange is called multiple times for the same add-on in the same
1318   * startup then only the most recent change will be remembered.
1319   *
1320   * @param  aType
1321   *         The type of change as a string. Providers can define their own
1322   *         types of changes or use the existing defined STARTUP_CHANGE_*
1323   *         constants
1324   * @param  aID
1325   *         The ID of the add-on
1326   */
1327  addStartupChange(aType, aID) {
1328    if (!aType || typeof aType != "string") {
1329      throw Components.Exception(
1330        "aType must be a non-empty string",
1331        Cr.NS_ERROR_INVALID_ARG
1332      );
1333    }
1334
1335    if (!aID || typeof aID != "string") {
1336      throw Components.Exception(
1337        "aID must be a non-empty string",
1338        Cr.NS_ERROR_INVALID_ARG
1339      );
1340    }
1341
1342    if (gStartupComplete) {
1343      return;
1344    }
1345    logger.debug("Registering startup change '" + aType + "' for " + aID);
1346
1347    // Ensure that an ID is only listed in one type of change
1348    for (let type in this.startupChanges) {
1349      this.removeStartupChange(type, aID);
1350    }
1351
1352    if (!(aType in this.startupChanges)) {
1353      this.startupChanges[aType] = [];
1354    }
1355    this.startupChanges[aType].push(aID);
1356  },
1357
1358  /**
1359   * Removes a startup change for an add-on.
1360   *
1361   * @param  aType
1362   *         The type of change
1363   * @param  aID
1364   *         The ID of the add-on
1365   */
1366  removeStartupChange(aType, aID) {
1367    if (!aType || typeof aType != "string") {
1368      throw Components.Exception(
1369        "aType must be a non-empty string",
1370        Cr.NS_ERROR_INVALID_ARG
1371      );
1372    }
1373
1374    if (!aID || typeof aID != "string") {
1375      throw Components.Exception(
1376        "aID must be a non-empty string",
1377        Cr.NS_ERROR_INVALID_ARG
1378      );
1379    }
1380
1381    if (gStartupComplete) {
1382      return;
1383    }
1384
1385    if (!(aType in this.startupChanges)) {
1386      return;
1387    }
1388
1389    this.startupChanges[aType] = this.startupChanges[aType].filter(
1390      aItem => aItem != aID
1391    );
1392  },
1393
1394  /**
1395   * Calls all registered AddonManagerListeners with an event. Any parameters
1396   * after the method parameter are passed to the listener.
1397   *
1398   * @param  aMethod
1399   *         The method on the listeners to call
1400   */
1401  callManagerListeners(aMethod, ...aArgs) {
1402    if (!gStarted) {
1403      throw Components.Exception(
1404        "AddonManager is not initialized",
1405        Cr.NS_ERROR_NOT_INITIALIZED
1406      );
1407    }
1408
1409    if (!aMethod || typeof aMethod != "string") {
1410      throw Components.Exception(
1411        "aMethod must be a non-empty string",
1412        Cr.NS_ERROR_INVALID_ARG
1413      );
1414    }
1415
1416    let managerListeners = new Set(this.managerListeners);
1417    for (let listener of managerListeners) {
1418      try {
1419        if (aMethod in listener) {
1420          listener[aMethod].apply(listener, aArgs);
1421        }
1422      } catch (e) {
1423        logger.warn(
1424          "AddonManagerListener threw exception when calling " + aMethod,
1425          e
1426        );
1427      }
1428    }
1429  },
1430
1431  /**
1432   * Calls all registered InstallListeners with an event. Any parameters after
1433   * the extraListeners parameter are passed to the listener.
1434   *
1435   * @param  aMethod
1436   *         The method on the listeners to call
1437   * @param  aExtraListeners
1438   *         An optional array of extra InstallListeners to also call
1439   * @return false if any of the listeners returned false, true otherwise
1440   */
1441  callInstallListeners(aMethod, aExtraListeners, ...aArgs) {
1442    if (!gStarted) {
1443      throw Components.Exception(
1444        "AddonManager is not initialized",
1445        Cr.NS_ERROR_NOT_INITIALIZED
1446      );
1447    }
1448
1449    if (!aMethod || typeof aMethod != "string") {
1450      throw Components.Exception(
1451        "aMethod must be a non-empty string",
1452        Cr.NS_ERROR_INVALID_ARG
1453      );
1454    }
1455
1456    if (aExtraListeners && !Array.isArray(aExtraListeners)) {
1457      throw Components.Exception(
1458        "aExtraListeners must be an array or null",
1459        Cr.NS_ERROR_INVALID_ARG
1460      );
1461    }
1462
1463    let result = true;
1464    let listeners;
1465    if (aExtraListeners) {
1466      listeners = new Set(
1467        aExtraListeners.concat(Array.from(this.installListeners))
1468      );
1469    } else {
1470      listeners = new Set(this.installListeners);
1471    }
1472
1473    for (let listener of listeners) {
1474      try {
1475        if (aMethod in listener) {
1476          if (listener[aMethod].apply(listener, aArgs) === false) {
1477            result = false;
1478          }
1479        }
1480      } catch (e) {
1481        logger.warn(
1482          "InstallListener threw exception when calling " + aMethod,
1483          e
1484        );
1485      }
1486    }
1487    return result;
1488  },
1489
1490  /**
1491   * Calls all registered AddonListeners with an event. Any parameters after
1492   * the method parameter are passed to the listener.
1493   *
1494   * @param  aMethod
1495   *         The method on the listeners to call
1496   */
1497  callAddonListeners(aMethod, ...aArgs) {
1498    if (!gStarted) {
1499      throw Components.Exception(
1500        "AddonManager is not initialized",
1501        Cr.NS_ERROR_NOT_INITIALIZED
1502      );
1503    }
1504
1505    if (!aMethod || typeof aMethod != "string") {
1506      throw Components.Exception(
1507        "aMethod must be a non-empty string",
1508        Cr.NS_ERROR_INVALID_ARG
1509      );
1510    }
1511
1512    let addonListeners = new Set(this.addonListeners);
1513    for (let listener of addonListeners) {
1514      try {
1515        if (aMethod in listener) {
1516          listener[aMethod].apply(listener, aArgs);
1517        }
1518      } catch (e) {
1519        logger.warn("AddonListener threw exception when calling " + aMethod, e);
1520      }
1521    }
1522  },
1523
1524  /**
1525   * Notifies all providers that an add-on has been enabled when that type of
1526   * add-on only supports a single add-on being enabled at a time. This allows
1527   * the providers to disable theirs if necessary.
1528   *
1529   * @param  aID
1530   *         The ID of the enabled add-on
1531   * @param  aType
1532   *         The type of the enabled add-on
1533   * @param  aPendingRestart
1534   *         A boolean indicating if the change will only take place the next
1535   *         time the application is restarted
1536   */
1537  async notifyAddonChanged(aID, aType, aPendingRestart) {
1538    if (!gStarted) {
1539      throw Components.Exception(
1540        "AddonManager is not initialized",
1541        Cr.NS_ERROR_NOT_INITIALIZED
1542      );
1543    }
1544
1545    if (aID && typeof aID != "string") {
1546      throw Components.Exception(
1547        "aID must be a string or null",
1548        Cr.NS_ERROR_INVALID_ARG
1549      );
1550    }
1551
1552    if (!aType || typeof aType != "string") {
1553      throw Components.Exception(
1554        "aType must be a non-empty string",
1555        Cr.NS_ERROR_INVALID_ARG
1556      );
1557    }
1558
1559    // Temporary hack until bug 520124 lands.
1560    // We can get here during synchronous startup, at which point it's
1561    // considered unsafe (and therefore disallowed by AddonManager.jsm) to
1562    // access providers that haven't been initialized yet. Since this is when
1563    // XPIProvider is starting up, XPIProvider can't access itself via APIs
1564    // going through AddonManager.jsm. Thankfully, this is the only use
1565    // of this API, and we know it's safe to use this API with both
1566    // providers; so we have this hack to allow bypassing the normal
1567    // safetey guard.
1568    // The notifyAddonChanged/addonChanged API will be unneeded and therefore
1569    // removed by bug 520124, so this is a temporary quick'n'dirty hack.
1570    let providers = [...this.providers, ...this.pendingProviders];
1571    for (let provider of providers) {
1572      let result = callProvider(
1573        provider,
1574        "addonChanged",
1575        null,
1576        aID,
1577        aType,
1578        aPendingRestart
1579      );
1580      if (result) {
1581        await result;
1582      }
1583    }
1584  },
1585
1586  /**
1587   * Notifies all providers they need to update the appDisabled property for
1588   * their add-ons in response to an application change such as a blocklist
1589   * update.
1590   */
1591  updateAddonAppDisabledStates() {
1592    if (!gStarted) {
1593      throw Components.Exception(
1594        "AddonManager is not initialized",
1595        Cr.NS_ERROR_NOT_INITIALIZED
1596      );
1597    }
1598
1599    this.callProviders("updateAddonAppDisabledStates");
1600  },
1601
1602  /**
1603   * Notifies all providers that the repository has updated its data for
1604   * installed add-ons.
1605   */
1606  updateAddonRepositoryData() {
1607    if (!gStarted) {
1608      throw Components.Exception(
1609        "AddonManager is not initialized",
1610        Cr.NS_ERROR_NOT_INITIALIZED
1611      );
1612    }
1613
1614    return (async () => {
1615      for (let provider of this.providers) {
1616        await promiseCallProvider(provider, "updateAddonRepositoryData");
1617      }
1618
1619      // only tests should care about this
1620      Services.obs.notifyObservers(null, "TEST:addon-repository-data-updated");
1621    })();
1622  },
1623
1624  /**
1625   * Asynchronously gets an AddonInstall for a URL.
1626   *
1627   * @param  aUrl
1628   *         The string represenation of the URL where the add-on is located
1629   * @param  {Object} [aOptions = {}]
1630   *         Additional options for this install
1631   * @param  {string} [aOptions.hash]
1632   *         An optional hash of the add-on
1633   * @param  {string} [aOptions.name]
1634   *         An optional placeholder name while the add-on is being downloaded
1635   * @param  {string|Object} [aOptions.icons]
1636   *         Optional placeholder icons while the add-on is being downloaded
1637   * @param  {string} [aOptions.version]
1638   *         An optional placeholder version while the add-on is being downloaded
1639   * @param  {XULElement} [aOptions.browser]
1640   *         An optional <browser> element for download permissions prompts.
1641   * @param  {nsIPrincipal} [aOptions.triggeringPrincipal]
1642   *         The principal which is attempting to install the add-on.
1643   * @param  {Object} [aOptions.telemetryInfo]
1644   *         An optional object which provides details about the installation source
1645   *         included in the addon manager telemetry events.
1646   * @throws if aUrl is not specified or if an optional argument of
1647   *         an improper type is passed.
1648   */
1649  async getInstallForURL(aUrl, aOptions = {}) {
1650    if (!gStarted) {
1651      throw Components.Exception(
1652        "AddonManager is not initialized",
1653        Cr.NS_ERROR_NOT_INITIALIZED
1654      );
1655    }
1656
1657    if (!aUrl || typeof aUrl != "string") {
1658      throw Components.Exception(
1659        "aURL must be a non-empty string",
1660        Cr.NS_ERROR_INVALID_ARG
1661      );
1662    }
1663
1664    if (aOptions.hash && typeof aOptions.hash != "string") {
1665      throw Components.Exception(
1666        "hash must be a string or null",
1667        Cr.NS_ERROR_INVALID_ARG
1668      );
1669    }
1670
1671    if (aOptions.name && typeof aOptions.name != "string") {
1672      throw Components.Exception(
1673        "name must be a string or null",
1674        Cr.NS_ERROR_INVALID_ARG
1675      );
1676    }
1677
1678    if (aOptions.icons) {
1679      if (typeof aOptions.icons == "string") {
1680        aOptions.icons = { "32": aOptions.icons };
1681      } else if (typeof aOptions.icons != "object") {
1682        throw Components.Exception(
1683          "icons must be a string, an object or null",
1684          Cr.NS_ERROR_INVALID_ARG
1685        );
1686      }
1687    } else {
1688      aOptions.icons = {};
1689    }
1690
1691    if (aOptions.version && typeof aOptions.version != "string") {
1692      throw Components.Exception(
1693        "version must be a string or null",
1694        Cr.NS_ERROR_INVALID_ARG
1695      );
1696    }
1697
1698    if (aOptions.browser && !Element.isInstance(aOptions.browser)) {
1699      throw Components.Exception(
1700        "aOptions.browser must be an Element or null",
1701        Cr.NS_ERROR_INVALID_ARG
1702      );
1703    }
1704
1705    for (let provider of this.providers) {
1706      let install = await promiseCallProvider(
1707        provider,
1708        "getInstallForURL",
1709        aUrl,
1710        aOptions
1711      );
1712      if (install) {
1713        return install;
1714      }
1715    }
1716
1717    return null;
1718  },
1719
1720  /**
1721   * Asynchronously gets an AddonInstall for an nsIFile.
1722   *
1723   * @param  aFile
1724   *         The nsIFile where the add-on is located
1725   * @param  aMimetype
1726   *         An optional mimetype hint for the add-on
1727   * @param  aTelemetryInfo
1728   *         An optional object which provides details about the installation source
1729   *         included in the addon manager telemetry events.
1730   * @param  aUseSystemLocation
1731   *         If true the addon is installed into the system profile location.
1732   * @throws if the aFile or aCallback arguments are not specified
1733   */
1734  getInstallForFile(aFile, aMimetype, aTelemetryInfo, aUseSystemLocation) {
1735    if (!gStarted) {
1736      throw Components.Exception(
1737        "AddonManager is not initialized",
1738        Cr.NS_ERROR_NOT_INITIALIZED
1739      );
1740    }
1741
1742    if (!(aFile instanceof Ci.nsIFile)) {
1743      throw Components.Exception(
1744        "aFile must be a nsIFile",
1745        Cr.NS_ERROR_INVALID_ARG
1746      );
1747    }
1748
1749    if (aMimetype && typeof aMimetype != "string") {
1750      throw Components.Exception(
1751        "aMimetype must be a string or null",
1752        Cr.NS_ERROR_INVALID_ARG
1753      );
1754    }
1755
1756    return (async () => {
1757      for (let provider of this.providers) {
1758        let install = await promiseCallProvider(
1759          provider,
1760          "getInstallForFile",
1761          aFile,
1762          aTelemetryInfo,
1763          aUseSystemLocation
1764        );
1765
1766        if (install) {
1767          return install;
1768        }
1769      }
1770
1771      return null;
1772    })();
1773  },
1774
1775  /**
1776   * Uninstall an addon from the system profile location.
1777   *
1778   * @param {string} aID
1779   *         The ID of the addon to remove.
1780   * @returns A promise that resolves when the addon is uninstalled.
1781   */
1782  uninstallSystemProfileAddon(aID) {
1783    if (!gStarted) {
1784      throw Components.Exception(
1785        "AddonManager is not initialized",
1786        Cr.NS_ERROR_NOT_INITIALIZED
1787      );
1788    }
1789    return AddonManagerInternal._getProviderByName(
1790      "XPIProvider"
1791    ).uninstallSystemProfileAddon(aID);
1792  },
1793
1794  /**
1795   * Asynchronously gets all current AddonInstalls optionally limiting to a list
1796   * of types.
1797   *
1798   * @param  aTypes
1799   *         An optional array of types to retrieve. Each type is a string name
1800   * @throws If the aCallback argument is not specified
1801   */
1802  getInstallsByTypes(aTypes) {
1803    if (!gStarted) {
1804      throw Components.Exception(
1805        "AddonManager is not initialized",
1806        Cr.NS_ERROR_NOT_INITIALIZED
1807      );
1808    }
1809
1810    if (aTypes && !Array.isArray(aTypes)) {
1811      throw Components.Exception(
1812        "aTypes must be an array or null",
1813        Cr.NS_ERROR_INVALID_ARG
1814      );
1815    }
1816
1817    return (async () => {
1818      let installs = [];
1819
1820      for (let provider of this.providers) {
1821        let providerInstalls = await promiseCallProvider(
1822          provider,
1823          "getInstallsByTypes",
1824          aTypes
1825        );
1826
1827        if (providerInstalls) {
1828          installs.push(...providerInstalls);
1829        }
1830      }
1831
1832      return installs;
1833    })();
1834  },
1835
1836  /**
1837   * Asynchronously gets all current AddonInstalls.
1838   */
1839  getAllInstalls() {
1840    if (!gStarted) {
1841      throw Components.Exception(
1842        "AddonManager is not initialized",
1843        Cr.NS_ERROR_NOT_INITIALIZED
1844      );
1845    }
1846
1847    return this.getInstallsByTypes(null);
1848  },
1849
1850  /**
1851   * Checks whether installation is enabled for a particular mimetype.
1852   *
1853   * @param  aMimetype
1854   *         The mimetype to check
1855   * @return true if installation is enabled for the mimetype
1856   */
1857  isInstallEnabled(aMimetype) {
1858    if (!gStarted) {
1859      throw Components.Exception(
1860        "AddonManager is not initialized",
1861        Cr.NS_ERROR_NOT_INITIALIZED
1862      );
1863    }
1864
1865    if (!aMimetype || typeof aMimetype != "string") {
1866      throw Components.Exception(
1867        "aMimetype must be a non-empty string",
1868        Cr.NS_ERROR_INVALID_ARG
1869      );
1870    }
1871
1872    let providers = [...this.providers];
1873    for (let provider of providers) {
1874      if (
1875        callProvider(provider, "supportsMimetype", false, aMimetype) &&
1876        callProvider(provider, "isInstallEnabled")
1877      ) {
1878        return true;
1879      }
1880    }
1881    return false;
1882  },
1883
1884  /**
1885   * Checks whether a particular source is allowed to install add-ons of a
1886   * given mimetype.
1887   *
1888   * @param  aMimetype
1889   *         The mimetype of the add-on
1890   * @param  aInstallingPrincipal
1891   *         The nsIPrincipal that initiated the install
1892   * @return true if the source is allowed to install this mimetype
1893   */
1894  isInstallAllowed(aMimetype, aInstallingPrincipal) {
1895    if (!gStarted) {
1896      throw Components.Exception(
1897        "AddonManager is not initialized",
1898        Cr.NS_ERROR_NOT_INITIALIZED
1899      );
1900    }
1901
1902    if (!aMimetype || typeof aMimetype != "string") {
1903      throw Components.Exception(
1904        "aMimetype must be a non-empty string",
1905        Cr.NS_ERROR_INVALID_ARG
1906      );
1907    }
1908
1909    if (
1910      !aInstallingPrincipal ||
1911      !(aInstallingPrincipal instanceof Ci.nsIPrincipal)
1912    ) {
1913      throw Components.Exception(
1914        "aInstallingPrincipal must be a nsIPrincipal",
1915        Cr.NS_ERROR_INVALID_ARG
1916      );
1917    }
1918
1919    if (
1920      this.isInstallAllowedByPolicy(
1921        aInstallingPrincipal,
1922        null,
1923        true /* explicit */
1924      )
1925    ) {
1926      return true;
1927    }
1928
1929    let providers = [...this.providers];
1930    for (let provider of providers) {
1931      if (
1932        callProvider(provider, "supportsMimetype", false, aMimetype) &&
1933        callProvider(provider, "isInstallAllowed", null, aInstallingPrincipal)
1934      ) {
1935        return true;
1936      }
1937    }
1938    return false;
1939  },
1940
1941  /**
1942   * Checks whether a particular source is allowed to install add-ons based
1943   * on policy.
1944   *
1945   * @param  aInstallingPrincipal
1946   *         The nsIPrincipal that initiated the install
1947   * @param  aInstall
1948   *         The AddonInstall to be installed
1949   * @param  explicit
1950   *         If this is set, we only return true if the source is explicitly
1951   *         blocked via policy.
1952   *
1953   * @return boolean
1954   *         By default, returns true if the source is blocked by policy
1955   *         or there is no policy.
1956   *         If explicit is set, only returns true of the source is
1957   *         blocked by policy, false otherwise. This is needed for
1958   *         handling inverse cases.
1959   */
1960  isInstallAllowedByPolicy(aInstallingPrincipal, aInstall, explicit) {
1961    if (Services.policies) {
1962      let extensionSettings = Services.policies.getExtensionSettings("*");
1963      if (extensionSettings && extensionSettings.install_sources) {
1964        if (
1965          (!aInstall ||
1966            Services.policies.allowedInstallSource(aInstall.sourceURI)) &&
1967          (!aInstallingPrincipal ||
1968            !aInstallingPrincipal.URI ||
1969            Services.policies.allowedInstallSource(aInstallingPrincipal.URI))
1970        ) {
1971          return true;
1972        }
1973        return false;
1974      }
1975    }
1976    return !explicit;
1977  },
1978
1979  installNotifyObservers(
1980    aTopic,
1981    aBrowser,
1982    aUri,
1983    aInstall,
1984    aInstallFn,
1985    aCancelFn
1986  ) {
1987    let info = {
1988      wrappedJSObject: {
1989        browser: aBrowser,
1990        originatingURI: aUri,
1991        installs: [aInstall],
1992        install: aInstallFn,
1993        cancel: aCancelFn,
1994      },
1995    };
1996    Services.obs.notifyObservers(info, aTopic);
1997  },
1998
1999  startInstall(browser, url, install) {
2000    this.installNotifyObservers("addon-install-started", browser, url, install);
2001
2002    // Local installs may already be in a failed state in which case
2003    // we won't get any further events, detect those cases now.
2004    if (
2005      install.state == AddonManager.STATE_DOWNLOADED &&
2006      install.addon.appDisabled
2007    ) {
2008      install.cancel();
2009      this.installNotifyObservers(
2010        "addon-install-failed",
2011        browser,
2012        url,
2013        install
2014      );
2015      return;
2016    }
2017
2018    let self = this;
2019    let listener = {
2020      onDownloadCancelled() {
2021        install.removeListener(listener);
2022      },
2023
2024      onDownloadFailed() {
2025        install.removeListener(listener);
2026        self.installNotifyObservers(
2027          "addon-install-failed",
2028          browser,
2029          url,
2030          install
2031        );
2032      },
2033
2034      onDownloadEnded() {
2035        if (install.addon.appDisabled) {
2036          // App disabled items are not compatible and so fail to install.
2037          install.removeListener(listener);
2038          install.cancel();
2039          self.installNotifyObservers(
2040            "addon-install-failed",
2041            browser,
2042            url,
2043            install
2044          );
2045        }
2046      },
2047
2048      onInstallCancelled() {
2049        install.removeListener(listener);
2050      },
2051
2052      onInstallFailed() {
2053        install.removeListener(listener);
2054        self.installNotifyObservers(
2055          "addon-install-failed",
2056          browser,
2057          url,
2058          install
2059        );
2060      },
2061
2062      onInstallEnded() {
2063        install.removeListener(listener);
2064
2065        // If installing a theme that is disabled and can be enabled
2066        // then enable it
2067        if (
2068          install.addon.type == "theme" &&
2069          !!install.addon.userDisabled &&
2070          !install.addon.appDisabled
2071        ) {
2072          install.addon.enable();
2073        }
2074
2075        let needsRestart =
2076          install.addon.pendingOperations != AddonManager.PENDING_NONE;
2077
2078        if (!needsRestart) {
2079          let subject = {
2080            wrappedJSObject: { target: browser, addon: install.addon },
2081          };
2082          Services.obs.notifyObservers(subject, "webextension-install-notify");
2083        } else {
2084          self.installNotifyObservers(
2085            "addon-install-complete",
2086            browser,
2087            url,
2088            install
2089          );
2090        }
2091      },
2092    };
2093
2094    install.addListener(listener);
2095
2096    // Start downloading if it hasn't already begun
2097    install.install();
2098  },
2099
2100  /**
2101   * Starts installation of an AddonInstall notifying the registered
2102   * web install listener of a blocked or started install.
2103   *
2104   * @param  aMimetype
2105   *         The mimetype of the add-on being installed
2106   * @param  aBrowser
2107   *         The optional browser element that started the install
2108   * @param  aInstallingPrincipal
2109   *         The nsIPrincipal that initiated the install
2110   * @param  aInstall
2111   *         The AddonInstall to be installed
2112   */
2113  installAddonFromWebpage(aMimetype, aBrowser, aInstallingPrincipal, aInstall) {
2114    if (!gStarted) {
2115      throw Components.Exception(
2116        "AddonManager is not initialized",
2117        Cr.NS_ERROR_NOT_INITIALIZED
2118      );
2119    }
2120
2121    if (!aMimetype || typeof aMimetype != "string") {
2122      throw Components.Exception(
2123        "aMimetype must be a non-empty string",
2124        Cr.NS_ERROR_INVALID_ARG
2125      );
2126    }
2127
2128    if (aBrowser && !Element.isInstance(aBrowser)) {
2129      throw Components.Exception(
2130        "aSource must be an Element, or null",
2131        Cr.NS_ERROR_INVALID_ARG
2132      );
2133    }
2134
2135    if (
2136      !aInstallingPrincipal ||
2137      !(aInstallingPrincipal instanceof Ci.nsIPrincipal)
2138    ) {
2139      throw Components.Exception(
2140        "aInstallingPrincipal must be a nsIPrincipal",
2141        Cr.NS_ERROR_INVALID_ARG
2142      );
2143    }
2144
2145    // When a chrome in-content UI has loaded a <browser> inside to host a
2146    // website we want to do our security checks on the inner-browser but
2147    // notify front-end that install events came from the outer-browser (the
2148    // main tab's browser). Check this by seeing if the browser we've been
2149    // passed is in a content type docshell and if so get the outer-browser.
2150    let topBrowser = aBrowser;
2151    // GeckoView does not pass a browser.
2152    if (aBrowser) {
2153      let docShell = aBrowser.ownerGlobal.docShell;
2154      if (docShell.itemType == Ci.nsIDocShellTreeItem.typeContent) {
2155        topBrowser = docShell.chromeEventHandler;
2156      }
2157    }
2158
2159    try {
2160      // Use fullscreenElement to check for DOM fullscreen, while still allowing
2161      // macOS fullscreen, which still has a browser chrome.
2162      if (topBrowser && topBrowser.ownerDocument.fullscreenElement) {
2163        // Addon installation and the resulting notifications should be
2164        // blocked in DOM fullscreen for security and usability reasons.
2165        // Installation prompts in fullscreen can trick the user into
2166        // installing unwanted addons.
2167        // In fullscreen the notification box does not have a clear
2168        // visual association with its parent anymore.
2169        aInstall.cancel();
2170
2171        this.installNotifyObservers(
2172          "addon-install-fullscreen-blocked",
2173          topBrowser,
2174          aInstallingPrincipal.URI,
2175          aInstall
2176        );
2177        return;
2178      } else if (!this.isInstallEnabled(aMimetype)) {
2179        aInstall.cancel();
2180
2181        this.installNotifyObservers(
2182          "addon-install-disabled",
2183          topBrowser,
2184          aInstallingPrincipal.URI,
2185          aInstall
2186        );
2187        return;
2188      } else if (
2189        aInstallingPrincipal.isNullPrincipal ||
2190        (aBrowser &&
2191          (!aBrowser.contentPrincipal ||
2192            // When we attempt to handle an XPI load immediately after a
2193            // process switch, the DocShell it's being loaded into will have
2194            // a null principal, since it won't have been initialized yet.
2195            // Allowing installs in this case is relatively safe, since
2196            // there isn't much to gain by spoofing an install request from
2197            // a null principal in any case. This exception can be removed
2198            // once content handlers are triggered by DocumentChannel in the
2199            // parent process.
2200            !(
2201              aBrowser.contentPrincipal.isNullPrincipal ||
2202              aInstallingPrincipal.subsumes(aBrowser.contentPrincipal)
2203            ))) ||
2204        !this.isInstallAllowedByPolicy(
2205          aInstallingPrincipal,
2206          aInstall,
2207          false /* explicit */
2208        )
2209      ) {
2210        aInstall.cancel();
2211
2212        this.installNotifyObservers(
2213          "addon-install-origin-blocked",
2214          topBrowser,
2215          aInstallingPrincipal.URI,
2216          aInstall
2217        );
2218        return;
2219      }
2220
2221      if (aBrowser) {
2222        // The install may start now depending on the web install listener,
2223        // listen for the browser navigating to a new origin and cancel the
2224        // install in that case.
2225        new BrowserListener(aBrowser, aInstallingPrincipal, aInstall);
2226      }
2227
2228      let startInstall = source => {
2229        AddonManagerInternal.setupPromptHandler(
2230          aBrowser,
2231          aInstallingPrincipal.URI,
2232          aInstall,
2233          true,
2234          source
2235        );
2236
2237        AddonManagerInternal.startInstall(
2238          aBrowser,
2239          aInstallingPrincipal.URI,
2240          aInstall
2241        );
2242      };
2243
2244      let installAllowed = this.isInstallAllowed(
2245        aMimetype,
2246        aInstallingPrincipal
2247      );
2248      let installPerm = Services.perms.testPermissionFromPrincipal(
2249        aInstallingPrincipal,
2250        "install"
2251      );
2252
2253      if (installAllowed) {
2254        startInstall("AMO");
2255      } else if (installPerm === Ci.nsIPermissionManager.DENY_ACTION) {
2256        // Block without prompt
2257        aInstall.cancel();
2258        this.installNotifyObservers(
2259          "addon-install-blocked-silent",
2260          topBrowser,
2261          aInstallingPrincipal.URI,
2262          aInstall
2263        );
2264      } else if (!WEBEXT_POSTDOWNLOAD_THIRD_PARTY) {
2265        // Block with prompt
2266        this.installNotifyObservers(
2267          "addon-install-blocked",
2268          topBrowser,
2269          aInstallingPrincipal.URI,
2270          aInstall,
2271          () => startInstall("other")
2272        );
2273      } else {
2274        // We download the addon and validate whether a 3rd party
2275        // install prompt should be shown using e.g. recommended
2276        // state and install_origins.
2277        logger.info(`Addon download before validation.`);
2278        startInstall("other");
2279      }
2280    } catch (e) {
2281      // In the event that the weblistener throws during instantiation or when
2282      // calling onWebInstallBlocked or onWebInstallRequested the
2283      // install should get cancelled.
2284      logger.warn("Failure calling web installer", e);
2285      aInstall.cancel();
2286    }
2287  },
2288
2289  /**
2290   * Starts installation of an AddonInstall created from add-ons manager
2291   * front-end code (e.g., drag-and-drop of xpis or "Install Add-on from File"
2292   *
2293   * @param  browser
2294   *         The browser element where the installation was initiated
2295   * @param  uri
2296   *         The URI of the page where the installation was initiated
2297   * @param  install
2298   *         The AddonInstall to be installed
2299   */
2300  installAddonFromAOM(browser, uri, install) {
2301    if (!this.isInstallAllowedByPolicy(null, install)) {
2302      install.cancel();
2303
2304      this.installNotifyObservers(
2305        "addon-install-origin-blocked",
2306        browser,
2307        install.sourceURI,
2308        install
2309      );
2310      return;
2311    }
2312    if (!gStarted) {
2313      throw Components.Exception(
2314        "AddonManager is not initialized",
2315        Cr.NS_ERROR_NOT_INITIALIZED
2316      );
2317    }
2318
2319    AddonManagerInternal.setupPromptHandler(
2320      browser,
2321      uri,
2322      install,
2323      true,
2324      "local"
2325    );
2326    AddonManagerInternal.startInstall(browser, uri, install);
2327  },
2328
2329  /**
2330   * Adds a new InstallListener if the listener is not already registered.
2331   *
2332   * @param  aListener
2333   *         The InstallListener to add
2334   */
2335  addInstallListener(aListener) {
2336    if (!aListener || typeof aListener != "object") {
2337      throw Components.Exception(
2338        "aListener must be a InstallListener object",
2339        Cr.NS_ERROR_INVALID_ARG
2340      );
2341    }
2342
2343    this.installListeners.add(aListener);
2344  },
2345
2346  /**
2347   * Removes an InstallListener if the listener is registered.
2348   *
2349   * @param  aListener
2350   *         The InstallListener to remove
2351   */
2352  removeInstallListener(aListener) {
2353    if (!aListener || typeof aListener != "object") {
2354      throw Components.Exception(
2355        "aListener must be a InstallListener object",
2356        Cr.NS_ERROR_INVALID_ARG
2357      );
2358    }
2359
2360    this.installListeners.delete(aListener);
2361  },
2362  /**
2363   * Adds new or overrides existing UpgradeListener.
2364   *
2365   * @param  aInstanceID
2366   *         The instance ID of an addon to register a listener for.
2367   * @param  aCallback
2368   *         The callback to invoke when updates are available for this addon.
2369   * @throws if there is no addon matching the instanceID
2370   */
2371  addUpgradeListener(aInstanceID, aCallback) {
2372    if (!aInstanceID || typeof aInstanceID != "symbol") {
2373      throw Components.Exception(
2374        "aInstanceID must be a symbol",
2375        Cr.NS_ERROR_INVALID_ARG
2376      );
2377    }
2378
2379    if (!aCallback || typeof aCallback != "function") {
2380      throw Components.Exception(
2381        "aCallback must be a function",
2382        Cr.NS_ERROR_INVALID_ARG
2383      );
2384    }
2385
2386    let addonId = this.syncGetAddonIDByInstanceID(aInstanceID);
2387    if (!addonId) {
2388      throw Error(`No addon matching instanceID: ${String(aInstanceID)}`);
2389    }
2390    logger.debug(`Registering upgrade listener for ${addonId}`);
2391    this.upgradeListeners.set(addonId, aCallback);
2392  },
2393
2394  /**
2395   * Removes an UpgradeListener if the listener is registered.
2396   *
2397   * @param  aInstanceID
2398   *         The instance ID of the addon to remove
2399   */
2400  removeUpgradeListener(aInstanceID) {
2401    if (!aInstanceID || typeof aInstanceID != "symbol") {
2402      throw Components.Exception(
2403        "aInstanceID must be a symbol",
2404        Cr.NS_ERROR_INVALID_ARG
2405      );
2406    }
2407
2408    let addonId = this.syncGetAddonIDByInstanceID(aInstanceID);
2409    if (!addonId) {
2410      throw Error(`No addon for instanceID: ${aInstanceID}`);
2411    }
2412    if (this.upgradeListeners.has(addonId)) {
2413      this.upgradeListeners.delete(addonId);
2414    } else {
2415      throw Error(`No upgrade listener registered for addon ID: ${addonId}`);
2416    }
2417  },
2418
2419  addExternalExtensionLoader(loader) {
2420    this.externalExtensionLoaders.set(loader.name, loader);
2421  },
2422
2423  /**
2424   * Installs a temporary add-on from a local file or directory.
2425   *
2426   * @param  aFile
2427   *         An nsIFile for the file or directory of the add-on to be
2428   *         temporarily installed.
2429   * @returns a Promise that rejects if the add-on is not a valid restartless
2430   *          add-on or if the same ID is already temporarily installed.
2431   */
2432  installTemporaryAddon(aFile) {
2433    if (!gStarted) {
2434      throw Components.Exception(
2435        "AddonManager is not initialized",
2436        Cr.NS_ERROR_NOT_INITIALIZED
2437      );
2438    }
2439
2440    if (!(aFile instanceof Ci.nsIFile)) {
2441      throw Components.Exception(
2442        "aFile must be a nsIFile",
2443        Cr.NS_ERROR_INVALID_ARG
2444      );
2445    }
2446
2447    return AddonManagerInternal._getProviderByName(
2448      "XPIProvider"
2449    ).installTemporaryAddon(aFile);
2450  },
2451
2452  /**
2453   * Installs an add-on from a built-in location
2454   *  (ie a resource: url referencing assets shipped with the application)
2455   *
2456   * @param  aBase
2457   *         A string containing the base URL.  Must be a resource: URL.
2458   * @returns a Promise that resolves when the addon is installed.
2459   */
2460  installBuiltinAddon(aBase) {
2461    if (!gStarted) {
2462      throw Components.Exception(
2463        "AddonManager is not initialized",
2464        Cr.NS_ERROR_NOT_INITIALIZED
2465      );
2466    }
2467
2468    return AddonManagerInternal._getProviderByName(
2469      "XPIProvider"
2470    ).installBuiltinAddon(aBase);
2471  },
2472
2473  /**
2474   * Like `installBuiltinAddon`, but only installs the addon at `aBase`
2475   * if an existing built-in addon with the ID `aID` and version doesn't
2476   * already exist.
2477   *
2478   * @param {string} aID
2479   *        The ID of the add-on being registered.
2480   * @param {string} aVersion
2481   *        The version of the add-on being registered.
2482   * @param {string} aBase
2483   *        A string containing the base URL.  Must be a resource: URL.
2484   * @returns a Promise that resolves when the addon is installed.
2485   */
2486  maybeInstallBuiltinAddon(aID, aVersion, aBase) {
2487    if (!gStarted) {
2488      throw Components.Exception(
2489        "AddonManager is not initialized",
2490        Cr.NS_ERROR_NOT_INITIALIZED
2491      );
2492    }
2493
2494    return AddonManagerInternal._getProviderByName(
2495      "XPIProvider"
2496    ).maybeInstallBuiltinAddon(aID, aVersion, aBase);
2497  },
2498
2499  syncGetAddonIDByInstanceID(aInstanceID) {
2500    if (!gStarted) {
2501      throw Components.Exception(
2502        "AddonManager is not initialized",
2503        Cr.NS_ERROR_NOT_INITIALIZED
2504      );
2505    }
2506
2507    if (!aInstanceID || typeof aInstanceID != "symbol") {
2508      throw Components.Exception(
2509        "aInstanceID must be a Symbol()",
2510        Cr.NS_ERROR_INVALID_ARG
2511      );
2512    }
2513
2514    return AddonManagerInternal._getProviderByName(
2515      "XPIProvider"
2516    ).getAddonIDByInstanceID(aInstanceID);
2517  },
2518
2519  /**
2520   * Gets an icon from the icon set provided by the add-on
2521   * that is closest to the specified size.
2522   *
2523   * The optional window parameter will be used to determine
2524   * the screen resolution and select a more appropriate icon.
2525   * Calling this method with 48px on retina screens will try to
2526   * match an icon of size 96px.
2527   *
2528   * @param  aAddon
2529   *         An addon object, meaning:
2530   *         An object with either an icons property that is a key-value list
2531   *         of icon size and icon URL, or an object having an iconURL property.
2532   * @param  aSize
2533   *         Ideal icon size in pixels
2534   * @param  aWindow
2535   *         Optional window object for determining the correct scale.
2536   * @return {String} The absolute URL of the icon or null if the addon doesn't have icons
2537   */
2538  getPreferredIconURL(aAddon, aSize, aWindow = undefined) {
2539    if (aWindow && aWindow.devicePixelRatio) {
2540      aSize *= aWindow.devicePixelRatio;
2541    }
2542
2543    let icons = aAddon.icons;
2544
2545    // certain addon-types only have iconURLs
2546    if (!icons) {
2547      icons = {};
2548      if (aAddon.iconURL) {
2549        icons[32] = aAddon.iconURL;
2550        icons[48] = aAddon.iconURL;
2551      }
2552    }
2553
2554    // quick return if the exact size was found
2555    if (icons[aSize]) {
2556      return icons[aSize];
2557    }
2558
2559    let bestSize = null;
2560
2561    for (let size of Object.keys(icons)) {
2562      if (!INTEGER.test(size)) {
2563        throw Components.Exception(
2564          "Invalid icon size, must be an integer",
2565          Cr.NS_ERROR_ILLEGAL_VALUE
2566        );
2567      }
2568
2569      size = parseInt(size, 10);
2570
2571      if (!bestSize) {
2572        bestSize = size;
2573        continue;
2574      }
2575
2576      if (size > aSize && bestSize > aSize) {
2577        // If both best size and current size are larger than the wanted size then choose
2578        // the one closest to the wanted size
2579        bestSize = Math.min(bestSize, size);
2580      } else {
2581        // Otherwise choose the largest of the two so we'll prefer sizes as close to below aSize
2582        // or above aSize
2583        bestSize = Math.max(bestSize, size);
2584      }
2585    }
2586
2587    return icons[bestSize] || null;
2588  },
2589
2590  /**
2591   * Asynchronously gets an add-on with a specific ID.
2592   *
2593   * @type {function}
2594   * @param  {string} aID
2595   *         The ID of the add-on to retrieve
2596   * @returns {Promise} resolves with the found Addon or null if no such add-on exists. Never rejects.
2597   * @throws if the aID argument is not specified
2598   */
2599  getAddonByID(aID) {
2600    if (!gStarted) {
2601      throw Components.Exception(
2602        "AddonManager is not initialized",
2603        Cr.NS_ERROR_NOT_INITIALIZED
2604      );
2605    }
2606
2607    if (!aID || typeof aID != "string") {
2608      throw Components.Exception(
2609        "aID must be a non-empty string",
2610        Cr.NS_ERROR_INVALID_ARG
2611      );
2612    }
2613
2614    let promises = Array.from(this.providers, p =>
2615      promiseCallProvider(p, "getAddonByID", aID)
2616    );
2617    return Promise.all(promises).then(aAddons => {
2618      return aAddons.find(a => !!a) || null;
2619    });
2620  },
2621
2622  /**
2623   * Asynchronously get an add-on with a specific Sync GUID.
2624   *
2625   * @param  aGUID
2626   *         String GUID of add-on to retrieve
2627   * @throws if the aGUID argument is not specified
2628   */
2629  getAddonBySyncGUID(aGUID) {
2630    if (!gStarted) {
2631      throw Components.Exception(
2632        "AddonManager is not initialized",
2633        Cr.NS_ERROR_NOT_INITIALIZED
2634      );
2635    }
2636
2637    if (!aGUID || typeof aGUID != "string") {
2638      throw Components.Exception(
2639        "aGUID must be a non-empty string",
2640        Cr.NS_ERROR_INVALID_ARG
2641      );
2642    }
2643
2644    return (async () => {
2645      for (let provider of this.providers) {
2646        let addon = await promiseCallProvider(
2647          provider,
2648          "getAddonBySyncGUID",
2649          aGUID
2650        );
2651
2652        if (addon) {
2653          return addon;
2654        }
2655      }
2656
2657      return null;
2658    })();
2659  },
2660
2661  /**
2662   * Asynchronously gets an array of add-ons.
2663   *
2664   * @param  aIDs
2665   *         The array of IDs to retrieve
2666   * @return {Promise}
2667   * @resolves The array of found add-ons.
2668   * @rejects  Never
2669   * @throws if the aIDs argument is not specified
2670   */
2671  getAddonsByIDs(aIDs) {
2672    if (!gStarted) {
2673      throw Components.Exception(
2674        "AddonManager is not initialized",
2675        Cr.NS_ERROR_NOT_INITIALIZED
2676      );
2677    }
2678
2679    if (!Array.isArray(aIDs)) {
2680      throw Components.Exception(
2681        "aIDs must be an array",
2682        Cr.NS_ERROR_INVALID_ARG
2683      );
2684    }
2685
2686    let promises = aIDs.map(a => AddonManagerInternal.getAddonByID(a));
2687    return Promise.all(promises);
2688  },
2689
2690  /**
2691   * Asynchronously gets add-ons of specific types.
2692   *
2693   * @param  aTypes
2694   *         An optional array of types to retrieve. Each type is a string name
2695   */
2696  getAddonsByTypes(aTypes) {
2697    if (!gStarted) {
2698      throw Components.Exception(
2699        "AddonManager is not initialized",
2700        Cr.NS_ERROR_NOT_INITIALIZED
2701      );
2702    }
2703
2704    if (aTypes && !Array.isArray(aTypes)) {
2705      throw Components.Exception(
2706        "aTypes must be an array or null",
2707        Cr.NS_ERROR_INVALID_ARG
2708      );
2709    }
2710
2711    return (async () => {
2712      let addons = [];
2713
2714      for (let provider of this.providers) {
2715        let providerAddons = await promiseCallProvider(
2716          provider,
2717          "getAddonsByTypes",
2718          aTypes
2719        );
2720
2721        if (providerAddons) {
2722          addons.push(...providerAddons);
2723        }
2724      }
2725
2726      return addons;
2727    })();
2728  },
2729
2730  /**
2731   * Gets active add-ons of specific types.
2732   *
2733   * This is similar to getAddonsByTypes() but it may return a limited
2734   * amount of information about only active addons.  Consequently, it
2735   * can be implemented by providers using only immediately available
2736   * data as opposed to getAddonsByTypes which may require I/O).
2737   *
2738   * @param  aTypes
2739   *         An optional array of types to retrieve. Each type is a string name
2740   *
2741   * @resolve {addons: Array, fullData: bool}
2742   *          fullData is true if addons contains all the data we have on those
2743   *          addons. It is false if addons only contains partial data.
2744   */
2745  async getActiveAddons(aTypes) {
2746    if (!gStarted) {
2747      throw Components.Exception(
2748        "AddonManager is not initialized",
2749        Cr.NS_ERROR_NOT_INITIALIZED
2750      );
2751    }
2752
2753    if (aTypes && !Array.isArray(aTypes)) {
2754      throw Components.Exception(
2755        "aTypes must be an array or null",
2756        Cr.NS_ERROR_INVALID_ARG
2757      );
2758    }
2759
2760    let addons = [],
2761      fullData = true;
2762
2763    for (let provider of this.providers) {
2764      let providerAddons, providerFullData;
2765      if ("getActiveAddons" in provider) {
2766        ({
2767          addons: providerAddons,
2768          fullData: providerFullData,
2769        } = await callProvider(provider, "getActiveAddons", null, aTypes));
2770      } else {
2771        providerAddons = await promiseCallProvider(
2772          provider,
2773          "getAddonsByTypes",
2774          aTypes
2775        );
2776        providerAddons = providerAddons.filter(a => a.isActive);
2777        providerFullData = true;
2778      }
2779
2780      if (providerAddons) {
2781        addons.push(...providerAddons);
2782        fullData = fullData && providerFullData;
2783      }
2784    }
2785
2786    return { addons, fullData };
2787  },
2788
2789  /**
2790   * Asynchronously gets all installed add-ons.
2791   */
2792  getAllAddons() {
2793    if (!gStarted) {
2794      throw Components.Exception(
2795        "AddonManager is not initialized",
2796        Cr.NS_ERROR_NOT_INITIALIZED
2797      );
2798    }
2799
2800    return this.getAddonsByTypes(null);
2801  },
2802
2803  /**
2804   * Adds a new AddonManagerListener if the listener is not already registered.
2805   *
2806   * @param {AddonManagerListener} aListener
2807   *         The listener to add
2808   */
2809  addManagerListener(aListener) {
2810    if (!aListener || typeof aListener != "object") {
2811      throw Components.Exception(
2812        "aListener must be an AddonManagerListener object",
2813        Cr.NS_ERROR_INVALID_ARG
2814      );
2815    }
2816
2817    this.managerListeners.add(aListener);
2818  },
2819
2820  /**
2821   * Removes an AddonManagerListener if the listener is registered.
2822   *
2823   * @param {AddonManagerListener} aListener
2824   *         The listener to remove
2825   */
2826  removeManagerListener(aListener) {
2827    if (!aListener || typeof aListener != "object") {
2828      throw Components.Exception(
2829        "aListener must be an AddonManagerListener object",
2830        Cr.NS_ERROR_INVALID_ARG
2831      );
2832    }
2833
2834    this.managerListeners.delete(aListener);
2835  },
2836
2837  /**
2838   * Adds a new AddonListener if the listener is not already registered.
2839   *
2840   * @param {AddonManagerListener} aListener
2841   *        The AddonListener to add.
2842   */
2843  addAddonListener(aListener) {
2844    if (!aListener || typeof aListener != "object") {
2845      throw Components.Exception(
2846        "aListener must be an AddonListener object",
2847        Cr.NS_ERROR_INVALID_ARG
2848      );
2849    }
2850
2851    this.addonListeners.add(aListener);
2852  },
2853
2854  /**
2855   * Removes an AddonListener if the listener is registered.
2856   *
2857   * @param {object}  aListener
2858   *         The AddonListener to remove
2859   */
2860  removeAddonListener(aListener) {
2861    if (!aListener || typeof aListener != "object") {
2862      throw Components.Exception(
2863        "aListener must be an AddonListener object",
2864        Cr.NS_ERROR_INVALID_ARG
2865      );
2866    }
2867
2868    this.addonListeners.delete(aListener);
2869  },
2870
2871  /**
2872   * @param {string} addonType
2873   * @returns {boolean}
2874   *          Whether there is a provider that provides the given addon type.
2875   */
2876  hasAddonType(addonType) {
2877    if (!gStarted) {
2878      throw Components.Exception(
2879        "AddonManager is not initialized",
2880        Cr.NS_ERROR_NOT_INITIALIZED
2881      );
2882    }
2883
2884    for (let addonTypes of this.typesByProvider.values()) {
2885      if (addonTypes.has(addonType)) {
2886        return true;
2887      }
2888    }
2889    return false;
2890  },
2891
2892  get autoUpdateDefault() {
2893    return gAutoUpdateDefault;
2894  },
2895
2896  set autoUpdateDefault(aValue) {
2897    aValue = !!aValue;
2898    if (aValue != gAutoUpdateDefault) {
2899      Services.prefs.setBoolPref(PREF_EM_AUTOUPDATE_DEFAULT, aValue);
2900    }
2901  },
2902
2903  get checkCompatibility() {
2904    return gCheckCompatibility;
2905  },
2906
2907  set checkCompatibility(aValue) {
2908    aValue = !!aValue;
2909    if (aValue != gCheckCompatibility) {
2910      if (!aValue) {
2911        Services.prefs.setBoolPref(PREF_EM_CHECK_COMPATIBILITY, false);
2912      } else {
2913        Services.prefs.clearUserPref(PREF_EM_CHECK_COMPATIBILITY);
2914      }
2915    }
2916  },
2917
2918  get strictCompatibility() {
2919    return gStrictCompatibility;
2920  },
2921
2922  set strictCompatibility(aValue) {
2923    aValue = !!aValue;
2924    if (aValue != gStrictCompatibility) {
2925      Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, aValue);
2926    }
2927  },
2928
2929  get checkUpdateSecurityDefault() {
2930    return gCheckUpdateSecurityDefault;
2931  },
2932
2933  get checkUpdateSecurity() {
2934    return gCheckUpdateSecurity;
2935  },
2936
2937  set checkUpdateSecurity(aValue) {
2938    aValue = !!aValue;
2939    if (aValue != gCheckUpdateSecurity) {
2940      if (aValue != gCheckUpdateSecurityDefault) {
2941        Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, aValue);
2942      } else {
2943        Services.prefs.clearUserPref(PREF_EM_CHECK_UPDATE_SECURITY);
2944      }
2945    }
2946  },
2947
2948  get updateEnabled() {
2949    return gUpdateEnabled;
2950  },
2951
2952  set updateEnabled(aValue) {
2953    aValue = !!aValue;
2954    if (aValue != gUpdateEnabled) {
2955      Services.prefs.setBoolPref(PREF_EM_UPDATE_ENABLED, aValue);
2956    }
2957  },
2958
2959  /**
2960   * Verify whether we need to show the 3rd party install prompt.
2961   *
2962   * Bypass the third party install prompt if this is an install:
2963   *   - is an install from a recognized source
2964   *   - is a an addon that can bypass the panel, such as a recommended addon
2965   *
2966   * @param {browser}      browser browser user is installing from
2967   * @param {nsIURI}       url     URI for the principal of the installing source
2968   * @param {AddonInstallWrapper} install
2969   * @param {Object}       info    information such as addon wrapper
2970   * @param {AddonWrapper} info.addon
2971   * @param {string}       source  simplified string describing source of install and is
2972   *                               generated based on the installing principal and checking
2973   *                               against site permissions and enterprise policy.
2974   *                               It may be one of "AMO", "local" or "other".
2975   * @returns {Promise}            Rejected when the installation should not proceed.
2976   */
2977  _verifyThirdPartyInstall(browser, url, install, info, source) {
2978    // If we are not post-download processing, this panel was already shown.
2979    // Otherwise, if this is from AMO or local, bypass the prompt.
2980    if (!WEBEXT_POSTDOWNLOAD_THIRD_PARTY || ["AMO", "local"].includes(source)) {
2981      return Promise.resolve();
2982    }
2983
2984    // verify both the installing source and the xpi url are allowed.
2985    if (
2986      !info.addon.validInstallOrigins({
2987        installFrom: url,
2988        source: install.sourceURI,
2989      })
2990    ) {
2991      install.error = AddonManager.ERROR_INVALID_DOMAIN;
2992      return Promise.reject();
2993    }
2994
2995    // Some addons such as recommended addons do not result in this prompt.
2996    if (info.addon.canBypassThirdParyInstallPrompt) {
2997      return Promise.resolve();
2998    }
2999
3000    return new Promise((resolve, reject) => {
3001      this.installNotifyObservers(
3002        "addon-install-blocked",
3003        browser,
3004        url,
3005        install,
3006        resolve,
3007        reject
3008      );
3009    });
3010  },
3011
3012  setupPromptHandler(browser, url, install, requireConfirm, source) {
3013    install.promptHandler = info =>
3014      new Promise((resolve, reject) => {
3015        this._verifyThirdPartyInstall(browser, url, install, info, source)
3016          .then(() => {
3017            // All installs end up in this callback when the add-on is available
3018            // for installation.  There are numerous different things that can
3019            // happen from here though.  For webextensions, if the application
3020            // implements webextension permission prompts, those always take
3021            // precedence.
3022            // If this add-on is not a webextension or if the application does not
3023            // implement permission prompts, no confirmation is displayed for
3024            // installs created from about:addons (in which case requireConfirm
3025            // is false).
3026            // In the remaining cases, a confirmation prompt is displayed but the
3027            // application may override it either by implementing the
3028            // "@mozilla.org/addons/web-install-prompt;1" contract or by setting
3029            // the customConfirmationUI preference and responding to the
3030            // "addon-install-confirmation" notification.  If the application
3031            // does not implement its own prompt, use the built-in xul dialog.
3032            if (info.addon.userPermissions) {
3033              let subject = {
3034                wrappedJSObject: {
3035                  target: browser,
3036                  info: Object.assign({ resolve, reject, source }, info),
3037                },
3038              };
3039              subject.wrappedJSObject.info.permissions =
3040                info.addon.userPermissions;
3041              Services.obs.notifyObservers(
3042                subject,
3043                "webextension-permission-prompt"
3044              );
3045            } else if (info.addon.sitePermissions) {
3046              // Handle prompting for DOM permissions in SitePermission addons.
3047              let { sitePermissions, siteOrigin } = info.addon;
3048              let subject = {
3049                wrappedJSObject: {
3050                  target: browser,
3051                  info: Object.assign(
3052                    { resolve, reject, source, sitePermissions, siteOrigin },
3053                    info
3054                  ),
3055                },
3056              };
3057              Services.obs.notifyObservers(
3058                subject,
3059                "webextension-permission-prompt"
3060              );
3061            } else if (requireConfirm) {
3062              // The methods below all want to call the install() or cancel()
3063              // method on the provided AddonInstall object to either accept
3064              // or reject the confirmation.  Fit that into our promise-based
3065              // control flow by wrapping the install object.  However,
3066              // xpInstallConfirm.xul matches the install object it is passed
3067              // with the argument passed to an InstallListener, so give it
3068              // access to the underlying object through the .wrapped property.
3069              let proxy = new Proxy(install, {
3070                get(target, property) {
3071                  if (property == "install") {
3072                    return resolve;
3073                  } else if (property == "cancel") {
3074                    return reject;
3075                  } else if (property == "wrapped") {
3076                    return target;
3077                  }
3078                  let result = target[property];
3079                  return typeof result == "function"
3080                    ? result.bind(target)
3081                    : result;
3082                },
3083              });
3084
3085              // Check for a custom installation prompt that may be provided by the
3086              // applicaton
3087              if ("@mozilla.org/addons/web-install-prompt;1" in Cc) {
3088                try {
3089                  let prompt = Cc[
3090                    "@mozilla.org/addons/web-install-prompt;1"
3091                  ].getService(Ci.amIWebInstallPrompt);
3092                  prompt.confirm(browser, url, [proxy]);
3093                  return;
3094                } catch (e) {}
3095              }
3096
3097              this.installNotifyObservers(
3098                "addon-install-confirmation",
3099                browser,
3100                url,
3101                proxy
3102              );
3103            } else {
3104              resolve();
3105            }
3106          })
3107          .catch(e => {
3108            // Error is undefined if the promise was rejected.
3109            if (e) {
3110              Cu.reportError(`Install prompt handler error: ${e}`);
3111            }
3112            reject();
3113          });
3114      });
3115  },
3116
3117  webAPI: {
3118    // installs maps integer ids to AddonInstall instances.
3119    installs: new Map(),
3120    nextInstall: 0,
3121
3122    sendEvent: null,
3123    setEventHandler(fn) {
3124      this.sendEvent = fn;
3125    },
3126
3127    async getAddonByID(target, id) {
3128      return webAPIForAddon(await AddonManager.getAddonByID(id));
3129    },
3130
3131    // helper to copy (and convert) the properties we care about
3132    copyProps(install, obj) {
3133      obj.state = AddonManager.stateToString(install.state);
3134      obj.error = AddonManager.errorToString(install.error);
3135      obj.progress = install.progress;
3136      obj.maxProgress = install.maxProgress;
3137    },
3138
3139    forgetInstall(id) {
3140      let info = this.installs.get(id);
3141      if (!info) {
3142        throw new Error(`forgetInstall cannot find ${id}`);
3143      }
3144      info.install.removeListener(info.listener);
3145      this.installs.delete(id);
3146    },
3147
3148    createInstall(target, options) {
3149      // Throw an appropriate error if the given URL is not valid
3150      // as an installation source.  Return silently if it is okay.
3151      function checkInstallUri(uri) {
3152        if (!Services.policies.allowedInstallSource(uri)) {
3153          // eslint-disable-next-line no-throw-literal
3154          return {
3155            success: false,
3156            code: "addon-install-webapi-blocked-policy",
3157            message: `Install from ${uri.spec} not permitted by policy`,
3158          };
3159        }
3160
3161        if (WEBAPI_INSTALL_HOSTS.includes(uri.host)) {
3162          return { success: true };
3163        }
3164        if (
3165          Services.prefs.getBoolPref(PREF_WEBAPI_TESTING, false) &&
3166          WEBAPI_TEST_INSTALL_HOSTS.includes(uri.host)
3167        ) {
3168          return { success: true };
3169        }
3170
3171        // eslint-disable-next-line no-throw-literal
3172        return {
3173          success: false,
3174          code: "addon-install-webapi-blocked",
3175          message: `Install from ${uri.host} not permitted`,
3176        };
3177      }
3178
3179      const makeListener = (id, mm) => {
3180        const events = [
3181          "onDownloadStarted",
3182          "onDownloadProgress",
3183          "onDownloadEnded",
3184          "onDownloadCancelled",
3185          "onDownloadFailed",
3186          "onInstallStarted",
3187          "onInstallEnded",
3188          "onInstallCancelled",
3189          "onInstallFailed",
3190        ];
3191
3192        let listener = {};
3193        let installPromise = new Promise((resolve, reject) => {
3194          events.forEach(event => {
3195            listener[event] = (install, addon) => {
3196              let data = { event, id };
3197              AddonManager.webAPI.copyProps(install, data);
3198              this.sendEvent(mm, data);
3199              if (event == "onInstallEnded") {
3200                resolve(addon);
3201              } else if (
3202                event == "onDownloadFailed" ||
3203                event == "onInstallFailed"
3204              ) {
3205                reject({ message: "install failed" });
3206              } else if (
3207                event == "onDownloadCancelled" ||
3208                event == "onInstallCancelled"
3209              ) {
3210                reject({ message: "install cancelled" });
3211              } else if (event == "onDownloadEnded") {
3212                if (install.addon.appDisabled) {
3213                  // App disabled items are not compatible and so fail to install
3214                  install.cancel();
3215                  AddonManagerInternal.installNotifyObservers(
3216                    "addon-install-failed",
3217                    target,
3218                    Services.io.newURI(options.url),
3219                    install
3220                  );
3221                }
3222              }
3223            };
3224          });
3225        });
3226
3227        // We create the promise here since this is where we're setting
3228        // up the InstallListener, but if the install is never started,
3229        // no handlers will be attached so make sure we terminate errors.
3230        installPromise.catch(() => {});
3231
3232        return { listener, installPromise };
3233      };
3234
3235      let uri;
3236      try {
3237        uri = Services.io.newURI(options.url);
3238        const { success, code, message } = checkInstallUri(uri);
3239        if (!success) {
3240          let info = {
3241            wrappedJSObject: {
3242              browser: target,
3243              originatingURI: uri,
3244              installs: [],
3245            },
3246          };
3247          Cu.reportError(`${code}: ${message}`);
3248          Services.obs.notifyObservers(info, code);
3249          return Promise.reject({ code, message });
3250        }
3251      } catch (err) {
3252        // Reject Components.Exception errors (e.g. NS_ERROR_MALFORMED_URI) as is.
3253        if (err instanceof Components.Exception) {
3254          return Promise.reject({ message: err.message });
3255        }
3256        return Promise.reject({
3257          message: "Install Failed on unexpected error",
3258        });
3259      }
3260
3261      return AddonManagerInternal.getInstallForURL(options.url, {
3262        browser: target,
3263        triggeringPrincipal: options.triggeringPrincipal,
3264        hash: options.hash,
3265        telemetryInfo: {
3266          source: AddonManager.getInstallSourceFromHost(options.sourceHost),
3267          sourceURL: options.sourceURL,
3268          method: "amWebAPI",
3269        },
3270      }).then(install => {
3271        let requireConfirm = true;
3272        if (
3273          target.contentDocument &&
3274          target.contentDocument.nodePrincipal.isSystemPrincipal
3275        ) {
3276          requireConfirm = false;
3277        }
3278        AddonManagerInternal.setupPromptHandler(
3279          target,
3280          null,
3281          install,
3282          requireConfirm,
3283          "AMO"
3284        );
3285
3286        let id = this.nextInstall++;
3287        let { listener, installPromise } = makeListener(
3288          id,
3289          target.messageManager
3290        );
3291        install.addListener(listener);
3292
3293        this.installs.set(id, {
3294          install,
3295          target,
3296          listener,
3297          installPromise,
3298          messageManager: target.messageManager,
3299        });
3300
3301        let result = { id };
3302        this.copyProps(install, result);
3303        return result;
3304      });
3305    },
3306
3307    async addonUninstall(target, id) {
3308      let addon = await AddonManager.getAddonByID(id);
3309      if (!addon) {
3310        return false;
3311      }
3312
3313      if (!(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
3314        return Promise.reject({ message: "Addon cannot be uninstalled" });
3315      }
3316
3317      try {
3318        addon.uninstall();
3319        return true;
3320      } catch (err) {
3321        Cu.reportError(err);
3322        return false;
3323      }
3324    },
3325
3326    async addonSetEnabled(target, id, value) {
3327      let addon = await AddonManager.getAddonByID(id);
3328      if (!addon) {
3329        throw new Error(`No such addon ${id}`);
3330      }
3331
3332      if (value) {
3333        await addon.enable();
3334      } else {
3335        await addon.disable();
3336      }
3337    },
3338
3339    async addonInstallDoInstall(target, id) {
3340      let state = this.installs.get(id);
3341      if (!state) {
3342        throw new Error(`invalid id ${id}`);
3343      }
3344
3345      let addon = await state.install.install();
3346
3347      if (addon.type == "theme" && !addon.appDisabled) {
3348        await addon.enable();
3349      }
3350
3351      await new Promise(resolve => {
3352        let subject = {
3353          wrappedJSObject: { target, addon, callback: resolve },
3354        };
3355        Services.obs.notifyObservers(subject, "webextension-install-notify");
3356      });
3357    },
3358
3359    addonInstallCancel(target, id) {
3360      let state = this.installs.get(id);
3361      if (!state) {
3362        return Promise.reject(`invalid id ${id}`);
3363      }
3364      return Promise.resolve(state.install.cancel());
3365    },
3366
3367    clearInstalls(ids) {
3368      for (let id of ids) {
3369        this.forgetInstall(id);
3370      }
3371    },
3372
3373    clearInstallsFrom(mm) {
3374      for (let [id, info] of this.installs) {
3375        if (info.messageManager == mm) {
3376          this.forgetInstall(id);
3377        }
3378      }
3379    },
3380
3381    async addonReportAbuse(target, id) {
3382      if (!Services.prefs.getBoolPref(PREF_AMO_ABUSEREPORT, false)) {
3383        return Promise.reject({
3384          message: "amWebAPI reportAbuse not supported",
3385        });
3386      }
3387
3388      let existingDialog = AbuseReporter.getOpenDialog();
3389      if (existingDialog) {
3390        existingDialog.close();
3391      }
3392
3393      const dialog = await AbuseReporter.openDialog(id, "amo", target).catch(
3394        err => {
3395          Cu.reportError(err);
3396          return Promise.reject({
3397            message: "Error creating abuse report",
3398          });
3399        }
3400      );
3401
3402      return dialog.promiseReport.then(
3403        async report => {
3404          if (!report) {
3405            return false;
3406          }
3407
3408          await report.submit().catch(err => {
3409            Cu.reportError(err);
3410            return Promise.reject({
3411              message: "Error submitting abuse report",
3412            });
3413          });
3414
3415          return true;
3416        },
3417        err => {
3418          Cu.reportError(err);
3419          dialog.close();
3420          return Promise.reject({
3421            message: "Error creating abuse report",
3422          });
3423        }
3424      );
3425    },
3426  },
3427};
3428
3429/**
3430 * Should not be used outside of core Mozilla code. This is a private API for
3431 * the startup and platform integration code to use. Refer to the methods on
3432 * AddonManagerInternal for documentation however note that these methods are
3433 * subject to change at any time.
3434 */
3435var AddonManagerPrivate = {
3436  startup() {
3437    AddonManagerInternal.startup();
3438  },
3439
3440  addonIsActive(addonId) {
3441    return AddonManagerInternal._getProviderByName("XPIProvider").addonIsActive(
3442      addonId
3443    );
3444  },
3445
3446  /**
3447   * Gets an array of add-ons which were side-loaded prior to the last
3448   * startup, and are currently disabled.
3449   *
3450   * @returns {Promise<Array<Addon>>}
3451   */
3452  getNewSideloads() {
3453    return AddonManagerInternal._getProviderByName(
3454      "XPIProvider"
3455    ).getNewSideloads();
3456  },
3457
3458  get browserUpdated() {
3459    return gBrowserUpdated;
3460  },
3461
3462  registerProvider(aProvider, aTypes) {
3463    AddonManagerInternal.registerProvider(aProvider, aTypes);
3464  },
3465
3466  unregisterProvider(aProvider) {
3467    AddonManagerInternal.unregisterProvider(aProvider);
3468  },
3469
3470  /**
3471   * Get a list of addon types that was passed to registerProvider for the
3472   * provider with the given name.
3473   *
3474   * @param {string} aProviderName
3475   * @returns {Array<string>}
3476   */
3477  getAddonTypesByProvider(aProviderName) {
3478    if (!gStarted) {
3479      throw Components.Exception(
3480        "AddonManager is not initialized",
3481        Cr.NS_ERROR_NOT_INITIALIZED
3482      );
3483    }
3484
3485    for (let [provider, addonTypes] of AddonManagerInternal.typesByProvider) {
3486      if (providerName(provider) === aProviderName) {
3487        // Return an array because methods such as getAddonsByTypes expect
3488        // aTypes to be an array.
3489        return Array.from(addonTypes);
3490      }
3491    }
3492    throw Components.Exception(
3493      `No addonTypes found for provider: ${aProviderName}`,
3494      Cr.NS_ERROR_INVALID_ARG
3495    );
3496  },
3497
3498  markProviderSafe(aProvider) {
3499    AddonManagerInternal.markProviderSafe(aProvider);
3500  },
3501
3502  backgroundUpdateCheck() {
3503    return AddonManagerInternal.backgroundUpdateCheck();
3504  },
3505
3506  backgroundUpdateTimerHandler() {
3507    // Don't return the promise here, since the caller doesn't care.
3508    AddonManagerInternal.backgroundUpdateCheck();
3509  },
3510
3511  addStartupChange(aType, aID) {
3512    AddonManagerInternal.addStartupChange(aType, aID);
3513  },
3514
3515  removeStartupChange(aType, aID) {
3516    AddonManagerInternal.removeStartupChange(aType, aID);
3517  },
3518
3519  notifyAddonChanged(aID, aType, aPendingRestart) {
3520    return AddonManagerInternal.notifyAddonChanged(aID, aType, aPendingRestart);
3521  },
3522
3523  updateAddonAppDisabledStates() {
3524    AddonManagerInternal.updateAddonAppDisabledStates();
3525  },
3526
3527  updateAddonRepositoryData() {
3528    return AddonManagerInternal.updateAddonRepositoryData();
3529  },
3530
3531  callInstallListeners(...aArgs) {
3532    return AddonManagerInternal.callInstallListeners.apply(
3533      AddonManagerInternal,
3534      aArgs
3535    );
3536  },
3537
3538  callAddonListeners(...aArgs) {
3539    AddonManagerInternal.callAddonListeners.apply(AddonManagerInternal, aArgs);
3540  },
3541
3542  AddonAuthor,
3543
3544  AddonScreenshot,
3545
3546  get BOOTSTRAP_REASONS() {
3547    return AddonManagerInternal._getProviderByName("XPIProvider")
3548      .BOOTSTRAP_REASONS;
3549  },
3550
3551  recordTimestamp(name, value) {
3552    AddonManagerInternal.recordTimestamp(name, value);
3553  },
3554
3555  _simpleMeasures: {},
3556  recordSimpleMeasure(name, value) {
3557    this._simpleMeasures[name] = value;
3558  },
3559
3560  recordException(aModule, aContext, aException) {
3561    let report = {
3562      module: aModule,
3563      context: aContext,
3564    };
3565
3566    if (typeof aException == "number") {
3567      report.message = Components.Exception("", aException).name;
3568    } else {
3569      report.message = aException.toString();
3570      if (aException.fileName) {
3571        report.file = aException.fileName;
3572        report.line = aException.lineNumber;
3573      }
3574    }
3575
3576    this._simpleMeasures.exception = report;
3577  },
3578
3579  getSimpleMeasures() {
3580    return this._simpleMeasures;
3581  },
3582
3583  getTelemetryDetails() {
3584    return AddonManagerInternal.telemetryDetails;
3585  },
3586
3587  setTelemetryDetails(aProvider, aDetails) {
3588    AddonManagerInternal.telemetryDetails[aProvider] = aDetails;
3589  },
3590
3591  // Start a timer, record a simple measure of the time interval when
3592  // timer.done() is called
3593  simpleTimer(aName) {
3594    let startTime = Cu.now();
3595    return {
3596      done: () =>
3597        this.recordSimpleMeasure(aName, Math.round(Cu.now() - startTime)),
3598    };
3599  },
3600
3601  async recordTiming(name, task) {
3602    let timer = this.simpleTimer(name);
3603    try {
3604      return await task();
3605    } finally {
3606      timer.done();
3607    }
3608  },
3609
3610  /**
3611   * Helper to call update listeners when no update is available.
3612   *
3613   * This can be used as an implementation for Addon.findUpdates() when
3614   * no update mechanism is available.
3615   */
3616  callNoUpdateListeners(addon, listener, reason, appVersion, platformVersion) {
3617    if ("onNoCompatibilityUpdateAvailable" in listener) {
3618      safeCall(listener.onNoCompatibilityUpdateAvailable.bind(listener), addon);
3619    }
3620    if ("onNoUpdateAvailable" in listener) {
3621      safeCall(listener.onNoUpdateAvailable.bind(listener), addon);
3622    }
3623    if ("onUpdateFinished" in listener) {
3624      safeCall(listener.onUpdateFinished.bind(listener), addon);
3625    }
3626  },
3627
3628  get webExtensionsMinPlatformVersion() {
3629    return gWebExtensionsMinPlatformVersion;
3630  },
3631
3632  hasUpgradeListener(aId) {
3633    return AddonManagerInternal.upgradeListeners.has(aId);
3634  },
3635
3636  getUpgradeListener(aId) {
3637    return AddonManagerInternal.upgradeListeners.get(aId);
3638  },
3639
3640  get externalExtensionLoaders() {
3641    return AddonManagerInternal.externalExtensionLoaders;
3642  },
3643
3644  /**
3645   * Predicate that returns true if we think the given extension ID
3646   * might have been generated by XPIProvider.
3647   */
3648  isTemporaryInstallID(extensionId) {
3649    if (!gStarted) {
3650      throw Components.Exception(
3651        "AddonManager is not initialized",
3652        Cr.NS_ERROR_NOT_INITIALIZED
3653      );
3654    }
3655
3656    if (!extensionId || typeof extensionId != "string") {
3657      throw Components.Exception(
3658        "extensionId must be a string",
3659        Cr.NS_ERROR_INVALID_ARG
3660      );
3661    }
3662
3663    return AddonManagerInternal._getProviderByName(
3664      "XPIProvider"
3665    ).isTemporaryInstallID(extensionId);
3666  },
3667
3668  isDBLoaded() {
3669    let provider = AddonManagerInternal._getProviderByName("XPIProvider");
3670    return provider ? provider.isDBLoaded : false;
3671  },
3672
3673  get databaseReady() {
3674    let provider = AddonManagerInternal._getProviderByName("XPIProvider");
3675    return provider ? provider.databaseReady : new Promise(() => {});
3676  },
3677
3678  /**
3679   * Async shutdown barrier which blocks the completion of add-on
3680   * manager shutdown. This should generally only be used by add-on
3681   * providers (i.e., XPIProvider) to complete their final shutdown
3682   * tasks.
3683   */
3684  get finalShutdown() {
3685    return gFinalShutdownBarrier.client;
3686  },
3687};
3688
3689/**
3690 * This is the public API that UI and developers should be calling. All methods
3691 * just forward to AddonManagerInternal.
3692 * @class
3693 */
3694var AddonManager = {
3695  // Map used to convert the known install source hostnames into the value to set into the
3696  // telemetry events.
3697  _installHostSource: new Map([
3698    ["addons.mozilla.org", "amo"],
3699    ["discovery.addons.mozilla.org", "disco"],
3700  ]),
3701
3702  // Constants for the AddonInstall.state property
3703  // These will show up as AddonManager.STATE_* (eg, STATE_AVAILABLE)
3704  _states: new Map([
3705    // The install is available for download.
3706    ["STATE_AVAILABLE", 0],
3707    // The install is being downloaded.
3708    ["STATE_DOWNLOADING", 1],
3709    // The install is checking the update for compatibility information.
3710    ["STATE_CHECKING_UPDATE", 2],
3711    // The install is downloaded and ready to install.
3712    ["STATE_DOWNLOADED", 3],
3713    // The download failed.
3714    ["STATE_DOWNLOAD_FAILED", 4],
3715    // The install may not proceed until the user accepts a prompt
3716    ["STATE_AWAITING_PROMPT", 5],
3717    // Any prompts are done
3718    ["STATE_PROMPTS_DONE", 6],
3719    // The install has been postponed.
3720    ["STATE_POSTPONED", 7],
3721    // The install is ready to be applied.
3722    ["STATE_READY", 8],
3723    // The add-on is being installed.
3724    ["STATE_INSTALLING", 9],
3725    // The add-on has been installed.
3726    ["STATE_INSTALLED", 10],
3727    // The install failed.
3728    ["STATE_INSTALL_FAILED", 11],
3729    // The install has been cancelled.
3730    ["STATE_CANCELLED", 12],
3731  ]),
3732
3733  // Constants representing different types of errors while downloading an
3734  // add-on.
3735  // These will show up as AddonManager.ERROR_* (eg, ERROR_NETWORK_FAILURE)
3736  // The _errors codes are translated to text for a panel in browser-addons.js.
3737  // The text is located in browser.properties.
3738  _errors: new Map([
3739    // The download failed due to network problems.
3740    ["ERROR_NETWORK_FAILURE", -1],
3741    // The downloaded file did not match the provided hash.
3742    ["ERROR_INCORRECT_HASH", -2],
3743    // The downloaded file seems to be corrupted in some way.
3744    ["ERROR_CORRUPT_FILE", -3],
3745    // An error occurred trying to write to the filesystem.
3746    ["ERROR_FILE_ACCESS", -4],
3747    // The add-on must be signed and isn't.
3748    ["ERROR_SIGNEDSTATE_REQUIRED", -5],
3749    // The downloaded add-on had a different type than expected.
3750    // TODO Bug 1740792
3751    ["ERROR_UNEXPECTED_ADDON_TYPE", -6],
3752    // The addon did not have the expected ID
3753    // TODO Bug 1740792
3754    ["ERROR_INCORRECT_ID", -7],
3755    // The addon install_origins does not list the 3rd party domain.
3756    ["ERROR_INVALID_DOMAIN", -8],
3757  ]),
3758  // The update check timed out
3759  ERROR_TIMEOUT: -1,
3760  // There was an error while downloading the update information.
3761  ERROR_DOWNLOAD_ERROR: -2,
3762  // The update information was malformed in some way.
3763  ERROR_PARSE_ERROR: -3,
3764  // The update information was not in any known format.
3765  ERROR_UNKNOWN_FORMAT: -4,
3766  // The update information was not correctly signed or there was an SSL error.
3767  ERROR_SECURITY_ERROR: -5,
3768  // The update was cancelled
3769  ERROR_CANCELLED: -6,
3770  // These must be kept in sync with AddonUpdateChecker.
3771  // No error was encountered.
3772  UPDATE_STATUS_NO_ERROR: 0,
3773  // The update check timed out
3774  UPDATE_STATUS_TIMEOUT: -1,
3775  // There was an error while downloading the update information.
3776  UPDATE_STATUS_DOWNLOAD_ERROR: -2,
3777  // The update information was malformed in some way.
3778  UPDATE_STATUS_PARSE_ERROR: -3,
3779  // The update information was not in any known format.
3780  UPDATE_STATUS_UNKNOWN_FORMAT: -4,
3781  // The update information was not correctly signed or there was an SSL error.
3782  UPDATE_STATUS_SECURITY_ERROR: -5,
3783  // The update was cancelled.
3784  UPDATE_STATUS_CANCELLED: -6,
3785  // Constants to indicate why an update check is being performed
3786  // Update check has been requested by the user.
3787  UPDATE_WHEN_USER_REQUESTED: 1,
3788  // Update check is necessary to see if the Addon is compatibile with a new
3789  // version of the application.
3790  UPDATE_WHEN_NEW_APP_DETECTED: 2,
3791  // Update check is necessary because a new application has been installed.
3792  UPDATE_WHEN_NEW_APP_INSTALLED: 3,
3793  // Update check is a regular background update check.
3794  UPDATE_WHEN_PERIODIC_UPDATE: 16,
3795  // Update check is needed to check an Addon that is being installed.
3796  UPDATE_WHEN_ADDON_INSTALLED: 17,
3797
3798  // Constants for operations in Addon.pendingOperations
3799  // Indicates that the Addon has no pending operations.
3800  PENDING_NONE: 0,
3801  // Indicates that the Addon will be enabled after the application restarts.
3802  PENDING_ENABLE: 1,
3803  // Indicates that the Addon will be disabled after the application restarts.
3804  PENDING_DISABLE: 2,
3805  // Indicates that the Addon will be uninstalled after the application restarts.
3806  PENDING_UNINSTALL: 4,
3807  // Indicates that the Addon will be installed after the application restarts.
3808  PENDING_INSTALL: 8,
3809  PENDING_UPGRADE: 16,
3810
3811  // Constants for operations in Addon.operationsRequiringRestart
3812  // Indicates that restart isn't required for any operation.
3813  OP_NEEDS_RESTART_NONE: 0,
3814  // Indicates that restart is required for enabling the addon.
3815  OP_NEEDS_RESTART_ENABLE: 1,
3816  // Indicates that restart is required for disabling the addon.
3817  OP_NEEDS_RESTART_DISABLE: 2,
3818  // Indicates that restart is required for uninstalling the addon.
3819  OP_NEEDS_RESTART_UNINSTALL: 4,
3820  // Indicates that restart is required for installing the addon.
3821  OP_NEEDS_RESTART_INSTALL: 8,
3822
3823  // Constants for permissions in Addon.permissions.
3824  // Indicates that the Addon can be uninstalled.
3825  PERM_CAN_UNINSTALL: 1,
3826  // Indicates that the Addon can be enabled by the user.
3827  PERM_CAN_ENABLE: 2,
3828  // Indicates that the Addon can be disabled by the user.
3829  PERM_CAN_DISABLE: 4,
3830  // Indicates that the Addon can be upgraded.
3831  PERM_CAN_UPGRADE: 8,
3832  // Indicates that the Addon can be set to be allowed/disallowed
3833  // in private browsing windows.
3834  PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS: 32,
3835  // Indicates that internal APIs can uninstall the add-on, even if the
3836  // front-end cannot.
3837  PERM_API_CAN_UNINSTALL: 64,
3838
3839  // General descriptions of where items are installed.
3840  // Installed in this profile.
3841  SCOPE_PROFILE: 1,
3842  // Installed for all of this user's profiles.
3843  SCOPE_USER: 2,
3844  // Installed and owned by the application.
3845  SCOPE_APPLICATION: 4,
3846  // Installed for all users of the computer.
3847  SCOPE_SYSTEM: 8,
3848  // Installed temporarily
3849  SCOPE_TEMPORARY: 16,
3850  // The combination of all scopes.
3851  SCOPE_ALL: 31,
3852
3853  // Constants for Addon.applyBackgroundUpdates.
3854  // Indicates that the Addon should not update automatically.
3855  AUTOUPDATE_DISABLE: 0,
3856  // Indicates that the Addon should update automatically only if
3857  // that's the global default.
3858  AUTOUPDATE_DEFAULT: 1,
3859  // Indicates that the Addon should update automatically.
3860  AUTOUPDATE_ENABLE: 2,
3861
3862  // Constants for how Addon options should be shown.
3863  // Options will be displayed in a new tab, if possible
3864  OPTIONS_TYPE_TAB: 3,
3865  // Similar to OPTIONS_TYPE_INLINE, but rather than generating inline
3866  // options from a specially-formatted XUL file, the contents of the
3867  // file are simply displayed in an inline <browser> element.
3868  OPTIONS_TYPE_INLINE_BROWSER: 5,
3869
3870  // Constants for displayed or hidden options notifications
3871  // Options notification will be displayed
3872  OPTIONS_NOTIFICATION_DISPLAYED: "addon-options-displayed",
3873  // Options notification will be hidden
3874  OPTIONS_NOTIFICATION_HIDDEN: "addon-options-hidden",
3875
3876  // Constants for getStartupChanges, addStartupChange and removeStartupChange
3877  // Add-ons that were detected as installed during startup. Doesn't include
3878  // add-ons that were pending installation the last time the application ran.
3879  STARTUP_CHANGE_INSTALLED: "installed",
3880  // Add-ons that were detected as changed during startup. This includes an
3881  // add-on moving to a different location, changing version or just having
3882  // been detected as possibly changed.
3883  STARTUP_CHANGE_CHANGED: "changed",
3884  // Add-ons that were detected as uninstalled during startup. Doesn't include
3885  // add-ons that were pending uninstallation the last time the application ran.
3886  STARTUP_CHANGE_UNINSTALLED: "uninstalled",
3887  // Add-ons that were detected as disabled during startup, normally because of
3888  // an application change making an add-on incompatible. Doesn't include
3889  // add-ons that were pending being disabled the last time the application ran.
3890  STARTUP_CHANGE_DISABLED: "disabled",
3891  // Add-ons that were detected as enabled during startup, normally because of
3892  // an application change making an add-on compatible. Doesn't include
3893  // add-ons that were pending being enabled the last time the application ran.
3894  STARTUP_CHANGE_ENABLED: "enabled",
3895
3896  // Constants for Addon.signedState. Any states that should cause an add-on
3897  // to be unusable in builds that require signing should have negative values.
3898  // Add-on signing is not required, e.g. because the pref is disabled.
3899  SIGNEDSTATE_NOT_REQUIRED: undefined,
3900  // Add-on is signed but signature verification has failed.
3901  SIGNEDSTATE_BROKEN: -2,
3902  // Add-on may be signed but by an certificate that doesn't chain to our
3903  // our trusted certificate.
3904  SIGNEDSTATE_UNKNOWN: -1,
3905  // Add-on is unsigned.
3906  SIGNEDSTATE_MISSING: 0,
3907  // Add-on is preliminarily reviewed.
3908  SIGNEDSTATE_PRELIMINARY: 1,
3909  // Add-on is fully reviewed.
3910  SIGNEDSTATE_SIGNED: 2,
3911  // Add-on is system add-on.
3912  SIGNEDSTATE_SYSTEM: 3,
3913  // Add-on is signed with a "Mozilla Extensions" certificate
3914  SIGNEDSTATE_PRIVILEGED: 4,
3915
3916  get __AddonManagerInternal__() {
3917    return AppConstants.DEBUG ? AddonManagerInternal : undefined;
3918  },
3919
3920  /** Boolean indicating whether AddonManager startup has completed. */
3921  get isReady() {
3922    return gStartupComplete && !gShutdownInProgress;
3923  },
3924
3925  /**
3926   * A promise that is resolved when the AddonManager startup has completed.
3927   * This may be rejected if startup of the AddonManager is not successful, or
3928   * if shutdown is started before the AddonManager has finished starting.
3929   */
3930  get readyPromise() {
3931    return gStartedPromise.promise;
3932  },
3933
3934  /** @constructor */
3935  init() {
3936    this._stateToString = new Map();
3937    for (let [name, value] of this._states) {
3938      this[name] = value;
3939      this._stateToString.set(value, name);
3940    }
3941    this._errorToString = new Map();
3942    for (let [name, value] of this._errors) {
3943      this[name] = value;
3944      this._errorToString.set(value, name);
3945    }
3946  },
3947
3948  stateToString(state) {
3949    return this._stateToString.get(state);
3950  },
3951
3952  errorToString(err) {
3953    return err ? this._errorToString.get(err) : null;
3954  },
3955
3956  getInstallSourceFromHost(host) {
3957    if (this._installHostSource.has(host)) {
3958      return this._installHostSource.get(host);
3959    }
3960
3961    if (WEBAPI_TEST_INSTALL_HOSTS.includes(host)) {
3962      return "test-host";
3963    }
3964
3965    return "unknown";
3966  },
3967
3968  getInstallForURL(aUrl, aOptions) {
3969    return AddonManagerInternal.getInstallForURL(aUrl, aOptions);
3970  },
3971
3972  getInstallForFile(
3973    aFile,
3974    aMimetype,
3975    aTelemetryInfo,
3976    aUseSystemLocation = false
3977  ) {
3978    return AddonManagerInternal.getInstallForFile(
3979      aFile,
3980      aMimetype,
3981      aTelemetryInfo,
3982      aUseSystemLocation
3983    );
3984  },
3985
3986  uninstallSystemProfileAddon(aID) {
3987    return AddonManagerInternal.uninstallSystemProfileAddon(aID);
3988  },
3989
3990  stageLangpacksForAppUpdate(appVersion, platformVersion) {
3991    return AddonManagerInternal._getProviderByName(
3992      "XPIProvider"
3993    ).stageLangpacksForAppUpdate(appVersion, platformVersion);
3994  },
3995
3996  /**
3997   * Gets an array of add-on IDs that changed during the most recent startup.
3998   *
3999   * @param  aType
4000   *         The type of startup change to get
4001   * @return An array of add-on IDs
4002   */
4003  getStartupChanges(aType) {
4004    if (!(aType in AddonManagerInternal.startupChanges)) {
4005      return [];
4006    }
4007    return AddonManagerInternal.startupChanges[aType].slice(0);
4008  },
4009
4010  getAddonByID(aID) {
4011    return AddonManagerInternal.getAddonByID(aID);
4012  },
4013
4014  getAddonBySyncGUID(aGUID) {
4015    return AddonManagerInternal.getAddonBySyncGUID(aGUID);
4016  },
4017
4018  getAddonsByIDs(aIDs) {
4019    return AddonManagerInternal.getAddonsByIDs(aIDs);
4020  },
4021
4022  getAddonsByTypes(aTypes) {
4023    return AddonManagerInternal.getAddonsByTypes(aTypes);
4024  },
4025
4026  getActiveAddons(aTypes) {
4027    return AddonManagerInternal.getActiveAddons(aTypes);
4028  },
4029
4030  getAllAddons() {
4031    return AddonManagerInternal.getAllAddons();
4032  },
4033
4034  getInstallsByTypes(aTypes) {
4035    return AddonManagerInternal.getInstallsByTypes(aTypes);
4036  },
4037
4038  getAllInstalls() {
4039    return AddonManagerInternal.getAllInstalls();
4040  },
4041
4042  isInstallEnabled(aType) {
4043    return AddonManagerInternal.isInstallEnabled(aType);
4044  },
4045
4046  isInstallAllowed(aType, aInstallingPrincipal) {
4047    return AddonManagerInternal.isInstallAllowed(aType, aInstallingPrincipal);
4048  },
4049
4050  installAddonFromWebpage(aType, aBrowser, aInstallingPrincipal, aInstall) {
4051    AddonManagerInternal.installAddonFromWebpage(
4052      aType,
4053      aBrowser,
4054      aInstallingPrincipal,
4055      aInstall
4056    );
4057  },
4058
4059  installAddonFromAOM(aBrowser, aUri, aInstall) {
4060    AddonManagerInternal.installAddonFromAOM(aBrowser, aUri, aInstall);
4061  },
4062
4063  installTemporaryAddon(aDirectory) {
4064    return AddonManagerInternal.installTemporaryAddon(aDirectory);
4065  },
4066
4067  installBuiltinAddon(aBase) {
4068    return AddonManagerInternal.installBuiltinAddon(aBase);
4069  },
4070
4071  maybeInstallBuiltinAddon(aID, aVersion, aBase) {
4072    return AddonManagerInternal.maybeInstallBuiltinAddon(aID, aVersion, aBase);
4073  },
4074
4075  addManagerListener(aListener) {
4076    AddonManagerInternal.addManagerListener(aListener);
4077  },
4078
4079  removeManagerListener(aListener) {
4080    AddonManagerInternal.removeManagerListener(aListener);
4081  },
4082
4083  addInstallListener(aListener) {
4084    AddonManagerInternal.addInstallListener(aListener);
4085  },
4086
4087  removeInstallListener(aListener) {
4088    AddonManagerInternal.removeInstallListener(aListener);
4089  },
4090
4091  getUpgradeListener(aId) {
4092    return AddonManagerInternal.upgradeListeners.get(aId);
4093  },
4094
4095  addUpgradeListener(aInstanceID, aCallback) {
4096    AddonManagerInternal.addUpgradeListener(aInstanceID, aCallback);
4097  },
4098
4099  removeUpgradeListener(aInstanceID) {
4100    return AddonManagerInternal.removeUpgradeListener(aInstanceID);
4101  },
4102
4103  addExternalExtensionLoader(loader) {
4104    return AddonManagerInternal.addExternalExtensionLoader(loader);
4105  },
4106
4107  addAddonListener(aListener) {
4108    AddonManagerInternal.addAddonListener(aListener);
4109  },
4110
4111  removeAddonListener(aListener) {
4112    AddonManagerInternal.removeAddonListener(aListener);
4113  },
4114
4115  hasAddonType(addonType) {
4116    return AddonManagerInternal.hasAddonType(addonType);
4117  },
4118
4119  /**
4120   * Determines whether an Addon should auto-update or not.
4121   *
4122   * @param  aAddon
4123   *         The Addon representing the add-on
4124   * @return true if the addon should auto-update, false otherwise.
4125   */
4126  shouldAutoUpdate(aAddon) {
4127    if (!aAddon || typeof aAddon != "object") {
4128      throw Components.Exception(
4129        "aAddon must be specified",
4130        Cr.NS_ERROR_INVALID_ARG
4131      );
4132    }
4133
4134    if (!("applyBackgroundUpdates" in aAddon)) {
4135      return false;
4136    }
4137    if (!(aAddon.permissions & AddonManager.PERM_CAN_UPGRADE)) {
4138      return false;
4139    }
4140    if (aAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_ENABLE) {
4141      return true;
4142    }
4143    if (aAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DISABLE) {
4144      return false;
4145    }
4146    return this.autoUpdateDefault;
4147  },
4148
4149  get checkCompatibility() {
4150    return AddonManagerInternal.checkCompatibility;
4151  },
4152
4153  set checkCompatibility(aValue) {
4154    AddonManagerInternal.checkCompatibility = aValue;
4155  },
4156
4157  get strictCompatibility() {
4158    return AddonManagerInternal.strictCompatibility;
4159  },
4160
4161  set strictCompatibility(aValue) {
4162    AddonManagerInternal.strictCompatibility = aValue;
4163  },
4164
4165  get checkUpdateSecurityDefault() {
4166    return AddonManagerInternal.checkUpdateSecurityDefault;
4167  },
4168
4169  get checkUpdateSecurity() {
4170    return AddonManagerInternal.checkUpdateSecurity;
4171  },
4172
4173  set checkUpdateSecurity(aValue) {
4174    AddonManagerInternal.checkUpdateSecurity = aValue;
4175  },
4176
4177  get updateEnabled() {
4178    return AddonManagerInternal.updateEnabled;
4179  },
4180
4181  set updateEnabled(aValue) {
4182    AddonManagerInternal.updateEnabled = aValue;
4183  },
4184
4185  get autoUpdateDefault() {
4186    return AddonManagerInternal.autoUpdateDefault;
4187  },
4188
4189  set autoUpdateDefault(aValue) {
4190    AddonManagerInternal.autoUpdateDefault = aValue;
4191  },
4192
4193  escapeAddonURI(aAddon, aUri, aAppVersion) {
4194    return AddonManagerInternal.escapeAddonURI(aAddon, aUri, aAppVersion);
4195  },
4196
4197  getPreferredIconURL(aAddon, aSize, aWindow = undefined) {
4198    return AddonManagerInternal.getPreferredIconURL(aAddon, aSize, aWindow);
4199  },
4200
4201  get webAPI() {
4202    return AddonManagerInternal.webAPI;
4203  },
4204
4205  /**
4206   * Async shutdown barrier which blocks the start of AddonManager
4207   * shutdown. Callers should add blockers to this barrier if they need
4208   * to complete add-on manager operations before it shuts down.
4209   */
4210  get beforeShutdown() {
4211    return gBeforeShutdownBarrier.client;
4212  },
4213};
4214
4215/**
4216 * Listens to the AddonManager install and addon events and send telemetry events.
4217 */
4218AMTelemetry = {
4219  telemetrySetupDone: false,
4220
4221  init() {
4222    // Enable the addonsManager telemetry event category before the AddonManager
4223    // has completed its startup, otherwise telemetry events recorded during the
4224    // AddonManager/XPIProvider startup will not be recorded.
4225    Services.telemetry.setEventRecordingEnabled("addonsManager", true);
4226  },
4227
4228  // This method is called by the AddonManager, once it has been started, so that we can
4229  // init the telemetry event category and start listening for the events related to the
4230  // addons installation and management.
4231  onStartup() {
4232    if (this.telemetrySetupDone) {
4233      return;
4234    }
4235
4236    this.telemetrySetupDone = true;
4237
4238    Services.obs.addObserver(this, "addon-install-origin-blocked");
4239    Services.obs.addObserver(this, "addon-install-disabled");
4240    Services.obs.addObserver(this, "addon-install-blocked");
4241
4242    AddonManager.addInstallListener(this);
4243    AddonManager.addAddonListener(this);
4244  },
4245
4246  // Observer Service notification callback.
4247
4248  observe(subject, topic, data) {
4249    switch (topic) {
4250      case "addon-install-blocked": {
4251        const { installs } = subject.wrappedJSObject;
4252        this.recordInstallEvent(installs[0], { step: "site_warning" });
4253        break;
4254      }
4255      case "addon-install-origin-blocked": {
4256        const { installs } = subject.wrappedJSObject;
4257        this.recordInstallEvent(installs[0], { step: "site_blocked" });
4258        break;
4259      }
4260      case "addon-install-disabled": {
4261        const { installs } = subject.wrappedJSObject;
4262        this.recordInstallEvent(installs[0], {
4263          step: "install_disabled_warning",
4264        });
4265        break;
4266      }
4267    }
4268  },
4269
4270  // AddonManager install listener callbacks.
4271
4272  onNewInstall(install) {
4273    this.recordInstallEvent(install, { step: "started" });
4274  },
4275
4276  onInstallCancelled(install) {
4277    this.recordInstallEvent(install, { step: "cancelled" });
4278  },
4279
4280  onInstallPostponed(install) {
4281    this.recordInstallEvent(install, { step: "postponed" });
4282  },
4283
4284  onInstallFailed(install) {
4285    this.recordInstallEvent(install, { step: "failed" });
4286  },
4287
4288  onInstallEnded(install) {
4289    this.recordInstallEvent(install, { step: "completed" });
4290    // Skip install_stats events for install objects related to.
4291    // add-on updates.
4292    if (!install.existingAddon) {
4293      this.recordInstallStatsEvent(install);
4294    }
4295  },
4296
4297  onDownloadStarted(install) {
4298    this.recordInstallEvent(install, { step: "download_started" });
4299  },
4300
4301  onDownloadCancelled(install) {
4302    this.recordInstallEvent(install, { step: "cancelled" });
4303  },
4304
4305  onDownloadEnded(install) {
4306    let download_time = Math.round(Cu.now() - install.downloadStartedAt);
4307    this.recordInstallEvent(install, {
4308      step: "download_completed",
4309      download_time,
4310    });
4311  },
4312
4313  onDownloadFailed(install) {
4314    let download_time = Math.round(Cu.now() - install.downloadStartedAt);
4315    this.recordInstallEvent(install, {
4316      step: "download_failed",
4317      download_time,
4318    });
4319  },
4320
4321  // Addon listeners callbacks.
4322
4323  onUninstalled(addon) {
4324    this.recordManageEvent(addon, "uninstall");
4325  },
4326
4327  onEnabled(addon) {
4328    this.recordManageEvent(addon, "enable");
4329  },
4330
4331  onDisabled(addon) {
4332    this.recordManageEvent(addon, "disable");
4333  },
4334
4335  // Internal helpers methods.
4336
4337  /**
4338   * Get a trimmed version of the given string if it is longer than 80 chars.
4339   *
4340   * @param {string} str
4341   *        The original string content.
4342   *
4343   * @returns {string}
4344   *          The trimmed version of the string when longer than 80 chars, or the given string
4345   *          unmodified otherwise.
4346   */
4347  getTrimmedString(str) {
4348    if (str.length <= 80) {
4349      return str;
4350    }
4351
4352    const length = str.length;
4353
4354    // Trim the string to prevent a flood of warnings messages logged internally by recordEvent,
4355    // the trimmed version is going to be composed by the first 40 chars and the last 37 and 3 dots
4356    // that joins the two parts, to visually indicate that the string has been trimmed.
4357    return `${str.slice(0, 40)}...${str.slice(length - 37, length)}`;
4358  },
4359
4360  /**
4361   * Retrieve the addonId for the given AddonInstall instance.
4362   *
4363   * @param {AddonInstall} install
4364   *        The AddonInstall instance to retrieve the addonId from.
4365   *
4366   * @returns {string | null}
4367   *          The addonId for the given AddonInstall instance (if any).
4368   */
4369  getAddonIdFromInstall(install) {
4370    // Returns the id of the extension that is being installed, as soon as the
4371    // addon is available in the AddonInstall instance (after being downloaded
4372    // and validated successfully).
4373    if (install.addon) {
4374      return install.addon.id;
4375    }
4376
4377    // While updating an addon, the existing addon can be
4378    // used to retrieve the addon id since the first update event.
4379    if (install.existingAddon) {
4380      return install.existingAddon.id;
4381    }
4382
4383    return null;
4384  },
4385
4386  /**
4387   * Retrieve the telemetry event's object property value for the given
4388   * AddonInstall instance.
4389   *
4390   * @param {AddonInstall} install
4391   *        The AddonInstall instance to retrieve the event object from.
4392   *
4393   * @returns {string}
4394   *          The object for the given AddonInstall instance.
4395   */
4396  getEventObjectFromInstall(install) {
4397    let addonType;
4398
4399    if (install.type) {
4400      // The AddonInstall wrapper already provides a type (if it was known when the
4401      // install object has been created).
4402      addonType = install.type;
4403    } else if (install.addon) {
4404      // The install flow has reached a step that has an addon instance which we can
4405      // check to know the extension type (e.g. after download for the DownloadAddonInstall).
4406      addonType = install.addon.type;
4407    } else if (install.existingAddon) {
4408      // The install flow is an update and we can look the existingAddon to check which was
4409      // the add-on type that is being installed.
4410      addonType = install.existingAddon.type;
4411    }
4412
4413    return this.getEventObjectFromAddonType(addonType);
4414  },
4415
4416  /**
4417   * Retrieve the telemetry event source for the given AddonInstall instance.
4418   *
4419   * @param {AddonInstall} install
4420   *        The AddonInstall instance to retrieve the source from.
4421   *
4422   * @returns {Object | null}
4423   *          The telemetry infor ({source, method}) from the given AddonInstall instance.
4424   */
4425  getInstallTelemetryInfo(install) {
4426    if (install.installTelemetryInfo) {
4427      return install.installTelemetryInfo;
4428    } else if (
4429      install.existingAddon &&
4430      install.existingAddon.installTelemetryInfo
4431    ) {
4432      // Get the install source from the existing addon (e.g. for an extension update).
4433      return install.existingAddon.installTelemetryInfo;
4434    }
4435
4436    return null;
4437  },
4438
4439  /**
4440   * Get the telemetry event's object property for the given addon type
4441   *
4442   * @param {string} addonType
4443   *        The addon type to convert into the related telemetry event object.
4444   *
4445   * @returns {string}
4446   *          The object for the given addon type.
4447   */
4448  getEventObjectFromAddonType(addonType) {
4449    switch (addonType) {
4450      case undefined:
4451        return "unknown";
4452      case "extension":
4453      case "theme":
4454      case "locale":
4455      case "dictionary":
4456      case "sitepermission":
4457        return addonType;
4458      default:
4459        // Currently this should only include gmp-plugins ("plugin").
4460        return "other";
4461    }
4462  },
4463
4464  convertToString(value) {
4465    if (value == null) {
4466      // Convert null and undefined to empty strings.
4467      return "";
4468    }
4469    switch (typeof value) {
4470      case "string":
4471        return value;
4472      case "boolean":
4473        return value ? "1" : "0";
4474    }
4475    return String(value);
4476  },
4477
4478  /**
4479   * Return the UTM parameters found in `sourceURL` for AMO attribution data.
4480   *
4481   * @param {string} sourceURL
4482   *        The source URL from where the add-on has been installed.
4483   *
4484   * @returns {object}
4485   *          An object containing the attribution data for AMO if any. Keys
4486   *          are defined in `AMO_ATTRIBUTION_DATA_KEYS`. Values are strings.
4487   */
4488  parseAttributionDataForAMO(sourceURL) {
4489    let searchParams;
4490
4491    try {
4492      searchParams = new URL(sourceURL).searchParams;
4493    } catch {
4494      return {};
4495    }
4496
4497    const utmKeys = [...searchParams.keys()].filter(key =>
4498      AMO_ATTRIBUTION_DATA_KEYS.includes(key)
4499    );
4500
4501    return utmKeys.reduce((params, key) => {
4502      let value = searchParams.get(key);
4503      if (typeof value === "string") {
4504        value = value.slice(0, AMO_ATTRIBUTION_DATA_MAX_LENGTH);
4505      }
4506
4507      return { ...params, [key]: value };
4508    }, {});
4509  },
4510
4511  /**
4512   * Record an "install stats" event when the source is included in
4513   * `AMO_ATTRIBUTION_ALLOWED_SOURCES`.
4514   *
4515   * @param {AddonInstall} install
4516   *        The AddonInstall instance to record an install_stats event for.
4517   */
4518  recordInstallStatsEvent(install) {
4519    const telemetryInfo = this.getInstallTelemetryInfo(install);
4520
4521    if (!AMO_ATTRIBUTION_ALLOWED_SOURCES.includes(telemetryInfo?.source)) {
4522      return;
4523    }
4524
4525    const method = "install_stats";
4526    const object = this.getEventObjectFromInstall(install);
4527    const addonId = this.getAddonIdFromInstall(install);
4528
4529    if (!addonId) {
4530      Cu.reportError(
4531        "Missing addonId when trying to record an install_stats event"
4532      );
4533      return;
4534    }
4535
4536    let extra = {
4537      addon_id: this.getTrimmedString(addonId),
4538    };
4539
4540    if (
4541      telemetryInfo?.source === "amo" &&
4542      typeof telemetryInfo?.sourceURL === "string"
4543    ) {
4544      extra = {
4545        ...extra,
4546        ...this.parseAttributionDataForAMO(telemetryInfo.sourceURL),
4547      };
4548    }
4549
4550    if (
4551      telemetryInfo?.source === "disco" &&
4552      typeof telemetryInfo?.taarRecommended === "boolean"
4553    ) {
4554      extra = {
4555        ...extra,
4556        taar_based: this.convertToString(telemetryInfo.taarRecommended),
4557      };
4558    }
4559
4560    this.recordEvent({ method, object, value: install.hashedAddonId, extra });
4561  },
4562
4563  /**
4564   * Convert all the telemetry event's extra_vars into strings, if needed.
4565   *
4566   * @param {object} extraVars
4567   * @returns {object} The formatted extra vars.
4568   */
4569  formatExtraVars({ addon, ...extraVars }) {
4570    if (addon) {
4571      extraVars.addonId = addon.id;
4572      extraVars.type = addon.type;
4573    }
4574
4575    // All the extra_vars in a telemetry event have to be strings.
4576    for (var [key, value] of Object.entries(extraVars)) {
4577      if (value == undefined) {
4578        delete extraVars[key];
4579      } else {
4580        extraVars[key] = this.convertToString(value);
4581      }
4582    }
4583
4584    if (extraVars.addonId) {
4585      extraVars.addonId = this.getTrimmedString(extraVars.addonId);
4586    }
4587
4588    return extraVars;
4589  },
4590
4591  /**
4592   * Record an install or update event for the given AddonInstall instance.
4593   *
4594   * @param {AddonInstall} install
4595   *        The AddonInstall instance to record an install or update event for.
4596   * @param {object} extraVars
4597   *        The additional extra_vars to include in the recorded event.
4598   * @param {string} extraVars.step
4599   *        The current step in the install or update flow.
4600   * @param {string} extraVars.download_time
4601   *        The number of ms needed to download the extension.
4602   * @param {string} extraVars.num_strings
4603   *        The number of permission description string for the extension
4604   *        permission doorhanger.
4605   */
4606  recordInstallEvent(install, extraVars) {
4607    // Early exit if AMTelemetry's telemetry setup has not been done yet.
4608    if (!this.telemetrySetupDone) {
4609      return;
4610    }
4611
4612    let extra = {};
4613
4614    let telemetryInfo = this.getInstallTelemetryInfo(install);
4615    if (telemetryInfo && typeof telemetryInfo.source === "string") {
4616      extra.source = telemetryInfo.source;
4617    }
4618
4619    if (extra.source === "internal") {
4620      // Do not record the telemetry event for installation sources
4621      // that are marked as "internal".
4622      return;
4623    }
4624
4625    // Also include the install source's method when applicable (e.g. install events with
4626    // source "about:addons" may have "install-from-file" or "url" as their source method).
4627    if (telemetryInfo && typeof telemetryInfo.method === "string") {
4628      extra.method = telemetryInfo.method;
4629    }
4630
4631    let addonId = this.getAddonIdFromInstall(install);
4632    let object = this.getEventObjectFromInstall(install);
4633
4634    let installId = String(install.installId);
4635    let eventMethod = install.existingAddon ? "update" : "install";
4636
4637    if (addonId) {
4638      extra.addon_id = this.getTrimmedString(addonId);
4639    }
4640
4641    if (install.error) {
4642      extra.error = AddonManager.errorToString(install.error);
4643    }
4644
4645    if (
4646      eventMethod === "install" &&
4647      Services.prefs.getBoolPref("extensions.install_origins.enabled", true)
4648    ) {
4649      // This is converted to "1" / "0".
4650      extra.install_origins = Array.isArray(install.addon?.installOrigins);
4651    }
4652
4653    if (eventMethod === "update") {
4654      // For "update" telemetry events, also include an extra var which determine
4655      // if the update has been requested by the user.
4656      extra.updated_from = install.isUserRequestedUpdate ? "user" : "app";
4657    }
4658
4659    // All the extra vars in a telemetry event have to be strings.
4660    extra = this.formatExtraVars({ ...extraVars, ...extra });
4661
4662    this.recordEvent({ method: eventMethod, object, value: installId, extra });
4663  },
4664
4665  /**
4666   * Record a manage event for the given addon.
4667   *
4668   * @param {AddonWrapper} addon
4669   *        The AddonWrapper instance.
4670   * @param {object} extraVars
4671   *        The additional extra_vars to include in the recorded event.
4672   * @param {string} extraVars.num_strings
4673   *        The number of permission description string for the extension
4674   *        permission doorhanger.
4675   */
4676  recordManageEvent(addon, method, extraVars) {
4677    // Early exit if AMTelemetry's telemetry setup has not been done yet.
4678    if (!this.telemetrySetupDone) {
4679      return;
4680    }
4681
4682    let extra = {};
4683
4684    if (addon.installTelemetryInfo) {
4685      if ("source" in addon.installTelemetryInfo) {
4686        extra.source = addon.installTelemetryInfo.source;
4687      }
4688
4689      // Also include the install source's method when applicable (e.g. install events with
4690      // source "about:addons" may have "install-from-file" or "url" as their source method).
4691      if ("method" in addon.installTelemetryInfo) {
4692        extra.method = addon.installTelemetryInfo.method;
4693      }
4694    }
4695
4696    if (extra.source === "internal") {
4697      // Do not record the telemetry event for installation sources
4698      // that are marked as "internal".
4699      return;
4700    }
4701
4702    let object = this.getEventObjectFromAddonType(addon.type);
4703    let value = this.getTrimmedString(addon.id);
4704
4705    extra = { ...extraVars, ...extra };
4706
4707    let hasExtraVars = !!Object.keys(extra).length;
4708    extra = this.formatExtraVars(extra);
4709
4710    this.recordEvent({
4711      method,
4712      object,
4713      value,
4714      extra: hasExtraVars ? extra : null,
4715    });
4716  },
4717
4718  /**
4719   * Record an event for when a link is clicked.
4720   *
4721   * @param {object} opts
4722   * @param {string} opts.object
4723   *        The object of the event, should be an identifier for where the link
4724   *        is located. The accepted values are listed in the
4725   *        addonsManager.link object of the Events.yaml file.
4726   * @param {string} opts.value The identifier for the link destination.
4727   * @param {object} opts.extra
4728   *        The extra data to be sent, all keys must be registered in the
4729   *        extra_keys section of addonsManager.link in Events.yaml.
4730   */
4731  recordLinkEvent({ object, value, extra = null }) {
4732    this.recordEvent({ method: "link", object, value, extra });
4733  },
4734
4735  /**
4736   * Record an event for an action that took place.
4737   *
4738   * @param {object} opts
4739   * @param {string} opts.object
4740   *        The object of the event, should an identifier for where the action
4741   *        took place. The accepted values are listed in the
4742   *        addonsManager.action object of the Events.yaml file.
4743   * @param {string} opts.action The identifier for the action.
4744   * @param {string} opts.value An optional value for the action.
4745   * @param {object} opts.addon
4746   *        An optional object with the "id" and "type" properties, for example
4747   *        an AddonWrapper object. Passing this will set some extra properties.
4748   * @param {string} opts.addon.id
4749   *        The add-on ID to assign to extra.addonId.
4750   * @param {string} opts.addon.type
4751   *        The add-on type to assign to extra.type.
4752   * @param {string} opts.view The current view, when object is aboutAddons.
4753   * @param {object} opts.extra
4754   *        The extra data to be sent, all keys must be registered in the
4755   *        extra_keys section of addonsManager.action in Events.yaml. If
4756   *        opts.addon is passed then it will overwrite the addonId and type
4757   *        properties in this object, if they are set.
4758   */
4759  recordActionEvent({ object, action, value, addon, view, extra }) {
4760    extra = { ...extra, action, addon, view };
4761    if (action === "installFromRecommendation") {
4762      extra.taar_based = !!addon.taarRecommended;
4763    }
4764    this.recordEvent({
4765      method: "action",
4766      object,
4767      // Treat null and undefined as null.
4768      value: value == null ? null : this.convertToString(value),
4769      extra: this.formatExtraVars(extra),
4770    });
4771  },
4772
4773  /**
4774   * Record an event for a view load in about:addons.
4775   *
4776   * @param {object} opts
4777   * @param {string} opts.view
4778   *        The identifier for the view. The accepted values are listed in the
4779   *        object property of addonsManager.view object of the Events.yaml
4780   *        file.
4781   * @param {AddonWrapper} opts.addon
4782   *        An optional add-on object related to the event.
4783   * @param {string} opts.type
4784   *        An optional type for the view. If opts.addon is set it will
4785   *        overwrite this value with the type of the add-on.
4786   * @param {boolean} opts.taarEnabled
4787   *        Set to true if taar-based discovery was enabled when the user
4788   *        did switch between about:addons views.
4789   */
4790  recordViewEvent({ view, addon, type, taarEnabled }) {
4791    this.recordEvent({
4792      method: "view",
4793      object: "aboutAddons",
4794      value: view,
4795      extra: this.formatExtraVars({
4796        type,
4797        addon,
4798        taar_enabled: taarEnabled,
4799      }),
4800    });
4801  },
4802
4803  /**
4804   * Record an event on abuse report submissions.
4805   *
4806   * @params {object} opts
4807   * @params {string} opts.addonId
4808   *         The id of the addon being reported.
4809   * @params {string} [opts.addonType]
4810   *         The type of the addon being reported  (only present for an existing
4811   *         addonId).
4812   * @params {string} [opts.errorType]
4813   *         The AbuseReport errorType for a submission failure.
4814   * @params {string} opts.reportEntryPoint
4815   *         The entry point of the abuse report.
4816   */
4817  recordReportEvent({ addonId, addonType, errorType, reportEntryPoint }) {
4818    this.recordEvent({
4819      method: "report",
4820      object: reportEntryPoint,
4821      value: addonId,
4822      extra: this.formatExtraVars({
4823        addon_type: addonType,
4824        error_type: errorType,
4825      }),
4826    });
4827  },
4828
4829  recordEvent({ method, object, value, extra }) {
4830    if (typeof value != "string") {
4831      // The value must be a string or null, make sure it's valid so sending
4832      // the event doesn't fail.
4833      value = null;
4834    }
4835    try {
4836      Services.telemetry.recordEvent(
4837        "addonsManager",
4838        method,
4839        object,
4840        value,
4841        extra
4842      );
4843    } catch (err) {
4844      // If the telemetry throws just log the error so it doesn't break any
4845      // functionality.
4846      Cu.reportError(err);
4847    }
4848  },
4849};
4850
4851AddonManager.init();
4852
4853// Setup the AMTelemetry once the AddonManager has been started.
4854AddonManager.addManagerListener(AMTelemetry);
4855
4856// load the timestamps module into AddonManagerInternal
4857ChromeUtils.import(
4858  "resource://gre/modules/TelemetryTimestamps.jsm",
4859  AddonManagerInternal
4860);
4861Object.freeze(AddonManagerInternal);
4862Object.freeze(AddonManagerPrivate);
4863Object.freeze(AddonManager);
4864