diff options
Diffstat (limited to 'toolkit/mozapps/extensions/Blocklist.jsm')
-rw-r--r-- | toolkit/mozapps/extensions/Blocklist.jsm | 1898 |
1 files changed, 1898 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/Blocklist.jsm b/toolkit/mozapps/extensions/Blocklist.jsm new file mode 100644 index 0000000000..d8301ceb8b --- /dev/null +++ b/toolkit/mozapps/extensions/Blocklist.jsm @@ -0,0 +1,1898 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* eslint "valid-jsdoc": [2, {requireReturn: false}] */ + +var EXPORTED_SYMBOLS = ["Blocklist"]; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "AddonManager", + "resource://gre/modules/AddonManager.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "AddonManagerPrivate", + "resource://gre/modules/AddonManager.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "RemoteSettings", + "resource://services-settings/remote-settings.js" +); +ChromeUtils.defineModuleGetter( + this, + "jexlFilterFunc", + "resource://services-settings/remote-settings.js" +); + +const CascadeFilter = Components.Constructor( + "@mozilla.org/cascade-filter;1", + "nsICascadeFilter", + "setFilterData" +); + +// The whole ID should be surrounded by literal (). +// IDs may contain alphanumerics, _, -, {}, @ and a literal '.' +// They may also contain backslashes (needed to escape the {} and dot) +// We filter out backslash escape sequences (like `\w`) separately +// (see kEscapeSequences). +const kIdSubRegex = + "\\([" + + "\\\\" + // note: just a backslash, but between regex and string it needs escaping. + "\\w .{}@-]+\\)"; + +// prettier-ignore +// Find regular expressions of the form: +// /^((id1)|(id2)|(id3)|...|(idN))$/ +// The outer set of parens enclosing the entire list of IDs is optional. +const kIsMultipleIds = new RegExp( + // Start with literal sequence /^( + // (the `(` is optional) + "^/\\^\\(?" + + // Then at least one ID in parens (). + kIdSubRegex + + // Followed by any number of IDs in () separated by pipes. + // Note: using a non-capturing group because we don't care about the value. + "(?:\\|" + kIdSubRegex + ")*" + + // Finally, we need to end with literal sequence )$/ + // (the leading `)` is optional like at the start) + "\\)?\\$/$" +); + +// Check for a backslash followed by anything other than a literal . or curlies +const kEscapeSequences = /\\[^.{}]/; + +// Used to remove the following 3 things: +// leading literal /^( +// plus an optional ( +// any backslash +// trailing literal )$/ +// plus an optional ) before the )$/ +const kRegExpRemovalRegExp = /^\/\^\(\(?|\\|\)\)?\$\/$/g; + +// For a given input string matcher, produce either a string to compare with, +// a regular expression, or a set of strings to compare with. +function processMatcher(str) { + if (!str.startsWith("/")) { + return str; + } + // Process regexes matching multiple IDs into a set. + if (kIsMultipleIds.test(str) && !kEscapeSequences.test(str)) { + // Remove the regexp gunk at the start and end of the string, as well + // as all backslashes, and split by )|( to leave the list of IDs. + return new Set(str.replace(kRegExpRemovalRegExp, "").split(")|(")); + } + let lastSlash = str.lastIndexOf("/"); + let pattern = str.slice(1, lastSlash); + let flags = str.slice(lastSlash + 1); + return new RegExp(pattern, flags); +} + +// Returns true if the addonProps object passes the constraints set by matches. +// (For every non-null property in matches, the same key must exist in +// addonProps and its value must match) +function doesAddonEntryMatch(matches, addonProps) { + for (let [key, value] of Object.entries(matches)) { + if (value === null || value === undefined) { + continue; + } + if (addonProps[key]) { + // If this property matches (member of the set, matches regex, or + // an exact string match), continue to look at the other properties of + // the `matches` object. + // If not, return false immediately. + if (value.has && value.has(addonProps[key])) { + continue; + } + if (value.test && value.test(addonProps[key])) { + continue; + } + if (typeof value == "string" && value === addonProps[key]) { + continue; + } + } + // If we get here, the property doesn't match, so this entry doesn't match. + return false; + } + // If we get here, all the properties must have matched. + return true; +} + +const TOOLKIT_ID = "toolkit@mozilla.org"; +const PREF_BLOCKLIST_ITEM_URL = "extensions.blocklist.itemURL"; +const PREF_BLOCKLIST_ADDONITEM_URL = "extensions.blocklist.addonItemURL"; +const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled"; +const PREF_BLOCKLIST_LEVEL = "extensions.blocklist.level"; +const PREF_BLOCKLIST_SUPPRESSUI = "extensions.blocklist.suppressUI"; +const PREF_BLOCKLIST_USE_MLBF = "extensions.blocklist.useMLBF"; +const PREF_BLOCKLIST_USE_MLBF_STASHES = "extensions.blocklist.useMLBF.stashes"; +const PREF_EM_LOGGING_ENABLED = "extensions.logging.enabled"; +const URI_BLOCKLIST_DIALOG = + "chrome://mozapps/content/extensions/blocklist.xhtml"; +const DEFAULT_SEVERITY = 3; +const DEFAULT_LEVEL = 2; +const MAX_BLOCK_LEVEL = 3; +const SEVERITY_OUTDATED = 0; +const VULNERABILITYSTATUS_NONE = 0; +const VULNERABILITYSTATUS_UPDATE_AVAILABLE = 1; +const VULNERABILITYSTATUS_NO_UPDATE = 2; + +// Remote Settings blocklist constants +const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket"; +const PREF_BLOCKLIST_GFX_COLLECTION = "services.blocklist.gfx.collection"; +const PREF_BLOCKLIST_GFX_CHECKED_SECONDS = "services.blocklist.gfx.checked"; +const PREF_BLOCKLIST_GFX_SIGNER = "services.blocklist.gfx.signer"; +const PREF_BLOCKLIST_PLUGINS_COLLECTION = + "services.blocklist.plugins.collection"; +const PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS = + "services.blocklist.plugins.checked"; +const PREF_BLOCKLIST_PLUGINS_SIGNER = "services.blocklist.plugins.signer"; +// Blocklist v2 - legacy JSON format. +const PREF_BLOCKLIST_ADDONS_COLLECTION = "services.blocklist.addons.collection"; +const PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS = + "services.blocklist.addons.checked"; +const PREF_BLOCKLIST_ADDONS_SIGNER = "services.blocklist.addons.signer"; +// Blocklist v3 - MLBF format. +const PREF_BLOCKLIST_ADDONS3_COLLECTION = + "services.blocklist.addons-mlbf.collection"; +const PREF_BLOCKLIST_ADDONS3_CHECKED_SECONDS = + "services.blocklist.addons-mlbf.checked"; +const PREF_BLOCKLIST_ADDONS3_SIGNER = "services.blocklist.addons-mlbf.signer"; + +const BlocklistTelemetry = { + /** + * Record the RemoteSettings Blocklist lastModified server time into the + * "blocklist.lastModified_rs keyed scalar (or "Missing Date" when unable + * to retrieve a valid timestamp). + * + * @param {string} blocklistType + * The blocklist type that has been updated (one of "addons" or "plugins", + * or "addons_mlbf"; + * the "gfx" blocklist is not covered by this telemetry). + * @param {RemoteSettingsClient} remoteSettingsClient + * The RemoteSettings client to retrieve the lastModified timestamp from. + */ + async recordRSBlocklistLastModified(blocklistType, remoteSettingsClient) { + // In some tests overrides ensureInitialized and remoteSettingsClient + // can be undefined, and in that case we don't want to record any + // telemetry scalar. + if (!remoteSettingsClient) { + return; + } + + let lastModified = await remoteSettingsClient.getLastModified(); + BlocklistTelemetry.recordTimeScalar( + "lastModified_rs_" + blocklistType, + lastModified + ); + }, + + /** + * Record a timestamp in telemetry as a UTC string or "Missing Date" if the + * input is not a valid timestamp. + * + * @param {string} telemetryKey + * The part of after "blocklist.", as defined in Scalars.yaml. + * @param {number} time + * A timestamp to record. If invalid, "Missing Date" will be recorded. + */ + recordTimeScalar(telemetryKey, time) { + if (time > 0) { + // convert from timestamp in ms into UTC datetime string, so it is going + // to be record in the same format previously used by blocklist.lastModified_xml. + let dateString = new Date(time).toUTCString(); + Services.telemetry.scalarSet("blocklist." + telemetryKey, dateString); + } else { + Services.telemetry.scalarSet("blocklist." + telemetryKey, "Missing Date"); + } + }, +}; + +this.BlocklistTelemetry = BlocklistTelemetry; + +const Utils = { + /** + * Checks whether this entry is valid for the current OS and ABI. + * If the entry has an "os" property then the current OS must appear in + * its comma separated list for it to be valid. Similarly for the + * xpcomabi property. + * + * @param {Object} item + * The blocklist item. + * @returns {bool} + * Whether the entry matches the current OS. + */ + matchesOSABI(item) { + if (item.os) { + let os = item.os.split(","); + if (!os.includes(gAppOS)) { + return false; + } + } + + if (item.xpcomabi) { + let xpcomabi = item.xpcomabi.split(","); + if (!xpcomabi.includes(gApp.XPCOMABI)) { + return false; + } + } + return true; + }, + + /** + * Checks if a version is higher than or equal to the minVersion (if provided) + * and lower than or equal to the maxVersion (if provided). + * @param {string} version + * The version to test. + * @param {string?} minVersion + * The minimum version. If null it is assumed that version is always + * larger. + * @param {string?} maxVersion + * The maximum version. If null it is assumed that version is always + * smaller. + * @returns {boolean} + * Whether the item matches the range. + */ + versionInRange(version, minVersion, maxVersion) { + if (minVersion && Services.vc.compare(version, minVersion) < 0) { + return false; + } + if (maxVersion && Services.vc.compare(version, maxVersion) > 0) { + return false; + } + return true; + }, + + /** + * Tests if this versionRange matches the item specified, and has a matching + * targetApplication id and version. + * @param {Object} versionRange + * The versionRange to check against + * @param {string} itemVersion + * The version of the actual addon/plugin to test for. + * @param {string} appVersion + * The version of the application to test for. + * @param {string} toolkitVersion + * The version of toolkit to check for. + * @returns {boolean} + * True if this version range covers the item and app/toolkit version given. + */ + versionsMatch(versionRange, itemVersion, appVersion, toolkitVersion) { + // Some platforms have no version for plugins, these don't match if there + // was a min/maxVersion provided + if (!itemVersion && (versionRange.minVersion || versionRange.maxVersion)) { + return false; + } + + // Check if the item version matches + if ( + !this.versionInRange( + itemVersion, + versionRange.minVersion, + versionRange.maxVersion + ) + ) { + return false; + } + + // Check if the application or toolkit version matches + for (let tA of versionRange.targetApplication) { + if ( + tA.guid == gAppID && + this.versionInRange(appVersion, tA.minVersion, tA.maxVersion) + ) { + return true; + } + if ( + tA.guid == TOOLKIT_ID && + this.versionInRange(toolkitVersion, tA.minVersion, tA.maxVersion) + ) { + return true; + } + } + return false; + }, + + /** + * Given a blocklist JS object entry, ensure it has a versionRange property, where + * each versionRange property has valid severity and vulnerabilityStatus properties, + * and at least 1 valid targetApplication. + * If it didn't have a valid targetApplication array before and/or it was empty, + * fill it with an entry with null min/maxVersion properties, which will match + * every version. + * + * If there *are* targetApplications, if any of them don't have a guid property, + * assign them the current app's guid. + * + * @param {Object} entry + * blocklist entry object. + */ + ensureVersionRangeIsSane(entry) { + if (!entry.versionRange.length) { + entry.versionRange.push({}); + } + for (let vr of entry.versionRange) { + if (!vr.hasOwnProperty("severity")) { + vr.severity = DEFAULT_SEVERITY; + } + if (!vr.hasOwnProperty("vulnerabilityStatus")) { + vr.vulnerabilityStatus = VULNERABILITYSTATUS_NONE; + } + + if (!Array.isArray(vr.targetApplication)) { + vr.targetApplication = []; + } + if (!vr.targetApplication.length) { + vr.targetApplication.push({ minVersion: null, maxVersion: null }); + } + vr.targetApplication.forEach(tA => { + if (!tA.guid) { + tA.guid = gAppID; + } + }); + } + }, + + /** + * Create a blocklist URL for the given blockID + * @param {String} id the blockID to use + * @returns {String} the blocklist URL. + */ + _createBlocklistURL(id) { + let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ITEM_URL); + return url.replace(/%blockID%/g, id); + }, +}; + +/** + * This custom filter function is used to limit the entries returned + * by `RemoteSettings("...").get()` depending on the target app information + * defined on entries. + * + * Note that this is async because `jexlFilterFunc` is async. + * + * @param {Object} entry a Remote Settings record + * @param {Object} environment the JEXL environment object. + * @returns {Object} The entry if it matches, `null` otherwise. + */ +async function targetAppFilter(entry, environment) { + // If the entry has a JEXL filter expression, it should prevail. + // The legacy target app mechanism will be kept in place for old entries. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1463377 + const { filter_expression } = entry; + if (filter_expression) { + return jexlFilterFunc(entry, environment); + } + + // Keep entries without target information. + if (!("versionRange" in entry)) { + return entry; + } + + const { versionRange } = entry; + + // Everywhere in this method, we avoid checking the minVersion, because + // we want to retain items whose minVersion is higher than the current + // app version, so that we have the items around for app updates. + + // Gfx blocklist has a specific versionRange object, which is not a list. + if (!Array.isArray(versionRange)) { + const { maxVersion = "*" } = versionRange; + const matchesRange = Services.vc.compare(gApp.version, maxVersion) <= 0; + return matchesRange ? entry : null; + } + + // Iterate the targeted applications, at least one of them must match. + // If no target application, keep the entry. + if (!versionRange.length) { + return entry; + } + for (const vr of versionRange) { + const { targetApplication = [] } = vr; + if (!targetApplication.length) { + return entry; + } + for (const ta of targetApplication) { + const { guid } = ta; + if (!guid) { + return entry; + } + const { maxVersion = "*" } = ta; + if ( + guid == gAppID && + Services.vc.compare(gApp.version, maxVersion) <= 0 + ) { + return entry; + } + if ( + guid == "toolkit@mozilla.org" && + Services.vc.compare(Services.appinfo.platformVersion, maxVersion) <= 0 + ) { + return entry; + } + } + } + // Skip this entry. + return null; +} + +/** + * The Graphics blocklist implementation. The JSON objects for graphics blocks look + * something like: + * + * { + * "blockID": "g35", + * "os": "WINNT 6.1", + * "vendor": "0xabcd", + * "devices": [ + * "0x2783", + * "0x1234", + * ], + * "feature": " DIRECT2D ", + * "featureStatus": " BLOCKED_DRIVER_VERSION ", + * "driverVersion": " 8.52.322.2202 ", + * "driverVersionComparator": " LESS_THAN ", + * "versionRange": {"minVersion": "5.0", "maxVersion: "25.0"}, + * } + * + * The RemoteSetttings client takes care of filtering out versions that don't apply. + * The code here stores entries in memory and sends them to the gfx component in + * serialized text form, using ',', '\t' and '\n' as separators. + * + * Note: we assign to the global to allow tests to reach the object directly. + */ +this.GfxBlocklistRS = { + _ensureInitialized() { + if (this._initialized || !gBlocklistEnabled) { + return; + } + this._initialized = true; + this._client = RemoteSettings( + Services.prefs.getCharPref(PREF_BLOCKLIST_GFX_COLLECTION), + { + bucketNamePref: PREF_BLOCKLIST_BUCKET, + lastCheckTimePref: PREF_BLOCKLIST_GFX_CHECKED_SECONDS, + signerName: Services.prefs.getCharPref(PREF_BLOCKLIST_GFX_SIGNER), + filterFunc: targetAppFilter, + } + ); + this.checkForEntries = this.checkForEntries.bind(this); + this._client.on("sync", this.checkForEntries); + }, + + shutdown() { + if (this._client) { + this._client.off("sync", this.checkForEntries); + } + }, + + sync() { + this._ensureInitialized(); + return this._client.sync(); + }, + + async checkForEntries() { + this._ensureInitialized(); + if (!gBlocklistEnabled) { + return []; // return value expected by tests. + } + let entries = await this._client.get().catch(ex => Cu.reportError(ex)); + // Handle error silently. This can happen if our request to fetch data is aborted, + // e.g. by application shutdown. + if (!entries) { + return []; + } + // Trim helper (spaces, tabs, no-break spaces..) + const trim = s => + (s || "").replace(/(^[\s\uFEFF\xA0]+)|([\s\uFEFF\xA0]+$)/g, ""); + + entries = entries.map(entry => { + let props = [ + "blockID", + "driverVersion", + "driverVersionMax", + "driverVersionComparator", + "feature", + "featureStatus", + "os", + "vendor", + "devices", + ]; + let rv = {}; + for (let p of props) { + let val = entry[p]; + // Ignore falsy values or empty arrays. + if (!val || (Array.isArray(val) && !val.length)) { + continue; + } + if (typeof val == "string") { + val = trim(val); + } else if (p == "devices") { + let invalidDevices = []; + let validDevices = []; + // We serialize the array of devices as a comma-separated string, so + // we need to ensure that none of the entries contain commas, also in + // the future. + val.forEach(v => + v.includes(",") ? invalidDevices.push(v) : validDevices.push(v) + ); + for (let dev of invalidDevices) { + const e = new Error( + `Block ${entry.blockID} contains unsupported device: ${dev}` + ); + Cu.reportError(e); + } + if (!validDevices) { + continue; + } + val = validDevices; + } + rv[p] = val; + } + if (entry.versionRange) { + rv.versionRange = { + minVersion: trim(entry.versionRange.minVersion) || "0", + maxVersion: trim(entry.versionRange.maxVersion) || "*", + }; + } + return rv; + }); + if (entries.length) { + let sortedProps = [ + "blockID", + "devices", + "driverVersion", + "driverVersionComparator", + "driverVersionMax", + "feature", + "featureStatus", + "hardware", + "manufacturer", + "model", + "os", + "osversion", + "product", + "vendor", + "versionRange", + ]; + // Notify `GfxInfoBase`, by passing a string serialization. + let payload = []; + for (let gfxEntry of entries) { + let entryLines = []; + for (let key of sortedProps) { + if (gfxEntry[key]) { + let value = gfxEntry[key]; + if (Array.isArray(value)) { + value = value.join(","); + } else if (value.maxVersion) { + // Both minVersion and maxVersion are always set on each entry. + value = value.minVersion + "," + value.maxVersion; + } + entryLines.push(key + ":" + value); + } + } + payload.push(entryLines.join("\t")); + } + Services.obs.notifyObservers( + null, + "blocklist-data-gfxItems", + payload.join("\n") + ); + } + // The return value is only used by tests. + return entries; + }, +}; + +/** + * The plugins blocklist implementation. The JSON objects for plugin blocks look + * something like: + * + * { + * "blockID":"p906", + * "details": { + * "bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1159917", + * "who":"Which users it affects", + * "why":"Why it's being blocklisted", + * "name":"Java Plugin 7 update 45 to 78 (click-to-play), Windows", + * "created":"2015-05-19T09:02:45Z" + * }, + * "enabled":true, + * "infoURL":"https://java.com/", + * "matchName":"Java\\(TM\\) Platform SE 7 U(4[5-9]|(5|6)\\d|7[0-8])(\\s[^\\d\\._U]|$)", + * "versionRange":[ + * { + * "severity":0, + * "targetApplication":[ + * { + * "guid":"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}", + * "maxVersion":"57.0.*", + * "minVersion":"0" + * } + * ], + * "vulnerabilityStatus":1 + * } + * ], + * "matchFilename":"npjp2\\.dll", + * "id":"f254e5bc-12c7-7954-fe6b-8f1fdab0ae88", + * "last_modified":1519390914542, + * } + * + * Note: we assign to the global to allow tests to reach the object directly. + */ +this.PluginBlocklistRS = { + _matchProps: { + matchDescription: "description", + matchFilename: "filename", + matchName: "name", + }, + + async _ensureEntries() { + await this.ensureInitialized(); + if (!this._entries && gBlocklistEnabled) { + await this._updateEntries(); + + // Dispatch to mainthread because consumers may try to construct nsIPluginHost + // again based on this notification, while we were called from nsIPluginHost + // anyway, leading to re-entrancy. + Services.tm.dispatchToMainThread(function() { + Services.obs.notifyObservers(null, "plugin-blocklist-loaded"); + }); + } + }, + + async _updateEntries() { + if (!gBlocklistEnabled) { + this._entries = []; + return; + } + this._entries = await this._client.get().catch(ex => Cu.reportError(ex)); + // Handle error silently. This can happen if our request to fetch data is aborted, + // e.g. by application shutdown. + if (!this._entries) { + this._entries = []; + return; + } + this._entries.forEach(entry => { + entry.matches = {}; + for (let k of Object.keys(this._matchProps)) { + if (entry[k]) { + try { + entry.matches[this._matchProps[k]] = new RegExp(entry[k], "m"); + } catch (ex) { + /* Ignore invalid regexes */ + } + } + } + Utils.ensureVersionRangeIsSane(entry); + }); + + BlocklistTelemetry.recordRSBlocklistLastModified("plugins", this._client); + }, + + async _filterItem(entry, environment) { + if (!(await targetAppFilter(entry, environment))) { + return null; + } + if (!Utils.matchesOSABI(entry)) { + return null; + } + if (!entry.matchFilename && !entry.matchName && !entry.matchDescription) { + let blockID = entry.blockID || entry.id; + Cu.reportError(new Error(`Nothing to filter plugin item ${blockID}`)); + return null; + } + return entry; + }, + + sync() { + this.ensureInitialized(); + return this._client.sync(); + }, + + ensureInitialized() { + if (!gBlocklistEnabled || this._initialized) { + return; + } + this._initialized = true; + this._client = RemoteSettings( + Services.prefs.getCharPref(PREF_BLOCKLIST_PLUGINS_COLLECTION), + { + bucketNamePref: PREF_BLOCKLIST_BUCKET, + lastCheckTimePref: PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS, + signerName: Services.prefs.getCharPref(PREF_BLOCKLIST_PLUGINS_SIGNER), + filterFunc: this._filterItem, + } + ); + this._onUpdate = this._onUpdate.bind(this); + this._client.on("sync", this._onUpdate); + }, + + shutdown() { + if (this._client) { + this._client.off("sync", this._onUpdate); + } + }, + + async _onUpdate() { + let oldEntries = this._entries || []; + this.ensureInitialized(); + await this._updateEntries(); + const pluginHost = Cc["@mozilla.org/plugin/host;1"].getService( + Ci.nsIPluginHost + ); + const plugins = pluginHost.getPluginTags(); + + let blockedItems = []; + + for (let plugin of plugins) { + let oldState = this._getState(plugin, oldEntries); + let state = this._getState(plugin, this._entries); + LOG( + "Blocklist state for " + + plugin.name + + " changed from " + + oldState + + " to " + + state + ); + // We don't want to re-warn about items + if (state == oldState) { + continue; + } + + if (oldState == Ci.nsIBlocklistService.STATE_BLOCKED) { + if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) { + plugin.enabledState = Ci.nsIPluginTag.STATE_DISABLED; + } + } else if ( + !plugin.disabled && + state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED + ) { + if ( + state != Ci.nsIBlocklistService.STATE_OUTDATED && + state != Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE && + state != Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE + ) { + blockedItems.push({ + name: plugin.name, + version: plugin.version, + icon: "chrome://mozapps/skin/plugins/plugin.svg", + disable: false, + blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED, + item: plugin, + url: await this.getURL(plugin), + }); + } + } + } + + if (blockedItems.length) { + this._showBlockedPluginsPrompt(blockedItems); + } else { + this._notifyUpdate(); + } + }, + + _showBlockedPluginsPrompt(blockedPlugins) { + let args = { + restart: false, + list: blockedPlugins, + }; + // This lets the dialog get the raw js object + args.wrappedJSObject = args; + + /* + Some tests run without UI, so the async code listens to a message + that can be sent programatically + */ + let applyBlocklistChanges = async () => { + Services.obs.removeObserver( + applyBlocklistChanges, + "addon-blocklist-closed" + ); + + for (let blockedData of blockedPlugins) { + if (!blockedData.disable) { + continue; + } + + // This will disable all the plugins immediately. + if (blockedData.item instanceof Ci.nsIPluginTag) { + blockedData.item.enabledState = Ci.nsIPluginTag.STATE_DISABLED; + } + } + + if (!args.restart) { + this._notifyUpdate(); + return; + } + + // We need to ensure the new blocklist state is written to disk before restarting. + // We'll notify about the blocklist update, then wait for nsIPluginHost + // to finish processing it, then restart the browser. + let pluginUpdatesFinishedPromise = new Promise(resolve => { + Services.obs.addObserver(function updatesFinished() { + Services.obs.removeObserver( + updatesFinished, + "plugin-blocklist-updates-finished" + ); + resolve(); + }, "plugin-blocklist-updates-finished"); + }); + this._notifyUpdate(); + await pluginUpdatesFinishedPromise; + + // Notify all windows that an application quit has been requested. + var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested"); + + // Something aborted the quit process. + if (cancelQuit.data) { + return; + } + + Services.startup.quit( + Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit + ); + }; + + Services.obs.addObserver(applyBlocklistChanges, "addon-blocklist-closed"); + + if (Services.prefs.getBoolPref(PREF_BLOCKLIST_SUPPRESSUI, false)) { + applyBlocklistChanges(); + return; + } + + function blocklistUnloadHandler(event) { + if (event.target.location == URI_BLOCKLIST_DIALOG) { + applyBlocklistChanges(); + blocklistWindow.removeEventListener("unload", blocklistUnloadHandler); + } + } + + let blocklistWindow = Services.ww.openWindow( + null, + URI_BLOCKLIST_DIALOG, + "", + "chrome,centerscreen,dialog,titlebar", + args + ); + if (blocklistWindow) { + blocklistWindow.addEventListener("unload", blocklistUnloadHandler); + } + }, + + _notifyUpdate() { + Services.obs.notifyObservers(null, "plugin-blocklist-updated"); + }, + + async getURL(plugin) { + await this._ensureEntries(); + let r = this._getEntry(plugin, this._entries); + if (!r) { + return null; + } + let blockEntry = r.entry; + let blockID = blockEntry.blockID || blockEntry.id; + return blockEntry.infoURL || Utils._createBlocklistURL(blockID); + }, + + async getState(plugin, appVersion, toolkitVersion) { + if (AppConstants.platform == "android") { + return Ci.nsIBlocklistService.STATE_NOT_BLOCKED; + } + await this._ensureEntries(); + return this._getState(plugin, this._entries, appVersion, toolkitVersion); + }, + + /** + * Private helper to get the blocklist entry for a plugin given a set of + * blocklist entries and versions. + * + * @param {nsIPluginTag} plugin + * The nsIPluginTag to get the blocklist state for. + * @param {object[]} pluginEntries + * The plugin blocklist entries to compare against. + * @param {string?} appVersion + * The application version to compare to, will use the current + * version if null. + * @param {string?} toolkitVersion + * The toolkit version to compare to, will use the current version if + * null. + * @returns {object?} + * {entry: blocklistEntry, version: blocklistEntryVersion}, + * or null if there is no matching entry. + */ + _getEntry(plugin, pluginEntries, appVersion, toolkitVersion) { + if (!gBlocklistEnabled) { + return null; + } + + // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't). + if (!appVersion && !gApp.version) { + return Ci.nsIBlocklistService.STATE_NOT_BLOCKED; + } + + if (!appVersion) { + appVersion = gApp.version; + } + if (!toolkitVersion) { + toolkitVersion = gApp.platformVersion; + } + + const pluginProperties = { + description: plugin.description, + filename: plugin.filename, + name: plugin.name, + version: plugin.version, + }; + if (!pluginEntries) { + Cu.reportError( + new Error("There are no plugin entries. This should never happen.") + ); + } + for (let blockEntry of pluginEntries) { + var matchFailed = false; + for (var name in blockEntry.matches) { + let pluginProperty = pluginProperties[name]; + if ( + typeof pluginProperty != "string" || + !blockEntry.matches[name].test(pluginProperty) + ) { + matchFailed = true; + break; + } + } + + if (matchFailed) { + continue; + } + + for (let versionRange of blockEntry.versionRange) { + if ( + Utils.versionsMatch( + versionRange, + pluginProperties.version, + appVersion, + toolkitVersion + ) + ) { + return { entry: blockEntry, version: versionRange }; + } + } + } + + return null; + }, + + /** + * Private version of getState that allows the caller to pass in + * the plugin blocklist entries. + * + * @param {nsIPluginTag} plugin + * The nsIPluginTag to get the blocklist state for. + * @param {object[]} pluginEntries + * The plugin blocklist entries to compare against. + * @param {string?} appVersion + * The application version to compare to, will use the current + * version if null. + * @param {string?} toolkitVersion + * The toolkit version to compare to, will use the current version if + * null. + * @returns {integer} + * The blocklist state for the item, one of the STATE constants as + * defined in nsIBlocklistService. + */ + _getState(plugin, pluginEntries, appVersion, toolkitVersion) { + let r = this._getEntry(plugin, pluginEntries, appVersion, toolkitVersion); + if (!r) { + return Ci.nsIBlocklistService.STATE_NOT_BLOCKED; + } + + let { version: versionRange } = r; + + if (versionRange.severity >= gBlocklistLevel) { + return Ci.nsIBlocklistService.STATE_BLOCKED; + } + if (versionRange.severity == SEVERITY_OUTDATED) { + let vulnerabilityStatus = versionRange.vulnerabilityStatus; + if (vulnerabilityStatus == VULNERABILITYSTATUS_UPDATE_AVAILABLE) { + return Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE; + } + if (vulnerabilityStatus == VULNERABILITYSTATUS_NO_UPDATE) { + return Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE; + } + return Ci.nsIBlocklistService.STATE_OUTDATED; + } + return Ci.nsIBlocklistService.STATE_SOFTBLOCKED; + }, +}; + +/** + * The extensions blocklist implementation. The JSON objects for extension + * blocks look something like: + * + * { + * "guid": "someguid@addons.mozilla.org", + * "prefs": ["i.am.a.pref.that.needs.resetting"], + * "schema": 1480349193877, + * "blockID": "i12345", + * "details": { + * "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1234567", + * "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.", + * "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.", + * "name": "Some pretty name", + * "created": "2019-05-06T19:52:20Z" + * }, + * "enabled": true, + * "versionRange": [ + * { + * "severity": 1, + * "maxVersion": "*", + * "minVersion": "0", + * "targetApplication": [] + * } + * ], + * "id": "<unique guid>", + * "last_modified": 1480349215672, + * } + * + * This is a legacy format, and implements deprecated operations (bug 1620580). + * ExtensionBlocklistMLBF supersedes this implementation. + * + * Note: we assign to the global to allow tests to reach the object directly. + */ +this.ExtensionBlocklistRS = { + async _ensureEntries() { + this.ensureInitialized(); + if (!this._entries && gBlocklistEnabled) { + await this._updateEntries(); + } + }, + + async _updateEntries() { + if (!gBlocklistEnabled) { + this._entries = []; + return; + } + this._entries = await this._client.get().catch(ex => Cu.reportError(ex)); + // Handle error silently. This can happen if our request to fetch data is aborted, + // e.g. by application shutdown. + if (!this._entries) { + this._entries = []; + return; + } + this._entries.forEach(entry => { + entry.matches = {}; + if (entry.guid) { + entry.matches.id = processMatcher(entry.guid); + } + for (let key of EXTENSION_BLOCK_FILTERS) { + if (key == "id" || !entry[key]) { + continue; + } + entry.matches[key] = processMatcher(entry[key]); + } + Utils.ensureVersionRangeIsSane(entry); + }); + + BlocklistTelemetry.recordRSBlocklistLastModified("addons", this._client); + }, + + async _filterItem(entry, environment) { + if (!(await targetAppFilter(entry, environment))) { + return null; + } + if (!Utils.matchesOSABI(entry)) { + return null; + } + // Need something to filter on - at least a guid or name (either could be a regex): + if (!entry.guid && !entry.name) { + let blockID = entry.blockID || entry.id; + Cu.reportError(new Error(`Nothing to filter add-on item ${blockID} on`)); + return null; + } + return entry; + }, + + sync() { + this.ensureInitialized(); + return this._client.sync(); + }, + + ensureInitialized() { + if (!gBlocklistEnabled || this._initialized) { + return; + } + this._initialized = true; + this._client = RemoteSettings( + Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS_COLLECTION), + { + bucketNamePref: PREF_BLOCKLIST_BUCKET, + lastCheckTimePref: PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS, + signerName: Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS_SIGNER), + filterFunc: this._filterItem, + } + ); + this._onUpdate = this._onUpdate.bind(this); + this._client.on("sync", this._onUpdate); + }, + + shutdown() { + if (this._client) { + this._client.off("sync", this._onUpdate); + this._didShutdown = true; + } + }, + + // Called when the blocklist implementation is changed via a pref. + undoShutdown() { + if (this._didShutdown) { + this._client.on("sync", this._onUpdate); + this._didShutdown = false; + } + }, + + async _onUpdate() { + let oldEntries = this._entries || []; + await this.ensureInitialized(); + await this._updateEntries(); + + const types = ["extension", "theme", "locale", "dictionary", "service"]; + let addons = await AddonManager.getAddonsByTypes(types); + for (let addon of addons) { + let oldState = addon.blocklistState; + if (addon.updateBlocklistState) { + await addon.updateBlocklistState(false); + } else if (oldEntries) { + let oldEntry = this._getEntry(addon, oldEntries); + oldState = oldEntry + ? oldEntry.state + : Ci.nsIBlocklistService.STATE_NOT_BLOCKED; + } else { + oldState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED; + } + let state = addon.blocklistState; + + LOG( + "Blocklist state for " + + addon.id + + " changed from " + + oldState + + " to " + + state + ); + + // We don't want to re-warn about add-ons + if (state == oldState) { + continue; + } + + // Ensure that softDisabled is false if the add-on is not soft blocked + if (state != Ci.nsIBlocklistService.STATE_SOFTBLOCKED) { + await addon.setSoftDisabled(false); + } + + // If an add-on has dropped from hard to soft blocked just mark it as + // soft disabled and don't warn about it. + if ( + state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED && + oldState == Ci.nsIBlocklistService.STATE_BLOCKED + ) { + await addon.setSoftDisabled(true); + } + + if ( + state == Ci.nsIBlocklistService.STATE_BLOCKED || + state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED + ) { + // Mark it as softblocked if necessary. Note that we avoid setting + // softDisabled at the same time as userDisabled to make it clear + // which was the original cause of the add-on becoming disabled in a + // way that the user can change. + if ( + state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED && + !addon.userDisabled + ) { + await addon.setSoftDisabled(true); + } + // It's a block. We must reset certain preferences. + let entry = this._getEntry(addon, this._entries); + if (entry.prefs && entry.prefs.length) { + for (let pref of entry.prefs) { + Services.prefs.clearUserPref(pref); + } + } + } + } + + AddonManagerPrivate.updateAddonAppDisabledStates(); + }, + + async getState(addon, appVersion, toolkitVersion) { + let entry = await this.getEntry(addon, appVersion, toolkitVersion); + return entry ? entry.state : Ci.nsIBlocklistService.STATE_NOT_BLOCKED; + }, + + async getEntry(addon, appVersion, toolkitVersion) { + await this._ensureEntries(); + return this._getEntry(addon, this._entries, appVersion, toolkitVersion); + }, + + _getEntry(addon, addonEntries, appVersion, toolkitVersion) { + if (!gBlocklistEnabled || !addon) { + return null; + } + + // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't). + if (!appVersion && !gApp.version) { + return null; + } + + if (!appVersion) { + appVersion = gApp.version; + } + if (!toolkitVersion) { + toolkitVersion = gApp.platformVersion; + } + + let addonProps = {}; + for (let key of EXTENSION_BLOCK_FILTERS) { + addonProps[key] = addon[key]; + } + if (addonProps.creator) { + addonProps.creator = addonProps.creator.name; + } + + for (let entry of addonEntries) { + // First check if it matches our properties. If not, just skip to the next item. + if (!doesAddonEntryMatch(entry.matches, addonProps)) { + continue; + } + // If those match, check the app or toolkit version works: + for (let versionRange of entry.versionRange) { + if ( + Utils.versionsMatch( + versionRange, + addon.version, + appVersion, + toolkitVersion + ) + ) { + let blockID = entry.blockID || entry.id; + return { + state: + versionRange.severity >= gBlocklistLevel + ? Ci.nsIBlocklistService.STATE_BLOCKED + : Ci.nsIBlocklistService.STATE_SOFTBLOCKED, + url: Utils._createBlocklistURL(blockID), + prefs: entry.prefs || [], + }; + } + } + } + return null; + }, +}; + +/** + * The extensions blocklist implementation, the third version. + * + * The current blocklist is represented by a multi-level bloom filter (MLBF) + * (aka "Cascade Bloom Filter") that works like a set, i.e. supports a has() + * operation, except it is probabilistic. The MLBF is 100% accurate for known + * entries and unreliable for unknown entries. When the backend generates the + * MLBF, all known add-ons are recorded, including their block state. Unknown + * add-ons are identified by their signature date being newer than the MLBF's + * generation time, and they are considered to not be blocked. + * + * Legacy blocklists used to distinguish between "soft block" and "hard block", + * but the current blocklist only supports one type of block ("hard block"). + * After checking the blocklist states, any previous "soft blocked" addons will + * either be (hard) blocked or unblocked based on the blocklist. + * + * The MLBF is attached to a RemoteSettings record, as follows: + * + * { + * "generation_time": 1585692000000, + * "attachment": { ... RemoteSettings attachment ... } + * "attachment_type": "bloomfilter-base", + * } + * + * To update the blocklist, a replacement MLBF is published: + * + * { + * "generation_time": 1585692000000, + * "attachment": { ... RemoteSettings attachment ... } + * "attachment_type": "bloomfilter-full", + * } + * + * The collection can also contain stashes: + * + * { + * "stash_time": 1585692000001, + * "stash": { + * "blocked": [ "addonid:1.0", ... ], + * "unblocked": [ "addonid:1.0", ... ] + * } + * + * Stashes can be used to update the blocklist without forcing the whole MLBF + * to be downloaded again. These stashes are applied on top of the base MLBF. + * The use of stashes is currently optional, and toggled via the + * extensions.blocklist.useMLBF.stashes preference (true = use stashes). + * + * Note: we assign to the global to allow tests to reach the object directly. + */ +this.ExtensionBlocklistMLBF = { + RS_ATTACHMENT_ID: "addons-mlbf.bin", + + async _fetchMLBF(record) { + // |record| may be unset. In that case, the MLBF dump is used instead + // (provided that the client has been built with it included). + let hash = record?.attachment.hash; + if (this._mlbfData && hash && this._mlbfData.cascadeHash === hash) { + // MLBF not changed, save the efforts of downloading the data again. + + // Although the MLBF has not changed, the time in the record has. This + // means that the MLBF is known to provide accurate results for add-ons + // that were signed after the previously known date (but before the newly + // given date). To ensure that add-ons in this time range are also blocked + // as expected, update the cached generationTime. + if (record.generation_time > this._mlbfData.generationTime) { + this._mlbfData.generationTime = record.generation_time; + } + return this._mlbfData; + } + const { + buffer, + record: actualRecord, + } = await this._client.attachments.download(record, { + attachmentId: this.RS_ATTACHMENT_ID, + useCache: true, + fallbackToCache: true, + fallbackToDump: true, + }); + return { + cascadeHash: actualRecord.attachment.hash, + cascadeFilter: new CascadeFilter(new Uint8Array(buffer)), + // Note: generation_time is semantically distinct from last_modified. + // generation_time is compared with the signing date of the add-on, so it + // should be in sync with the signing service's clock. + // In contrast, last_modified does not have such strong requirements. + generationTime: actualRecord.generation_time, + }; + }, + + async _updateMLBF(forceUpdate = false) { + // The update process consists of fetching the collection, followed by + // potentially multiple network requests. As long as the collection has not + // been changed, repeated update requests can be coalesced. But when the + // collection has been updated, all pending update requests should await the + // new update request instead of the previous one. + if (!forceUpdate && this._updatePromise) { + return this._updatePromise; + } + const isUpdateReplaced = () => this._updatePromise != updatePromise; + const updatePromise = (async () => { + if (!gBlocklistEnabled) { + this._mlbfData = null; + this._stashes = null; + return; + } + let records = await this._client.get(); + if (isUpdateReplaced()) { + return; + } + + let mlbfRecords = records + .filter(r => r.attachment) + // Newest attachments first. + .sort((a, b) => b.generation_time - a.generation_time); + let mlbfRecord; + if (this.stashesEnabled) { + mlbfRecord = mlbfRecords.find( + r => r.attachment_type == "bloomfilter-base" + ); + this._stashes = records + .filter(({ stash }) => { + return ( + // Exclude non-stashes, e.g. MLBF attachments. + stash && + // Sanity check for type. + Array.isArray(stash.blocked) && + Array.isArray(stash.unblocked) + ); + }) + // Sort by stash time - newest first. + .sort((a, b) => b.stash_time - a.stash_time) + .map(({ stash, stash_time }) => ({ + blocked: new Set(stash.blocked), + unblocked: new Set(stash.unblocked), + stash_time, + })); + } else { + mlbfRecord = mlbfRecords.find( + r => + r.attachment_type == "bloomfilter-full" || + r.attachment_type == "bloomfilter-base" + ); + this._stashes = null; + } + + let mlbf = await this._fetchMLBF(mlbfRecord); + // When a MLBF dump is packaged with the browser, mlbf will always be + // non-null at this point. + if (isUpdateReplaced()) { + return; + } + this._mlbfData = mlbf; + })() + .catch(e => { + Cu.reportError(e); + }) + .then(() => { + if (!isUpdateReplaced()) { + this._updatePromise = null; + this._recordPostUpdateTelemetry(); + } + return this._updatePromise; + }); + this._updatePromise = updatePromise; + return updatePromise; + }, + + // Update the telemetry of the blocklist. This is always called, even if + // the update request failed (e.g. due to network errors or data corruption). + _recordPostUpdateTelemetry() { + BlocklistTelemetry.recordRSBlocklistLastModified( + "addons_mlbf", + this._client + ); + BlocklistTelemetry.recordTimeScalar( + "mlbf_generation_time", + this._mlbfData?.generationTime + ); + // stashes has conveniently already been sorted by stash_time, newest first. + let stashes = this._stashes || []; + BlocklistTelemetry.recordTimeScalar( + "mlbf_stash_time_oldest", + stashes[stashes.length - 1]?.stash_time + ); + BlocklistTelemetry.recordTimeScalar( + "mlbf_stash_time_newest", + stashes[0]?.stash_time + ); + }, + + ensureInitialized() { + if (!gBlocklistEnabled || this._initialized) { + return; + } + this._initialized = true; + this._client = RemoteSettings( + Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS3_COLLECTION), + { + bucketNamePref: PREF_BLOCKLIST_BUCKET, + lastCheckTimePref: PREF_BLOCKLIST_ADDONS3_CHECKED_SECONDS, + signerName: Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS3_SIGNER), + } + ); + this._onUpdate = this._onUpdate.bind(this); + this._client.on("sync", this._onUpdate); + this.stashesEnabled = Services.prefs.getBoolPref( + PREF_BLOCKLIST_USE_MLBF_STASHES, + false + ); + Services.telemetry.scalarSet("blocklist.mlbf_stashes", this.stashesEnabled); + }, + + shutdown() { + if (this._client) { + this._client.off("sync", this._onUpdate); + this._didShutdown = true; + } + }, + + // Called when the blocklist implementation is changed via a pref. + undoShutdown() { + if (this._didShutdown) { + this._client.on("sync", this._onUpdate); + this._didShutdown = false; + } + }, + + async _onUpdate() { + this.ensureInitialized(); + await this._updateMLBF(true); + + // Check add-ons from XPIProvider. + const types = ["extension", "theme", "locale", "dictionary"]; + let addons = await AddonManager.getAddonsByTypes(types); + for (let addon of addons) { + let oldState = addon.blocklistState; + await addon.updateBlocklistState(false); + let state = addon.blocklistState; + + LOG( + "Blocklist state for " + + addon.id + + " changed from " + + oldState + + " to " + + state + ); + + // We don't want to re-warn about add-ons + if (state == oldState) { + continue; + } + + // Ensure that softDisabled is false if the add-on is not soft blocked + // (by a previous implementation of the blocklist). + if (state != Ci.nsIBlocklistService.STATE_SOFTBLOCKED) { + await addon.setSoftDisabled(false); + } + } + + AddonManagerPrivate.updateAddonAppDisabledStates(); + }, + + async getState(addon) { + let state = await this.getEntry(addon); + return state ? state.state : Ci.nsIBlocklistService.STATE_NOT_BLOCKED; + }, + + async getEntry(addon) { + if (!this._mlbfData) { + this.ensureInitialized(); + await this._updateMLBF(false); + } + + let blockKey = addon.id + ":" + addon.version; + + if (this._stashes) { + // Stashes are ordered by newest first. + for (let stash of this._stashes) { + // blocked and unblocked do not have overlapping entries. + if (stash.blocked.has(blockKey)) { + return this._createBlockEntry(addon); + } + if (stash.unblocked.has(blockKey)) { + return null; + } + } + } + + // signedDate is a Date if the add-on is signed, null if not signed, + // undefined if it's an addon update descriptor instead of an addon wrapper. + let { signedDate } = addon; + if (!signedDate) { + // The MLBF does not apply to unsigned add-ons. + return null; + } + + if (!this._mlbfData) { + // This could happen in theory in any of the following cases: + // - the blocklist is disabled. + // - The RemoteSettings backend served a malformed MLBF. + // - The RemoteSettings backend is unreachable, and this client was built + // without including a dump of the MLBF. + // + // ... in other words, this shouldn't happen in practice. + return null; + } + let { cascadeFilter, generationTime } = this._mlbfData; + if (!cascadeFilter.has(blockKey)) { + // Add-on not blocked or unknown. + return null; + } + // Add-on blocked, or unknown add-on inadvertently labeled as blocked. + + let { signedState } = addon; + if ( + signedState !== AddonManager.SIGNEDSTATE_PRELIMINARY && + signedState !== AddonManager.SIGNEDSTATE_SIGNED + ) { + // The block decision can only be relied upon for known add-ons, i.e. + // signed via AMO. Anything else is unknown and ignored: + // + // - SIGNEDSTATE_SYSTEM and SIGNEDSTATE_PRIVILEGED are signed + // independently of AMO. + // + // - SIGNEDSTATE_NOT_REQUIRED already has an early return above due to + // signedDate being unset for these kinds of add-ons. + // + // - SIGNEDSTATE_BROKEN, SIGNEDSTATE_UNKNOWN and SIGNEDSTATE_MISSING + // means that the signature cannot be relied upon. It is equivalent to + // removing the signature from the XPI file, which already causes them + // to be disabled on release builds (where MOZ_REQUIRE_SIGNING=true). + return null; + } + + if (signedDate.getTime() > generationTime) { + // The bloom filter only reports 100% accurate results for known add-ons. + // Since the add-on was unknown when the bloom filter was generated, the + // block decision is incorrect and should be treated as unblocked. + return null; + } + + if (AppConstants.NIGHTLY_BUILD && addon.type === "locale") { + // Only Mozilla can create langpacks with a valid signature. + // Langpacks for Release, Beta and ESR are submitted to AMO. + // DevEd does not support external langpacks (bug 1563923), only builtins. + // (and built-in addons are not subjected to the blocklist). + // Langpacks for Nightly are not known to AMO, so the MLBF cannot be used. + return null; + } + + return this._createBlockEntry(addon); + }, + + _createBlockEntry(addon) { + return { + state: Ci.nsIBlocklistService.STATE_BLOCKED, + url: this.createBlocklistURL(addon.id, addon.version), + }; + }, + + createBlocklistURL(id, version) { + let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ADDONITEM_URL); + return url.replace(/%addonID%/g, id).replace(/%addonVersion%/g, version); + }, +}; + +const EXTENSION_BLOCK_FILTERS = [ + "id", + "name", + "creator", + "homepageURL", + "updateURL", +]; + +var gLoggingEnabled = null; +var gBlocklistEnabled = true; +var gBlocklistLevel = DEFAULT_LEVEL; + +/** + * @class nsIBlocklistPrompt + * + * nsIBlocklistPrompt is used, if available, by the default implementation of + * nsIBlocklistService to display a confirmation UI to the user before blocking + * extensions/plugins. + */ +/** + * @method prompt + * + * Prompt the user about newly blocked addons. The prompt is then resposible + * for soft-blocking any addons that need to be afterwards + * + * @param {object[]} aAddons + * An array of addons and plugins that are blocked. These are javascript + * objects with properties: + * name - the plugin or extension name, + * version - the version of the extension or plugin, + * icon - the plugin or extension icon, + * disable - can be used by the nsIBlocklistPrompt to allows users to decide + * whether a soft-blocked add-on should be disabled, + * blocked - true if the item is hard-blocked, false otherwise, + * item - the nsIPluginTag or Addon object + */ + +// From appinfo in Services.jsm. It is not possible to use the one in +// Services.jsm since it will not successfully QueryInterface nsIXULAppInfo in +// xpcshell tests due to other code calling Services.appinfo before the +// nsIXULAppInfo is created by the tests. +XPCOMUtils.defineLazyGetter(this, "gApp", function() { + // eslint-disable-next-line mozilla/use-services + let appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); + try { + appinfo.QueryInterface(Ci.nsIXULAppInfo); + } catch (ex) { + // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't). + if ( + !(ex instanceof Components.Exception) || + ex.result != Cr.NS_NOINTERFACE + ) { + throw ex; + } + } + return appinfo; +}); + +XPCOMUtils.defineLazyGetter(this, "gAppID", function() { + return gApp.ID; +}); +XPCOMUtils.defineLazyGetter(this, "gAppOS", function() { + return gApp.OS; +}); + +/** + * Logs a string to the error console. + * @param {string} string + * The string to write to the error console.. + */ +function LOG(string) { + if (gLoggingEnabled) { + dump("*** " + string + "\n"); + Services.console.logStringMessage(string); + } +} + +let Blocklist = { + _init() { + Services.obs.addObserver(this, "xpcom-shutdown"); + gLoggingEnabled = Services.prefs.getBoolPref( + PREF_EM_LOGGING_ENABLED, + false + ); + gBlocklistEnabled = Services.prefs.getBoolPref( + PREF_BLOCKLIST_ENABLED, + true + ); + gBlocklistLevel = Math.min( + Services.prefs.getIntPref(PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL), + MAX_BLOCK_LEVEL + ); + this._chooseExtensionBlocklistImplementationFromPref(); + Services.prefs.addObserver("extensions.blocklist.", this); + Services.prefs.addObserver(PREF_EM_LOGGING_ENABLED, this); + + // If the stub blocklist service deferred any queries because we + // weren't loaded yet, execute them now. + for (let entry of Services.blocklist.pluginQueries.splice(0)) { + entry.resolve( + this.getPluginBlocklistState( + entry.plugin, + entry.appVersion, + entry.toolkitVersion + ) + ); + } + }, + isLoaded: true, + + shutdown() { + GfxBlocklistRS.shutdown(); + PluginBlocklistRS.shutdown(); + this.ExtensionBlocklist.shutdown(); + + Services.obs.removeObserver(this, "xpcom-shutdown"); + Services.prefs.removeObserver("extensions.blocklist.", this); + Services.prefs.removeObserver(PREF_EM_LOGGING_ENABLED, this); + }, + + observe(subject, topic, prefName) { + switch (topic) { + case "xpcom-shutdown": + this.shutdown(); + break; + case "nsPref:changed": + switch (prefName) { + case PREF_EM_LOGGING_ENABLED: + gLoggingEnabled = Services.prefs.getBoolPref( + PREF_EM_LOGGING_ENABLED, + false + ); + break; + case PREF_BLOCKLIST_ENABLED: + gBlocklistEnabled = Services.prefs.getBoolPref( + PREF_BLOCKLIST_ENABLED, + true + ); + this._blocklistUpdated(); + break; + case PREF_BLOCKLIST_LEVEL: + gBlocklistLevel = Math.min( + Services.prefs.getIntPref(PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL), + MAX_BLOCK_LEVEL + ); + this._blocklistUpdated(); + break; + case PREF_BLOCKLIST_USE_MLBF: + let oldImpl = this.ExtensionBlocklist; + this._chooseExtensionBlocklistImplementationFromPref(); + if (oldImpl._initialized) { + oldImpl.shutdown(); + this.ExtensionBlocklist.undoShutdown(); + this.ExtensionBlocklist._onUpdate(); + } // else neither has been initialized yet. Wait for it to happen. + break; + case PREF_BLOCKLIST_USE_MLBF_STASHES: + ExtensionBlocklistMLBF.stashesEnabled = Services.prefs.getBoolPref( + PREF_BLOCKLIST_USE_MLBF_STASHES, + false + ); + if ( + ExtensionBlocklistMLBF._initialized && + !ExtensionBlocklistMLBF._didShutdown + ) { + Services.telemetry.scalarSet( + "blocklist.mlbf_stashes", + ExtensionBlocklistMLBF.stashesEnabled + ); + ExtensionBlocklistMLBF._onUpdate(); + } + break; + } + break; + } + }, + + loadBlocklistAsync() { + // Need to ensure we notify gfx of new stuff. + GfxBlocklistRS.checkForEntries(); + this.ExtensionBlocklist.ensureInitialized(); + PluginBlocklistRS.ensureInitialized(); + }, + + getPluginBlocklistState(plugin, appVersion, toolkitVersion) { + return PluginBlocklistRS.getState(plugin, appVersion, toolkitVersion); + }, + + getPluginBlockURL(plugin) { + return PluginBlocklistRS.getURL(plugin); + }, + + getAddonBlocklistState(addon, appVersion, toolkitVersion) { + // NOTE: appVersion/toolkitVersion are only used by ExtensionBlocklistRS. + return this.ExtensionBlocklist.getState(addon, appVersion, toolkitVersion); + }, + + getAddonBlocklistEntry(addon, appVersion, toolkitVersion) { + // NOTE: appVersion/toolkitVersion are only used by ExtensionBlocklistRS. + return this.ExtensionBlocklist.getEntry(addon, appVersion, toolkitVersion); + }, + + _chooseExtensionBlocklistImplementationFromPref() { + if (Services.prefs.getBoolPref(PREF_BLOCKLIST_USE_MLBF, false)) { + this.ExtensionBlocklist = ExtensionBlocklistMLBF; + Services.telemetry.scalarSet("blocklist.mlbf_enabled", true); + } else { + this.ExtensionBlocklist = ExtensionBlocklistRS; + Services.telemetry.scalarSet("blocklist.mlbf_enabled", false); + } + }, + + _blocklistUpdated() { + this.ExtensionBlocklist._onUpdate(); + PluginBlocklistRS._onUpdate(); + }, +}; + +Blocklist._init(); |