Backed out changeset 6ceb34e525f4 (bug 1888975) for causing bc failures @ toolkit...
[gecko.git] / toolkit / components / extensions / ExtensionPermissions.sys.mjs
blob1ee9afdc32e74ee07670c241bea9dd8cd712ad78
1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 import { computeSha1HashAsString } from "resource://gre/modules/addons/crypto-utils.sys.mjs";
8 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
9 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
11 const lazy = {};
13 ChromeUtils.defineESModuleGetters(lazy, {
14   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
15   AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
16   ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
17   FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
18   JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
19   KeyValueService: "resource://gre/modules/kvstore.sys.mjs",
20 });
22 ChromeUtils.defineLazyGetter(
23   lazy,
24   "StartupCache",
25   () => lazy.ExtensionParent.StartupCache
28 ChromeUtils.defineLazyGetter(
29   lazy,
30   "Management",
31   () => lazy.ExtensionParent.apiManager
34 function emptyPermissions() {
35   return { permissions: [], origins: [] };
38 const DEFAULT_VALUE = JSON.stringify(emptyPermissions());
40 const KEY_PREFIX = "id-";
42 // This is the old preference file pre-migration to rkv.
43 const OLD_JSON_FILENAME = "extension-preferences.json";
44 // This is the old path to the rkv store dir (which used to be shared with ExtensionScriptingStore).
45 const OLD_RKV_DIRNAME = "extension-store";
46 // This is the new path to the rkv store dir.
47 const RKV_DIRNAME = "extension-store-permissions";
49 const VERSION_KEY = "_version";
51 const VERSION_VALUE = 1;
53 // Bug 1646182: remove once we fully migrate to rkv
54 let prefs;
56 // Bug 1646182: remove once we fully migrate to rkv
57 class LegacyPermissionStore {
58   async lazyInit() {
59     if (!this._initPromise) {
60       this._initPromise = this._init();
61     }
62     return this._initPromise;
63   }
65   async _init() {
66     let path = PathUtils.join(
67       Services.dirsvc.get("ProfD", Ci.nsIFile).path,
68       OLD_JSON_FILENAME
69     );
71     prefs = new lazy.JSONFile({ path });
72     prefs.data = {};
74     try {
75       prefs.data = await IOUtils.readJSON(path);
76     } catch (e) {
77       if (!(DOMException.isInstance(e) && e.name == "NotFoundError")) {
78         Cu.reportError(e);
79       }
80     }
81   }
83   async has(extensionId) {
84     await this.lazyInit();
85     return !!prefs.data[extensionId];
86   }
88   async get(extensionId) {
89     await this.lazyInit();
91     let perms = prefs.data[extensionId];
92     if (!perms) {
93       perms = emptyPermissions();
94     }
96     return perms;
97   }
99   async put(extensionId, permissions) {
100     await this.lazyInit();
101     prefs.data[extensionId] = permissions;
102     prefs.saveSoon();
103   }
105   async delete(extensionId) {
106     await this.lazyInit();
107     if (prefs.data[extensionId]) {
108       delete prefs.data[extensionId];
109       prefs.saveSoon();
110     }
111   }
113   async uninitForTest() {
114     if (!this._initPromise) {
115       return;
116     }
118     await this._initPromise;
119     await prefs.finalize();
120     prefs = null;
121     this._initPromise = null;
122   }
124   async resetVersionForTest() {
125     throw new Error("Not supported");
126   }
129 class PermissionStore {
130   _shouldMigrateFromOldKVStorePath = AppConstants.NIGHTLY_BUILD;
132   async _init() {
133     const storePath = lazy.FileUtils.getDir("ProfD", [RKV_DIRNAME]).path;
134     // Make sure the folder exists
135     await IOUtils.makeDirectory(storePath, { ignoreExisting: true });
136     this._store = await lazy.KeyValueService.getOrCreateWithOptions(
137       storePath,
138       "permissions",
139       { strategy: lazy.KeyValueService.RecoveryStrategy.RENAME }
140     );
141     if (!(await this._store.has(VERSION_KEY))) {
142       // If _shouldMigrateFromOldKVStorePath is true (default only on Nightly channel
143       // where the rkv store has been enabled by default for a while), we need to check
144       // if we would need to import data from the old kvstore path (ProfD/extensions-store)
145       // first, and fallback to try to import from the JSONFile if there was no data in
146       // the old kvstore path.
147       // NOTE: _shouldMigrateFromOldKVStorePath is also explicitly set to true in unit tests
148       // that are meant to explicitly cover this path also when running on on non-Nightly channels.
149       if (this._shouldMigrateFromOldKVStorePath) {
150         // Try to import data from the old kvstore path (ProfD/extensions-store).
151         await this.maybeImportFromOldKVStorePath();
152         if (!(await this._store.has(VERSION_KEY))) {
153           // There was no data in the old kvstore path, migrate any data
154           // available from the LegacyPermissionStore JSONFile if any.
155           await this.maybeMigrateDataFromOldJSONFile();
156         }
157       } else {
158         // On non-Nightly channels, where LegacyPermissionStore was still the
159         // only backend ever enabled, try to import permissions data from the
160         // legacy JSONFile, if any data is available there.
161         await this.maybeMigrateDataFromOldJSONFile();
162       }
163     }
164   }
166   lazyInit() {
167     if (!this._initPromise) {
168       this._initPromise = this._init();
169     }
170     return this._initPromise;
171   }
173   validateMigratedData(json) {
174     let data = {};
175     for (let [extensionId, permissions] of Object.entries(json)) {
176       // If both arrays are empty there's no need to include the value since
177       // it's the default
178       if (
179         "permissions" in permissions &&
180         "origins" in permissions &&
181         (permissions.permissions.length || permissions.origins.length)
182       ) {
183         data[extensionId] = permissions;
184       }
185     }
186     return data;
187   }
189   async maybeMigrateDataFromOldJSONFile() {
190     let migrationWasSuccessful = false;
191     let oldStore = PathUtils.join(
192       Services.dirsvc.get("ProfD", Ci.nsIFile).path,
193       OLD_JSON_FILENAME
194     );
195     try {
196       await this.migrateFrom(oldStore);
197       migrationWasSuccessful = true;
198     } catch (e) {
199       if (!(DOMException.isInstance(e) && e.name == "NotFoundError")) {
200         Cu.reportError(e);
201       }
202     }
204     await this._store.put(VERSION_KEY, VERSION_VALUE);
206     if (migrationWasSuccessful) {
207       IOUtils.remove(oldStore);
208     }
209   }
211   async maybeImportFromOldKVStorePath() {
212     try {
213       const oldStorePath = lazy.FileUtils.getDir("ProfD", [
214         OLD_RKV_DIRNAME,
215       ]).path;
216       if (!(await IOUtils.exists(oldStorePath))) {
217         return;
218       }
219       const oldStore = await lazy.KeyValueService.getOrCreate(
220         oldStorePath,
221         "permissions"
222       );
223       const enumerator = await oldStore.enumerate();
224       const kvpairs = [];
225       while (enumerator.hasMoreElements()) {
226         const { key, value } = enumerator.getNext();
227         kvpairs.push([key, value]);
228       }
230       // NOTE: we don't add a VERSION_KEY entry explicitly here because
231       // if the database was not empty the VERSION_KEY is already set to
232       // 1 and will be copied into the new file as part of the pairs
233       // written below (along with the entries for the actual extensions
234       // permissions).
235       if (kvpairs.length) {
236         await this._store.writeMany(kvpairs);
237       }
239       // NOTE: the old rkv store path used to be shared with the
240       // ExtensionScriptingStore, and so we are not removing the old
241       // rkv store dir here (that is going to be left to a separate
242       // migration we will be adding to ExtensionScriptingStore).
243     } catch (err) {
244       Cu.reportError(err);
245     }
246   }
248   async migrateFrom(oldStore) {
249     // Some other migration job might have started and not completed, let's
250     // start from scratch
251     await this._store.clear();
253     let json = await IOUtils.readJSON(oldStore);
254     let data = this.validateMigratedData(json);
256     if (data) {
257       let entries = Object.entries(data).map(([extensionId, permissions]) => [
258         this.makeKey(extensionId),
259         JSON.stringify(permissions),
260       ]);
261       if (entries.length) {
262         await this._store.writeMany(entries);
263       }
264     }
265   }
267   makeKey(extensionId) {
268     // We do this so that the extensionId field cannot clash with internal
269     // fields like `_version`
270     return KEY_PREFIX + extensionId;
271   }
273   async has(extensionId) {
274     await this.lazyInit();
275     return this._store.has(this.makeKey(extensionId));
276   }
278   async get(extensionId) {
279     await this.lazyInit();
280     return this._store
281       .get(this.makeKey(extensionId), DEFAULT_VALUE)
282       .then(JSON.parse);
283   }
285   async put(extensionId, permissions) {
286     await this.lazyInit();
287     return this._store.put(
288       this.makeKey(extensionId),
289       JSON.stringify(permissions)
290     );
291   }
293   async delete(extensionId) {
294     await this.lazyInit();
295     return this._store.delete(this.makeKey(extensionId));
296   }
298   async resetVersionForTest() {
299     await this.lazyInit();
300     return this._store.delete(VERSION_KEY);
301   }
303   async uninitForTest() {
304     // Nothing special to do to unitialize, let's just
305     // make sure we're not in the middle of initialization
306     return this._initPromise;
307   }
310 // Bug 1646182: turn on rkv on all channels
311 function createStore(useRkv = AppConstants.NIGHTLY_BUILD) {
312   if (useRkv) {
313     return new PermissionStore();
314   }
315   return new LegacyPermissionStore();
318 let store = createStore();
320 export var ExtensionPermissions = {
321   async _update(extensionId, perms) {
322     await store.put(extensionId, perms);
323     return lazy.StartupCache.permissions.set(extensionId, perms);
324   },
326   async _get(extensionId) {
327     return store.get(extensionId);
328   },
330   async _getCached(extensionId) {
331     return lazy.StartupCache.permissions.get(extensionId, () =>
332       this._get(extensionId)
333     );
334   },
336   /**
337    * Retrieves the optional permissions for the given extension.
338    * The information may be retrieved from the StartupCache, and otherwise fall
339    * back to data from the disk (and cache the result in the StartupCache).
340    *
341    * @param {string} extensionId The extensionId
342    * @returns {object} An object with "permissions" and "origins" array.
343    *   The object may be a direct reference to the storage or cache, so its
344    *   value should immediately be used and not be modified by callers.
345    */
346   get(extensionId) {
347     return this._getCached(extensionId);
348   },
350   _fixupAllUrlsPerms(perms) {
351     // Unfortunately, we treat <all_urls> as an API permission as well.
352     // If it is added to either, ensure it is added to both.
353     if (perms.origins.includes("<all_urls>")) {
354       perms.permissions.push("<all_urls>");
355     } else if (perms.permissions.includes("<all_urls>")) {
356       perms.origins.push("<all_urls>");
357     }
358   },
360   /**
361    * Add new permissions for the given extension.  `permissions` is
362    * in the format that is passed to browser.permissions.request().
363    *
364    * @param {string} extensionId The extension id
365    * @param {object} perms Object with permissions and origins array.
366    * @param {EventEmitter} [emitter] optional object implementing emitter interfaces
367    */
368   async add(extensionId, perms, emitter) {
369     let { permissions, origins } = await this._get(extensionId);
371     let added = emptyPermissions();
373     this._fixupAllUrlsPerms(perms);
375     for (let perm of perms.permissions) {
376       if (!permissions.includes(perm)) {
377         added.permissions.push(perm);
378         permissions.push(perm);
379       }
380     }
382     for (let origin of perms.origins) {
383       origin = new MatchPattern(origin, { ignorePath: true }).pattern;
384       if (!origins.includes(origin)) {
385         added.origins.push(origin);
386         origins.push(origin);
387       }
388     }
390     if (added.permissions.length || added.origins.length) {
391       await this._update(extensionId, { permissions, origins });
392       lazy.Management.emit("change-permissions", { extensionId, added });
393       if (emitter) {
394         emitter.emit("add-permissions", added);
395       }
396     }
397   },
399   /**
400    * Revoke permissions from the given extension.  `permissions` is
401    * in the format that is passed to browser.permissions.request().
402    *
403    * @param {string} extensionId The extension id
404    * @param {object} perms Object with permissions and origins array.
405    * @param {EventEmitter} [emitter] optional object implementing emitter interfaces
406    */
407   async remove(extensionId, perms, emitter) {
408     let { permissions, origins } = await this._get(extensionId);
410     let removed = emptyPermissions();
412     this._fixupAllUrlsPerms(perms);
414     for (let perm of perms.permissions) {
415       let i = permissions.indexOf(perm);
416       if (i >= 0) {
417         removed.permissions.push(perm);
418         permissions.splice(i, 1);
419       }
420     }
422     for (let origin of perms.origins) {
423       origin = new MatchPattern(origin, { ignorePath: true }).pattern;
425       let i = origins.indexOf(origin);
426       if (i >= 0) {
427         removed.origins.push(origin);
428         origins.splice(i, 1);
429       }
430     }
432     if (removed.permissions.length || removed.origins.length) {
433       await this._update(extensionId, { permissions, origins });
434       lazy.Management.emit("change-permissions", { extensionId, removed });
435       if (emitter) {
436         emitter.emit("remove-permissions", removed);
437       }
438     }
439   },
441   async removeAll(extensionId) {
442     lazy.StartupCache.permissions.delete(extensionId);
444     let removed = store.get(extensionId);
445     await store.delete(extensionId);
446     lazy.Management.emit("change-permissions", {
447       extensionId,
448       removed: await removed,
449     });
450   },
452   // This is meant for tests only
453   async _has(extensionId) {
454     return store.has(extensionId);
455   },
457   // This is meant for tests only
458   async _resetVersion() {
459     await store.resetVersionForTest();
460   },
462   // This is meant for tests only
463   _useLegacyStorageBackend: false,
465   // This is meant for tests only
466   async _uninit({ recreateStore = true } = {}) {
467     await store?.uninitForTest();
468     store = null;
469     if (recreateStore) {
470       store = createStore(!this._useLegacyStorageBackend);
471     }
472   },
474   // This is meant for tests only
475   _getStore() {
476     return store;
477   },
479   // Convenience listener members for all permission changes.
480   addListener(listener) {
481     lazy.Management.on("change-permissions", listener);
482   },
484   removeListener(listener) {
485     lazy.Management.off("change-permissions", listener);
486   },
489 export var OriginControls = {
490   allDomains: new MatchPattern("*://*/*"),
492   /**
493    * @typedef {object} OriginControlState
494    * @param {boolean} noAccess        no options, can never access host.
495    * @param {boolean} whenClicked     option to access host when clicked.
496    * @param {boolean} alwaysOn        option to always access this host.
497    * @param {boolean} allDomains      option to access to all domains.
498    * @param {boolean} hasAccess       extension currently has access to host.
499    * @param {boolean} temporaryAccess extension has temporary access to the tab.
500    */
502   /**
503    * Get origin controls state for a given extension on a given tab.
504    *
505    * @param {WebExtensionPolicy} policy
506    * @param {NativeTab} nativeTab
507    * @returns {OriginControlState} Extension origin controls for this host include:
508    */
509   getState(policy, nativeTab) {
510     // Note: don't use the nativeTab directly because it's different on mobile.
511     let tab = policy?.extension?.tabManager.getWrapper(nativeTab);
512     let temporaryAccess = tab?.hasActiveTabPermission;
513     let uri = tab?.browser.currentURI;
515     if (!uri) {
516       return { noAccess: true };
517     }
519     // activeTab and the resulting whenClicked state is only applicable for MV2
520     // extensions with a browser action and MV3 extensions (with or without).
521     let activeTab =
522       policy.permissions.includes("activeTab") &&
523       (policy.manifestVersion >= 3 || policy.extension?.hasBrowserActionUI);
525     let couldRequest = policy.extension.optionalOrigins.matches(uri);
526     let hasAccess = policy.canAccessURI(uri);
528     // If any of (MV2) content script patterns match the URI.
529     let csPatternMatches = false;
530     let quarantinedFrom = policy.quarantinedFromURI(uri);
532     if (policy.manifestVersion < 3 && !hasAccess) {
533       csPatternMatches = policy.contentScripts.some(cs =>
534         cs.matches.patterns.some(p => p.matches(uri))
535       );
536       // MV2 access through content scripts is implicit.
537       hasAccess = csPatternMatches && !quarantinedFrom;
538     }
540     // If extension is quarantined from this host, but could otherwise have
541     // access (via activeTab, optional, allowedOrigins or content scripts).
542     let quarantined =
543       quarantinedFrom &&
544       (activeTab ||
545         couldRequest ||
546         csPatternMatches ||
547         policy.allowedOrigins.matches(uri));
549     if (
550       quarantined ||
551       !this.allDomains.matches(uri) ||
552       WebExtensionPolicy.isRestrictedURI(uri) ||
553       (!couldRequest && !hasAccess && !activeTab)
554     ) {
555       return { noAccess: true, quarantined };
556     }
558     if (!couldRequest && !hasAccess && activeTab) {
559       return { whenClicked: true, temporaryAccess };
560     }
561     if (policy.allowedOrigins.subsumes(this.allDomains)) {
562       return { allDomains: true, hasAccess };
563     }
565     return {
566       whenClicked: true,
567       alwaysOn: true,
568       temporaryAccess,
569       hasAccess,
570     };
571   },
573   /**
574    * Whether to show the attention indicator for extension on current tab. We
575    * usually show attention when:
576    *
577    * - some permissions are needed (in MV3+)
578    * - the extension is not allowed on the domain (quarantined)
579    *
580    * @param {WebExtensionPolicy} policy an extension's policy
581    * @param {Window} window The window for which we can get the attention state
582    * @returns {{attention: boolean, quarantined: boolean}}
583    */
584   getAttentionState(policy, window) {
585     if (policy?.manifestVersion >= 3) {
586       const state = this.getState(policy, window.gBrowser.selectedTab);
587       // quarantined is always false when the feature is disabled.
588       const quarantined = !!state.quarantined;
589       const attention =
590         quarantined ||
591         (!!state.whenClicked && !state.hasAccess && !state.temporaryAccess);
593       return { attention, quarantined };
594     }
596     // No need to check whether the Quarantined Domains feature is enabled
597     // here, it's already done in `getState()`.
598     const state = this.getState(policy, window.gBrowser.selectedTab);
599     const attention = !!state.quarantined;
600     // If it needs attention, it's because of the quarantined domains.
601     return { attention, quarantined: attention };
602   },
604   // Grant extension host permission to always run on this host.
605   setAlwaysOn(policy, uri) {
606     if (!policy.active) {
607       return;
608     }
609     let perms = { permissions: [], origins: ["*://" + uri.host] };
610     return ExtensionPermissions.add(policy.id, perms, policy.extension);
611   },
613   // Revoke permission, extension should run only when clicked on this host.
614   setWhenClicked(policy, uri) {
615     if (!policy.active) {
616       return;
617     }
618     let perms = { permissions: [], origins: ["*://" + uri.host] };
619     return ExtensionPermissions.remove(policy.id, perms, policy.extension);
620   },
622   /**
623    * @typedef {object} FluentIdInfo
624    * @param {string} default      the message ID corresponding to the state
625    *                              that should be displayed by default.
626    * @param {string | null} onHover an optional message ID to be shown when
627    *                              users hover interactive elements (e.g. a
628    *                              button).
629    */
631   /**
632    * Get origin controls messages (fluent IDs) to be shown to users for a given
633    * extension on a given host. The messages might be different for extensions
634    * with a browser action (that might or might not open a popup).
635    *
636    * @param {object} params
637    * @param {WebExtensionPolicy} params.policy an extension's policy
638    * @param {NativeTab} params.tab             the current tab
639    * @param {boolean} params.isAction          this should be true for
640    *                                           extensions with a browser
641    *                                           action, false otherwise.
642    * @param {boolean} params.hasPopup          this should be true when the
643    *                                           browser action opens a popup,
644    *                                           false otherwise.
645    *
646    * @returns {FluentIdInfo?} An object with origin controls message IDs or
647    *                        `null` when there is no message for the state.
648    */
649   getStateMessageIDs({ policy, tab, isAction = false, hasPopup = false }) {
650     const state = this.getState(policy, tab);
652     const onHoverForAction = hasPopup
653       ? "origin-controls-state-runnable-hover-open"
654       : "origin-controls-state-runnable-hover-run";
656     if (state.noAccess) {
657       return {
658         default: state.quarantined
659           ? "origin-controls-state-quarantined"
660           : "origin-controls-state-no-access",
661         onHover: isAction ? onHoverForAction : null,
662       };
663     }
665     if (state.allDomains || (state.alwaysOn && state.hasAccess)) {
666       return {
667         default: "origin-controls-state-always-on",
668         onHover: isAction ? onHoverForAction : null,
669       };
670     }
672     if (state.whenClicked) {
673       return {
674         default: state.temporaryAccess
675           ? "origin-controls-state-temporary-access"
676           : "origin-controls-state-when-clicked",
677         onHover: "origin-controls-state-hover-run-visit-only",
678       };
679     }
681     return null;
682   },
685 export var QuarantinedDomains = {
686   getUserAllowedAddonIdPrefName(addonId) {
687     return `${this.PREF_ADDONS_BRANCH_NAME}${addonId}`;
688   },
689   isUserAllowedAddonId(addonId) {
690     return Services.prefs.getBoolPref(
691       this.getUserAllowedAddonIdPrefName(addonId),
692       false
693     );
694   },
695   setUserAllowedAddonIdPref(addonId, userAllowed) {
696     Services.prefs.setBoolPref(
697       this.getUserAllowedAddonIdPrefName(addonId),
698       userAllowed
699     );
700   },
701   clearUserPref(addonId) {
702     Services.prefs.clearUserPref(this.getUserAllowedAddonIdPrefName(addonId));
703   },
705   // Implementation internals.
707   PREF_ADDONS_BRANCH_NAME: `extensions.quarantineIgnoredByUser.`,
708   PREF_DOMAINSLIST_NAME: `extensions.quarantinedDomains.list`,
709   _initialized: false,
710   _init() {
711     if (this._initialized) {
712       return;
713     }
715     const onUserAllowedPrefChanged = this._onUserAllowedPrefChanged.bind(this);
716     Services.prefs.addObserver(
717       this.PREF_ADDONS_BRANCH_NAME,
718       onUserAllowedPrefChanged
719     );
721     const onUpdatedDomainsListTelemetry =
722       this._onUpdatedDomainsListTelemetry.bind(this);
723     XPCOMUtils.defineLazyPreferenceGetter(
724       this,
725       "currentDomainsList",
726       this.PREF_DOMAINSLIST_NAME,
727       "",
728       onUpdatedDomainsListTelemetry,
729       value => this._transformDomainsListPrefValue(value || "")
730     );
731     // Collect it at least once per session (and update it when the pref value changes).
732     onUpdatedDomainsListTelemetry();
734     const onAMRemoteSettingsSetPref =
735       this._onAMRemoteSettingsSetPref.bind(this);
736     Services.obs.addObserver(
737       onAMRemoteSettingsSetPref,
738       "am-remote-settings-setpref"
739     );
741     this._initialized = true;
742   },
743   async _onAMRemoteSettingsSetPref(subject, _topic) {
744     const { prefName, prefValue } = subject?.wrappedJSObject ?? {};
745     if (prefName !== this.PREF_DOMAINSLIST_NAME) {
746       return;
747     }
748     Glean.extensionsQuarantinedDomains.remotehash.set(
749       computeSha1HashAsString(prefValue || "")
750     );
751   },
752   async _onUserAllowedPrefChanged(_subject, _topic, prefName) {
753     let addonId = prefName.slice(this.PREF_ADDONS_BRANCH_NAME.length);
754     // Sanity check.
755     if (!addonId || prefName !== this.getUserAllowedAddonIdPrefName(addonId)) {
756       return;
757     }
759     // Notify listeners, e.g. to update details in TelemetryEnvironment.
760     const addon = await lazy.AddonManager.getAddonByID(addonId);
761     // Do not call onPropertyChanged listeners if the addon cannot be found
762     // anymore (e.g. it has been uninstalled).
763     if (addon) {
764       lazy.AddonManagerPrivate.callAddonListeners("onPropertyChanged", addon, [
765         "quarantineIgnoredByUser",
766       ]);
767     }
768   },
769   _onUpdatedDomainsListTelemetry(_subject, _topic, _prefName) {
770     Glean.extensionsQuarantinedDomains.listsize.set(
771       this.currentDomainsList.set.size
772     );
773     Glean.extensionsQuarantinedDomains.listhash.set(
774       this.currentDomainsList.hash
775     );
776   },
777   _transformDomainsListPrefValue(value) {
778     try {
779       return {
780         // NOTE: using a sha1 hash to make sure the resulting string will fit into the
781         // unified telemetry scalar string the glean metrics is mirrored to (which is
782         // limited to 50 characters).
783         hash: computeSha1HashAsString(value || ""),
784         set: new Set(
785           value
786             .split(",")
787             .map(v => v.trim())
788             .filter(v => v.length)
789         ),
790       };
791     } catch (err) {
792       return { hash: "unexpected-error", set: new Set() };
793     }
794   },
796 QuarantinedDomains._init();
798 // Constants exported for testing purpose.
799 export {
800   OLD_JSON_FILENAME,
801   OLD_RKV_DIRNAME,
802   RKV_DIRNAME,
803   VERSION_KEY,
804   VERSION_VALUE,