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