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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7/** 8 * This file contains most of the logic required to maintain the 9 * extensions database, including querying and modifying extension 10 * metadata. In general, we try to avoid loading it during startup when 11 * at all possible. Please keep that in mind when deciding whether to 12 * add code here or elsewhere. 13 */ 14 15/* eslint "valid-jsdoc": [2, {requireReturn: false, requireReturnDescription: false, prefer: {return: "returns"}}] */ 16 17var EXPORTED_SYMBOLS = ["AddonInternal", "XPIDatabase", "XPIDatabaseReconcile"]; 18 19const { XPCOMUtils } = ChromeUtils.import( 20 "resource://gre/modules/XPCOMUtils.jsm" 21); 22 23XPCOMUtils.defineLazyModuleGetters(this, { 24 AddonManager: "resource://gre/modules/AddonManager.jsm", 25 AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm", 26 AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm", 27 AddonSettings: "resource://gre/modules/addons/AddonSettings.jsm", 28 DeferredTask: "resource://gre/modules/DeferredTask.jsm", 29 ExtensionUtils: "resource://gre/modules/ExtensionUtils.jsm", 30 FileUtils: "resource://gre/modules/FileUtils.jsm", 31 PermissionsUtils: "resource://gre/modules/PermissionsUtils.jsm", 32 Services: "resource://gre/modules/Services.jsm", 33 34 Blocklist: "resource://gre/modules/Blocklist.jsm", 35 UpdateChecker: "resource://gre/modules/addons/XPIInstall.jsm", 36 XPIInstall: "resource://gre/modules/addons/XPIInstall.jsm", 37 XPIInternal: "resource://gre/modules/addons/XPIProvider.jsm", 38 XPIProvider: "resource://gre/modules/addons/XPIProvider.jsm", 39 verifyBundleSignedState: "resource://gre/modules/addons/XPIInstall.jsm", 40}); 41 42const { nsIBlocklistService } = Ci; 43 44// These are injected from XPIProvider.jsm 45/* globals BOOTSTRAP_REASONS, DB_SCHEMA, XPIStates, migrateAddonLoader */ 46 47for (let sym of [ 48 "BOOTSTRAP_REASONS", 49 "DB_SCHEMA", 50 "XPIStates", 51 "migrateAddonLoader", 52]) { 53 XPCOMUtils.defineLazyGetter(this, sym, () => XPIInternal[sym]); 54} 55 56const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm"); 57const LOGGER_ID = "addons.xpi-utils"; 58 59const nsIFile = Components.Constructor( 60 "@mozilla.org/file/local;1", 61 "nsIFile", 62 "initWithPath" 63); 64 65// Create a new logger for use by the Addons XPI Provider Utils 66// (Requires AddonManager.jsm) 67var logger = Log.repository.getLogger(LOGGER_ID); 68 69const KEY_PROFILEDIR = "ProfD"; 70const FILE_JSON_DB = "extensions.json"; 71 72const PREF_DB_SCHEMA = "extensions.databaseSchema"; 73const PREF_EM_AUTO_DISABLED_SCOPES = "extensions.autoDisableScopes"; 74const PREF_PENDING_OPERATIONS = "extensions.pendingOperations"; 75const PREF_XPI_PERMISSIONS_BRANCH = "xpinstall."; 76const PREF_XPI_SIGNATURES_DEV_ROOT = "xpinstall.signatures.dev-root"; 77 78const TOOLKIT_ID = "toolkit@mozilla.org"; 79 80const KEY_APP_SYSTEM_ADDONS = "app-system-addons"; 81const KEY_APP_SYSTEM_DEFAULTS = "app-system-defaults"; 82const KEY_APP_SYSTEM_PROFILE = "app-system-profile"; 83const KEY_APP_BUILTINS = "app-builtin"; 84const KEY_APP_SYSTEM_LOCAL = "app-system-local"; 85const KEY_APP_SYSTEM_SHARE = "app-system-share"; 86const KEY_APP_GLOBAL = "app-global"; 87const KEY_APP_PROFILE = "app-profile"; 88const KEY_APP_TEMPORARY = "app-temporary"; 89 90const DEFAULT_THEME_ID = "default-theme@mozilla.org"; 91 92// Properties to cache and reload when an addon installation is pending 93const PENDING_INSTALL_METADATA = [ 94 "syncGUID", 95 "targetApplications", 96 "userDisabled", 97 "softDisabled", 98 "embedderDisabled", 99 "existingAddonID", 100 "sourceURI", 101 "releaseNotesURI", 102 "installDate", 103 "updateDate", 104 "applyBackgroundUpdates", 105 "installTelemetryInfo", 106]; 107 108// Properties to save in JSON file 109const PROP_JSON_FIELDS = [ 110 "id", 111 "syncGUID", 112 "version", 113 "type", 114 "loader", 115 "updateURL", 116 "optionsURL", 117 "optionsType", 118 "optionsBrowserStyle", 119 "aboutURL", 120 "defaultLocale", 121 "visible", 122 "active", 123 "userDisabled", 124 "appDisabled", 125 "embedderDisabled", 126 "pendingUninstall", 127 "installDate", 128 "updateDate", 129 "applyBackgroundUpdates", 130 "path", 131 "skinnable", 132 "sourceURI", 133 "releaseNotesURI", 134 "softDisabled", 135 "foreignInstall", 136 "strictCompatibility", 137 "locales", 138 "targetApplications", 139 "targetPlatforms", 140 "signedState", 141 "signedDate", 142 "seen", 143 "dependencies", 144 "incognito", 145 "userPermissions", 146 "optionalPermissions", 147 "icons", 148 "iconURL", 149 "blocklistState", 150 "blocklistURL", 151 "startupData", 152 "previewImage", 153 "hidden", 154 "installTelemetryInfo", 155 "recommendationState", 156 "rootURI", 157]; 158 159const SIGNED_TYPES = new Set(["extension", "locale", "theme"]); 160 161// Time to wait before async save of XPI JSON database, in milliseconds 162const ASYNC_SAVE_DELAY_MS = 20; 163 164const LOCALE_BUNDLES = [ 165 "chrome://global/locale/global-extension-fields.properties", 166 "chrome://global/locale/app-extension-fields.properties", 167].map(url => Services.strings.createBundle(url)); 168 169/** 170 * Schedules an idle task, and returns a promise which resolves to an 171 * IdleDeadline when an idle slice is available. The caller should 172 * perform all of its idle work in the same micro-task, before the 173 * deadline is reached. 174 * 175 * @returns {Promise<IdleDeadline>} 176 */ 177function promiseIdleSlice() { 178 return new Promise(resolve => { 179 ChromeUtils.idleDispatch(resolve); 180 }); 181} 182 183let arrayForEach = Function.call.bind(Array.prototype.forEach); 184 185/** 186 * Loops over the given array, in the same way as Array forEach, but 187 * splitting the work among idle tasks. 188 * 189 * @param {Array} array 190 * The array to loop over. 191 * @param {function} func 192 * The function to call on each array element. 193 * @param {integer} [taskTimeMS = 5] 194 * The minimum time to allocate to each task. If less time than 195 * this is available in a given idle slice, and there are more 196 * elements to loop over, they will be deferred until the next 197 * idle slice. 198 */ 199async function idleForEach(array, func, taskTimeMS = 5) { 200 let deadline; 201 for (let i = 0; i < array.length; i++) { 202 if (!deadline || deadline.timeRemaining() < taskTimeMS) { 203 deadline = await promiseIdleSlice(); 204 } 205 func(array[i], i); 206 } 207} 208 209/** 210 * Asynchronously fill in the _repositoryAddon field for one addon 211 * 212 * @param {AddonInternal} aAddon 213 * The add-on to annotate. 214 * @returns {AddonInternal} 215 * The annotated add-on. 216 */ 217async function getRepositoryAddon(aAddon) { 218 if (aAddon) { 219 aAddon._repositoryAddon = await AddonRepository.getCachedAddonByID( 220 aAddon.id 221 ); 222 } 223 return aAddon; 224} 225 226/** 227 * Copies properties from one object to another. If no target object is passed 228 * a new object will be created and returned. 229 * 230 * @param {object} aObject 231 * An object to copy from 232 * @param {string[]} aProperties 233 * An array of properties to be copied 234 * @param {object?} [aTarget] 235 * An optional target object to copy the properties to 236 * @returns {Object} 237 * The object that the properties were copied onto 238 */ 239function copyProperties(aObject, aProperties, aTarget) { 240 if (!aTarget) { 241 aTarget = {}; 242 } 243 aProperties.forEach(function(aProp) { 244 if (aProp in aObject) { 245 aTarget[aProp] = aObject[aProp]; 246 } 247 }); 248 return aTarget; 249} 250 251// Maps instances of AddonInternal to AddonWrapper 252const wrapperMap = new WeakMap(); 253let addonFor = wrapper => wrapperMap.get(wrapper); 254 255const EMPTY_ARRAY = Object.freeze([]); 256 257let AddonWrapper; 258 259/** 260 * The AddonInternal is an internal only representation of add-ons. It 261 * may have come from the database or an extension manifest. 262 */ 263class AddonInternal { 264 constructor(addonData) { 265 this._wrapper = null; 266 this._selectedLocale = null; 267 this.active = false; 268 this.visible = false; 269 this.userDisabled = false; 270 this.appDisabled = false; 271 this.softDisabled = false; 272 this.embedderDisabled = false; 273 this.blocklistState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED; 274 this.blocklistURL = null; 275 this.sourceURI = null; 276 this.releaseNotesURI = null; 277 this.foreignInstall = false; 278 this.seen = true; 279 this.skinnable = false; 280 this.startupData = null; 281 this._hidden = false; 282 this.installTelemetryInfo = null; 283 this.rootURI = null; 284 this._updateInstall = null; 285 this.recommendationState = null; 286 287 this.inDatabase = false; 288 289 /** 290 * @property {Array<string>} dependencies 291 * An array of bootstrapped add-on IDs on which this add-on depends. 292 * The add-on will remain appDisabled if any of the dependent 293 * add-ons is not installed and enabled. 294 */ 295 this.dependencies = EMPTY_ARRAY; 296 297 if (addonData) { 298 copyProperties(addonData, PROP_JSON_FIELDS, this); 299 this.location = addonData.location; 300 301 if (!this.dependencies) { 302 this.dependencies = []; 303 } 304 Object.freeze(this.dependencies); 305 306 if (this.location) { 307 this.addedToDatabase(); 308 } 309 310 this.sourceBundle = addonData._sourceBundle; 311 } 312 } 313 314 get sourceBundle() { 315 return this._sourceBundle; 316 } 317 318 set sourceBundle(file) { 319 this._sourceBundle = file; 320 if (file) { 321 this.rootURI = XPIInternal.getURIForResourceInFile(file, "").spec; 322 } 323 } 324 325 get wrapper() { 326 if (!this._wrapper) { 327 this._wrapper = new AddonWrapper(this); 328 } 329 return this._wrapper; 330 } 331 332 get resolvedRootURI() { 333 return XPIInternal.maybeResolveURI(Services.io.newURI(this.rootURI)); 334 } 335 336 addedToDatabase() { 337 this._key = `${this.location.name}:${this.id}`; 338 this.inDatabase = true; 339 } 340 341 get isWebExtension() { 342 return this.loader == null; 343 } 344 345 get selectedLocale() { 346 if (this._selectedLocale) { 347 return this._selectedLocale; 348 } 349 350 /** 351 * this.locales is a list of objects that have property `locales`. 352 * It's value is an array of locale codes. 353 * 354 * First, we reduce this nested structure to a flat list of locale codes. 355 */ 356 const locales = [].concat(...this.locales.map(loc => loc.locales)); 357 358 let requestedLocales = Services.locale.requestedLocales; 359 360 /** 361 * If en-US is not in the list, add it as the last fallback. 362 */ 363 if (!requestedLocales.includes("en-US")) { 364 requestedLocales.push("en-US"); 365 } 366 367 /** 368 * Then we negotiate best locale code matching the app locales. 369 */ 370 let bestLocale = Services.locale.negotiateLanguages( 371 requestedLocales, 372 locales, 373 "und", 374 Services.locale.langNegStrategyLookup 375 )[0]; 376 377 /** 378 * If no match has been found, we'll assign the default locale as 379 * the selected one. 380 */ 381 if (bestLocale === "und") { 382 this._selectedLocale = this.defaultLocale; 383 } else { 384 /** 385 * Otherwise, we'll go through all locale entries looking for the one 386 * that has the best match in it's locales list. 387 */ 388 this._selectedLocale = this.locales.find(loc => 389 loc.locales.includes(bestLocale) 390 ); 391 } 392 393 return this._selectedLocale; 394 } 395 396 get providesUpdatesSecurely() { 397 return !this.updateURL || this.updateURL.startsWith("https:"); 398 } 399 400 get isCorrectlySigned() { 401 switch (this.location.name) { 402 case KEY_APP_SYSTEM_PROFILE: 403 // Add-ons installed via Normandy must be signed by the system 404 // key or the "Mozilla Extensions" key. 405 return [ 406 AddonManager.SIGNEDSTATE_SYSTEM, 407 AddonManager.SIGNEDSTATE_PRIVILEGED, 408 ].includes(this.signedState); 409 case KEY_APP_SYSTEM_ADDONS: 410 // System add-ons must be signed by the system key. 411 return this.signedState == AddonManager.SIGNEDSTATE_SYSTEM; 412 413 case KEY_APP_SYSTEM_DEFAULTS: 414 case KEY_APP_BUILTINS: 415 case KEY_APP_TEMPORARY: 416 // Temporary and built-in add-ons do not require signing. 417 return true; 418 419 case KEY_APP_SYSTEM_SHARE: 420 case KEY_APP_SYSTEM_LOCAL: 421 // On UNIX platforms except OSX, an additional location for system 422 // add-ons exists in /usr/{lib,share}/mozilla/extensions. Add-ons 423 // installed there do not require signing. 424 if (Services.appinfo.OS != "Darwin") { 425 return true; 426 } 427 break; 428 } 429 430 if (this.signedState === AddonManager.SIGNEDSTATE_NOT_REQUIRED) { 431 return true; 432 } 433 return this.signedState > AddonManager.SIGNEDSTATE_MISSING; 434 } 435 436 get isCompatible() { 437 return this.isCompatibleWith(); 438 } 439 440 // This matches Extension.isPrivileged with the exception of temporarily installed extensions. 441 get isPrivileged() { 442 return ( 443 this.signedState === AddonManager.SIGNEDSTATE_PRIVILEGED || 444 this.signedState === AddonManager.SIGNEDSTATE_SYSTEM || 445 this.location.isBuiltin 446 ); 447 } 448 449 get hidden() { 450 return this.location.hidden || (this._hidden && this.isPrivileged) || false; 451 } 452 453 set hidden(val) { 454 this._hidden = val; 455 } 456 457 get disabled() { 458 return ( 459 this.userDisabled || 460 this.appDisabled || 461 this.softDisabled || 462 this.embedderDisabled 463 ); 464 } 465 466 get isPlatformCompatible() { 467 if (!this.targetPlatforms.length) { 468 return true; 469 } 470 471 let matchedOS = false; 472 473 // If any targetPlatform matches the OS and contains an ABI then we will 474 // only match a targetPlatform that contains both the current OS and ABI 475 let needsABI = false; 476 477 // Some platforms do not specify an ABI, test against null in that case. 478 let abi = null; 479 try { 480 abi = Services.appinfo.XPCOMABI; 481 } catch (e) {} 482 483 // Something is causing errors in here 484 try { 485 for (let platform of this.targetPlatforms) { 486 if (platform.os == "Linux" || platform.os == Services.appinfo.OS) { 487 if (platform.abi) { 488 needsABI = true; 489 if (platform.abi === abi) { 490 return true; 491 } 492 } else { 493 matchedOS = true; 494 } 495 } 496 } 497 } catch (e) { 498 let message = 499 "Problem with addon " + 500 this.id + 501 " targetPlatforms " + 502 JSON.stringify(this.targetPlatforms); 503 logger.error(message, e); 504 AddonManagerPrivate.recordException("XPI", message, e); 505 // don't trust this add-on 506 return false; 507 } 508 509 return matchedOS && !needsABI; 510 } 511 512 isCompatibleWith(aAppVersion, aPlatformVersion) { 513 let app = this.matchingTargetApplication; 514 if (!app) { 515 return false; 516 } 517 518 // set reasonable defaults for minVersion and maxVersion 519 let minVersion = app.minVersion || "0"; 520 let maxVersion = app.maxVersion || "*"; 521 522 if (!aAppVersion) { 523 aAppVersion = Services.appinfo.version; 524 } 525 if (!aPlatformVersion) { 526 aPlatformVersion = Services.appinfo.platformVersion; 527 } 528 529 let version; 530 if (app.id == Services.appinfo.ID) { 531 version = aAppVersion; 532 } else if (app.id == TOOLKIT_ID) { 533 version = aPlatformVersion; 534 } 535 536 // Only extensions and dictionaries can be compatible by default; themes 537 // and language packs always use strict compatibility checking. 538 // Dictionaries are compatible by default unless requested by the dictinary. 539 if ( 540 !this.strictCompatibility && 541 (!AddonManager.strictCompatibility || this.type == "dictionary") 542 ) { 543 return Services.vc.compare(version, minVersion) >= 0; 544 } 545 546 return ( 547 Services.vc.compare(version, minVersion) >= 0 && 548 Services.vc.compare(version, maxVersion) <= 0 549 ); 550 } 551 552 get matchingTargetApplication() { 553 let app = null; 554 for (let targetApp of this.targetApplications) { 555 if (targetApp.id == Services.appinfo.ID) { 556 return targetApp; 557 } 558 if (targetApp.id == TOOLKIT_ID) { 559 app = targetApp; 560 } 561 } 562 return app; 563 } 564 565 async findBlocklistEntry() { 566 return Blocklist.getAddonBlocklistEntry(this.wrapper); 567 } 568 569 async updateBlocklistState(options = {}) { 570 if (this.location.isSystem || this.location.isBuiltin) { 571 return; 572 } 573 574 let { applySoftBlock = true, updateDatabase = true } = options; 575 576 let oldState = this.blocklistState; 577 578 let entry = await this.findBlocklistEntry(); 579 let newState = entry ? entry.state : Services.blocklist.STATE_NOT_BLOCKED; 580 581 this.blocklistState = newState; 582 this.blocklistURL = entry && entry.url; 583 584 let userDisabled, softDisabled; 585 // After a blocklist update, the blocklist service manually applies 586 // new soft blocks after displaying a UI, in which cases we need to 587 // skip updating it here. 588 if (applySoftBlock && oldState != newState) { 589 if (newState == Services.blocklist.STATE_SOFTBLOCKED) { 590 if (this.type == "theme") { 591 userDisabled = true; 592 } else { 593 softDisabled = !this.userDisabled; 594 } 595 } else { 596 softDisabled = false; 597 } 598 } 599 600 if (this.inDatabase && updateDatabase) { 601 await XPIDatabase.updateAddonDisabledState(this, { 602 userDisabled, 603 softDisabled, 604 }); 605 XPIDatabase.saveChanges(); 606 } else { 607 this.appDisabled = !XPIDatabase.isUsableAddon(this); 608 if (userDisabled !== undefined) { 609 this.userDisabled = userDisabled; 610 } 611 if (softDisabled !== undefined) { 612 this.softDisabled = softDisabled; 613 } 614 } 615 } 616 617 recordAddonBlockChangeTelemetry(reason) { 618 Blocklist.recordAddonBlockChangeTelemetry(this.wrapper, reason); 619 } 620 621 async setUserDisabled(val, allowSystemAddons = false) { 622 if (val == (this.userDisabled || this.softDisabled)) { 623 return; 624 } 625 626 if (this.inDatabase) { 627 // System add-ons should not be user disabled, as there is no UI to 628 // re-enable them. 629 if (this.location.isSystem && !allowSystemAddons) { 630 throw new Error(`Cannot disable system add-on ${this.id}`); 631 } 632 await XPIDatabase.updateAddonDisabledState(this, { userDisabled: val }); 633 } else { 634 this.userDisabled = val; 635 // When enabling remove the softDisabled flag 636 if (!val) { 637 this.softDisabled = false; 638 } 639 } 640 } 641 642 applyCompatibilityUpdate(aUpdate, aSyncCompatibility) { 643 let wasCompatible = this.isCompatible; 644 645 for (let targetApp of this.targetApplications) { 646 for (let updateTarget of aUpdate.targetApplications) { 647 if ( 648 targetApp.id == updateTarget.id && 649 (aSyncCompatibility || 650 Services.vc.compare(targetApp.maxVersion, updateTarget.maxVersion) < 651 0) 652 ) { 653 targetApp.minVersion = updateTarget.minVersion; 654 targetApp.maxVersion = updateTarget.maxVersion; 655 656 if (this.inDatabase) { 657 XPIDatabase.saveChanges(); 658 } 659 } 660 } 661 } 662 663 if (wasCompatible != this.isCompatible) { 664 if (this.inDatabase) { 665 XPIDatabase.updateAddonDisabledState(this); 666 } else { 667 this.appDisabled = !XPIDatabase.isUsableAddon(this); 668 } 669 } 670 } 671 672 toJSON() { 673 let obj = copyProperties(this, PROP_JSON_FIELDS); 674 obj.location = this.location.name; 675 return obj; 676 } 677 678 /** 679 * When an add-on install is pending its metadata will be cached in a file. 680 * This method reads particular properties of that metadata that may be newer 681 * than that in the extension manifest, like compatibility information. 682 * 683 * @param {Object} aObj 684 * A JS object containing the cached metadata 685 */ 686 importMetadata(aObj) { 687 for (let prop of PENDING_INSTALL_METADATA) { 688 if (!(prop in aObj)) { 689 continue; 690 } 691 692 this[prop] = aObj[prop]; 693 } 694 695 // Compatibility info may have changed so update appDisabled 696 this.appDisabled = !XPIDatabase.isUsableAddon(this); 697 } 698 699 permissions() { 700 let permissions = 0; 701 702 // Add-ons that aren't installed cannot be modified in any way 703 if (!this.inDatabase) { 704 return permissions; 705 } 706 707 if (!this.appDisabled) { 708 if (this.userDisabled || this.softDisabled) { 709 permissions |= AddonManager.PERM_CAN_ENABLE; 710 } else if (this.type != "theme" || this.id != DEFAULT_THEME_ID) { 711 // We do not expose disabling the default theme. 712 permissions |= AddonManager.PERM_CAN_DISABLE; 713 } 714 } 715 716 // Add-ons that are in locked install locations, or are pending uninstall 717 // cannot be uninstalled or upgraded. One caveat is extensions sideloaded 718 // from non-profile locations. Since Firefox 73(?), new sideloaded extensions 719 // from outside the profile have not been installed so any such extensions 720 // must be from an older profile. Users may uninstall such an extension which 721 // removes the related state from this profile but leaves the actual file alone 722 // (since it is outside this profile and may be in use in other profiles) 723 let changesAllowed = !this.location.locked && !this.pendingUninstall; 724 if (changesAllowed) { 725 // System add-on upgrades are triggered through a different mechanism (see updateSystemAddons()) 726 // Builtin addons are only upgraded with Firefox (or app) updates. 727 let isSystem = this.location.isSystem || this.location.isBuiltin; 728 // Add-ons that are installed by a file link cannot be upgraded. 729 if (!isSystem && !this.location.isLinkedAddon(this.id)) { 730 permissions |= AddonManager.PERM_CAN_UPGRADE; 731 } 732 } 733 734 // We allow uninstall of legacy sideloaded extensions, even when in locked locations, 735 // but we do not remove the addon file in that case. 736 let isLegacySideload = 737 this.foreignInstall && 738 !(this.location.scope & AddonSettings.SCOPES_SIDELOAD); 739 if (changesAllowed || isLegacySideload) { 740 permissions |= AddonManager.PERM_API_CAN_UNINSTALL; 741 if (!this.location.isBuiltin) { 742 permissions |= AddonManager.PERM_CAN_UNINSTALL; 743 } 744 } 745 746 // The permission to "toggle the private browsing access" is locked down 747 // when the extension has opted out or it gets the permission automatically 748 // on every extension startup (as system, privileged and builtin addons). 749 if ( 750 this.type === "extension" && 751 this.incognito !== "not_allowed" && 752 this.signedState !== AddonManager.SIGNEDSTATE_PRIVILEGED && 753 this.signedState !== AddonManager.SIGNEDSTATE_SYSTEM && 754 !this.location.isBuiltin 755 ) { 756 permissions |= AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS; 757 } 758 759 if (Services.policies) { 760 if (!Services.policies.isAllowed(`uninstall-extension:${this.id}`)) { 761 permissions &= ~AddonManager.PERM_CAN_UNINSTALL; 762 } 763 if (!Services.policies.isAllowed(`disable-extension:${this.id}`)) { 764 permissions &= ~AddonManager.PERM_CAN_DISABLE; 765 } 766 if (Services.policies.getExtensionSettings(this.id)?.updates_disabled) { 767 permissions &= ~AddonManager.PERM_CAN_UPGRADE; 768 } 769 } 770 771 return permissions; 772 } 773 774 propagateDisabledState(oldAddon) { 775 if (oldAddon) { 776 this.userDisabled = oldAddon.userDisabled; 777 this.embedderDisabled = oldAddon.embedderDisabled; 778 this.softDisabled = oldAddon.softDisabled; 779 this.blocklistState = oldAddon.blocklistState; 780 } 781 } 782} 783 784/** 785 * The AddonWrapper wraps an Addon to provide the data visible to consumers of 786 * the public API. 787 * 788 * @param {AddonInternal} aAddon 789 * The add-on object to wrap. 790 */ 791AddonWrapper = class { 792 constructor(aAddon) { 793 wrapperMap.set(this, aAddon); 794 } 795 796 get __AddonInternal__() { 797 return addonFor(this); 798 } 799 800 get seen() { 801 return addonFor(this).seen; 802 } 803 804 markAsSeen() { 805 addonFor(this).seen = true; 806 XPIDatabase.saveChanges(); 807 } 808 809 get installTelemetryInfo() { 810 const addon = addonFor(this); 811 if (!addon.installTelemetryInfo && addon.location) { 812 if (addon.location.isSystem) { 813 return { source: "system-addon" }; 814 } 815 816 if (addon.location.isTemporary) { 817 return { source: "temporary-addon" }; 818 } 819 } 820 821 return addon.installTelemetryInfo; 822 } 823 824 get temporarilyInstalled() { 825 return addonFor(this).location.isTemporary; 826 } 827 828 get aboutURL() { 829 return this.isActive ? addonFor(this).aboutURL : null; 830 } 831 832 get optionsURL() { 833 if (!this.isActive) { 834 return null; 835 } 836 837 let addon = addonFor(this); 838 if (addon.optionsURL) { 839 if (this.isWebExtension) { 840 // The internal object's optionsURL property comes from the addons 841 // DB and should be a relative URL. However, extensions with 842 // options pages installed before bug 1293721 was fixed got absolute 843 // URLs in the addons db. This code handles both cases. 844 let policy = WebExtensionPolicy.getByID(addon.id); 845 if (!policy) { 846 return null; 847 } 848 let base = policy.getURL(); 849 return new URL(addon.optionsURL, base).href; 850 } 851 return addon.optionsURL; 852 } 853 854 return null; 855 } 856 857 get optionsType() { 858 if (!this.isActive) { 859 return null; 860 } 861 862 let addon = addonFor(this); 863 let hasOptionsURL = !!this.optionsURL; 864 865 if (addon.optionsType) { 866 switch (parseInt(addon.optionsType, 10)) { 867 case AddonManager.OPTIONS_TYPE_TAB: 868 case AddonManager.OPTIONS_TYPE_INLINE_BROWSER: 869 return hasOptionsURL ? addon.optionsType : null; 870 } 871 return null; 872 } 873 874 return null; 875 } 876 877 get optionsBrowserStyle() { 878 let addon = addonFor(this); 879 return addon.optionsBrowserStyle; 880 } 881 882 get incognito() { 883 return addonFor(this).incognito; 884 } 885 886 async getBlocklistURL() { 887 return addonFor(this).blocklistURL; 888 } 889 890 get iconURL() { 891 return AddonManager.getPreferredIconURL(this, 48); 892 } 893 894 get icons() { 895 let addon = addonFor(this); 896 let icons = {}; 897 898 if (addon._repositoryAddon) { 899 for (let size in addon._repositoryAddon.icons) { 900 icons[size] = addon._repositoryAddon.icons[size]; 901 } 902 } 903 904 if (addon.icons) { 905 for (let size in addon.icons) { 906 let path = addon.icons[size].replace(/^\//, ""); 907 icons[size] = this.getResourceURI(path).spec; 908 } 909 } 910 911 let canUseIconURLs = this.isActive; 912 if (canUseIconURLs && addon.iconURL) { 913 icons[32] = addon.iconURL; 914 icons[48] = addon.iconURL; 915 } 916 917 Object.freeze(icons); 918 return icons; 919 } 920 921 get screenshots() { 922 let addon = addonFor(this); 923 let repositoryAddon = addon._repositoryAddon; 924 if (repositoryAddon && "screenshots" in repositoryAddon) { 925 let repositoryScreenshots = repositoryAddon.screenshots; 926 if (repositoryScreenshots && repositoryScreenshots.length) { 927 return repositoryScreenshots; 928 } 929 } 930 931 if (addon.previewImage) { 932 let url = this.getResourceURI(addon.previewImage).spec; 933 return [new AddonManagerPrivate.AddonScreenshot(url)]; 934 } 935 936 return null; 937 } 938 939 get recommendationStates() { 940 let addon = addonFor(this); 941 let state = addon.recommendationState; 942 if ( 943 state && 944 state.validNotBefore < addon.updateDate && 945 state.validNotAfter > addon.updateDate && 946 addon.isCorrectlySigned && 947 !this.temporarilyInstalled 948 ) { 949 return state.states; 950 } 951 return []; 952 } 953 954 get isRecommended() { 955 return this.recommendationStates.includes("recommended"); 956 } 957 958 get canBypassThirdParyInstallPrompt() { 959 // We only bypass if the extension is signed (to support distributions 960 // that turn off the signing requirement) and has recommendation states, 961 // or the extension is signed as privileged. 962 return ( 963 this.signedState == AddonManager.SIGNEDSTATE_PRIVILEGED || 964 (this.signedState >= AddonManager.SIGNEDSTATE_SIGNED && 965 this.recommendationStates.length) 966 ); 967 } 968 969 get applyBackgroundUpdates() { 970 return addonFor(this).applyBackgroundUpdates; 971 } 972 set applyBackgroundUpdates(val) { 973 let addon = addonFor(this); 974 if ( 975 val != AddonManager.AUTOUPDATE_DEFAULT && 976 val != AddonManager.AUTOUPDATE_DISABLE && 977 val != AddonManager.AUTOUPDATE_ENABLE 978 ) { 979 val = val 980 ? AddonManager.AUTOUPDATE_DEFAULT 981 : AddonManager.AUTOUPDATE_DISABLE; 982 } 983 984 if (val == addon.applyBackgroundUpdates) { 985 return; 986 } 987 988 XPIDatabase.setAddonProperties(addon, { 989 applyBackgroundUpdates: val, 990 }); 991 AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, [ 992 "applyBackgroundUpdates", 993 ]); 994 } 995 996 set syncGUID(val) { 997 let addon = addonFor(this); 998 if (addon.syncGUID == val) { 999 return; 1000 } 1001 1002 if (addon.inDatabase) { 1003 XPIDatabase.setAddonSyncGUID(addon, val); 1004 } 1005 1006 addon.syncGUID = val; 1007 } 1008 1009 get install() { 1010 let addon = addonFor(this); 1011 if (!("_install" in addon) || !addon._install) { 1012 return null; 1013 } 1014 return addon._install.wrapper; 1015 } 1016 1017 get updateInstall() { 1018 let addon = addonFor(this); 1019 return addon._updateInstall ? addon._updateInstall.wrapper : null; 1020 } 1021 1022 get pendingUpgrade() { 1023 let addon = addonFor(this); 1024 return addon.pendingUpgrade ? addon.pendingUpgrade.wrapper : null; 1025 } 1026 1027 get scope() { 1028 let addon = addonFor(this); 1029 if (addon.location) { 1030 return addon.location.scope; 1031 } 1032 1033 return AddonManager.SCOPE_PROFILE; 1034 } 1035 1036 get pendingOperations() { 1037 let addon = addonFor(this); 1038 let pending = 0; 1039 if (!addon.inDatabase) { 1040 // Add-on is pending install if there is no associated install (shouldn't 1041 // happen here) or if the install is in the process of or has successfully 1042 // completed the install. If an add-on is pending install then we ignore 1043 // any other pending operations. 1044 if ( 1045 !addon._install || 1046 addon._install.state == AddonManager.STATE_INSTALLING || 1047 addon._install.state == AddonManager.STATE_INSTALLED 1048 ) { 1049 return AddonManager.PENDING_INSTALL; 1050 } 1051 } else if (addon.pendingUninstall) { 1052 // If an add-on is pending uninstall then we ignore any other pending 1053 // operations 1054 return AddonManager.PENDING_UNINSTALL; 1055 } 1056 1057 if (addon.active && addon.disabled) { 1058 pending |= AddonManager.PENDING_DISABLE; 1059 } else if (!addon.active && !addon.disabled) { 1060 pending |= AddonManager.PENDING_ENABLE; 1061 } 1062 1063 if (addon.pendingUpgrade) { 1064 pending |= AddonManager.PENDING_UPGRADE; 1065 } 1066 1067 return pending; 1068 } 1069 1070 get operationsRequiringRestart() { 1071 return 0; 1072 } 1073 1074 get isDebuggable() { 1075 return this.isActive; 1076 } 1077 1078 get permissions() { 1079 return addonFor(this).permissions(); 1080 } 1081 1082 get isActive() { 1083 let addon = addonFor(this); 1084 if (!addon.active) { 1085 return false; 1086 } 1087 if (!Services.appinfo.inSafeMode) { 1088 return true; 1089 } 1090 return XPIInternal.canRunInSafeMode(addon); 1091 } 1092 1093 get startupPromise() { 1094 let addon = addonFor(this); 1095 if (!this.isActive) { 1096 return null; 1097 } 1098 1099 let activeAddon = XPIProvider.activeAddons.get(addon.id); 1100 if (activeAddon) { 1101 return activeAddon.startupPromise || null; 1102 } 1103 return null; 1104 } 1105 1106 updateBlocklistState(applySoftBlock = true) { 1107 return addonFor(this).updateBlocklistState({ applySoftBlock }); 1108 } 1109 1110 get userDisabled() { 1111 let addon = addonFor(this); 1112 return addon.softDisabled || addon.userDisabled; 1113 } 1114 1115 /** 1116 * Get the embedderDisabled property for this addon. 1117 * 1118 * This is intended for embedders of Gecko like GeckoView apps to control 1119 * which addons are usable on their app. 1120 * 1121 * @returns {boolean} 1122 */ 1123 get embedderDisabled() { 1124 if (!AddonSettings.IS_EMBEDDED) { 1125 return undefined; 1126 } 1127 1128 return addonFor(this).embedderDisabled; 1129 } 1130 1131 /** 1132 * Set the embedderDisabled property for this addon. 1133 * 1134 * This is intended for embedders of Gecko like GeckoView apps to control 1135 * which addons are usable on their app. 1136 * 1137 * Embedders can disable addons for various reasons, e.g. the addon is not 1138 * compatible with their implementation of the WebExtension API. 1139 * 1140 * When an addon is embedderDisabled it will behave like it was appDisabled. 1141 * 1142 * @param {boolean} val 1143 * whether this addon should be embedder disabled or not. 1144 */ 1145 async setEmbedderDisabled(val) { 1146 if (!AddonSettings.IS_EMBEDDED) { 1147 throw new Error("Setting embedder disabled while not embedding."); 1148 } 1149 1150 let addon = addonFor(this); 1151 if (addon.embedderDisabled == val) { 1152 return val; 1153 } 1154 1155 if (addon.inDatabase) { 1156 await XPIDatabase.updateAddonDisabledState(addon, { 1157 embedderDisabled: val, 1158 }); 1159 } else { 1160 addon.embedderDisabled = val; 1161 } 1162 1163 return val; 1164 } 1165 1166 enable(options = {}) { 1167 const { allowSystemAddons = false } = options; 1168 return addonFor(this).setUserDisabled(false, allowSystemAddons); 1169 } 1170 1171 disable(options = {}) { 1172 const { allowSystemAddons = false } = options; 1173 return addonFor(this).setUserDisabled(true, allowSystemAddons); 1174 } 1175 1176 async setSoftDisabled(val) { 1177 let addon = addonFor(this); 1178 if (val == addon.softDisabled) { 1179 return val; 1180 } 1181 1182 if (addon.inDatabase) { 1183 // When softDisabling a theme just enable the active theme 1184 if (addon.type === "theme" && val && !addon.userDisabled) { 1185 if (addon.isWebExtension) { 1186 await XPIDatabase.updateAddonDisabledState(addon, { 1187 softDisabled: val, 1188 }); 1189 } 1190 } else { 1191 await XPIDatabase.updateAddonDisabledState(addon, { 1192 softDisabled: val, 1193 }); 1194 } 1195 } else if (!addon.userDisabled) { 1196 // Only set softDisabled if not already disabled 1197 addon.softDisabled = val; 1198 } 1199 1200 return val; 1201 } 1202 1203 get isPrivileged() { 1204 return addonFor(this).isPrivileged; 1205 } 1206 1207 get hidden() { 1208 return addonFor(this).hidden; 1209 } 1210 1211 get isSystem() { 1212 let addon = addonFor(this); 1213 return addon.location.isSystem; 1214 } 1215 1216 get isBuiltin() { 1217 return addonFor(this).location.isBuiltin; 1218 } 1219 1220 // Returns true if Firefox Sync should sync this addon. Only addons 1221 // in the profile install location are considered syncable. 1222 get isSyncable() { 1223 let addon = addonFor(this); 1224 return addon.location.name == KEY_APP_PROFILE; 1225 } 1226 1227 get userPermissions() { 1228 return addonFor(this).userPermissions; 1229 } 1230 1231 get optionalPermissions() { 1232 return addonFor(this).optionalPermissions; 1233 } 1234 1235 isCompatibleWith(aAppVersion, aPlatformVersion) { 1236 return addonFor(this).isCompatibleWith(aAppVersion, aPlatformVersion); 1237 } 1238 1239 async uninstall(alwaysAllowUndo) { 1240 let addon = addonFor(this); 1241 return XPIInstall.uninstallAddon(addon, alwaysAllowUndo); 1242 } 1243 1244 cancelUninstall() { 1245 let addon = addonFor(this); 1246 XPIInstall.cancelUninstallAddon(addon); 1247 } 1248 1249 findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) { 1250 new UpdateChecker( 1251 addonFor(this), 1252 aListener, 1253 aReason, 1254 aAppVersion, 1255 aPlatformVersion 1256 ); 1257 } 1258 1259 // Returns true if there was an update in progress, false if there was no update to cancel 1260 cancelUpdate() { 1261 let addon = addonFor(this); 1262 if (addon._updateCheck) { 1263 addon._updateCheck.cancel(); 1264 return true; 1265 } 1266 return false; 1267 } 1268 1269 /** 1270 * Reloads the add-on. 1271 * 1272 * For temporarily installed add-ons, this uninstalls and re-installs the 1273 * add-on. Otherwise, the addon is disabled and then re-enabled, and the cache 1274 * is flushed. 1275 */ 1276 async reload() { 1277 const addon = addonFor(this); 1278 1279 logger.debug(`reloading add-on ${addon.id}`); 1280 1281 if (!this.temporarilyInstalled) { 1282 await XPIDatabase.updateAddonDisabledState(addon, { userDisabled: true }); 1283 await XPIDatabase.updateAddonDisabledState(addon, { 1284 userDisabled: false, 1285 }); 1286 } else { 1287 // This function supports re-installing an existing add-on. 1288 await AddonManager.installTemporaryAddon(addon._sourceBundle); 1289 } 1290 } 1291 1292 /** 1293 * Returns a URI to the selected resource or to the add-on bundle if aPath 1294 * is null. URIs to the bundle will always be file: URIs. URIs to resources 1295 * will be file: URIs if the add-on is unpacked or jar: URIs if the add-on is 1296 * still an XPI file. 1297 * 1298 * @param {string?} aPath 1299 * The path in the add-on to get the URI for or null to get a URI to 1300 * the file or directory the add-on is installed as. 1301 * @returns {nsIURI} 1302 */ 1303 getResourceURI(aPath) { 1304 let addon = addonFor(this); 1305 let url = Services.io.newURI(addon.rootURI); 1306 if (aPath) { 1307 if (aPath.startsWith("/")) { 1308 throw new Error("getResourceURI() must receive a relative path"); 1309 } 1310 url = Services.io.newURI(aPath, null, url); 1311 } 1312 return url; 1313 } 1314}; 1315 1316function chooseValue(aAddon, aObj, aProp) { 1317 let repositoryAddon = aAddon._repositoryAddon; 1318 let objValue = aObj[aProp]; 1319 1320 if ( 1321 repositoryAddon && 1322 aProp in repositoryAddon && 1323 (aProp === "creator" || objValue == null) 1324 ) { 1325 return [repositoryAddon[aProp], true]; 1326 } 1327 1328 let id = `extension.${aAddon.id}.${aProp}`; 1329 for (let bundle of LOCALE_BUNDLES) { 1330 try { 1331 return [bundle.GetStringFromName(id), false]; 1332 } catch (e) { 1333 // Ignore missing overrides. 1334 } 1335 } 1336 1337 return [objValue, false]; 1338} 1339 1340function defineAddonWrapperProperty(name, getter) { 1341 Object.defineProperty(AddonWrapper.prototype, name, { 1342 get: getter, 1343 enumerable: true, 1344 }); 1345} 1346 1347[ 1348 "id", 1349 "syncGUID", 1350 "version", 1351 "type", 1352 "isWebExtension", 1353 "isCompatible", 1354 "isPlatformCompatible", 1355 "providesUpdatesSecurely", 1356 "blocklistState", 1357 "appDisabled", 1358 "softDisabled", 1359 "skinnable", 1360 "foreignInstall", 1361 "strictCompatibility", 1362 "updateURL", 1363 "dependencies", 1364 "signedState", 1365 "isCorrectlySigned", 1366].forEach(function(aProp) { 1367 defineAddonWrapperProperty(aProp, function() { 1368 let addon = addonFor(this); 1369 return aProp in addon ? addon[aProp] : undefined; 1370 }); 1371}); 1372 1373[ 1374 "fullDescription", 1375 "developerComments", 1376 "supportURL", 1377 "contributionURL", 1378 "averageRating", 1379 "reviewCount", 1380 "reviewURL", 1381 "weeklyDownloads", 1382].forEach(function(aProp) { 1383 defineAddonWrapperProperty(aProp, function() { 1384 let addon = addonFor(this); 1385 if (addon._repositoryAddon) { 1386 return addon._repositoryAddon[aProp]; 1387 } 1388 1389 return null; 1390 }); 1391}); 1392 1393["installDate", "updateDate"].forEach(function(aProp) { 1394 defineAddonWrapperProperty(aProp, function() { 1395 let addon = addonFor(this); 1396 // installDate is always set, updateDate is sometimes missing. 1397 return new Date(addon[aProp] ?? addon.installDate); 1398 }); 1399}); 1400 1401defineAddonWrapperProperty("signedDate", function() { 1402 let addon = addonFor(this); 1403 let { signedDate } = addon; 1404 if (signedDate != null) { 1405 return new Date(signedDate); 1406 } 1407 return null; 1408}); 1409 1410["sourceURI", "releaseNotesURI"].forEach(function(aProp) { 1411 defineAddonWrapperProperty(aProp, function() { 1412 let addon = addonFor(this); 1413 1414 // Temporary Installed Addons do not have a "sourceURI", 1415 // But we can use the "_sourceBundle" as an alternative, 1416 // which points to the path of the addon xpi installed 1417 // or its source dir (if it has been installed from a 1418 // directory). 1419 if (aProp == "sourceURI" && this.temporarilyInstalled) { 1420 return Services.io.newFileURI(addon._sourceBundle); 1421 } 1422 1423 let [target, fromRepo] = chooseValue(addon, addon, aProp); 1424 if (!target) { 1425 return null; 1426 } 1427 if (fromRepo) { 1428 return target; 1429 } 1430 return Services.io.newURI(target); 1431 }); 1432}); 1433 1434["name", "description", "creator", "homepageURL"].forEach(function(aProp) { 1435 defineAddonWrapperProperty(aProp, function() { 1436 let addon = addonFor(this); 1437 1438 let [result, usedRepository] = chooseValue( 1439 addon, 1440 addon.selectedLocale, 1441 aProp 1442 ); 1443 1444 if (result == null) { 1445 // Legacy add-ons may be partially localized. Fall back to the default 1446 // locale ensure that the result is a string where possible. 1447 [result, usedRepository] = chooseValue(addon, addon.defaultLocale, aProp); 1448 } 1449 1450 if (result && !usedRepository && aProp == "creator") { 1451 return new AddonManagerPrivate.AddonAuthor(result); 1452 } 1453 1454 return result; 1455 }); 1456}); 1457 1458["developers", "translators", "contributors"].forEach(function(aProp) { 1459 defineAddonWrapperProperty(aProp, function() { 1460 let addon = addonFor(this); 1461 1462 let [results, usedRepository] = chooseValue( 1463 addon, 1464 addon.selectedLocale, 1465 aProp 1466 ); 1467 1468 if (results && !usedRepository) { 1469 results = results.map(function(aResult) { 1470 return new AddonManagerPrivate.AddonAuthor(aResult); 1471 }); 1472 } 1473 1474 return results; 1475 }); 1476}); 1477 1478/** 1479 * @typedef {Map<string, AddonInternal>} AddonDB 1480 */ 1481 1482/** 1483 * Internal interface: find an addon from an already loaded addonDB. 1484 * 1485 * @param {AddonDB} addonDB 1486 * The add-on database. 1487 * @param {function(AddonInternal) : boolean} aFilter 1488 * The filter predecate. The first add-on for which it returns 1489 * true will be returned. 1490 * @returns {AddonInternal?} 1491 * The first matching add-on, if one is found. 1492 */ 1493function _findAddon(addonDB, aFilter) { 1494 for (let addon of addonDB.values()) { 1495 if (aFilter(addon)) { 1496 return addon; 1497 } 1498 } 1499 return null; 1500} 1501 1502/** 1503 * Internal interface to get a filtered list of addons from a loaded addonDB 1504 * 1505 * @param {AddonDB} addonDB 1506 * The add-on database. 1507 * @param {function(AddonInternal) : boolean} aFilter 1508 * The filter predecate. Add-ons which match this predicate will 1509 * be returned. 1510 * @returns {Array<AddonInternal>} 1511 * The list of matching add-ons. 1512 */ 1513function _filterDB(addonDB, aFilter) { 1514 return Array.from(addonDB.values()).filter(aFilter); 1515} 1516 1517this.XPIDatabase = { 1518 // true if the database connection has been opened 1519 initialized: false, 1520 // The database file 1521 jsonFile: FileUtils.getFile(KEY_PROFILEDIR, [FILE_JSON_DB], true), 1522 rebuildingDatabase: false, 1523 syncLoadingDB: false, 1524 // Add-ons from the database in locations which are no longer 1525 // supported. 1526 orphanedAddons: [], 1527 1528 _saveTask: null, 1529 1530 // Saved error object if we fail to read an existing database 1531 _loadError: null, 1532 1533 // Saved error object if we fail to save the database 1534 _saveError: null, 1535 1536 // Error reported by our most recent attempt to read or write the database, if any 1537 get lastError() { 1538 if (this._loadError) { 1539 return this._loadError; 1540 } 1541 if (this._saveError) { 1542 return this._saveError; 1543 } 1544 return null; 1545 }, 1546 1547 async _saveNow() { 1548 try { 1549 let path = this.jsonFile.path; 1550 await IOUtils.writeJSON(path, this, { tmpPath: `${path}.tmp` }); 1551 1552 if (!this._schemaVersionSet) { 1553 // Update the XPIDB schema version preference the first time we 1554 // successfully save the database. 1555 logger.debug( 1556 "XPI Database saved, setting schema version preference to " + 1557 DB_SCHEMA 1558 ); 1559 Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA); 1560 this._schemaVersionSet = true; 1561 1562 // Reading the DB worked once, so we don't need the load error 1563 this._loadError = null; 1564 } 1565 } catch (error) { 1566 logger.warn("Failed to save XPI database", error); 1567 this._saveError = error; 1568 1569 if (!(error instanceof DOMException) || error.name !== "AbortError") { 1570 throw error; 1571 } 1572 } 1573 }, 1574 1575 /** 1576 * Mark the current stored data dirty, and schedule a flush to disk 1577 */ 1578 saveChanges() { 1579 if (!this.initialized) { 1580 throw new Error("Attempt to use XPI database when it is not initialized"); 1581 } 1582 1583 if (XPIProvider._closing) { 1584 // use an Error here so we get a stack trace. 1585 let err = new Error("XPI database modified after shutdown began"); 1586 logger.warn(err); 1587 AddonManagerPrivate.recordSimpleMeasure( 1588 "XPIDB_late_stack", 1589 Log.stackTrace(err) 1590 ); 1591 } 1592 1593 if (!this._saveTask) { 1594 this._saveTask = new DeferredTask( 1595 () => this._saveNow(), 1596 ASYNC_SAVE_DELAY_MS 1597 ); 1598 } 1599 1600 this._saveTask.arm(); 1601 }, 1602 1603 async finalize() { 1604 // handle the "in memory only" and "saveChanges never called" cases 1605 if (!this._saveTask) { 1606 return; 1607 } 1608 1609 await this._saveTask.finalize(); 1610 }, 1611 1612 /** 1613 * Converts the current internal state of the XPI addon database to 1614 * a JSON.stringify()-ready structure 1615 * 1616 * @returns {Object} 1617 */ 1618 toJSON() { 1619 if (!this.addonDB) { 1620 // We never loaded the database? 1621 throw new Error("Attempt to save database without loading it first"); 1622 } 1623 1624 let toSave = { 1625 schemaVersion: DB_SCHEMA, 1626 addons: Array.from(this.addonDB.values()).filter( 1627 addon => !addon.location.isTemporary 1628 ), 1629 }; 1630 return toSave; 1631 }, 1632 1633 /** 1634 * Synchronously loads the database, by running the normal async load 1635 * operation with idle dispatch disabled, and spinning the event loop 1636 * until it finishes. 1637 * 1638 * @param {boolean} aRebuildOnError 1639 * A boolean indicating whether add-on information should be loaded 1640 * from the install locations if the database needs to be rebuilt. 1641 * (if false, caller is XPIProvider.checkForChanges() which will rebuild) 1642 */ 1643 syncLoadDB(aRebuildOnError) { 1644 let err = new Error("Synchronously loading the add-ons database"); 1645 logger.debug(err.message); 1646 AddonManagerPrivate.recordSimpleMeasure( 1647 "XPIDB_sync_stack", 1648 Log.stackTrace(err) 1649 ); 1650 try { 1651 this.syncLoadingDB = true; 1652 XPIInternal.awaitPromise(this.asyncLoadDB(aRebuildOnError)); 1653 } finally { 1654 this.syncLoadingDB = false; 1655 } 1656 }, 1657 1658 _recordStartupError(reason) { 1659 AddonManagerPrivate.recordSimpleMeasure("XPIDB_startupError", reason); 1660 }, 1661 1662 /** 1663 * Parse loaded data, reconstructing the database if the loaded data is not valid 1664 * 1665 * @param {object} aInputAddons 1666 * The add-on JSON to parse. 1667 * @param {boolean} aRebuildOnError 1668 * If true, synchronously reconstruct the database from installed add-ons 1669 */ 1670 async parseDB(aInputAddons, aRebuildOnError) { 1671 try { 1672 let parseTimer = AddonManagerPrivate.simpleTimer("XPIDB_parseDB_MS"); 1673 1674 if (!("schemaVersion" in aInputAddons) || !("addons" in aInputAddons)) { 1675 let error = new Error("Bad JSON file contents"); 1676 error.rebuildReason = "XPIDB_rebuildBadJSON_MS"; 1677 throw error; 1678 } 1679 1680 if (aInputAddons.schemaVersion <= 27) { 1681 // Types were translated in bug 857456. 1682 for (let addon of aInputAddons.addons) { 1683 migrateAddonLoader(addon); 1684 } 1685 } else if (aInputAddons.schemaVersion != DB_SCHEMA) { 1686 // For now, we assume compatibility for JSON data with a 1687 // mismatched schema version, though we throw away any fields we 1688 // don't know about (bug 902956) 1689 this._recordStartupError( 1690 `schemaMismatch-${aInputAddons.schemaVersion}` 1691 ); 1692 logger.debug( 1693 `JSON schema mismatch: expected ${DB_SCHEMA}, actual ${aInputAddons.schemaVersion}` 1694 ); 1695 } 1696 1697 let forEach = this.syncLoadingDB ? arrayForEach : idleForEach; 1698 1699 // If we got here, we probably have good data 1700 // Make AddonInternal instances from the loaded data and save them 1701 let addonDB = new Map(); 1702 await forEach(aInputAddons.addons, loadedAddon => { 1703 if (loadedAddon.path) { 1704 try { 1705 loadedAddon._sourceBundle = new nsIFile(loadedAddon.path); 1706 } catch (e) { 1707 // We can fail here when the path is invalid, usually from the 1708 // wrong OS 1709 logger.warn( 1710 "Could not find source bundle for add-on " + loadedAddon.id, 1711 e 1712 ); 1713 } 1714 } 1715 loadedAddon.location = XPIStates.getLocation(loadedAddon.location); 1716 1717 let newAddon = new AddonInternal(loadedAddon); 1718 if (loadedAddon.location) { 1719 addonDB.set(newAddon._key, newAddon); 1720 } else { 1721 this.orphanedAddons.push(newAddon); 1722 } 1723 }); 1724 1725 parseTimer.done(); 1726 this.addonDB = addonDB; 1727 logger.debug("Successfully read XPI database"); 1728 this.initialized = true; 1729 } catch (e) { 1730 if (e.name == "SyntaxError") { 1731 logger.error("Syntax error parsing saved XPI JSON data"); 1732 this._recordStartupError("syntax"); 1733 } else { 1734 logger.error("Failed to load XPI JSON data from profile", e); 1735 this._recordStartupError("other"); 1736 } 1737 1738 this.timeRebuildDatabase( 1739 e.rebuildReason || "XPIDB_rebuildReadFailed_MS", 1740 aRebuildOnError 1741 ); 1742 } 1743 }, 1744 1745 async maybeIdleDispatch() { 1746 if (!this.syncLoadingDB) { 1747 await promiseIdleSlice(); 1748 } 1749 }, 1750 1751 /** 1752 * Open and read the XPI database asynchronously, upgrading if 1753 * necessary. If any DB load operation fails, we need to 1754 * synchronously rebuild the DB from the installed extensions. 1755 * 1756 * @param {boolean} [aRebuildOnError = true] 1757 * A boolean indicating whether add-on information should be loaded 1758 * from the install locations if the database needs to be rebuilt. 1759 * (if false, caller is XPIProvider.checkForChanges() which will rebuild) 1760 * @returns {Promise<AddonDB>} 1761 * Resolves to the Map of loaded JSON data stored in 1762 * this.addonDB; never rejects. 1763 */ 1764 asyncLoadDB(aRebuildOnError = true) { 1765 // Already started (and possibly finished) loading 1766 if (this._dbPromise) { 1767 return this._dbPromise; 1768 } 1769 1770 logger.debug(`Starting async load of XPI database ${this.jsonFile.path}`); 1771 this._dbPromise = (async () => { 1772 try { 1773 let json = await IOUtils.readJSON(this.jsonFile.path); 1774 1775 logger.debug("Finished async read of XPI database, parsing..."); 1776 await this.maybeIdleDispatch(); 1777 await this.parseDB(json, true); 1778 } catch (error) { 1779 if (error instanceof DOMException && error.name === "NotFoundError") { 1780 if (Services.prefs.getIntPref(PREF_DB_SCHEMA, 0)) { 1781 this._recordStartupError("dbMissing"); 1782 } 1783 } else { 1784 logger.warn( 1785 `Extensions database ${this.jsonFile.path} exists but is not readable; rebuilding`, 1786 error 1787 ); 1788 this._loadError = error; 1789 } 1790 this.timeRebuildDatabase( 1791 "XPIDB_rebuildUnreadableDB_MS", 1792 aRebuildOnError 1793 ); 1794 } 1795 return this.addonDB; 1796 })(); 1797 1798 XPIInternal.resolveDBReady(this._dbPromise); 1799 1800 return this._dbPromise; 1801 }, 1802 1803 timeRebuildDatabase(timerName, rebuildOnError) { 1804 AddonManagerPrivate.recordTiming(timerName, () => { 1805 return this.rebuildDatabase(rebuildOnError); 1806 }); 1807 }, 1808 1809 /** 1810 * Rebuild the database from addon install directories. 1811 * 1812 * @param {boolean} aRebuildOnError 1813 * A boolean indicating whether add-on information should be loaded 1814 * from the install locations if the database needs to be rebuilt. 1815 * (if false, caller is XPIProvider.checkForChanges() which will rebuild) 1816 */ 1817 rebuildDatabase(aRebuildOnError) { 1818 this.addonDB = new Map(); 1819 this.initialized = true; 1820 1821 if (XPIStates.size == 0) { 1822 // No extensions installed, so we're done 1823 logger.debug("Rebuilding XPI database with no extensions"); 1824 return; 1825 } 1826 1827 this.rebuildingDatabase = !!aRebuildOnError; 1828 1829 if (aRebuildOnError) { 1830 logger.warn("Rebuilding add-ons database from installed extensions."); 1831 try { 1832 XPIDatabaseReconcile.processFileChanges({}, false); 1833 } catch (e) { 1834 logger.error( 1835 "Failed to rebuild XPI database from installed extensions", 1836 e 1837 ); 1838 } 1839 // Make sure to update the active add-ons and add-ons list on shutdown 1840 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); 1841 } 1842 }, 1843 1844 /** 1845 * Shuts down the database connection and releases all cached objects. 1846 * Return: Promise{integer} resolves / rejects with the result of the DB 1847 * flush after the database is flushed and 1848 * all cleanup is done 1849 */ 1850 async shutdown() { 1851 logger.debug("shutdown"); 1852 if (this.initialized) { 1853 // If our last database I/O had an error, try one last time to save. 1854 if (this.lastError) { 1855 this.saveChanges(); 1856 } 1857 1858 this.initialized = false; 1859 1860 // If we're shutting down while still loading, finish loading 1861 // before everything else! 1862 if (this._dbPromise) { 1863 await this._dbPromise; 1864 } 1865 1866 // Await any pending DB writes and finish cleaning up. 1867 await this.finalize(); 1868 1869 if (this._saveError) { 1870 // If our last attempt to read or write the DB failed, force a new 1871 // extensions.ini to be written to disk on the next startup 1872 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); 1873 } 1874 1875 // Clear out the cached addons data loaded from JSON 1876 delete this.addonDB; 1877 delete this._dbPromise; 1878 // same for the deferred save 1879 delete this._saveTask; 1880 // re-enable the schema version setter 1881 delete this._schemaVersionSet; 1882 } 1883 }, 1884 1885 /** 1886 * Verifies that all installed add-ons are still correctly signed. 1887 */ 1888 async verifySignatures() { 1889 try { 1890 let addons = await this.getAddonList(a => true); 1891 1892 let changes = { 1893 enabled: [], 1894 disabled: [], 1895 }; 1896 1897 for (let addon of addons) { 1898 // The add-on might have vanished, we'll catch that on the next startup 1899 if (!addon._sourceBundle || !addon._sourceBundle.exists()) { 1900 continue; 1901 } 1902 1903 let signedState = await verifyBundleSignedState( 1904 addon._sourceBundle, 1905 addon 1906 ); 1907 1908 if (signedState != addon.signedState) { 1909 addon.signedState = signedState; 1910 AddonManagerPrivate.callAddonListeners( 1911 "onPropertyChanged", 1912 addon.wrapper, 1913 ["signedState"] 1914 ); 1915 } 1916 1917 let disabled = await this.updateAddonDisabledState(addon); 1918 if (disabled !== undefined) { 1919 changes[disabled ? "disabled" : "enabled"].push(addon.id); 1920 } 1921 } 1922 1923 this.saveChanges(); 1924 1925 Services.obs.notifyObservers( 1926 null, 1927 "xpi-signature-changed", 1928 JSON.stringify(changes) 1929 ); 1930 } catch (err) { 1931 logger.error("XPI_verifySignature: " + err); 1932 } 1933 }, 1934 1935 /** 1936 * Imports the xpinstall permissions from preferences into the permissions 1937 * manager for the user to change later. 1938 */ 1939 importPermissions() { 1940 PermissionsUtils.importFromPrefs( 1941 PREF_XPI_PERMISSIONS_BRANCH, 1942 XPIInternal.XPI_PERMISSION 1943 ); 1944 }, 1945 1946 /** 1947 * Called when a new add-on has been enabled when only one add-on of that type 1948 * can be enabled. 1949 * 1950 * @param {string} aId 1951 * The ID of the newly enabled add-on 1952 * @param {string} aType 1953 * The type of the newly enabled add-on 1954 */ 1955 async addonChanged(aId, aType) { 1956 // We only care about themes in this provider 1957 if (aType !== "theme") { 1958 return; 1959 } 1960 1961 Services.prefs.setCharPref( 1962 "extensions.activeThemeID", 1963 aId || DEFAULT_THEME_ID 1964 ); 1965 1966 let enableTheme; 1967 1968 let addons = this.getAddonsByType("theme"); 1969 let updateDisabledStatePromises = []; 1970 1971 for (let theme of addons) { 1972 if (theme.visible) { 1973 if (!aId && theme.id == DEFAULT_THEME_ID) { 1974 enableTheme = theme; 1975 } else if (theme.id != aId && !theme.pendingUninstall) { 1976 updateDisabledStatePromises.push( 1977 this.updateAddonDisabledState(theme, { 1978 userDisabled: true, 1979 becauseSelecting: true, 1980 }) 1981 ); 1982 } 1983 } 1984 } 1985 1986 await Promise.all(updateDisabledStatePromises); 1987 1988 if (enableTheme) { 1989 await this.updateAddonDisabledState(enableTheme, { 1990 userDisabled: false, 1991 becauseSelecting: true, 1992 }); 1993 } 1994 }, 1995 1996 SIGNED_TYPES, 1997 1998 /** 1999 * Asynchronously list all addons that match the filter function 2000 * 2001 * @param {function(AddonInternal) : boolean} aFilter 2002 * Function that takes an addon instance and returns 2003 * true if that addon should be included in the selected array 2004 * 2005 * @returns {Array<AddonInternal>} 2006 * A Promise that resolves to the list of add-ons matching 2007 * aFilter or an empty array if none match 2008 */ 2009 async getAddonList(aFilter) { 2010 try { 2011 let addonDB = await this.asyncLoadDB(); 2012 let addonList = _filterDB(addonDB, aFilter); 2013 let addons = await Promise.all( 2014 addonList.map(addon => getRepositoryAddon(addon)) 2015 ); 2016 return addons; 2017 } catch (error) { 2018 logger.error("getAddonList failed", error); 2019 return []; 2020 } 2021 }, 2022 2023 /** 2024 * Get the first addon that matches the filter function 2025 * 2026 * @param {function(AddonInternal) : boolean} aFilter 2027 * Function that takes an addon instance and returns 2028 * true if that addon should be selected 2029 * @returns {Promise<AddonInternal?>} 2030 */ 2031 getAddon(aFilter) { 2032 return this.asyncLoadDB() 2033 .then(addonDB => getRepositoryAddon(_findAddon(addonDB, aFilter))) 2034 .catch(error => { 2035 logger.error("getAddon failed", error); 2036 }); 2037 }, 2038 2039 /** 2040 * Asynchronously gets an add-on with a particular ID in a particular 2041 * install location. 2042 * 2043 * @param {string} aId 2044 * The ID of the add-on to retrieve 2045 * @param {string} aLocation 2046 * The name of the install location 2047 * @returns {Promise<AddonInternal?>} 2048 */ 2049 getAddonInLocation(aId, aLocation) { 2050 return this.asyncLoadDB().then(addonDB => 2051 getRepositoryAddon(addonDB.get(aLocation + ":" + aId)) 2052 ); 2053 }, 2054 2055 /** 2056 * Asynchronously get all the add-ons in a particular install location. 2057 * 2058 * @param {string} aLocation 2059 * The name of the install location 2060 * @returns {Promise<Array<AddonInternal>>} 2061 */ 2062 getAddonsInLocation(aLocation) { 2063 return this.getAddonList(aAddon => aAddon.location.name == aLocation); 2064 }, 2065 2066 /** 2067 * Asynchronously gets the add-on with the specified ID that is visible. 2068 * 2069 * @param {string} aId 2070 * The ID of the add-on to retrieve 2071 * @returns {Promise<AddonInternal?>} 2072 */ 2073 getVisibleAddonForID(aId) { 2074 return this.getAddon(aAddon => aAddon.id == aId && aAddon.visible); 2075 }, 2076 2077 /** 2078 * Asynchronously gets the visible add-ons, optionally restricting by type. 2079 * 2080 * @param {Set<string>?} aTypes 2081 * An array of types to include or null to include all types 2082 * @returns {Promise<Array<AddonInternal>>} 2083 */ 2084 getVisibleAddons(aTypes) { 2085 return this.getAddonList( 2086 aAddon => aAddon.visible && (!aTypes || aTypes.has(aAddon.type)) 2087 ); 2088 }, 2089 2090 /** 2091 * Synchronously gets all add-ons of a particular type(s). 2092 * 2093 * @param {Array<string>} aTypes 2094 * The type(s) of add-on to retrieve 2095 * @returns {Array<AddonInternal>} 2096 */ 2097 getAddonsByType(...aTypes) { 2098 if (!this.addonDB) { 2099 // jank-tastic! Must synchronously load DB if the theme switches from 2100 // an XPI theme to a lightweight theme before the DB has loaded, 2101 // because we're called from sync XPIProvider.addonChanged 2102 logger.warn( 2103 `Synchronous load of XPI database due to ` + 2104 `getAddonsByType([${aTypes.join(", ")}]) ` + 2105 `Stack: ${Error().stack}` 2106 ); 2107 this.syncLoadDB(true); 2108 } 2109 2110 return _filterDB(this.addonDB, aAddon => aTypes.includes(aAddon.type)); 2111 }, 2112 2113 /** 2114 * Asynchronously gets all add-ons with pending operations. 2115 * 2116 * @param {Set<string>?} aTypes 2117 * The types of add-ons to retrieve or null to get all types 2118 * @returns {Promise<Array<AddonInternal>>} 2119 */ 2120 getVisibleAddonsWithPendingOperations(aTypes) { 2121 return this.getAddonList( 2122 aAddon => 2123 aAddon.visible && 2124 aAddon.pendingUninstall && 2125 (!aTypes || aTypes.has(aAddon.type)) 2126 ); 2127 }, 2128 2129 /** 2130 * Synchronously gets all add-ons in the database. 2131 * This is only called from the preference observer for the default 2132 * compatibility version preference, so we can return an empty list if 2133 * we haven't loaded the database yet. 2134 * 2135 * @returns {Array<AddonInternal>} 2136 */ 2137 getAddons() { 2138 if (!this.addonDB) { 2139 return []; 2140 } 2141 return _filterDB(this.addonDB, aAddon => true); 2142 }, 2143 2144 /** 2145 * Called to get an Addon with a particular ID. 2146 * 2147 * @param {string} aId 2148 * The ID of the add-on to retrieve 2149 * @returns {Addon?} 2150 */ 2151 async getAddonByID(aId) { 2152 let aAddon = await this.getVisibleAddonForID(aId); 2153 return aAddon ? aAddon.wrapper : null; 2154 }, 2155 2156 /** 2157 * Obtain an Addon having the specified Sync GUID. 2158 * 2159 * @param {string} aGUID 2160 * String GUID of add-on to retrieve 2161 * @returns {Addon?} 2162 */ 2163 async getAddonBySyncGUID(aGUID) { 2164 let addon = await this.getAddon(aAddon => aAddon.syncGUID == aGUID); 2165 return addon ? addon.wrapper : null; 2166 }, 2167 2168 /** 2169 * Called to get Addons of a particular type. 2170 * 2171 * @param {Array<string>?} aTypes 2172 * An array of types to fetch. Can be null to get all types. 2173 * @returns {Addon[]} 2174 */ 2175 async getAddonsByTypes(aTypes) { 2176 let addons = await this.getVisibleAddons(aTypes ? new Set(aTypes) : null); 2177 return addons.map(a => a.wrapper); 2178 }, 2179 2180 /** 2181 * Returns true if signing is required for the given add-on type. 2182 * 2183 * @param {string} aType 2184 * The add-on type to check. 2185 * @returns {boolean} 2186 */ 2187 mustSign(aType) { 2188 if (!SIGNED_TYPES.has(aType)) { 2189 return false; 2190 } 2191 2192 if (aType == "locale") { 2193 return AddonSettings.LANGPACKS_REQUIRE_SIGNING; 2194 } 2195 2196 return AddonSettings.REQUIRE_SIGNING; 2197 }, 2198 2199 /** 2200 * Determine if this addon should be disabled due to being legacy 2201 * 2202 * @param {Addon} addon The addon to check 2203 * 2204 * @returns {boolean} Whether the addon should be disabled for being legacy 2205 */ 2206 isDisabledLegacy(addon) { 2207 // We still have tests that use a legacy addon type, allow them 2208 // if we're in automation. Otherwise, disable if not a webextension. 2209 if (!Cu.isInAutomation) { 2210 return !addon.isWebExtension; 2211 } 2212 2213 return ( 2214 !addon.isWebExtension && 2215 addon.type === "extension" && 2216 // Test addons are privileged unless forced otherwise. 2217 addon.signedState !== AddonManager.SIGNEDSTATE_PRIVILEGED 2218 ); 2219 }, 2220 2221 /** 2222 * Calculates whether an add-on should be appDisabled or not. 2223 * 2224 * @param {AddonInternal} aAddon 2225 * The add-on to check 2226 * @returns {boolean} 2227 * True if the add-on should not be appDisabled 2228 */ 2229 isUsableAddon(aAddon) { 2230 if (this.mustSign(aAddon.type) && !aAddon.isCorrectlySigned) { 2231 logger.warn(`Add-on ${aAddon.id} is not correctly signed.`); 2232 if (Services.prefs.getBoolPref(PREF_XPI_SIGNATURES_DEV_ROOT, false)) { 2233 logger.warn(`Preference ${PREF_XPI_SIGNATURES_DEV_ROOT} is set.`); 2234 } 2235 return false; 2236 } 2237 2238 if (aAddon.blocklistState == nsIBlocklistService.STATE_BLOCKED) { 2239 logger.warn(`Add-on ${aAddon.id} is blocklisted.`); 2240 return false; 2241 } 2242 2243 // If we can't read it, it's not usable: 2244 if (aAddon.brokenManifest) { 2245 return false; 2246 } 2247 2248 if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely) { 2249 logger.warn( 2250 `Updates for add-on ${aAddon.id} must be provided over HTTPS.` 2251 ); 2252 return false; 2253 } 2254 2255 if (!aAddon.isPlatformCompatible) { 2256 logger.warn(`Add-on ${aAddon.id} is not compatible with platform.`); 2257 return false; 2258 } 2259 2260 if (aAddon.dependencies.length) { 2261 let isActive = id => { 2262 let active = XPIProvider.activeAddons.get(id); 2263 return active && !active._pendingDisable; 2264 }; 2265 2266 if (aAddon.dependencies.some(id => !isActive(id))) { 2267 return false; 2268 } 2269 } 2270 2271 if (this.isDisabledLegacy(aAddon)) { 2272 logger.warn(`disabling legacy extension ${aAddon.id}`); 2273 return false; 2274 } 2275 2276 if (AddonManager.checkCompatibility) { 2277 if (!aAddon.isCompatible) { 2278 logger.warn( 2279 `Add-on ${aAddon.id} is not compatible with application version.` 2280 ); 2281 return false; 2282 } 2283 } else { 2284 let app = aAddon.matchingTargetApplication; 2285 if (!app) { 2286 logger.warn( 2287 `Add-on ${aAddon.id} is not compatible with target application.` 2288 ); 2289 return false; 2290 } 2291 } 2292 2293 if (aAddon.location.isSystem || aAddon.location.isBuiltin) { 2294 return true; 2295 } 2296 2297 if (Services.policies && !Services.policies.mayInstallAddon(aAddon)) { 2298 return false; 2299 } 2300 2301 return true; 2302 }, 2303 2304 /** 2305 * Synchronously adds an AddonInternal's metadata to the database. 2306 * 2307 * @param {AddonInternal} aAddon 2308 * AddonInternal to add 2309 * @param {string} aPath 2310 * The file path of the add-on 2311 * @returns {AddonInternal} 2312 * the AddonInternal that was added to the database 2313 */ 2314 addToDatabase(aAddon, aPath) { 2315 aAddon.addedToDatabase(); 2316 aAddon.path = aPath; 2317 this.addonDB.set(aAddon._key, aAddon); 2318 if (aAddon.visible) { 2319 this.makeAddonVisible(aAddon); 2320 } 2321 2322 this.saveChanges(); 2323 return aAddon; 2324 }, 2325 2326 /** 2327 * Synchronously updates an add-on's metadata in the database. Currently just 2328 * removes and recreates. 2329 * 2330 * @param {AddonInternal} aOldAddon 2331 * The AddonInternal to be replaced 2332 * @param {AddonInternal} aNewAddon 2333 * The new AddonInternal to add 2334 * @param {string} aPath 2335 * The file path of the add-on 2336 * @returns {AddonInternal} 2337 * The AddonInternal that was added to the database 2338 */ 2339 updateAddonMetadata(aOldAddon, aNewAddon, aPath) { 2340 this.removeAddonMetadata(aOldAddon); 2341 aNewAddon.syncGUID = aOldAddon.syncGUID; 2342 aNewAddon.installDate = aOldAddon.installDate; 2343 aNewAddon.applyBackgroundUpdates = aOldAddon.applyBackgroundUpdates; 2344 aNewAddon.foreignInstall = aOldAddon.foreignInstall; 2345 aNewAddon.seen = aOldAddon.seen; 2346 aNewAddon.active = 2347 aNewAddon.visible && !aNewAddon.disabled && !aNewAddon.pendingUninstall; 2348 aNewAddon.installTelemetryInfo = aOldAddon.installTelemetryInfo; 2349 2350 return this.addToDatabase(aNewAddon, aPath); 2351 }, 2352 2353 /** 2354 * Synchronously removes an add-on from the database. 2355 * 2356 * @param {AddonInternal} aAddon 2357 * The AddonInternal being removed 2358 */ 2359 removeAddonMetadata(aAddon) { 2360 this.addonDB.delete(aAddon._key); 2361 this.saveChanges(); 2362 }, 2363 2364 updateXPIStates(addon) { 2365 let state = addon.location && addon.location.get(addon.id); 2366 if (state) { 2367 state.syncWithDB(addon); 2368 XPIStates.save(); 2369 } 2370 }, 2371 2372 /** 2373 * Synchronously marks a AddonInternal as visible marking all other 2374 * instances with the same ID as not visible. 2375 * 2376 * @param {AddonInternal} aAddon 2377 * The AddonInternal to make visible 2378 */ 2379 makeAddonVisible(aAddon) { 2380 logger.debug("Make addon " + aAddon._key + " visible"); 2381 for (let [, otherAddon] of this.addonDB) { 2382 if (otherAddon.id == aAddon.id && otherAddon._key != aAddon._key) { 2383 logger.debug("Hide addon " + otherAddon._key); 2384 otherAddon.visible = false; 2385 otherAddon.active = false; 2386 2387 this.updateXPIStates(otherAddon); 2388 } 2389 } 2390 aAddon.visible = true; 2391 this.updateXPIStates(aAddon); 2392 this.saveChanges(); 2393 }, 2394 2395 /** 2396 * Synchronously marks a given add-on ID visible in a given location, 2397 * instances with the same ID as not visible. 2398 * 2399 * @param {string} aId 2400 * The ID of the add-on to make visible 2401 * @param {XPIStateLocation} aLocation 2402 * The location in which to make the add-on visible. 2403 * @returns {AddonInternal?} 2404 * The add-on instance which was marked visible, if any. 2405 */ 2406 makeAddonLocationVisible(aId, aLocation) { 2407 logger.debug(`Make addon ${aId} visible in location ${aLocation}`); 2408 let result; 2409 for (let [, addon] of this.addonDB) { 2410 if (addon.id != aId) { 2411 continue; 2412 } 2413 if (addon.location == aLocation) { 2414 logger.debug("Reveal addon " + addon._key); 2415 addon.visible = true; 2416 addon.active = true; 2417 this.updateXPIStates(addon); 2418 result = addon; 2419 } else { 2420 logger.debug("Hide addon " + addon._key); 2421 addon.visible = false; 2422 addon.active = false; 2423 this.updateXPIStates(addon); 2424 } 2425 } 2426 this.saveChanges(); 2427 return result; 2428 }, 2429 2430 /** 2431 * Synchronously sets properties for an add-on. 2432 * 2433 * @param {AddonInternal} aAddon 2434 * The AddonInternal being updated 2435 * @param {Object} aProperties 2436 * A dictionary of properties to set 2437 */ 2438 setAddonProperties(aAddon, aProperties) { 2439 for (let key in aProperties) { 2440 aAddon[key] = aProperties[key]; 2441 } 2442 this.saveChanges(); 2443 }, 2444 2445 /** 2446 * Synchronously sets the Sync GUID for an add-on. 2447 * Only called when the database is already loaded. 2448 * 2449 * @param {AddonInternal} aAddon 2450 * The AddonInternal being updated 2451 * @param {string} aGUID 2452 * GUID string to set the value to 2453 * @throws if another addon already has the specified GUID 2454 */ 2455 setAddonSyncGUID(aAddon, aGUID) { 2456 // Need to make sure no other addon has this GUID 2457 function excludeSyncGUID(otherAddon) { 2458 return otherAddon._key != aAddon._key && otherAddon.syncGUID == aGUID; 2459 } 2460 let otherAddon = _findAddon(this.addonDB, excludeSyncGUID); 2461 if (otherAddon) { 2462 throw new Error( 2463 "Addon sync GUID conflict for addon " + 2464 aAddon._key + 2465 ": " + 2466 otherAddon._key + 2467 " already has GUID " + 2468 aGUID 2469 ); 2470 } 2471 aAddon.syncGUID = aGUID; 2472 this.saveChanges(); 2473 }, 2474 2475 /** 2476 * Synchronously updates an add-on's active flag in the database. 2477 * 2478 * @param {AddonInternal} aAddon 2479 * The AddonInternal to update 2480 * @param {boolean} aActive 2481 * The new active state for the add-on. 2482 */ 2483 updateAddonActive(aAddon, aActive) { 2484 logger.debug( 2485 "Updating active state for add-on " + aAddon.id + " to " + aActive 2486 ); 2487 2488 aAddon.active = aActive; 2489 this.saveChanges(); 2490 }, 2491 2492 /** 2493 * Synchronously calculates and updates all the active flags in the database. 2494 */ 2495 updateActiveAddons() { 2496 logger.debug("Updating add-on states"); 2497 for (let [, addon] of this.addonDB) { 2498 let newActive = 2499 addon.visible && !addon.disabled && !addon.pendingUninstall; 2500 if (newActive != addon.active) { 2501 addon.active = newActive; 2502 this.saveChanges(); 2503 } 2504 } 2505 2506 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false); 2507 }, 2508 2509 /** 2510 * Updates the disabled state for an add-on. Its appDisabled property will be 2511 * calculated and if the add-on is changed the database will be saved and 2512 * appropriate notifications will be sent out to the registered AddonListeners. 2513 * 2514 * @param {AddonInternal} aAddon 2515 * The AddonInternal to update 2516 * @param {Object} properties - Properties to set on the addon 2517 * @param {boolean?} [properties.userDisabled] 2518 * Value for the userDisabled property. If undefined the value will 2519 * not change 2520 * @param {boolean?} [properties.softDisabled] 2521 * Value for the softDisabled property. If undefined the value will 2522 * not change. If true this will force userDisabled to be true 2523 * @param {boolean?} [properties.embedderDisabled] 2524 * Value for the embedderDisabled property. If undefined the value will 2525 * not change. 2526 * @param {boolean?} [properties.becauseSelecting] 2527 * True if we're disabling this add-on because we're selecting 2528 * another. 2529 * @returns {Promise<boolean?>} 2530 * A tri-state indicating the action taken for the add-on: 2531 * - undefined: The add-on did not change state 2532 * - true: The add-on became disabled 2533 * - false: The add-on became enabled 2534 * @throws if addon is not a AddonInternal 2535 */ 2536 async updateAddonDisabledState( 2537 aAddon, 2538 { userDisabled, softDisabled, embedderDisabled, becauseSelecting } = {} 2539 ) { 2540 if (!aAddon.inDatabase) { 2541 throw new Error("Can only update addon states for installed addons."); 2542 } 2543 if (userDisabled !== undefined && softDisabled !== undefined) { 2544 throw new Error( 2545 "Cannot change userDisabled and softDisabled at the same time" 2546 ); 2547 } 2548 2549 if (userDisabled === undefined) { 2550 userDisabled = aAddon.userDisabled; 2551 } else if (!userDisabled) { 2552 // If enabling the add-on then remove softDisabled 2553 softDisabled = false; 2554 } 2555 2556 // If not changing softDisabled or the add-on is already userDisabled then 2557 // use the existing value for softDisabled 2558 if (softDisabled === undefined || userDisabled) { 2559 softDisabled = aAddon.softDisabled; 2560 } 2561 2562 if (!AddonSettings.IS_EMBEDDED) { 2563 // If embedderDisabled was accidentally set somehow, this will revert it 2564 // back to false. 2565 embedderDisabled = false; 2566 } else if (embedderDisabled === undefined) { 2567 embedderDisabled = aAddon.embedderDisabled; 2568 } 2569 2570 let appDisabled = !this.isUsableAddon(aAddon); 2571 // No change means nothing to do here 2572 if ( 2573 aAddon.userDisabled == userDisabled && 2574 aAddon.appDisabled == appDisabled && 2575 aAddon.softDisabled == softDisabled && 2576 aAddon.embedderDisabled == embedderDisabled 2577 ) { 2578 return undefined; 2579 } 2580 2581 let wasDisabled = aAddon.disabled; 2582 let isDisabled = 2583 userDisabled || softDisabled || appDisabled || embedderDisabled; 2584 2585 // If appDisabled changes but addon.disabled doesn't, 2586 // no onDisabling/onEnabling is sent - so send a onPropertyChanged. 2587 let appDisabledChanged = aAddon.appDisabled != appDisabled; 2588 2589 // Update the properties in the database. 2590 this.setAddonProperties(aAddon, { 2591 userDisabled, 2592 appDisabled, 2593 softDisabled, 2594 embedderDisabled, 2595 }); 2596 2597 let wrapper = aAddon.wrapper; 2598 2599 if (appDisabledChanged) { 2600 AddonManagerPrivate.callAddonListeners("onPropertyChanged", wrapper, [ 2601 "appDisabled", 2602 ]); 2603 } 2604 2605 // If the add-on is not visible or the add-on is not changing state then 2606 // there is no need to do anything else 2607 if (!aAddon.visible || wasDisabled == isDisabled) { 2608 return undefined; 2609 } 2610 2611 // Flag that active states in the database need to be updated on shutdown 2612 Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true); 2613 2614 this.updateXPIStates(aAddon); 2615 2616 // Have we just gone back to the current state? 2617 if (isDisabled != aAddon.active) { 2618 AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper); 2619 } else { 2620 if (isDisabled) { 2621 AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false); 2622 } else { 2623 AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, false); 2624 } 2625 2626 this.updateAddonActive(aAddon, !isDisabled); 2627 2628 let bootstrap = XPIInternal.BootstrapScope.get(aAddon); 2629 if (isDisabled) { 2630 await bootstrap.disable(); 2631 AddonManagerPrivate.callAddonListeners("onDisabled", wrapper); 2632 } else { 2633 await bootstrap.startup(BOOTSTRAP_REASONS.ADDON_ENABLE); 2634 AddonManagerPrivate.callAddonListeners("onEnabled", wrapper); 2635 } 2636 } 2637 2638 // Notify any other providers that a new theme has been enabled 2639 if (aAddon.type === "theme") { 2640 if (!isDisabled) { 2641 await AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type); 2642 } else if (isDisabled && !becauseSelecting) { 2643 await AddonManagerPrivate.notifyAddonChanged(null, "theme"); 2644 } 2645 } 2646 2647 return isDisabled; 2648 }, 2649 2650 /** 2651 * Update the appDisabled property for all add-ons. 2652 */ 2653 updateAddonAppDisabledStates() { 2654 for (let addon of this.getAddons()) { 2655 this.updateAddonDisabledState(addon); 2656 } 2657 }, 2658 2659 /** 2660 * Update the repositoryAddon property for all add-ons. 2661 */ 2662 async updateAddonRepositoryData() { 2663 let addons = await this.getVisibleAddons(null); 2664 logger.debug( 2665 "updateAddonRepositoryData found " + addons.length + " visible add-ons" 2666 ); 2667 2668 await Promise.all( 2669 addons.map(addon => 2670 AddonRepository.getCachedAddonByID(addon.id).then(aRepoAddon => { 2671 if (aRepoAddon) { 2672 logger.debug("updateAddonRepositoryData got info for " + addon.id); 2673 addon._repositoryAddon = aRepoAddon; 2674 return this.updateAddonDisabledState(addon); 2675 } 2676 return undefined; 2677 }) 2678 ) 2679 ); 2680 }, 2681 2682 /** 2683 * Adds the add-on's name and creator to the telemetry payload. 2684 * 2685 * @param {AddonInternal} aAddon 2686 * The addon to record 2687 */ 2688 recordAddonTelemetry(aAddon) { 2689 let locale = aAddon.defaultLocale; 2690 XPIProvider.addTelemetry(aAddon.id, { 2691 name: locale.name, 2692 creator: locale.creator, 2693 }); 2694 }, 2695}; 2696 2697this.XPIDatabaseReconcile = { 2698 /** 2699 * Returns a map of ID -> add-on. When the same add-on ID exists in multiple 2700 * install locations the highest priority location is chosen. 2701 * 2702 * @param {Map<String, AddonInternal>} addonMap 2703 * The add-on map to flatten. 2704 * @param {string?} [hideLocation] 2705 * An optional location from which to hide any add-ons. 2706 * @returns {Map<string, AddonInternal>} 2707 */ 2708 flattenByID(addonMap, hideLocation) { 2709 let map = new Map(); 2710 2711 for (let loc of XPIStates.locations()) { 2712 if (loc.name == hideLocation) { 2713 continue; 2714 } 2715 2716 let locationMap = addonMap.get(loc.name); 2717 if (!locationMap) { 2718 continue; 2719 } 2720 2721 for (let [id, addon] of locationMap) { 2722 if (!map.has(id)) { 2723 map.set(id, addon); 2724 } 2725 } 2726 } 2727 2728 return map; 2729 }, 2730 2731 /** 2732 * Finds the visible add-ons from the map. 2733 * 2734 * @param {Map<String, AddonInternal>} addonMap 2735 * The add-on map to filter. 2736 * @returns {Map<string, AddonInternal>} 2737 */ 2738 getVisibleAddons(addonMap) { 2739 let map = new Map(); 2740 2741 for (let addons of addonMap.values()) { 2742 for (let [id, addon] of addons) { 2743 if (!addon.visible) { 2744 continue; 2745 } 2746 2747 if (map.has(id)) { 2748 logger.warn( 2749 "Previous database listed more than one visible add-on with id " + 2750 id 2751 ); 2752 continue; 2753 } 2754 2755 map.set(id, addon); 2756 } 2757 } 2758 2759 return map; 2760 }, 2761 2762 /** 2763 * Called to add the metadata for an add-on in one of the install locations 2764 * to the database. This can be called in three different cases. Either an 2765 * add-on has been dropped into the location from outside of Firefox, or 2766 * an add-on has been installed through the application, or the database 2767 * has been upgraded or become corrupt and add-on data has to be reloaded 2768 * into it. 2769 * 2770 * @param {XPIStateLocation} aLocation 2771 * The install location containing the add-on 2772 * @param {string} aId 2773 * The ID of the add-on 2774 * @param {XPIState} aAddonState 2775 * The new state of the add-on 2776 * @param {AddonInternal?} [aNewAddon] 2777 * The manifest for the new add-on if it has already been loaded 2778 * @param {string?} [aOldAppVersion] 2779 * The version of the application last run with this profile or null 2780 * if it is a new profile or the version is unknown 2781 * @param {string?} [aOldPlatformVersion] 2782 * The version of the platform last run with this profile or null 2783 * if it is a new profile or the version is unknown 2784 * @returns {boolean} 2785 * A boolean indicating if flushing caches is required to complete 2786 * changing this add-on 2787 */ 2788 addMetadata( 2789 aLocation, 2790 aId, 2791 aAddonState, 2792 aNewAddon, 2793 aOldAppVersion, 2794 aOldPlatformVersion 2795 ) { 2796 logger.debug(`New add-on ${aId} installed in ${aLocation.name}`); 2797 2798 // We treat this is a new install if, 2799 // 2800 // a) It was explicitly registered as a staged install in the last 2801 // session, or, 2802 // b) We're not currently migrating or rebuilding a corrupt database. In 2803 // that case, we can assume this add-on was found during a routine 2804 // directory scan. 2805 let isNewInstall = !!aNewAddon || !XPIDatabase.rebuildingDatabase; 2806 2807 // If it's a new install and we haven't yet loaded the manifest then it 2808 // must be something dropped directly into the install location 2809 let isDetectedInstall = isNewInstall && !aNewAddon; 2810 2811 // Load the manifest if necessary and sanity check the add-on ID 2812 let unsigned; 2813 try { 2814 // Do not allow third party installs if xpinstall is disabled by policy 2815 if ( 2816 isDetectedInstall && 2817 Services.policies && 2818 !Services.policies.isAllowed("xpinstall") 2819 ) { 2820 throw new Error( 2821 "Extension installs are disabled by enterprise policy." 2822 ); 2823 } 2824 2825 if (!aNewAddon) { 2826 // Load the manifest from the add-on. 2827 aNewAddon = XPIInstall.syncLoadManifest(aAddonState, aLocation); 2828 } 2829 // The add-on in the manifest should match the add-on ID. 2830 if (aNewAddon.id != aId) { 2831 throw new Error( 2832 `Invalid addon ID: expected addon ID ${aId}, found ${aNewAddon.id} in manifest` 2833 ); 2834 } 2835 2836 unsigned = 2837 XPIDatabase.mustSign(aNewAddon.type) && !aNewAddon.isCorrectlySigned; 2838 if (unsigned) { 2839 throw Error(`Extension ${aNewAddon.id} is not correctly signed`); 2840 } 2841 } catch (e) { 2842 logger.warn(`addMetadata: Add-on ${aId} is invalid`, e); 2843 2844 // Remove the invalid add-on from the install location if the install 2845 // location isn't locked 2846 if (aLocation.isLinkedAddon(aId)) { 2847 logger.warn("Not uninstalling invalid item because it is a proxy file"); 2848 } else if (aLocation.locked) { 2849 logger.warn( 2850 "Could not uninstall invalid item from locked install location" 2851 ); 2852 } else if (unsigned && !isNewInstall) { 2853 logger.warn("Not uninstalling existing unsigned add-on"); 2854 } else if (aLocation.name == KEY_APP_BUILTINS) { 2855 // If a builtin has been removed from the build, we need to remove it from our 2856 // data sets. We cannot use location.isBuiltin since the system addon locations 2857 // mix it up. 2858 XPIDatabase.removeAddonMetadata(aAddonState); 2859 aLocation.removeAddon(aId); 2860 } else { 2861 aLocation.installer.uninstallAddon(aId); 2862 } 2863 return null; 2864 } 2865 2866 // Update the AddonInternal properties. 2867 aNewAddon.installDate = aAddonState.mtime; 2868 aNewAddon.updateDate = aAddonState.mtime; 2869 2870 // Assume that add-ons in the system add-ons install location aren't 2871 // foreign and should default to enabled. 2872 aNewAddon.foreignInstall = 2873 isDetectedInstall && !aLocation.isSystem && !aLocation.isBuiltin; 2874 2875 // appDisabled depends on whether the add-on is a foreignInstall so update 2876 aNewAddon.appDisabled = !XPIDatabase.isUsableAddon(aNewAddon); 2877 2878 if (isDetectedInstall && aNewAddon.foreignInstall) { 2879 // Add the installation source info for the sideloaded extension. 2880 aNewAddon.installTelemetryInfo = { 2881 source: aLocation.name, 2882 method: "sideload", 2883 }; 2884 2885 // If the add-on is a foreign install and is in a scope where add-ons 2886 // that were dropped in should default to disabled then disable it 2887 let disablingScopes = Services.prefs.getIntPref( 2888 PREF_EM_AUTO_DISABLED_SCOPES, 2889 0 2890 ); 2891 if (aLocation.scope & disablingScopes) { 2892 logger.warn( 2893 `Disabling foreign installed add-on ${aNewAddon.id} in ${aLocation.name}` 2894 ); 2895 aNewAddon.userDisabled = true; 2896 aNewAddon.seen = false; 2897 } 2898 } 2899 2900 return XPIDatabase.addToDatabase(aNewAddon, aAddonState.path); 2901 }, 2902 2903 /** 2904 * Called when an add-on has been removed. 2905 * 2906 * @param {AddonInternal} aOldAddon 2907 * The AddonInternal as it appeared the last time the application 2908 * ran 2909 */ 2910 removeMetadata(aOldAddon) { 2911 // This add-on has disappeared 2912 logger.debug( 2913 "Add-on " + aOldAddon.id + " removed from " + aOldAddon.location.name 2914 ); 2915 XPIDatabase.removeAddonMetadata(aOldAddon); 2916 }, 2917 2918 /** 2919 * Updates an add-on's metadata and determines. This is called when either the 2920 * add-on's install directory path or last modified time has changed. 2921 * 2922 * @param {XPIStateLocation} aLocation 2923 * The install location containing the add-on 2924 * @param {AddonInternal} aOldAddon 2925 * The AddonInternal as it appeared the last time the application 2926 * ran 2927 * @param {XPIState} aAddonState 2928 * The new state of the add-on 2929 * @param {AddonInternal?} [aNewAddon] 2930 * The manifest for the new add-on if it has already been loaded 2931 * @returns {AddonInternal} 2932 * The AddonInternal that was added to the database 2933 */ 2934 updateMetadata(aLocation, aOldAddon, aAddonState, aNewAddon) { 2935 logger.debug(`Add-on ${aOldAddon.id} modified in ${aLocation.name}`); 2936 2937 try { 2938 // If there isn't an updated install manifest for this add-on then load it. 2939 if (!aNewAddon) { 2940 aNewAddon = XPIInstall.syncLoadManifest( 2941 aAddonState, 2942 aLocation, 2943 aOldAddon 2944 ); 2945 } else { 2946 aNewAddon.rootURI = aOldAddon.rootURI; 2947 } 2948 2949 // The ID in the manifest that was loaded must match the ID of the old 2950 // add-on. 2951 if (aNewAddon.id != aOldAddon.id) { 2952 throw new Error( 2953 `Incorrect id in install manifest for existing add-on ${aOldAddon.id}` 2954 ); 2955 } 2956 } catch (e) { 2957 logger.warn(`updateMetadata: Add-on ${aOldAddon.id} is invalid`, e); 2958 2959 XPIDatabase.removeAddonMetadata(aOldAddon); 2960 aOldAddon.location.removeAddon(aOldAddon.id); 2961 2962 if (!aLocation.locked) { 2963 aLocation.installer.uninstallAddon(aOldAddon.id); 2964 } else { 2965 logger.warn( 2966 "Could not uninstall invalid item from locked install location" 2967 ); 2968 } 2969 2970 return null; 2971 } 2972 2973 // Set the additional properties on the new AddonInternal 2974 aNewAddon.updateDate = aAddonState.mtime; 2975 2976 XPIProvider.persistStartupData(aNewAddon, aAddonState); 2977 2978 // Update the database 2979 return XPIDatabase.updateAddonMetadata( 2980 aOldAddon, 2981 aNewAddon, 2982 aAddonState.path 2983 ); 2984 }, 2985 2986 /** 2987 * Updates an add-on's path for when the add-on has moved in the 2988 * filesystem but hasn't changed in any other way. 2989 * 2990 * @param {XPIStateLocation} aLocation 2991 * The install location containing the add-on 2992 * @param {AddonInternal} aOldAddon 2993 * The AddonInternal as it appeared the last time the application 2994 * ran 2995 * @param {XPIState} aAddonState 2996 * The new state of the add-on 2997 * @returns {AddonInternal} 2998 */ 2999 updatePath(aLocation, aOldAddon, aAddonState) { 3000 logger.debug(`Add-on ${aOldAddon.id} moved to ${aAddonState.path}`); 3001 aOldAddon.path = aAddonState.path; 3002 aOldAddon._sourceBundle = new nsIFile(aAddonState.path); 3003 aOldAddon.rootURI = XPIInternal.getURIForResourceInFile( 3004 aOldAddon._sourceBundle, 3005 "" 3006 ).spec; 3007 3008 return aOldAddon; 3009 }, 3010 3011 /** 3012 * Called when no change has been detected for an add-on's metadata but the 3013 * application has changed so compatibility may have changed. 3014 * 3015 * @param {XPIStateLocation} aLocation 3016 * The install location containing the add-on 3017 * @param {AddonInternal} aOldAddon 3018 * The AddonInternal as it appeared the last time the application 3019 * ran 3020 * @param {XPIState} aAddonState 3021 * The new state of the add-on 3022 * @param {boolean} [aReloadMetadata = false] 3023 * A boolean which indicates whether metadata should be reloaded from 3024 * the addon manifests. Default to false. 3025 * @returns {AddonInternal} 3026 * The new addon. 3027 */ 3028 updateCompatibility(aLocation, aOldAddon, aAddonState, aReloadMetadata) { 3029 logger.debug( 3030 `Updating compatibility for add-on ${aOldAddon.id} in ${aLocation.name}` 3031 ); 3032 3033 let checkSigning = 3034 aOldAddon.signedState === undefined && SIGNED_TYPES.has(aOldAddon.type); 3035 // signedDate must be set if signedState is set. 3036 let signedDateMissing = 3037 aOldAddon.signedDate === undefined && 3038 (aOldAddon.signedState || checkSigning); 3039 3040 // If maxVersion was inadvertently updated for a locale, force a reload 3041 // from the manifest. See Bug 1646016 for details. 3042 if ( 3043 !aReloadMetadata && 3044 aOldAddon.type === "locale" && 3045 aOldAddon.matchingTargetApplication 3046 ) { 3047 aReloadMetadata = aOldAddon.matchingTargetApplication.maxVersion === "*"; 3048 } 3049 3050 let manifest = null; 3051 if (checkSigning || aReloadMetadata || signedDateMissing) { 3052 try { 3053 manifest = XPIInstall.syncLoadManifest(aAddonState, aLocation); 3054 } catch (err) { 3055 // If we can no longer read the manifest, it is no longer compatible. 3056 aOldAddon.brokenManifest = true; 3057 aOldAddon.appDisabled = true; 3058 return aOldAddon; 3059 } 3060 } 3061 3062 // If updating from a version of the app that didn't support signedState 3063 // then update that property now 3064 if (checkSigning) { 3065 aOldAddon.signedState = manifest.signedState; 3066 } 3067 3068 if (signedDateMissing) { 3069 aOldAddon.signedDate = manifest.signedDate; 3070 } 3071 3072 // May be updating from a version of the app that didn't support all the 3073 // properties of the currently-installed add-ons. 3074 if (aReloadMetadata) { 3075 // Avoid re-reading these properties from manifest, 3076 // use existing addon instead. 3077 let remove = [ 3078 "syncGUID", 3079 "foreignInstall", 3080 "visible", 3081 "active", 3082 "userDisabled", 3083 "embedderDisabled", 3084 "applyBackgroundUpdates", 3085 "sourceURI", 3086 "releaseNotesURI", 3087 "installTelemetryInfo", 3088 ]; 3089 3090 // TODO - consider re-scanning for targetApplications for other addon types. 3091 if (aOldAddon.type !== "locale") { 3092 remove.push("targetApplications"); 3093 } 3094 3095 let props = PROP_JSON_FIELDS.filter(a => !remove.includes(a)); 3096 copyProperties(manifest, props, aOldAddon); 3097 } 3098 3099 aOldAddon.appDisabled = !XPIDatabase.isUsableAddon(aOldAddon); 3100 3101 return aOldAddon; 3102 }, 3103 3104 /** 3105 * Returns true if this install location is part of the application 3106 * bundle. Add-ons in these locations are expected to change whenever 3107 * the application updates. 3108 * 3109 * @param {XPIStateLocation} location 3110 * The install location to check. 3111 * @returns {boolean} 3112 * True if this location is part of the application bundle. 3113 */ 3114 isAppBundledLocation(location) { 3115 return ( 3116 location.name == KEY_APP_GLOBAL || 3117 location.name == KEY_APP_SYSTEM_DEFAULTS || 3118 location.name == KEY_APP_BUILTINS 3119 ); 3120 }, 3121 3122 /** 3123 * Returns true if this install location holds system addons. 3124 * 3125 * @param {XPIStateLocation} location 3126 * The install location to check. 3127 * @returns {boolean} 3128 * True if this location contains system add-ons. 3129 */ 3130 isSystemAddonLocation(location) { 3131 return ( 3132 location.name === KEY_APP_SYSTEM_DEFAULTS || 3133 location.name === KEY_APP_SYSTEM_ADDONS 3134 ); 3135 }, 3136 3137 /** 3138 * Updates the databse metadata for an existing add-on during database 3139 * reconciliation. 3140 * 3141 * @param {AddonInternal} oldAddon 3142 * The existing database add-on entry. 3143 * @param {XPIState} xpiState 3144 * The XPIStates entry for this add-on. 3145 * @param {AddonInternal?} newAddon 3146 * The new add-on metadata for the add-on, as loaded from a 3147 * staged update in addonStartup.json. 3148 * @param {boolean} aUpdateCompatibility 3149 * true to update add-ons appDisabled property when the application 3150 * version has changed 3151 * @param {boolean} aSchemaChange 3152 * The schema has changed and all add-on manifests should be re-read. 3153 * @returns {AddonInternal?} 3154 * The updated AddonInternal object for the add-on, if one 3155 * could be created. 3156 */ 3157 updateExistingAddon( 3158 oldAddon, 3159 xpiState, 3160 newAddon, 3161 aUpdateCompatibility, 3162 aSchemaChange 3163 ) { 3164 XPIDatabase.recordAddonTelemetry(oldAddon); 3165 3166 let installLocation = oldAddon.location; 3167 3168 // Update the add-on's database metadata from on-disk metadata if: 3169 // 3170 // a) The add-on was staged for install in the last session, 3171 // b) The add-on has been modified since the last session, or, 3172 // c) The app has been updated since the last session, and the 3173 // add-on is part of the application bundle (and has therefore 3174 // likely been replaced in the update process). 3175 if ( 3176 newAddon || 3177 oldAddon.updateDate != xpiState.mtime || 3178 (aUpdateCompatibility && this.isAppBundledLocation(installLocation)) 3179 ) { 3180 newAddon = this.updateMetadata( 3181 installLocation, 3182 oldAddon, 3183 xpiState, 3184 newAddon 3185 ); 3186 } else if (oldAddon.path != xpiState.path) { 3187 newAddon = this.updatePath(installLocation, oldAddon, xpiState); 3188 } else if (aUpdateCompatibility || aSchemaChange) { 3189 newAddon = this.updateCompatibility( 3190 installLocation, 3191 oldAddon, 3192 xpiState, 3193 aSchemaChange 3194 ); 3195 } else { 3196 newAddon = oldAddon; 3197 } 3198 3199 if (newAddon) { 3200 newAddon.rootURI = newAddon.rootURI || xpiState.rootURI; 3201 } 3202 3203 return newAddon; 3204 }, 3205 3206 /** 3207 * Compares the add-ons that are currently installed to those that were 3208 * known to be installed when the application last ran and applies any 3209 * changes found to the database. 3210 * Always called after XPIDatabase.jsm and extensions.json have been loaded. 3211 * 3212 * @param {Object} aManifests 3213 * A dictionary of cached AddonInstalls for add-ons that have been 3214 * installed 3215 * @param {boolean} aUpdateCompatibility 3216 * true to update add-ons appDisabled property when the application 3217 * version has changed 3218 * @param {string?} [aOldAppVersion] 3219 * The version of the application last run with this profile or null 3220 * if it is a new profile or the version is unknown 3221 * @param {string?} [aOldPlatformVersion] 3222 * The version of the platform last run with this profile or null 3223 * if it is a new profile or the version is unknown 3224 * @param {boolean} aSchemaChange 3225 * The schema has changed and all add-on manifests should be re-read. 3226 * @returns {boolean} 3227 * A boolean indicating if a change requiring flushing the caches was 3228 * detected 3229 */ 3230 processFileChanges( 3231 aManifests, 3232 aUpdateCompatibility, 3233 aOldAppVersion, 3234 aOldPlatformVersion, 3235 aSchemaChange 3236 ) { 3237 let findManifest = (loc, id) => { 3238 return (aManifests[loc.name] && aManifests[loc.name][id]) || null; 3239 }; 3240 3241 let previousAddons = new ExtensionUtils.DefaultMap(() => new Map()); 3242 let currentAddons = new ExtensionUtils.DefaultMap(() => new Map()); 3243 3244 // Get the previous add-ons from the database and put them into maps by location 3245 for (let addon of XPIDatabase.getAddons()) { 3246 previousAddons.get(addon.location.name).set(addon.id, addon); 3247 } 3248 3249 // Keep track of add-ons whose blocklist status may have changed. We'll check this 3250 // after everything else. 3251 let addonsToCheckAgainstBlocklist = []; 3252 3253 // Build the list of current add-ons into similar maps. When add-ons are still 3254 // present we re-use the add-on objects from the database and update their 3255 // details directly 3256 let addonStates = new Map(); 3257 for (let location of XPIStates.locations()) { 3258 let locationAddons = currentAddons.get(location.name); 3259 3260 // Get all the on-disk XPI states for this location, and keep track of which 3261 // ones we see in the database. 3262 let dbAddons = previousAddons.get(location.name) || new Map(); 3263 for (let [id, oldAddon] of dbAddons) { 3264 // Check if the add-on is still installed 3265 let xpiState = location.get(id); 3266 if (xpiState && !xpiState.missing) { 3267 let newAddon = this.updateExistingAddon( 3268 oldAddon, 3269 xpiState, 3270 findManifest(location, id), 3271 aUpdateCompatibility, 3272 aSchemaChange 3273 ); 3274 if (newAddon) { 3275 locationAddons.set(newAddon.id, newAddon); 3276 3277 // We need to do a blocklist check later, but the add-on may have changed by then. 3278 // Avoid storing the current copy and just get one when we need one instead. 3279 addonsToCheckAgainstBlocklist.push(newAddon.id); 3280 } 3281 } else { 3282 // The add-on is in the DB, but not in xpiState (and thus not on disk). 3283 this.removeMetadata(oldAddon); 3284 } 3285 } 3286 3287 for (let [id, xpiState] of location) { 3288 if (locationAddons.has(id) || xpiState.missing) { 3289 continue; 3290 } 3291 let newAddon = findManifest(location, id); 3292 let addon = this.addMetadata( 3293 location, 3294 id, 3295 xpiState, 3296 newAddon, 3297 aOldAppVersion, 3298 aOldPlatformVersion 3299 ); 3300 if (addon) { 3301 locationAddons.set(addon.id, addon); 3302 addonStates.set(addon, xpiState); 3303 } 3304 } 3305 3306 if (this.isSystemAddonLocation(location)) { 3307 for (let [id, addon] of locationAddons.entries()) { 3308 const pref = `extensions.${id.split("@")[0]}.enabled`; 3309 addon.userDisabled = !Services.prefs.getBoolPref(pref, true); 3310 } 3311 } 3312 } 3313 3314 // Validate the updated system add-ons 3315 let hideLocation; 3316 { 3317 let systemAddonLocation = XPIStates.getLocation(KEY_APP_SYSTEM_ADDONS); 3318 let addons = currentAddons.get(systemAddonLocation.name); 3319 3320 if (!systemAddonLocation.installer.isValid(addons)) { 3321 // Hide the system add-on updates if any are invalid. 3322 logger.info( 3323 "One or more updated system add-ons invalid, falling back to defaults." 3324 ); 3325 hideLocation = systemAddonLocation.name; 3326 } 3327 } 3328 3329 // Apply startup changes to any currently-visible add-ons, and 3330 // uninstall any which were previously visible, but aren't anymore. 3331 let previousVisible = this.getVisibleAddons(previousAddons); 3332 let currentVisible = this.flattenByID(currentAddons, hideLocation); 3333 3334 for (let addon of XPIDatabase.orphanedAddons.splice(0)) { 3335 if (addon.visible) { 3336 previousVisible.set(addon.id, addon); 3337 } 3338 } 3339 3340 let promises = []; 3341 for (let [id, addon] of currentVisible) { 3342 // If we have a stored manifest for the add-on, it came from the 3343 // startup data cache, and supersedes any previous XPIStates entry. 3344 let xpiState = 3345 !findManifest(addon.location, id) && addonStates.get(addon); 3346 3347 promises.push( 3348 this.applyStartupChange(addon, previousVisible.get(id), xpiState) 3349 ); 3350 previousVisible.delete(id); 3351 } 3352 3353 if (promises.some(p => p)) { 3354 XPIInternal.awaitPromise(Promise.all(promises)); 3355 } 3356 3357 for (let [id, addon] of previousVisible) { 3358 if (addon.location) { 3359 if (addon.location.name == KEY_APP_BUILTINS) { 3360 continue; 3361 } 3362 XPIInternal.BootstrapScope.get(addon).uninstall(); 3363 addon.location.removeAddon(id); 3364 addon.visible = false; 3365 addon.active = false; 3366 } 3367 3368 AddonManagerPrivate.addStartupChange( 3369 AddonManager.STARTUP_CHANGE_UNINSTALLED, 3370 id 3371 ); 3372 } 3373 3374 // Finally update XPIStates to match everything 3375 for (let [locationName, locationAddons] of currentAddons) { 3376 for (let [id, addon] of locationAddons) { 3377 let xpiState = XPIStates.getAddon(locationName, id); 3378 xpiState.syncWithDB(addon); 3379 } 3380 } 3381 XPIStates.save(); 3382 XPIDatabase.saveChanges(); 3383 XPIDatabase.rebuildingDatabase = false; 3384 3385 if (aUpdateCompatibility || aSchemaChange) { 3386 // Do some blocklist checks. These will happen after we've just saved everything, 3387 // because they're async and depend on the blocklist loading. When we're done, save 3388 // the data if any of the add-ons' blocklist state has changed. 3389 AddonManager.beforeShutdown.addBlocker( 3390 "Update add-on blocklist state into add-on DB", 3391 (async () => { 3392 // Avoid querying the AddonManager immediately to give startup a chance 3393 // to complete. 3394 await Promise.resolve(); 3395 3396 let addons = await AddonManager.getAddonsByIDs( 3397 addonsToCheckAgainstBlocklist 3398 ); 3399 await Promise.all( 3400 addons.map(async addon => { 3401 if (!addon) { 3402 return; 3403 } 3404 let oldState = addon.blocklistState; 3405 // TODO 1712316: updateBlocklistState with object parameter only 3406 // works if addon is an AddonInternal instance. But addon is an 3407 // AddonWrapper instead. Consequently updateDate:false is ignored. 3408 await addon.updateBlocklistState({ updateDatabase: false }); 3409 if (oldState !== addon.blocklistState) { 3410 Blocklist.recordAddonBlockChangeTelemetry( 3411 addon, 3412 "addon_db_modified" 3413 ); 3414 } 3415 }) 3416 ); 3417 3418 XPIDatabase.saveChanges(); 3419 })() 3420 ); 3421 } 3422 3423 return true; 3424 }, 3425 3426 /** 3427 * Applies a startup change for the given add-on. 3428 * 3429 * @param {AddonInternal} currentAddon 3430 * The add-on as it exists in this session. 3431 * @param {AddonInternal?} previousAddon 3432 * The add-on as it existed in the previous session. 3433 * @param {XPIState?} xpiState 3434 * The XPIState entry for this add-on, if one exists. 3435 * @returns {Promise?} 3436 * If an update was performed, returns a promise which resolves 3437 * when the appropriate bootstrap methods have been called. 3438 */ 3439 applyStartupChange(currentAddon, previousAddon, xpiState) { 3440 let promise; 3441 let { id } = currentAddon; 3442 3443 let isActive = !currentAddon.disabled; 3444 let wasActive = previousAddon ? previousAddon.active : currentAddon.active; 3445 3446 if (previousAddon) { 3447 if (previousAddon !== currentAddon) { 3448 AddonManagerPrivate.addStartupChange( 3449 AddonManager.STARTUP_CHANGE_CHANGED, 3450 id 3451 ); 3452 3453 // Bug 1664144: If the addon changed on disk we will catch it during 3454 // the second scan initiated by getNewSideloads. The addon may have 3455 // already started, if so we need to ensure it restarts during the 3456 // update, otherwise we're left in a state where the addon is enabled 3457 // but not started. We use the bootstrap started state to check that. 3458 // isActive alone is not sufficient as that changes the characteristics 3459 // of other updates and breaks many tests. 3460 let restart = 3461 isActive && XPIInternal.BootstrapScope.get(currentAddon).started; 3462 if (restart) { 3463 logger.warn( 3464 `Updating and restart addon ${previousAddon.id} that changed on disk after being already started.` 3465 ); 3466 } 3467 promise = XPIInternal.BootstrapScope.get(previousAddon).update( 3468 currentAddon, 3469 restart 3470 ); 3471 } 3472 3473 if (isActive != wasActive) { 3474 let change = isActive 3475 ? AddonManager.STARTUP_CHANGE_ENABLED 3476 : AddonManager.STARTUP_CHANGE_DISABLED; 3477 AddonManagerPrivate.addStartupChange(change, id); 3478 } 3479 } else if (xpiState && xpiState.wasRestored) { 3480 isActive = xpiState.enabled; 3481 3482 if (currentAddon.isWebExtension && currentAddon.type == "theme") { 3483 currentAddon.userDisabled = !isActive; 3484 } 3485 3486 // If the add-on wasn't active and it isn't already disabled in some way 3487 // then it was probably either softDisabled or userDisabled 3488 if (!isActive && !currentAddon.disabled) { 3489 // If the add-on is softblocked then assume it is softDisabled 3490 if ( 3491 currentAddon.blocklistState == Services.blocklist.STATE_SOFTBLOCKED 3492 ) { 3493 currentAddon.softDisabled = true; 3494 } else { 3495 currentAddon.userDisabled = true; 3496 } 3497 } 3498 } else { 3499 AddonManagerPrivate.addStartupChange( 3500 AddonManager.STARTUP_CHANGE_INSTALLED, 3501 id 3502 ); 3503 let scope = XPIInternal.BootstrapScope.get(currentAddon); 3504 scope.install(); 3505 } 3506 3507 XPIDatabase.makeAddonVisible(currentAddon); 3508 currentAddon.active = isActive; 3509 return promise; 3510 }, 3511}; 3512