Bug 1867190 - Add prefs for PHC probablities r=glandium
[gecko.git] / toolkit / mozapps / extensions / Blocklist.sys.mjs
blob24d97556e18298a82d9784d8cf4ccf60ab6d0b3e
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
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 /* eslint "valid-jsdoc": [2, {requireReturn: false}] */
9 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
11 const lazy = {};
12 ChromeUtils.defineESModuleGetters(lazy, {
13   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
14   AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
15   RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
16   jexlFilterFunc: "resource://services-settings/remote-settings.sys.mjs",
17 });
19 const CascadeFilter = Components.Constructor(
20   "@mozilla.org/cascade-filter;1",
21   "nsICascadeFilter",
22   "setFilterData"
25 // The whole ID should be surrounded by literal ().
26 // IDs may contain alphanumerics, _, -, {}, @ and a literal '.'
27 // They may also contain backslashes (needed to escape the {} and dot)
28 // We filter out backslash escape sequences (like `\w`) separately
29 // (see kEscapeSequences).
30 const kIdSubRegex =
31   "\\([" +
32   "\\\\" + // note: just a backslash, but between regex and string it needs escaping.
33   "\\w .{}@-]+\\)";
35 // prettier-ignore
36 // Find regular expressions of the form:
37 // /^((id1)|(id2)|(id3)|...|(idN))$/
38 // The outer set of parens enclosing the entire list of IDs is optional.
39 const kIsMultipleIds = new RegExp(
40   // Start with literal sequence /^(
41   //  (the `(` is optional)
42   "^/\\^\\(?" +
43     // Then at least one ID in parens ().
44     kIdSubRegex +
45     // Followed by any number of IDs in () separated by pipes.
46     // Note: using a non-capturing group because we don't care about the value.
47     "(?:\\|" + kIdSubRegex + ")*" +
48   // Finally, we need to end with literal sequence )$/
49   //  (the leading `)` is optional like at the start)
50   "\\)?\\$/$"
53 // Check for a backslash followed by anything other than a literal . or curlies
54 const kEscapeSequences = /\\[^.{}]/;
56 // Used to remove the following 3 things:
57 // leading literal /^(
58 //    plus an optional (
59 // any backslash
60 // trailing literal )$/
61 //    plus an optional ) before the )$/
62 const kRegExpRemovalRegExp = /^\/\^\(\(?|\\|\)\)?\$\/$/g;
64 // In order to block add-ons, their type should be part of this list. There
65 // may be additional requirements such as requiring the add-on to be signed.
66 // See the uses of kXPIAddonTypes before introducing new addon types or
67 // providers that differ from the existing types.
68 ChromeUtils.defineLazyGetter(lazy, "kXPIAddonTypes", () => {
69   // In practice, this result is equivalent to ALL_XPI_TYPES in XPIProvider.jsm.
70   // "plugin" (from GMPProvider.sys.mjs) is intentionally omitted, as we decided to
71   // not support blocklisting of GMP plugins in bug 1086668.
72   return lazy.AddonManagerPrivate.getAddonTypesByProvider("XPIProvider");
73 });
75 // For a given input string matcher, produce either a string to compare with,
76 // a regular expression, or a set of strings to compare with.
77 function processMatcher(str) {
78   if (!str.startsWith("/")) {
79     return str;
80   }
81   // Process regexes matching multiple IDs into a set.
82   if (kIsMultipleIds.test(str) && !kEscapeSequences.test(str)) {
83     // Remove the regexp gunk at the start and end of the string, as well
84     // as all backslashes, and split by )|( to leave the list of IDs.
85     return new Set(str.replace(kRegExpRemovalRegExp, "").split(")|("));
86   }
87   let lastSlash = str.lastIndexOf("/");
88   let pattern = str.slice(1, lastSlash);
89   let flags = str.slice(lastSlash + 1);
90   return new RegExp(pattern, flags);
93 // Returns true if the addonProps object passes the constraints set by matches.
94 // (For every non-null property in matches, the same key must exist in
95 // addonProps and its value must match)
96 function doesAddonEntryMatch(matches, addonProps) {
97   for (let [key, value] of Object.entries(matches)) {
98     if (value === null || value === undefined) {
99       continue;
100     }
101     if (addonProps[key]) {
102       // If this property matches (member of the set, matches regex, or
103       // an exact string match), continue to look at the other properties of
104       // the `matches` object.
105       // If not, return false immediately.
106       if (value.has && value.has(addonProps[key])) {
107         continue;
108       }
109       if (value.test && value.test(addonProps[key])) {
110         continue;
111       }
112       if (typeof value == "string" && value === addonProps[key]) {
113         continue;
114       }
115     }
116     // If we get here, the property doesn't match, so this entry doesn't match.
117     return false;
118   }
119   // If we get here, all the properties must have matched.
120   return true;
123 const TOOLKIT_ID = "toolkit@mozilla.org";
124 const PREF_BLOCKLIST_ITEM_URL = "extensions.blocklist.itemURL";
125 const PREF_BLOCKLIST_ADDONITEM_URL = "extensions.blocklist.addonItemURL";
126 const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
127 const PREF_BLOCKLIST_LEVEL = "extensions.blocklist.level";
128 const PREF_BLOCKLIST_USE_MLBF = "extensions.blocklist.useMLBF";
129 const PREF_EM_LOGGING_ENABLED = "extensions.logging.enabled";
130 const DEFAULT_SEVERITY = 3;
131 const DEFAULT_LEVEL = 2;
132 const MAX_BLOCK_LEVEL = 3;
134 const BLOCKLIST_BUCKET = "blocklists";
136 const BlocklistTelemetry = {
137   init() {
138     // Used by BlocklistTelemetry.recordAddonBlockChangeTelemetry.
139     Services.telemetry.setEventRecordingEnabled("blocklist", true);
140   },
142   /**
143    * Record the RemoteSettings Blocklist lastModified server time into the
144    * "blocklist.lastModified_rs keyed scalar (or "Missing Date" when unable
145    * to retrieve a valid timestamp).
146    *
147    * @param {string} blocklistType
148    *        The blocklist type that has been updated ("addons" or "addons_mlbf");
149    *        the "gfx" blocklist is not covered by this telemetry).
150    * @param {RemoteSettingsClient} remoteSettingsClient
151    *        The RemoteSettings client to retrieve the lastModified timestamp from.
152    */
153   async recordRSBlocklistLastModified(blocklistType, remoteSettingsClient) {
154     // In some tests overrides ensureInitialized and remoteSettingsClient
155     // can be undefined, and in that case we don't want to record any
156     // telemetry scalar.
157     if (!remoteSettingsClient) {
158       return;
159     }
161     let lastModified = await remoteSettingsClient.getLastModified();
162     if (blocklistType === "addons_mlbf") {
163       BlocklistTelemetry.recordTimeScalar(
164         "lastModified_rs_" + blocklistType,
165         lastModified
166       );
167       BlocklistTelemetry.recordGleanDateTime(
168         Glean.blocklist.lastModifiedRsAddonsMblf,
169         lastModified
170       );
171     }
172   },
174   /**
175    * Record a timestamp in telemetry as a UTC string or "Missing Date" if the
176    * input is not a valid timestamp.
177    *
178    * @param {string} telemetryKey
179    *        The part of after "blocklist.", as defined in Scalars.yaml.
180    * @param {number} time
181    *        A timestamp to record. If invalid, "Missing Date" will be recorded.
182    */
183   recordTimeScalar(telemetryKey, time) {
184     if (time > 0) {
185       // convert from timestamp in ms into UTC datetime string, so it is going
186       // to be record in the same format previously used by blocklist.lastModified_xml.
187       let dateString = new Date(time).toUTCString();
188       Services.telemetry.scalarSet("blocklist." + telemetryKey, dateString);
189     } else {
190       Services.telemetry.scalarSet("blocklist." + telemetryKey, "Missing Date");
191     }
192   },
194   /**
195    * Records a glean datetime if time is > than 0, otherwise 0 is submitted.
196    *
197    * @param {nsIGleanDatetime} gleanTelemetry
198    *        A glean telemetry datetime object.
199    * @param {number} time
200    *        A timestamp to record.
201    */
202   recordGleanDateTime(gleanTelemetry, time) {
203     if (time > 0) {
204       // Glean date times are provided in nanoseconds, `getTime()` yields
205       // milliseconds (after the Unix epoch).
206       gleanTelemetry.set(time * 1000);
207     } else {
208       gleanTelemetry.set(0);
209     }
210   },
212   /**
213    * Record whether an add-on is blocked and the parameters that guided the
214    * decision to block or unblock the add-on.
215    *
216    * @param {AddonWrapper|object} addon
217    *        The blocked or unblocked add-on. Not necessarily installed.
218    *        Could be an object with the id, version and blocklistState
219    *        properties when the AddonWrapper is not available (e.g. during
220    *        update checks).
221    * @param {string} reason
222    *        The reason for recording the event,
223    *        "addon_install", "addon_update", "addon_update_check",
224    *        "addon_db_modified", "blocklist_update".
225    */
226   recordAddonBlockChangeTelemetry(addon, reason) {
227     // Reduce the timer resolution for anonymity.
228     let hoursSinceInstall = -1;
229     if (reason === "blocklist_update" || reason === "addon_db_modified") {
230       hoursSinceInstall = Math.round(
231         (Date.now() - addon.installDate.getTime()) / 3600000
232       );
233     }
235     const value = addon.id;
236     const extra = {
237       blocklistState: `${addon.blocklistState}`,
238       addon_version: addon.version,
239       signed_date: `${addon.signedDate?.getTime() || 0}`,
240       hours_since: `${hoursSinceInstall}`,
242       ...ExtensionBlocklistMLBF.getBlocklistMetadataForTelemetry(),
243     };
244     Glean.blocklist.addonBlockChange.record({
245       value,
246       object: reason,
247       blocklist_state: extra.blocklistState,
248       addon_version: extra.addon_version,
249       signed_date: extra.signed_date,
250       hours_since: extra.hours_since,
251       mlbf_last_time: extra.mlbf_last_time,
252       mlbf_generation: extra.mlbf_generation,
253       mlbf_source: extra.mlbf_source,
254     });
256     Services.telemetry.recordEvent(
257       "blocklist",
258       "addonBlockChange",
259       reason,
260       value,
261       extra
262     );
263   },
266 const Utils = {
267   /**
268    * Checks whether this entry is valid for the current OS and ABI.
269    * If the entry has an "os" property then the current OS must appear in
270    * its comma separated list for it to be valid. Similarly for the
271    * xpcomabi property.
272    *
273    * @param {Object} item
274    *        The blocklist item.
275    * @returns {bool}
276    *        Whether the entry matches the current OS.
277    */
278   matchesOSABI(item) {
279     if (item.os) {
280       let os = item.os.split(",");
281       if (!os.includes(lazy.gAppOS)) {
282         return false;
283       }
284     }
286     if (item.xpcomabi) {
287       let xpcomabi = item.xpcomabi.split(",");
288       if (!xpcomabi.includes(lazy.gApp.XPCOMABI)) {
289         return false;
290       }
291     }
292     return true;
293   },
295   /**
296    * Checks if a version is higher than or equal to the minVersion (if provided)
297    * and lower than or equal to the maxVersion (if provided).
298    * @param {string} version
299    *        The version to test.
300    * @param {string?} minVersion
301    *        The minimum version. If null it is assumed that version is always
302    *        larger.
303    * @param {string?} maxVersion
304    *        The maximum version. If null it is assumed that version is always
305    *        smaller.
306    * @returns {boolean}
307    *        Whether the item matches the range.
308    */
309   versionInRange(version, minVersion, maxVersion) {
310     if (minVersion && Services.vc.compare(version, minVersion) < 0) {
311       return false;
312     }
313     if (maxVersion && Services.vc.compare(version, maxVersion) > 0) {
314       return false;
315     }
316     return true;
317   },
319   /**
320    * Tests if this versionRange matches the item specified, and has a matching
321    * targetApplication id and version.
322    * @param {Object} versionRange
323    *        The versionRange to check against
324    * @param {string} itemVersion
325    *        The version of the actual addon/plugin to test for.
326    * @param {string} appVersion
327    *        The version of the application to test for.
328    * @param {string} toolkitVersion
329    *        The version of toolkit to check for.
330    * @returns {boolean}
331    *        True if this version range covers the item and app/toolkit version given.
332    */
333   versionsMatch(versionRange, itemVersion, appVersion, toolkitVersion) {
334     // Some platforms have no version for plugins, these don't match if there
335     // was a min/maxVersion provided
336     if (!itemVersion && (versionRange.minVersion || versionRange.maxVersion)) {
337       return false;
338     }
340     // Check if the item version matches
341     if (
342       !this.versionInRange(
343         itemVersion,
344         versionRange.minVersion,
345         versionRange.maxVersion
346       )
347     ) {
348       return false;
349     }
351     // Check if the application or toolkit version matches
352     for (let tA of versionRange.targetApplication) {
353       if (
354         tA.guid == lazy.gAppID &&
355         this.versionInRange(appVersion, tA.minVersion, tA.maxVersion)
356       ) {
357         return true;
358       }
359       if (
360         tA.guid == TOOLKIT_ID &&
361         this.versionInRange(toolkitVersion, tA.minVersion, tA.maxVersion)
362       ) {
363         return true;
364       }
365     }
366     return false;
367   },
369   /**
370    * Given a blocklist JS object entry, ensure it has a versionRange property, where
371    * each versionRange property has a valid severity property
372    * and at least 1 valid targetApplication.
373    * If it didn't have a valid targetApplication array before and/or it was empty,
374    * fill it with an entry with null min/maxVersion properties, which will match
375    * every version.
376    *
377    * If there *are* targetApplications, if any of them don't have a guid property,
378    * assign them the current app's guid.
379    *
380    * @param {Object} entry
381    *                 blocklist entry object.
382    */
383   ensureVersionRangeIsSane(entry) {
384     if (!entry.versionRange.length) {
385       entry.versionRange.push({});
386     }
387     for (let vr of entry.versionRange) {
388       if (!vr.hasOwnProperty("severity")) {
389         vr.severity = DEFAULT_SEVERITY;
390       }
391       if (!Array.isArray(vr.targetApplication)) {
392         vr.targetApplication = [];
393       }
394       if (!vr.targetApplication.length) {
395         vr.targetApplication.push({ minVersion: null, maxVersion: null });
396       }
397       vr.targetApplication.forEach(tA => {
398         if (!tA.guid) {
399           tA.guid = lazy.gAppID;
400         }
401       });
402     }
403   },
405   /**
406    * Create a blocklist URL for the given blockID
407    * @param {String} id the blockID to use
408    * @returns {String} the blocklist URL.
409    */
410   _createBlocklistURL(id) {
411     let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ITEM_URL);
412     return url.replace(/%blockID%/g, id);
413   },
417  * This custom filter function is used to limit the entries returned
418  * by `RemoteSettings("...").get()` depending on the target app information
419  * defined on entries.
421  * Note that this is async because `jexlFilterFunc` is async.
423  * @param {Object} entry a Remote Settings record
424  * @param {Object} environment the JEXL environment object.
425  * @returns {Object} The entry if it matches, `null` otherwise.
426  */
427 async function targetAppFilter(entry, environment) {
428   // If the entry has a JEXL filter expression, it should prevail.
429   // The legacy target app mechanism will be kept in place for old entries.
430   // See https://bugzilla.mozilla.org/show_bug.cgi?id=1463377
431   const { filter_expression } = entry;
432   if (filter_expression) {
433     return lazy.jexlFilterFunc(entry, environment);
434   }
436   // Keep entries without target information.
437   if (!("versionRange" in entry)) {
438     return entry;
439   }
441   const { versionRange } = entry;
443   // Everywhere in this method, we avoid checking the minVersion, because
444   // we want to retain items whose minVersion is higher than the current
445   // app version, so that we have the items around for app updates.
447   // Gfx blocklist has a specific versionRange object, which is not a list.
448   if (!Array.isArray(versionRange)) {
449     const { maxVersion = "*" } = versionRange;
450     const matchesRange =
451       Services.vc.compare(lazy.gApp.version, maxVersion) <= 0;
452     return matchesRange ? entry : null;
453   }
455   // Iterate the targeted applications, at least one of them must match.
456   // If no target application, keep the entry.
457   if (!versionRange.length) {
458     return entry;
459   }
460   for (const vr of versionRange) {
461     const { targetApplication = [] } = vr;
462     if (!targetApplication.length) {
463       return entry;
464     }
465     for (const ta of targetApplication) {
466       const { guid } = ta;
467       if (!guid) {
468         return entry;
469       }
470       const { maxVersion = "*" } = ta;
471       if (
472         guid == lazy.gAppID &&
473         Services.vc.compare(lazy.gApp.version, maxVersion) <= 0
474       ) {
475         return entry;
476       }
477       if (
478         guid == "toolkit@mozilla.org" &&
479         Services.vc.compare(Services.appinfo.platformVersion, maxVersion) <= 0
480       ) {
481         return entry;
482       }
483     }
484   }
485   // Skip this entry.
486   return null;
490  * The Graphics blocklist implementation. The JSON objects for graphics blocks look
491  * something like:
493  * {
494  *  "blockID": "g35",
495  *  "os": "WINNT 6.1",
496  *  "vendor": "0xabcd",
497  *  "devices": [
498  *    "0x2783",
499  *    "0x1234",
500  *  ],
501  *  "feature": " DIRECT2D ",
502  *  "featureStatus": " BLOCKED_DRIVER_VERSION ",
503  *  "driverVersion": " 8.52.322.2202 ",
504  *  "driverVersionComparator": " LESS_THAN ",
505  *  "versionRange": {"minVersion": "5.0", "maxVersion: "25.0"},
506  * }
508  * The RemoteSetttings client takes care of filtering out versions that don't apply.
509  * The code here stores entries in memory and sends them to the gfx component in
510  * serialized text form, using ',', '\t' and '\n' as separators.
511  */
512 const GfxBlocklistRS = {
513   _ensureInitialized() {
514     if (this._initialized || !gBlocklistEnabled) {
515       return;
516     }
517     this._initialized = true;
518     this._client = lazy.RemoteSettings("gfx", {
519       bucketName: BLOCKLIST_BUCKET,
520       filterFunc: targetAppFilter,
521     });
522     this.checkForEntries = this.checkForEntries.bind(this);
523     this._client.on("sync", this.checkForEntries);
524   },
526   shutdown() {
527     if (this._client) {
528       this._client.off("sync", this.checkForEntries);
529     }
530   },
532   sync() {
533     this._ensureInitialized();
534     return this._client.sync();
535   },
537   async checkForEntries() {
538     this._ensureInitialized();
539     if (!gBlocklistEnabled) {
540       return []; // return value expected by tests.
541     }
542     let entries = await this._client.get().catch(ex => Cu.reportError(ex));
543     // Handle error silently. This can happen if our request to fetch data is aborted,
544     // e.g. by application shutdown.
545     if (!entries) {
546       return [];
547     }
548     // Trim helper (spaces, tabs, no-break spaces..)
549     const trim = s =>
550       (s || "").replace(/(^[\s\uFEFF\xA0]+)|([\s\uFEFF\xA0]+$)/g, "");
552     entries = entries.map(entry => {
553       let props = [
554         "blockID",
555         "driverVersion",
556         "driverVersionMax",
557         "driverVersionComparator",
558         "feature",
559         "featureStatus",
560         "os",
561         "vendor",
562         "devices",
563       ];
564       let rv = {};
565       for (let p of props) {
566         let val = entry[p];
567         // Ignore falsy values or empty arrays.
568         if (!val || (Array.isArray(val) && !val.length)) {
569           continue;
570         }
571         if (typeof val == "string") {
572           val = trim(val);
573         } else if (p == "devices") {
574           let invalidDevices = [];
575           let validDevices = [];
576           // We serialize the array of devices as a comma-separated string, so
577           // we need to ensure that none of the entries contain commas, also in
578           // the future.
579           val.forEach(v =>
580             v.includes(",") ? invalidDevices.push(v) : validDevices.push(v)
581           );
582           for (let dev of invalidDevices) {
583             const e = new Error(
584               `Block ${entry.blockID} contains unsupported device: ${dev}`
585             );
586             Cu.reportError(e);
587           }
588           if (!validDevices) {
589             continue;
590           }
591           val = validDevices;
592         }
593         rv[p] = val;
594       }
595       if (entry.versionRange) {
596         rv.versionRange = {
597           minVersion: trim(entry.versionRange.minVersion) || "0",
598           maxVersion: trim(entry.versionRange.maxVersion) || "*",
599         };
600       }
601       return rv;
602     });
603     if (entries.length) {
604       let sortedProps = [
605         "blockID",
606         "devices",
607         "driverVersion",
608         "driverVersionComparator",
609         "driverVersionMax",
610         "feature",
611         "featureStatus",
612         "hardware",
613         "manufacturer",
614         "model",
615         "os",
616         "osversion",
617         "product",
618         "vendor",
619         "versionRange",
620       ];
621       // Notify `GfxInfoBase`, by passing a string serialization.
622       let payload = [];
623       for (let gfxEntry of entries) {
624         let entryLines = [];
625         for (let key of sortedProps) {
626           if (gfxEntry[key]) {
627             let value = gfxEntry[key];
628             if (Array.isArray(value)) {
629               value = value.join(",");
630             } else if (value.maxVersion) {
631               // Both minVersion and maxVersion are always set on each entry.
632               value = value.minVersion + "," + value.maxVersion;
633             }
634             entryLines.push(key + ":" + value);
635           }
636         }
637         payload.push(entryLines.join("\t"));
638       }
639       Services.obs.notifyObservers(
640         null,
641         "blocklist-data-gfxItems",
642         payload.join("\n")
643       );
644     }
645     // The return value is only used by tests.
646     return entries;
647   },
651  * The extensions blocklist implementation. The JSON objects for extension
652  * blocks look something like:
654  * {
655  *   "guid": "someguid@addons.mozilla.org",
656  *   "prefs": ["i.am.a.pref.that.needs.resetting"],
657  *   "schema": 1480349193877,
658  *   "blockID": "i12345",
659  *   "details": {
660  *     "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1234567",
661  *     "who": "All Firefox users who have this add-on installed. If you wish to continue using this add-on, you can enable it in the Add-ons Manager.",
662  *     "why": "This add-on is in violation of the <a href=\"https://developer.mozilla.org/en-US/Add-ons/Add-on_guidelines\">Add-on Guidelines</a>, using multiple add-on IDs and potentially doing other unwanted activities.",
663  *     "name": "Some pretty name",
664  *     "created": "2019-05-06T19:52:20Z"
665  *   },
666  *   "enabled": true,
667  *   "versionRange": [
668  *     {
669  *       "severity": 1,
670  *       "maxVersion": "*",
671  *       "minVersion": "0",
672  *       "targetApplication": []
673  *     }
674  *   ],
675  *   "id": "<unique guid>",
676  *   "last_modified": 1480349215672,
677  * }
679  * This is a legacy format, and implements deprecated operations (bug 1620580).
680  * ExtensionBlocklistMLBF supersedes this implementation.
681  */
682 const ExtensionBlocklistRS = {
683   async _ensureEntries() {
684     this.ensureInitialized();
685     if (!this._entries && gBlocklistEnabled) {
686       await this._updateEntries();
687     }
688   },
690   async _updateEntries() {
691     if (!gBlocklistEnabled) {
692       this._entries = [];
693       return;
694     }
695     this._entries = await this._client.get().catch(ex => Cu.reportError(ex));
696     // Handle error silently. This can happen if our request to fetch data is aborted,
697     // e.g. by application shutdown.
698     if (!this._entries) {
699       this._entries = [];
700       return;
701     }
702     this._entries.forEach(entry => {
703       entry.matches = {};
704       if (entry.guid) {
705         entry.matches.id = processMatcher(entry.guid);
706       }
707       for (let key of EXTENSION_BLOCK_FILTERS) {
708         if (key == "id" || !entry[key]) {
709           continue;
710         }
711         entry.matches[key] = processMatcher(entry[key]);
712       }
713       Utils.ensureVersionRangeIsSane(entry);
714     });
716     BlocklistTelemetry.recordRSBlocklistLastModified("addons", this._client);
717   },
719   async _filterItem(entry, environment) {
720     if (!(await targetAppFilter(entry, environment))) {
721       return null;
722     }
723     if (!Utils.matchesOSABI(entry)) {
724       return null;
725     }
726     // Need something to filter on - at least a guid or name (either could be a regex):
727     if (!entry.guid && !entry.name) {
728       let blockID = entry.blockID || entry.id;
729       Cu.reportError(new Error(`Nothing to filter add-on item ${blockID} on`));
730       return null;
731     }
732     return entry;
733   },
735   sync() {
736     this.ensureInitialized();
737     return this._client.sync();
738   },
740   ensureInitialized() {
741     if (!gBlocklistEnabled || this._initialized) {
742       return;
743     }
744     this._initialized = true;
745     this._client = lazy.RemoteSettings("addons", {
746       bucketName: BLOCKLIST_BUCKET,
747       filterFunc: this._filterItem,
748     });
749     this._onUpdate = this._onUpdate.bind(this);
750     this._client.on("sync", this._onUpdate);
751   },
753   shutdown() {
754     if (this._client) {
755       this._client.off("sync", this._onUpdate);
756       this._didShutdown = true;
757     }
758   },
760   // Called when the blocklist implementation is changed via a pref.
761   undoShutdown() {
762     if (this._didShutdown) {
763       this._client.on("sync", this._onUpdate);
764       this._didShutdown = false;
765     }
766   },
768   async _onUpdate() {
769     let oldEntries = this._entries || [];
770     await this.ensureInitialized();
771     await this._updateEntries();
773     let addons = await lazy.AddonManager.getAddonsByTypes(lazy.kXPIAddonTypes);
774     for (let addon of addons) {
775       let oldState = addon.blocklistState;
776       if (addon.updateBlocklistState) {
777         await addon.updateBlocklistState(false);
778       } else if (oldEntries) {
779         let oldEntry = this._getEntry(addon, oldEntries);
780         oldState = oldEntry
781           ? oldEntry.state
782           : Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
783       } else {
784         oldState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
785       }
786       let state = addon.blocklistState;
788       LOG(
789         "Blocklist state for " +
790           addon.id +
791           " changed from " +
792           oldState +
793           " to " +
794           state
795       );
797       // We don't want to re-warn about add-ons
798       if (state == oldState) {
799         continue;
800       }
802       // Ensure that softDisabled is false if the add-on is not soft blocked
803       if (state != Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
804         await addon.setSoftDisabled(false);
805       }
807       // If an add-on has dropped from hard to soft blocked just mark it as
808       // soft disabled and don't warn about it.
809       if (
810         state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED &&
811         oldState == Ci.nsIBlocklistService.STATE_BLOCKED
812       ) {
813         await addon.setSoftDisabled(true);
814       }
816       if (
817         state == Ci.nsIBlocklistService.STATE_BLOCKED ||
818         state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED
819       ) {
820         // Mark it as softblocked if necessary. Note that we avoid setting
821         // softDisabled at the same time as userDisabled to make it clear
822         // which was the original cause of the add-on becoming disabled in a
823         // way that the user can change.
824         if (
825           state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED &&
826           !addon.userDisabled
827         ) {
828           await addon.setSoftDisabled(true);
829         }
830         // It's a block. We must reset certain preferences.
831         let entry = this._getEntry(addon, this._entries);
832         if (entry.prefs && entry.prefs.length) {
833           for (let pref of entry.prefs) {
834             Services.prefs.clearUserPref(pref);
835           }
836         }
837       }
838     }
840     lazy.AddonManagerPrivate.updateAddonAppDisabledStates();
841   },
843   async getState(addon, appVersion, toolkitVersion) {
844     let entry = await this.getEntry(addon, appVersion, toolkitVersion);
845     return entry ? entry.state : Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
846   },
848   async getEntry(addon, appVersion, toolkitVersion) {
849     await this._ensureEntries();
850     return this._getEntry(addon, this._entries, appVersion, toolkitVersion);
851   },
853   _getEntry(addon, addonEntries, appVersion, toolkitVersion) {
854     if (!gBlocklistEnabled || !addon) {
855       return null;
856     }
858     // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
859     if (!appVersion && !lazy.gApp.version) {
860       return null;
861     }
863     if (!appVersion) {
864       appVersion = lazy.gApp.version;
865     }
866     if (!toolkitVersion) {
867       toolkitVersion = lazy.gApp.platformVersion;
868     }
870     let addonProps = {};
871     for (let key of EXTENSION_BLOCK_FILTERS) {
872       addonProps[key] = addon[key];
873     }
874     if (addonProps.creator) {
875       addonProps.creator = addonProps.creator.name;
876     }
878     for (let entry of addonEntries) {
879       // First check if it matches our properties. If not, just skip to the next item.
880       if (!doesAddonEntryMatch(entry.matches, addonProps)) {
881         continue;
882       }
883       // If those match, check the app or toolkit version works:
884       for (let versionRange of entry.versionRange) {
885         if (
886           Utils.versionsMatch(
887             versionRange,
888             addon.version,
889             appVersion,
890             toolkitVersion
891           )
892         ) {
893           let blockID = entry.blockID || entry.id;
894           return {
895             state:
896               versionRange.severity >= gBlocklistLevel
897                 ? Ci.nsIBlocklistService.STATE_BLOCKED
898                 : Ci.nsIBlocklistService.STATE_SOFTBLOCKED,
899             url: Utils._createBlocklistURL(blockID),
900             prefs: entry.prefs || [],
901           };
902         }
903       }
904     }
905     return null;
906   },
910  * The extensions blocklist implementation, the third version.
912  * The current blocklist is represented by a multi-level bloom filter (MLBF)
913  * (aka "Cascade Bloom Filter") that works like a set, i.e. supports a has()
914  * operation, except it is probabilistic. The MLBF is 100% accurate for known
915  * entries and unreliable for unknown entries.  When the backend generates the
916  * MLBF, all known add-ons are recorded, including their block state. Unknown
917  * add-ons are identified by their signature date being newer than the MLBF's
918  * generation time, and they are considered to not be blocked.
920  * Legacy blocklists used to distinguish between "soft block" and "hard block",
921  * but the current blocklist only supports one type of block ("hard block").
922  * After checking the blocklist states, any previous "soft blocked" addons will
923  * either be (hard) blocked or unblocked based on the blocklist.
925  * The MLBF is attached to a RemoteSettings record, as follows:
927  * {
928  *   "generation_time": 1585692000000,
929  *   "attachment": { ... RemoteSettings attachment ... }
930  *   "attachment_type": "bloomfilter-base",
931  * }
933  * The collection can also contain stashes:
935  * {
936  *   "stash_time": 1585692000001,
937  *   "stash": {
938  *     "blocked": [ "addonid:1.0", ... ],
939  *     "unblocked": [ "addonid:1.0", ... ]
940  *  }
942  * Stashes can be used to update the blocklist without forcing the whole MLBF
943  * to be downloaded again. These stashes are applied on top of the base MLBF.
944  */
945 const ExtensionBlocklistMLBF = {
946   RS_ATTACHMENT_ID: "addons-mlbf.bin",
948   async _fetchMLBF(record) {
949     // |record| may be unset. In that case, the MLBF dump is used instead
950     // (provided that the client has been built with it included).
951     let hash = record?.attachment.hash;
952     if (this._mlbfData && hash && this._mlbfData.cascadeHash === hash) {
953       // MLBF not changed, save the efforts of downloading the data again.
955       // Although the MLBF has not changed, the time in the record has. This
956       // means that the MLBF is known to provide accurate results for add-ons
957       // that were signed after the previously known date (but before the newly
958       // given date). To ensure that add-ons in this time range are also blocked
959       // as expected, update the cached generationTime.
960       if (record.generation_time > this._mlbfData.generationTime) {
961         this._mlbfData.generationTime = record.generation_time;
962       }
963       return this._mlbfData;
964     }
965     const {
966       buffer,
967       record: actualRecord,
968       _source: rsAttachmentSource,
969     } = await this._client.attachments.download(record, {
970       attachmentId: this.RS_ATTACHMENT_ID,
971       fallbackToCache: true,
972       fallbackToDump: true,
973     });
974     return {
975       cascadeHash: actualRecord.attachment.hash,
976       cascadeFilter: new CascadeFilter(new Uint8Array(buffer)),
977       // Note: generation_time is semantically distinct from last_modified.
978       // generation_time is compared with the signing date of the add-on, so it
979       // should be in sync with the signing service's clock.
980       // In contrast, last_modified does not have such strong requirements.
981       generationTime: actualRecord.generation_time,
982       // Used for telemetry.
983       rsAttachmentSource,
984     };
985   },
987   async _updateMLBF(forceUpdate = false) {
988     // The update process consists of fetching the collection, followed by
989     // potentially multiple network requests. As long as the collection has not
990     // been changed, repeated update requests can be coalesced. But when the
991     // collection has been updated, all pending update requests should await the
992     // new update request instead of the previous one.
993     if (!forceUpdate && this._updatePromise) {
994       return this._updatePromise;
995     }
996     const isUpdateReplaced = () => this._updatePromise != updatePromise;
997     const updatePromise = (async () => {
998       if (!gBlocklistEnabled) {
999         this._mlbfData = null;
1000         this._stashes = null;
1001         return;
1002       }
1003       let records = await this._client.get();
1004       if (isUpdateReplaced()) {
1005         return;
1006       }
1008       let mlbfRecords = records
1009         .filter(r => r.attachment)
1010         // Newest attachments first.
1011         .sort((a, b) => b.generation_time - a.generation_time);
1012       const mlbfRecord = mlbfRecords.find(
1013         r => r.attachment_type == "bloomfilter-base"
1014       );
1015       this._stashes = records
1016         .filter(({ stash }) => {
1017           return (
1018             // Exclude non-stashes, e.g. MLBF attachments.
1019             stash &&
1020             // Sanity check for type.
1021             Array.isArray(stash.blocked) &&
1022             Array.isArray(stash.unblocked)
1023           );
1024         })
1025         // Sort by stash time - newest first.
1026         .sort((a, b) => b.stash_time - a.stash_time)
1027         .map(({ stash, stash_time }) => ({
1028           blocked: new Set(stash.blocked),
1029           unblocked: new Set(stash.unblocked),
1030           stash_time,
1031         }));
1033       let mlbf = await this._fetchMLBF(mlbfRecord);
1034       // When a MLBF dump is packaged with the browser, mlbf will always be
1035       // non-null at this point.
1036       if (isUpdateReplaced()) {
1037         return;
1038       }
1039       this._mlbfData = mlbf;
1040     })()
1041       .catch(e => {
1042         Cu.reportError(e);
1043       })
1044       .then(() => {
1045         if (!isUpdateReplaced()) {
1046           this._updatePromise = null;
1047           this._recordPostUpdateTelemetry();
1048         }
1049         return this._updatePromise;
1050       });
1051     this._updatePromise = updatePromise;
1052     return updatePromise;
1053   },
1055   // Update the telemetry of the blocklist. This is always called, even if
1056   // the update request failed (e.g. due to network errors or data corruption).
1057   _recordPostUpdateTelemetry() {
1058     BlocklistTelemetry.recordRSBlocklistLastModified(
1059       "addons_mlbf",
1060       this._client
1061     );
1062     Glean.blocklist.mlbfSource.set(
1063       this._mlbfData?.rsAttachmentSource || "unknown"
1064     );
1065     BlocklistTelemetry.recordTimeScalar(
1066       "mlbf_generation_time",
1067       this._mlbfData?.generationTime
1068     );
1069     BlocklistTelemetry.recordGleanDateTime(
1070       Glean.blocklist.mlbfGenerationTime,
1071       this._mlbfData?.generationTime
1072     );
1073     // stashes has conveniently already been sorted by stash_time, newest first.
1074     let stashes = this._stashes || [];
1075     BlocklistTelemetry.recordTimeScalar(
1076       "mlbf_stash_time_oldest",
1077       stashes[stashes.length - 1]?.stash_time
1078     );
1079     BlocklistTelemetry.recordTimeScalar(
1080       "mlbf_stash_time_newest",
1081       stashes[0]?.stash_time
1082     );
1083     BlocklistTelemetry.recordGleanDateTime(
1084       Glean.blocklist.mlbfStashTimeOldest,
1085       stashes[stashes.length - 1]?.stash_time
1086     );
1088     BlocklistTelemetry.recordGleanDateTime(
1089       Glean.blocklist.mlbfStashTimeNewest,
1090       stashes[0]?.stash_time
1091     );
1092   },
1094   // Used by BlocklistTelemetry.recordAddonBlockChangeTelemetry.
1095   getBlocklistMetadataForTelemetry() {
1096     // Blocklist telemetry can only be reported when a blocklist decision
1097     // has been made. That implies that the blocklist has been loaded, so
1098     // ExtensionBlocklistMLBF should have been initialized.
1099     // (except when the blocklist is disabled, or blocklist v2 is used)
1100     const generationTime = this._mlbfData?.generationTime ?? 0;
1102     // Keys to include in the blocklist.addonBlockChange telemetry event.
1103     return {
1104       mlbf_last_time:
1105         // stashes are sorted, newest first. Stashes are newer than the MLBF.
1106         `${this._stashes?.[0]?.stash_time ?? generationTime}`,
1107       mlbf_generation: `${generationTime}`,
1108       mlbf_source: this._mlbfData?.rsAttachmentSource ?? "unknown",
1109     };
1110   },
1112   ensureInitialized() {
1113     if (!gBlocklistEnabled || this._initialized) {
1114       return;
1115     }
1116     this._initialized = true;
1117     this._client = lazy.RemoteSettings("addons-bloomfilters", {
1118       bucketName: BLOCKLIST_BUCKET,
1119       // Prevent the attachment for being pruned, since its ID does
1120       // not match any record.
1121       keepAttachmentsIds: [this.RS_ATTACHMENT_ID],
1122     });
1123     this._onUpdate = this._onUpdate.bind(this);
1124     this._client.on("sync", this._onUpdate);
1125   },
1127   shutdown() {
1128     if (this._client) {
1129       this._client.off("sync", this._onUpdate);
1130       this._didShutdown = true;
1131     }
1132   },
1134   // Called when the blocklist implementation is changed via a pref.
1135   undoShutdown() {
1136     if (this._didShutdown) {
1137       this._client.on("sync", this._onUpdate);
1138       this._didShutdown = false;
1139     }
1140   },
1142   async _onUpdate() {
1143     this.ensureInitialized();
1144     await this._updateMLBF(true);
1146     let addons = await lazy.AddonManager.getAddonsByTypes(lazy.kXPIAddonTypes);
1147     for (let addon of addons) {
1148       let oldState = addon.blocklistState;
1149       await addon.updateBlocklistState(false);
1150       let state = addon.blocklistState;
1152       LOG(
1153         "Blocklist state for " +
1154           addon.id +
1155           " changed from " +
1156           oldState +
1157           " to " +
1158           state
1159       );
1161       // We don't want to re-warn about add-ons
1162       if (state == oldState) {
1163         continue;
1164       }
1166       // Ensure that softDisabled is false if the add-on is not soft blocked
1167       // (by a previous implementation of the blocklist).
1168       if (state != Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
1169         await addon.setSoftDisabled(false);
1170       }
1172       BlocklistTelemetry.recordAddonBlockChangeTelemetry(
1173         addon,
1174         "blocklist_update"
1175       );
1176     }
1178     lazy.AddonManagerPrivate.updateAddonAppDisabledStates();
1179   },
1181   async getState(addon) {
1182     let state = await this.getEntry(addon);
1183     return state ? state.state : Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
1184   },
1186   async getEntry(addon) {
1187     if (!this._stashes) {
1188       this.ensureInitialized();
1189       await this._updateMLBF(false);
1190     } else if (this._updatePromise) {
1191       // _stashes has been initialized, but the initialization of _mlbfData is
1192       // still pending.
1193       await this._updatePromise;
1194     }
1196     let blockKey = addon.id + ":" + addon.version;
1198     // _stashes will be unset if !gBlocklistEnabled.
1199     if (this._stashes) {
1200       // Stashes are ordered by newest first.
1201       for (let stash of this._stashes) {
1202         // blocked and unblocked do not have overlapping entries.
1203         if (stash.blocked.has(blockKey)) {
1204           return this._createBlockEntry(addon);
1205         }
1206         if (stash.unblocked.has(blockKey)) {
1207           return null;
1208         }
1209       }
1210     }
1212     // signedDate is a Date if the add-on is signed, null if not signed,
1213     // undefined if it's an addon update descriptor instead of an addon wrapper.
1214     let { signedDate } = addon;
1215     if (!signedDate) {
1216       // The MLBF does not apply to unsigned add-ons.
1217       return null;
1218     }
1220     if (!this._mlbfData) {
1221       // This could happen in theory in any of the following cases:
1222       // - the blocklist is disabled.
1223       // - The RemoteSettings backend served a malformed MLBF.
1224       // - The RemoteSettings backend is unreachable, and this client was built
1225       //   without including a dump of the MLBF.
1226       //
1227       // ... in other words, this is unlikely to happen in practice.
1228       return null;
1229     }
1230     let { cascadeFilter, generationTime } = this._mlbfData;
1231     if (!cascadeFilter.has(blockKey)) {
1232       // Add-on not blocked or unknown.
1233       return null;
1234     }
1235     // Add-on blocked, or unknown add-on inadvertently labeled as blocked.
1237     let { signedState } = addon;
1238     if (
1239       signedState !== lazy.AddonManager.SIGNEDSTATE_PRELIMINARY &&
1240       signedState !== lazy.AddonManager.SIGNEDSTATE_SIGNED
1241     ) {
1242       // The block decision can only be relied upon for known add-ons, i.e.
1243       // signed via AMO. Anything else is unknown and ignored:
1244       //
1245       // - SIGNEDSTATE_SYSTEM and SIGNEDSTATE_PRIVILEGED are signed
1246       //   independently of AMO.
1247       //
1248       // - SIGNEDSTATE_NOT_REQUIRED already has an early return above due to
1249       //   signedDate being unset for these kinds of add-ons.
1250       //
1251       // - SIGNEDSTATE_BROKEN, SIGNEDSTATE_UNKNOWN and SIGNEDSTATE_MISSING
1252       //   means that the signature cannot be relied upon. It is equivalent to
1253       //   removing the signature from the XPI file, which already causes them
1254       //   to be disabled on release builds (where MOZ_REQUIRE_SIGNING=true).
1255       return null;
1256     }
1258     if (signedDate.getTime() > generationTime) {
1259       // The bloom filter only reports 100% accurate results for known add-ons.
1260       // Since the add-on was unknown when the bloom filter was generated, the
1261       // block decision is incorrect and should be treated as unblocked.
1262       return null;
1263     }
1265     if (AppConstants.NIGHTLY_BUILD && addon.type === "locale") {
1266       // Only Mozilla can create langpacks with a valid signature.
1267       // Langpacks for Release, Beta and ESR are submitted to AMO.
1268       // DevEd does not support external langpacks (bug 1563923), only builtins.
1269       //   (and built-in addons are not subjected to the blocklist).
1270       // Langpacks for Nightly are not known to AMO, so the MLBF cannot be used.
1271       return null;
1272     }
1274     return this._createBlockEntry(addon);
1275   },
1277   _createBlockEntry(addon) {
1278     return {
1279       state: Ci.nsIBlocklistService.STATE_BLOCKED,
1280       url: this.createBlocklistURL(addon.id, addon.version),
1281     };
1282   },
1284   createBlocklistURL(id, version) {
1285     let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ADDONITEM_URL);
1286     return url.replace(/%addonID%/g, id).replace(/%addonVersion%/g, version);
1287   },
1290 const EXTENSION_BLOCK_FILTERS = [
1291   "id",
1292   "name",
1293   "creator",
1294   "homepageURL",
1295   "updateURL",
1298 var gLoggingEnabled = null;
1299 var gBlocklistEnabled = true;
1300 var gBlocklistLevel = DEFAULT_LEVEL;
1303  * @class nsIBlocklistPrompt
1305  * nsIBlocklistPrompt is used, if available, by the default implementation of
1306  * nsIBlocklistService to display a confirmation UI to the user before blocking
1307  * extensions/plugins.
1308  */
1310  * @method prompt
1312  * Prompt the user about newly blocked addons. The prompt is then resposible
1313  * for soft-blocking any addons that need to be afterwards
1315  * @param {object[]} aAddons
1316  *         An array of addons and plugins that are blocked. These are javascript
1317  *         objects with properties:
1318  *          name    - the plugin or extension name,
1319  *          version - the version of the extension or plugin,
1320  *          icon    - the plugin or extension icon,
1321  *          disable - can be used by the nsIBlocklistPrompt to allows users to decide
1322  *                    whether a soft-blocked add-on should be disabled,
1323  *          blocked - true if the item is hard-blocked, false otherwise,
1324  *          item    - the Addon object
1325  */
1327 // It is not possible to use the one in Services since it will not successfully
1328 // QueryInterface nsIXULAppInfo in xpcshell tests due to other code calling
1329 // Services.appinfo before the nsIXULAppInfo is created by the tests.
1330 ChromeUtils.defineLazyGetter(lazy, "gApp", function () {
1331   // eslint-disable-next-line mozilla/use-services
1332   let appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
1333   try {
1334     appinfo.QueryInterface(Ci.nsIXULAppInfo);
1335   } catch (ex) {
1336     // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
1337     if (
1338       !(ex instanceof Components.Exception) ||
1339       ex.result != Cr.NS_NOINTERFACE
1340     ) {
1341       throw ex;
1342     }
1343   }
1344   return appinfo;
1347 ChromeUtils.defineLazyGetter(lazy, "gAppID", function () {
1348   return lazy.gApp.ID;
1350 ChromeUtils.defineLazyGetter(lazy, "gAppOS", function () {
1351   return lazy.gApp.OS;
1355  * Logs a string to the error console.
1356  * @param {string} string
1357  *        The string to write to the error console..
1358  */
1359 function LOG(string) {
1360   if (gLoggingEnabled) {
1361     dump("*** " + string + "\n");
1362     Services.console.logStringMessage(string);
1363   }
1366 export let Blocklist = {
1367   _init() {
1368     Services.obs.addObserver(this, "xpcom-shutdown");
1369     gLoggingEnabled = Services.prefs.getBoolPref(
1370       PREF_EM_LOGGING_ENABLED,
1371       false
1372     );
1373     gBlocklistEnabled = Services.prefs.getBoolPref(
1374       PREF_BLOCKLIST_ENABLED,
1375       true
1376     );
1377     gBlocklistLevel = Math.min(
1378       Services.prefs.getIntPref(PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
1379       MAX_BLOCK_LEVEL
1380     );
1381     this._chooseExtensionBlocklistImplementationFromPref();
1382     Services.prefs.addObserver("extensions.blocklist.", this);
1383     Services.prefs.addObserver(PREF_EM_LOGGING_ENABLED, this);
1384     BlocklistTelemetry.init();
1385   },
1386   isLoaded: true,
1388   shutdown() {
1389     GfxBlocklistRS.shutdown();
1390     this.ExtensionBlocklist.shutdown();
1392     Services.obs.removeObserver(this, "xpcom-shutdown");
1393     Services.prefs.removeObserver("extensions.blocklist.", this);
1394     Services.prefs.removeObserver(PREF_EM_LOGGING_ENABLED, this);
1395   },
1397   observe(subject, topic, prefName) {
1398     switch (topic) {
1399       case "xpcom-shutdown":
1400         this.shutdown();
1401         break;
1402       case "nsPref:changed":
1403         switch (prefName) {
1404           case PREF_EM_LOGGING_ENABLED:
1405             gLoggingEnabled = Services.prefs.getBoolPref(
1406               PREF_EM_LOGGING_ENABLED,
1407               false
1408             );
1409             break;
1410           case PREF_BLOCKLIST_ENABLED:
1411             gBlocklistEnabled = Services.prefs.getBoolPref(
1412               PREF_BLOCKLIST_ENABLED,
1413               true
1414             );
1415             this._blocklistUpdated();
1416             break;
1417           case PREF_BLOCKLIST_LEVEL:
1418             gBlocklistLevel = Math.min(
1419               Services.prefs.getIntPref(PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
1420               MAX_BLOCK_LEVEL
1421             );
1422             this._blocklistUpdated();
1423             break;
1424           case PREF_BLOCKLIST_USE_MLBF:
1425             let oldImpl = this.ExtensionBlocklist;
1426             this._chooseExtensionBlocklistImplementationFromPref();
1427             // The implementation may be unchanged when the pref is ignored.
1428             if (oldImpl != this.ExtensionBlocklist && oldImpl._initialized) {
1429               oldImpl.shutdown();
1430               this.ExtensionBlocklist.undoShutdown();
1431               this.ExtensionBlocklist._onUpdate();
1432             } // else neither has been initialized yet. Wait for it to happen.
1433             break;
1434         }
1435         break;
1436     }
1437   },
1439   loadBlocklistAsync() {
1440     // Need to ensure we notify gfx of new stuff.
1441     // Geckoview calls this for each new tab (bug 1730026), so ensure we only
1442     // check for entries when first initialized.
1443     if (!GfxBlocklistRS._initialized) {
1444       GfxBlocklistRS.checkForEntries();
1445     }
1446     this.ExtensionBlocklist.ensureInitialized();
1447   },
1449   getAddonBlocklistState(addon, appVersion, toolkitVersion) {
1450     // NOTE: appVersion/toolkitVersion are only used by ExtensionBlocklistRS.
1451     return this.ExtensionBlocklist.getState(addon, appVersion, toolkitVersion);
1452   },
1454   getAddonBlocklistEntry(addon, appVersion, toolkitVersion) {
1455     // NOTE: appVersion/toolkitVersion are only used by ExtensionBlocklistRS.
1456     return this.ExtensionBlocklist.getEntry(addon, appVersion, toolkitVersion);
1457   },
1459   recordAddonBlockChangeTelemetry(addon, reason) {
1460     BlocklistTelemetry.recordAddonBlockChangeTelemetry(addon, reason);
1461   },
1462   // TODO bug 1649906: Remove blocklist v2 (dead code).
1463   allowDeprecatedBlocklistV2: false,
1465   _chooseExtensionBlocklistImplementationFromPref() {
1466     if (
1467       this.allowDeprecatedBlocklistV2 &&
1468       !Services.prefs.getBoolPref(PREF_BLOCKLIST_USE_MLBF, false)
1469     ) {
1470       this.ExtensionBlocklist = ExtensionBlocklistRS;
1471     } else {
1472       this.ExtensionBlocklist = ExtensionBlocklistMLBF;
1473     }
1474   },
1476   _blocklistUpdated() {
1477     this.ExtensionBlocklist._onUpdate();
1478   },
1481 Blocklist._init();
1483 // Allow tests to reach implementation objects.
1484 export const BlocklistPrivate = {
1485   BlocklistTelemetry,
1486   ExtensionBlocklistMLBF,
1487   ExtensionBlocklistRS,
1488   GfxBlocklistRS,