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