--- a/services/common/blocklist-clients.js
+++ b/services/common/blocklist-clients.js
@@ -50,17 +50,16 @@ const PREF_BLOCKLIST_ENFORCE_SIGNING
const INVALID_SIGNATURE = "Invalid content/signature";
// FIXME: this was the default path in earlier versions of
// FirefoxAdapter, so for backwards compatibility we maintain this
// filename, even though it isn't descriptive of who is using it.
this.KINTO_STORAGE_PATH = "kinto.sqlite";
-
function mergeChanges(collection, localRecords, changes) {
const records = {};
// Local records by id.
localRecords.forEach((record) => records[record.id] = collection.cleanLocalFields(record));
// All existing records are replaced by the version from the server.
changes.forEach((record) => records[record.id] = record);
return Object.values(records)
--- a/toolkit/mozapps/extensions/nsBlocklistService.js
+++ b/toolkit/mozapps/extensions/nsBlocklistService.js
@@ -9,16 +9,18 @@
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/AppConstants.jsm");
+const BlocklistClients = Components.utils.import("resource://services-common/blocklist-clients.js", {});
+
try {
// AddonManager.jsm doesn't allow itself to be imported in the child
// process. We're used in the child process (for now), so guard against
// this.
Components.utils.import("resource://gre/modules/AddonManager.jsm");
/* globals AddonManagerPrivate*/
} catch (e) {
}
@@ -46,17 +48,17 @@ const FILE_BLOCKLIST =
const PREF_BLOCKLIST_LASTUPDATETIME = "app.update.lastUpdateTime.blocklist-background-update-timer";
const PREF_BLOCKLIST_URL = "extensions.blocklist.url";
const PREF_BLOCKLIST_ITEM_URL = "extensions.blocklist.itemURL";
const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
const PREF_BLOCKLIST_LEVEL = "extensions.blocklist.level";
const PREF_BLOCKLIST_PINGCOUNTTOTAL = "extensions.blocklist.pingCountTotal";
const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion";
const PREF_BLOCKLIST_SUPPRESSUI = "extensions.blocklist.suppressUI";
-const PREF_ONECRL_VIA_AMO = "security.onecrl.via.amo";
+const PREF_BLOCKLIST_VIA_AMO = "security.blocklist.via.amo";
const PREF_BLOCKLIST_UPDATE_ENABLED = "services.blocklist.update_enabled";
const PREF_APP_DISTRIBUTION = "distribution.id";
const PREF_APP_DISTRIBUTION_VERSION = "distribution.version";
const PREF_EM_LOGGING_ENABLED = "extensions.logging.enabled";
const XMLURI_BLOCKLIST = "http://www.mozilla.org/2006/addons-blocklist";
const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
const URI_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul"
const DEFAULT_SEVERITY = 3;
@@ -66,16 +68,17 @@ const SEVERITY_OUTDATED =
const VULNERABILITYSTATUS_NONE = 0;
const VULNERABILITYSTATUS_UPDATE_AVAILABLE = 1;
const VULNERABILITYSTATUS_NO_UPDATE = 2;
const EXTENSION_BLOCK_FILTERS = ["id", "name", "creator", "homepageURL", "updateURL"];
var gLoggingEnabled = null;
var gBlocklistEnabled = true;
+var gBlocklistFromXML = true;
var gBlocklistLevel = DEFAULT_LEVEL;
XPCOMUtils.defineLazyServiceGetter(this, "gConsole",
"@mozilla.org/consoleservice;1",
"nsIConsoleService");
XPCOMUtils.defineLazyServiceGetter(this, "gVersionChecker",
"@mozilla.org/xpcom/version-comparator;1",
@@ -214,34 +217,32 @@ function restartApp() {
return;
var as = Cc["@mozilla.org/toolkit/app-startup;1"].
getService(Ci.nsIAppStartup);
as.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
}
/**
- * Checks whether this blocklist element is valid for the current OS and ABI.
- * If the element has an "os" attribute then the current OS must appear in
- * its comma separated list for the element to be valid. Similarly for the
- * xpcomabi attribute.
+ * Checks whether the current OS and ABI appears in the specified lists of
+ * values to match.
+ *
+ * @param os
+ * String with comma-separated OS values to match.
+ * @param xpcomabi
+ * String with comma-separated ABI values to match.
+ * @return Boolean whether it matches or not.
*/
-function matchesOSABI(blocklistElement) {
- if (blocklistElement.hasAttribute("os")) {
- var choices = blocklistElement.getAttribute("os").split(",");
- if (choices.length > 0 && choices.indexOf(gApp.OS) < 0)
- return false;
+function matchesOSABI(os, xpcomabi) {
+ if (os && !os.split(",").includes(gApp.OS)) {
+ return false;
}
-
- if (blocklistElement.hasAttribute("xpcomabi")) {
- choices = blocklistElement.getAttribute("xpcomabi").split(",");
- if (choices.length > 0 && choices.indexOf(gApp.XPCOMABI) < 0)
- return false;
+ if (xpcomabi && !xpcomabi.split(",").includes(gApp.XPCOMABI)) {
+ return false;
}
-
return true;
}
/**
* Gets the current value of the locale. It's possible for this preference to
* be localized, so we have to do a little extra work here. Similar code
* exists in nsHttpHandler.cpp when building the UA string.
*/
@@ -279,16 +280,17 @@ function parseRegExp(aStr) {
function Blocklist() {
this._preloadedBlocklistContent = new Map();
Services.obs.addObserver(this, "xpcom-shutdown");
Services.obs.addObserver(this, "sessionstore-windows-restored");
gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false);
gBlocklistEnabled = getPref("getBoolPref", PREF_BLOCKLIST_ENABLED, true);
+ gBlocklistFromXML = getPref("getBoolPref", PREF_BLOCKLIST_VIA_AMO, true);
gBlocklistLevel = Math.min(getPref("getIntPref", PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
MAX_BLOCK_LEVEL);
gPref.addObserver("extensions.blocklist.", this);
gPref.addObserver(PREF_EM_LOGGING_ENABLED, this);
this.wrappedJSObject = this;
// requests from child processes come in here, see receiveMessage.
Services.ppmm.addMessageListener("Blocklist:getPluginBlocklistState", this);
Services.ppmm.addMessageListener("Blocklist:content-blocklist-updated", this);
@@ -332,16 +334,21 @@ Blocklist.prototype = {
case PREF_EM_LOGGING_ENABLED:
gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false);
break;
case PREF_BLOCKLIST_ENABLED:
gBlocklistEnabled = getPref("getBoolPref", PREF_BLOCKLIST_ENABLED, true);
this._loadBlocklist();
this._blocklistUpdated(null, null);
break;
+ case PREF_BLOCKLIST_VIA_AMO:
+ gBlocklistFromXML = getPref("getBoolPref", PREF_BLOCKLIST_VIA_AMO, true);
+ this._loadBlocklist();
+ this._blocklistUpdated(null, null);
+ break;
case PREF_BLOCKLIST_LEVEL:
gBlocklistLevel = Math.min(getPref("getIntPref", PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
MAX_BLOCK_LEVEL);
this._blocklistUpdated(null, null);
break;
}
break;
case "sessionstore-windows-restored":
@@ -528,16 +535,22 @@ Blocklist.prototype = {
return url;
},
notify(aTimer) {
if (!gBlocklistEnabled)
return;
+ // If blocklist does not rely on XML, do not download the file.
+ // Updates are performed in services/common/kinto-updater.js
+ if (!gBlocklistFromXML) {
+ return;
+ }
+
try {
var dsURI = gPref.getCharPref(PREF_BLOCKLIST_URL);
} catch (e) {
LOG("Blocklist::notify: The " + PREF_BLOCKLIST_URL + " preference" +
" is missing!");
return;
}
@@ -704,44 +717,56 @@ Blocklist.prototype = {
* Finds the newest blocklist file from the application and the profile and
* load it or does nothing if neither exist.
*/
_loadBlocklist() {
if (!gBlocklistEnabled) {
LOG("Blocklist::_loadBlocklistFromFile: blocklist is disabled");
return;
}
- const content = this._loadBlocklistFromFile(FILE_BLOCKLIST);
- this._loadBlocklistFromXMLString(content);
+
+ if (gBlocklistFromXML) {
+ const content = this._loadBlocklistFromFile(FILE_BLOCKLIST);
+ this._loadBlocklistFromXMLString(content);
+ } else {
+ // Rely on Kinto for blocklists synchronization.
+ // Load from distinct JSON files.
+ const addonsJson = this._loadBlocklistFromFile(BlocklistClients.AddonBlocklistClient.filename);
+ const pluginsJson = this._loadBlocklistFromFile(BlocklistClients.PluginBlocklistClient.filename);
+ const gfxJson = this._loadBlocklistFromFile(BlocklistClients.GfxBlocklistClient.filename);
+ this._loadBlocklistFromJSONStrings(addonsJson, pluginsJson, gfxJson);
+ // Certificates revocation happens directly in OneCRLBlocklistClient from
+ // services/common/blocklist-clients.js.
+ }
},
_loadBlocklistFromFile(filename) {
- let file = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
+ const {components: filePath} = OS.Path.split(filename);
+ let file = FileUtils.getFile(KEY_PROFILEDIR, filePath);
if (!file.exists()) {
- let appFile = FileUtils.getFile(KEY_APPDIR, [filename]);
- if (appFile.exists()) {
- file = appFile;
+ file = FileUtils.getFile(KEY_APPDIR, filePath);
+ if (!file.exists()) {
+ file = FileUtils.getFile(KEY_APPDIR, ["defaults"].concat(filePath));
+ if (!file.exists()) {
+ LOG("Blocklist::_loadBlocklistFromFile: File does not exist " + filename);
+ return "";
+ }
}
}
let telemetry = Services.telemetry;
// Check if preloaded content exists for this file.
if (this._preloadedBlocklistContent.has(file.path)) {
telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(false);
const text = this._preloadedBlocklistContent.get(file.path);
this._preloadedBlocklistContent.delete(file.path);
return text;
}
- if (!file.exists()) {
- LOG("Blocklist::_loadBlocklistFromFile: File does not exist " + file.path);
- return;
- }
-
telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(true);
let text = "";
let fstream = null;
let cstream = null;
try {
fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]
@@ -784,52 +809,56 @@ Blocklist.prototype = {
},
async _preloadBlocklist() {
if (!gBlocklistEnabled) {
LOG("Blocklist::_preloadBlocklist: blocklist is disabled");
return;
}
- await this._preloadBlocklistFile(FILE_BLOCKLIST);
- }),
+ if (gBlocklistFromXML) {
+ await this._preloadBlocklistFile(FILE_BLOCKLIST);
+ } else {
+ await this._preloadBlocklistFile(BlocklistClients.AddonBlocklistClient.filename);
+ await this._preloadBlocklistFile(BlocklistClients.PluginBlocklistClient.filename);
+ await this._preloadBlocklistFile(BlocklistClients.GfxBlocklistClient.filename);
+ }
+ },
async _preloadBlocklistFile(filename) {
- let file = FileUtils.getFile(KEY_PROFILEDIR, [filename]);
+ const {components: filePath} = OS.Path.split(filename);
+ let file = FileUtils.getFile(KEY_PROFILEDIR, filePath);
if (!file.exists()) {
- let appFile = FileUtils.getFile(KEY_APPDIR, [filename]);
- if (appFile.exists()) {
- file = appFile;
- } else {
- LOG(`Blocklist::_preloadBlocklistFile: no ${filename} file found`);
- return;
+ file = FileUtils.getFile(KEY_APPDIR, filePath);
+ if (!file.exists()) {
+ file = FileUtils.getFile(KEY_APPDIR, ["defaults"].concat(filePath));
+ if (!file.exists()) {
+ LOG(`Blocklist::_preloadBlocklistFile: no ${filename} file found`);
+ return;
+ }
}
}
const path = file.path;
if (this._preloadedBlocklistContent.has(path)) {
// The file has been already loaded.
return;
}
try {
let content = await OS.File.read(path, { encoding: "utf-8" });
if (!this._isBlocklistLoaded()) {
// Store the content only if a sync load has not been performed in the meantime.
this._preloadedBlocklistContent.set(path, content);
}
} catch (e) {
- LOG(`Blocklist::_preloadBlocklistFile: Failed to load ${path} file : ${e}`);
+ LOG(`Blocklist::_preloadBlocklist: Failed to load ${path} file : ${e}`);
}
},
- _isBlocklistLoaded() {
- return this._addonEntries != null && this._gfxEntries != null && this._pluginEntries != null;
- },
-
/**
# The blocklist XML file looks something like this:
#
# <blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
# <emItems>
# <emItem id="item_1@domain" blockID="i1">
# <prefs>
# <pref>accessibility.accesskeycausesactivation</pref>
@@ -901,47 +930,45 @@ Blocklist.prototype = {
var doc = parser.parseFromString(text, "text/xml");
if (doc.documentElement.namespaceURI != XMLURI_BLOCKLIST) {
LOG("Blocklist::_loadBlocklistFromXMLString: aborting due to incorrect " +
"XML Namespace.\r\nExpected: " + XMLURI_BLOCKLIST + "\r\n" +
"Received: " + doc.documentElement.namespaceURI);
return;
}
- var populateCertBlocklist = getPref("getBoolPref", PREF_ONECRL_VIA_AMO, true);
-
var childNodes = doc.documentElement.childNodes;
for (let element of childNodes) {
if (!(element instanceof Ci.nsIDOMElement))
continue;
switch (element.localName) {
case "emItems":
this._addonEntries = this._processItemNodes(element.childNodes, "emItem",
this._handleEmItemNode);
break;
case "pluginItems":
this._pluginEntries = this._processItemNodes(element.childNodes, "pluginItem",
this._handlePluginItemNode);
break;
case "certItems":
- if (populateCertBlocklist) {
+ if (gBlocklistFromXML) {
this._processItemNodes(element.childNodes, "certItem",
this._handleCertItemNode.bind(this));
}
break;
case "gfxItems":
// Parse as simple list of objects.
this._gfxEntries = this._processItemNodes(element.childNodes, "gfxBlacklistEntry",
this._handleGfxBlacklistNode);
break;
default:
LOG("Blocklist::_loadBlocklistFromXMLString: ignored entries " + element.localName);
}
}
- if (populateCertBlocklist) {
+ if (gBlocklistFromXML) {
gCertBlocklistService.saveEntries();
}
if (this._gfxEntries.length > 0) {
this._notifyObserversBlocklistGFX();
}
} catch (e) {
LOG("Blocklist::_loadBlocklistFromXMLString: Error constructing blocklist " + e);
}
@@ -983,18 +1010,20 @@ Blocklist.prototype = {
gCertBlocklistService.revokeCertBySubjectAndPubKey(subject, pubKeyHash);
} catch (e) {
LOG("Blocklist::_handleCertItemNode: Error adding revoked cert by Subject and PubKey" + e);
}
}
},
_handleEmItemNode(blocklistElement, result) {
- if (!matchesOSABI(blocklistElement))
+ if (!matchesOSABI(blocklistElement.getAttribute("os"),
+ blocklistElement.getAttribute("xpcomabi"))) {
return;
+ }
let blockEntry = {
versions: [],
prefs: [],
blockID: null,
attributes: new Map()
// Atleast one of EXTENSION_BLOCK_FILTERS must get added to attributes
};
@@ -1035,18 +1064,20 @@ Blocklist.prototype = {
blockEntry.versions.push(new BlocklistItemData(null));
blockEntry.blockID = blocklistElement.getAttribute("blockID");
result.push(blockEntry);
},
_handlePluginItemNode(blocklistElement, result) {
- if (!matchesOSABI(blocklistElement))
+ if (!matchesOSABI(blocklistElement.getAttribute("os"),
+ blocklistElement.getAttribute("xpcomabi"))) {
return;
+ }
var matchNodes = blocklistElement.childNodes;
var blockEntry = {
matches: {},
versions: [],
blockID: null,
infoURL: null,
};
@@ -1146,16 +1177,200 @@ Blocklist.prototype = {
}
if (value) {
blockEntry[matchElement.localName] = value;
}
}
result.push(blockEntry);
},
+ _loadBlocklistFromJSONStrings(addonsJson, pluginsJson, gfxJson) {
+ const blocklists = [
+ {content: addonsJson, handle: this._handleAddonItemJSON, dest: "_addonEntries"},
+ {content: pluginsJson, handle: this._handlePluginItemJSON, dest: "_pluginEntries"},
+ {content: gfxJson, handle: this._handleGfxItemJSON, dest: "_gfxEntries"}
+ ];
+
+ for (let {content, handle, dest} of blocklists) {
+ this[dest] = [];
+ if (!content) {
+ continue;
+ }
+ let parsed;
+ try {
+ parsed = JSON.parse(content);
+ this[dest] = parsed.data.map((entry) => handle(entry)).filter((entry) => entry);
+ } catch (e) {
+ LOG(`Blocklist::_loadBlocklistFromJSONStrings: Could not parse JSON for ${dest}: ${e}`);
+ }
+ }
+ },
+
+ _handleAddonItemJSON(data) {
+ /*
+ {
+ "prefs": [],
+ "blockID": "i446",
+ "last_modified": 1457434834683,
+ "versionRange": [{
+ "targetApplication": [],
+ "maxVersion": "*",
+ "minVersion": "0",
+ "severity": "1"
+ }],
+ "guid": "{E90FA778-C2B7-41D0-9FA9-3FEC1CA54D66}",
+ "id": "87a5dc56-1fec-ebf2-a09b-6f2cbd4eb2d3",
+ "os": "Linux,WINNT"
+ }
+ */
+ if (!matchesOSABI(data.os, data.xpcomabi)) {
+ return null;
+ }
+
+ const blockEntry = {
+ versions: [],
+ prefs: [],
+ blockID: null,
+ attributes: new Map()
+ // At least one of EXTENSION_BLOCK_FILTERS must get added to attributes
+ };
+
+ // Any filter starting with '/' is interpreted as a regex. So if an attribute
+ // starts with a '/' it must be checked via a regex.
+ function regExpCheck(attr) {
+ return attr.startsWith("/") ? parseRegExp(attr) : attr;
+ }
+
+ for (let filter of EXTENSION_BLOCK_FILTERS) {
+ // In JSON, addon `id` is `guid`.
+ let attr = data[filter == "id" ? "guid" : filter];
+ if (attr) {
+ blockEntry.attributes.set(filter, regExpCheck(attr));
+ }
+ }
+
+ blockEntry.prefs = data.prefs;
+ blockEntry.blockID = data.blockID || data.id; // Fallback to the JSON record id.
+
+ for (let versionRange of data.versionRange) {
+ const itemData = new BlocklistItemData(null);
+ const fields = ["minVersion", "maxVersion", "severity", "vulnerabilityStatus"];
+ for (let field of fields) {
+ if (versionRange[field]) {
+ itemData[field] = versionRange[field];
+ }
+ }
+ for (let targetApplication of versionRange.targetApplication) {
+ // default to the current application if id is not provided.
+ const appId = targetApplication.guid || gApp.ID;
+ itemData.targetApps[appId] = targetApplication;
+ }
+ blockEntry.versions.push(itemData);
+ }
+ if (blockEntry.versions.length == 0) {
+ blockEntry.versions.push(new BlocklistItemData(null));
+ }
+
+ return blockEntry;
+ },
+
+ _handlePluginItemJSON(data) {
+ /*
+ {
+ "matchFilename": "JavaPlugin2_NPAPI\\.plugin",
+ "blockID": "p123",
+ "id": "bdcf0717-a873-adbf-7603-83a49fb996bc",
+ "last_modified": 1457434851748,
+ "versionRange": [{
+ "targetApplication": [{
+ "minVersion": "0.1",
+ "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
+ "maxVersion": "17.*"
+ }],
+ "maxVersion": "14.2.0",
+ "minVersion": "0",
+ "severity": "1"
+ }],
+ "os": "WINNT",
+ "xpcomabi": "x86"
+ }
+ */
+ if (!matchesOSABI(data.os, data.xpcomabi)) {
+ return null;
+ }
+
+ const blockEntry = {
+ matches: {},
+ versions: [],
+ blockID: null,
+ infoURL: null,
+ };
+
+ blockEntry.infoURL = data.infoURL;
+ blockEntry.blockID = data.blockID || data.id; // Fallback to the JSON record id.
+
+ // Verify Matches
+ var hasMatch = false;
+ for (let prop of ["Name", "Description", "Filename"]) {
+ if (data.hasOwnProperty(`match${prop}`)) {
+ try {
+ blockEntry.matches[prop.toLowerCase()] = new RegExp(data[`match${prop}`], "m");
+ hasMatch = true;
+ } catch (e) {
+ // Ignore invalid regular expressions
+ }
+ }
+ }
+ // Plugin entries require *something* to match to an actual plugin
+ if (!hasMatch) {
+ LOG(`Blocklist::_handlePluginItemJSON: Ignored entry ${blockEntry.blockID} with no valid match attribute.`);
+ return null;
+ }
+
+ for (let versionRange of data.versionRange) {
+ const itemData = new BlocklistItemData(null);
+ const fields = ["minVersion", "maxVersion", "severity"];
+ for (let field of fields) {
+ if (versionRange[field])
+ itemData[field] = versionRange[field];
+ }
+ for (let targetApplication of versionRange.targetApplication) {
+ // default to the current application if id is not provided.
+ const appId = targetApplication.guid || gApp.ID;
+ itemData.targetApps[appId] = targetApplication;
+ }
+ blockEntry.versions.push(itemData);
+ }
+ // Add a default versionRange if there wasn't one specified
+ if (blockEntry.versions.length == 0)
+ blockEntry.versions.push(new BlocklistItemData(null));
+
+ return blockEntry;
+ },
+
+ _handleGfxItemJSON(data) {
+ /*
+ {
+ "driverVersionComparator": "LESS_THAN_OR_EQUAL",
+ "driverVersion": "8.17.12.5896",
+ "vendor": "0x10de",
+ "blockID": "g36",
+ "feature": "DIRECT3D_9_LAYERS",
+ "devices": ["0x0a6c"],
+ "featureStatus": "BLOCKED_DRIVER_VERSION",
+ "last_modified": 1458035931837,
+ "os": "WINNT 6.1",
+ "id": "3f947f16-37c2-4e96-d356-78b26363729b"
+ }
+ */
+ const blockEntry = Object.assign({}, data);
+ blockEntry.blockID = data.blockID || data.id; // Fallback to the JSON record id.
+ return blockEntry;
+ },
+
/* See nsIBlocklistService */
getPluginBlocklistState(plugin, appVersion, toolkitVersion) {
if (AppConstants.platform == "android") {
return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
}
if (!this._isBlocklistLoaded())
this._loadBlocklist();
return this._getPluginBlocklistState(plugin, this._pluginEntries,
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_json/sample.json
@@ -0,0 +1,50 @@
+{
+ "data": [
+ {
+ "blockID": "i53923",
+ "guid": "LionKiller@jetpack",
+ "id": "9d500963-d45e-3a91-6e32-66f3811b99cc",
+ "last_modified": 1457434774040,
+ "prefs": [],
+ "versionRange": [
+ {
+ "maxVersion": "*",
+ "minVersion": "0",
+ "severity": "3",
+ "targetApplication": []
+ }
+ ]
+ },
+
+ {
+ "blockID": "i539",
+ "guid": "ScorpionSaver@jetpack",
+ "id": "9d500963-d80e-3a91-6e74-66f3811b99cc",
+ "last_modified": 1457434774040,
+ "prefs": [],
+ "versionRange": [
+ {
+ "maxVersion": "*",
+ "minVersion": "0",
+ "severity": "1",
+ "targetApplication": []
+ }
+ ]
+ },
+ {
+ "blockID": "i808",
+ "guid": "{c96d1ae6-c4cf-4984-b110-f5f561b33b5a}",
+ "id": "9ccfac91-e463-c30c-f0bd-14143794a8dd",
+ "last_modified": 1457434774125,
+ "prefs": [],
+ "versionRange": [
+ {
+ "maxVersion": "*",
+ "minVersion": "0",
+ "severity": "3",
+ "targetApplication": []
+ }
+ ]
+ }
+ ]
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_json.js
@@ -0,0 +1,380 @@
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+const PREF_BLOCKLIST_VIA_AMO = "security.blocklist.via.amo";
+const KEY_APPDIR = "XCurProcD";
+const KEY_PROFILEDIR = "ProfD";
+const TEST_APP_ID = "xpcshell@tests.mozilla.org";
+
+
+const SAMPLE_FILE = do_get_file("data/test_blocklist_json/sample.json");
+
+const SAMPLE_ADDON_RECORD = {
+ "prefs": [],
+ "blockID": "i446",
+ "last_modified": 1457434834683,
+ "versionRange": [{
+ "targetApplication": [{
+ "minVersion": "0.1",
+ "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
+ "maxVersion": "17.*"
+ }],
+ "maxVersion": "*",
+ "minVersion": "0",
+ "severity": "1",
+ "vulnerabilityStatus": "0"
+ }],
+ "guid": "{E90FA778-C2B7-41D0-9FA9-3FEC1CA54D66}",
+ "id": "87a5dc56-1fec-ebf2-a09b-6f2cbd4eb2d3"
+};
+
+const SAMPLE_PLUGIN_RECORD = {
+ "matchFilename": "JavaPlugin2_NPAPI\\.plugin",
+ "blockID": "p123",
+ "id": "bdcf0717-a873-adbf-7603-83a49fb996bc",
+ "last_modified": 1457434851748,
+ "infoURL": "https://addons.mozilla.org/en-US/firefox/blocked/p123",
+ "versionRange": [{
+ "targetApplication": [{
+ "minVersion": "0.1",
+ "guid": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
+ "maxVersion": "17.*"
+ }],
+ "maxVersion": "14.2.0",
+ "minVersion": "0",
+ "severity": "1"
+ }]
+};
+
+const SAMPLE_GFX_RECORD = {
+ "driverVersionComparator": "LESS_THAN_OR_EQUAL",
+ "driverVersion": "8.17.12.5896",
+ "vendor": "0x10de",
+ "blockID": "g36",
+ "feature": "DIRECT3D_9_LAYERS",
+ "devices": ["0x0a6c"],
+ "featureStatus": "BLOCKED_DRIVER_VERSION",
+ "last_modified": 1458035931837,
+ "os": "WINNT 6.1",
+ "id": "3f947f16-37c2-4e96-d356-78b26363729b"
+};
+
+
+function clearProfile(name) {
+ let filename = name + ".json";
+ let blocklist = FileUtils.getFile(KEY_PROFILEDIR, ["blocklists", filename]);
+ if (blocklist.exists())
+ blocklist.remove(true);
+}
+
+
+function copyToProfile(file, name) {
+ const gProfDir = FileUtils.getFile(KEY_PROFILEDIR, ["blocklists"]);
+ let filename = name + ".json";
+ file = file.clone();
+ file.copyTo(gProfDir, filename);
+ file = gProfDir.clone();
+ file.append(filename);
+}
+
+
+function Blocklist() {
+ let blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService().wrappedJSObject;
+ blocklist._clear();
+ return blocklist;
+}
+
+
+function run_test() {
+ // Some blocklist code rely on gApp.ID.
+ createAppInfo(TEST_APP_ID, "XPCShell", "1", "1");
+ // Disable blocklist via AMO.
+ Services.prefs.setBoolPref(PREF_BLOCKLIST_VIA_AMO, false);
+
+ // Starts addons manager.
+ startupManager();
+
+ // Clean-up for profile data.
+ do_register_cleanup(function() {
+ for (let filename of ["addons.json", "plugins.json"]) {
+ const file = FileUtils.getFile(KEY_PROFILEDIR, ["blocklists", filename]);
+ if (file.exists()) file.remove(true);
+ }
+ });
+
+ run_next_test();
+}
+
+
+add_task(function* test_addon_entry_from_json_simple() {
+ const blocklist = Blocklist();
+ const data = Object.assign({}, SAMPLE_ADDON_RECORD);
+
+ const entry = blocklist._handleAddonItemJSON(data);
+
+ equal(entry.blockID, SAMPLE_ADDON_RECORD.blockID);
+ equal(entry.prefs, SAMPLE_ADDON_RECORD.prefs);
+ equal(entry.attributes.get("id"), SAMPLE_ADDON_RECORD.guid);
+ equal(entry.versions.length, 1);
+ const item = entry.versions[0];
+ equal(item.minVersion, "0");
+ equal(item.maxVersion, "*");
+ equal(item.severity, "1");
+ equal(item.vulnerabilityStatus, "0");
+ equal(item.targetApps["{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"].minVersion, "0.1");
+ equal(item.targetApps["{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"].maxVersion, "17.*");
+});
+
+
+add_task(function* test_addon_entry_from_json_no_version_range() {
+ const blocklist = Blocklist();
+ const data = Object.assign({}, SAMPLE_ADDON_RECORD);
+ data.versionRange = [];
+
+ const entry = blocklist._handleAddonItemJSON(data);
+
+ equal(entry.versions.length, 1);
+ const item = entry.versions[0];
+ equal(item.minVersion, null);
+ equal(item.maxVersion, null);
+ equal(item.severity, 3);
+ equal(item.vulnerabilityStatus, 0);
+ equal(item.targetApps[TEST_APP_ID].minVersion, null);
+ equal(item.targetApps[TEST_APP_ID].maxVersion, null);
+});
+
+
+add_task(function* test_addon_entry_from_json_without_blockid() {
+ const blocklist = Blocklist();
+ const data = Object.assign({}, SAMPLE_ADDON_RECORD);
+ delete data.blockID;
+
+ const entry = blocklist._handleAddonItemJSON(data);
+
+ equal(entry.blockID, SAMPLE_ADDON_RECORD.id);
+});
+
+
+add_task(function* test_addon_entry_without_target_application() {
+ const blocklist = Blocklist();
+ const data = Object.assign({}, SAMPLE_ADDON_RECORD);
+ data.versionRange[0].targetApplication = [];
+
+ const entry = blocklist._handleAddonItemJSON(data);
+
+ const item = entry.versions[0];
+ equal(item.minVersion, "0");
+ equal(item.maxVersion, "*");
+ equal(item.severity, "1");
+ equal(item.targetApps[TEST_APP_ID].minVersion, null);
+ equal(item.targetApps[TEST_APP_ID].maxVersion, null);
+});
+
+
+add_task(function* test_plugin_entry_from_json_simple() {
+ const blocklist = Blocklist();
+ const data = Object.assign({}, SAMPLE_PLUGIN_RECORD);
+
+ const entry = blocklist._handlePluginItemJSON(data);
+
+ equal(entry.blockID, SAMPLE_PLUGIN_RECORD.blockID);
+ equal(entry.infoURL, SAMPLE_PLUGIN_RECORD.infoURL);
+ equal(entry.matches["filename"].constructor.name, "RegExp");
+ equal(entry.versions.length, 1);
+ const item = entry.versions[0];
+ equal(item.minVersion, "0");
+ equal(item.maxVersion, "14.2.0");
+ equal(item.severity, "1");
+ equal(item.targetApps["{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"].minVersion, "0.1");
+ equal(item.targetApps["{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"].maxVersion, "17.*");
+});
+
+
+add_task(function* test_plugin_entry_from_json_no_match() {
+ const blocklist = Blocklist();
+ const data = Object.assign({}, SAMPLE_PLUGIN_RECORD);
+ delete data.matchFilename;
+
+ const entry = blocklist._handlePluginItemJSON(data);
+
+ equal(entry, undefined);
+});
+
+
+add_task(function* test_plugin_entry_from_json_no_version_range() {
+ const blocklist = Blocklist();
+ const data = Object.assign({}, SAMPLE_PLUGIN_RECORD);
+ data.versionRange = [];
+
+ const entry = blocklist._handlePluginItemJSON(data);
+
+ equal(entry.versions.length, 1);
+ const item = entry.versions[0];
+ equal(item.minVersion, null);
+ equal(item.maxVersion, null);
+ equal(item.severity, 3);
+ equal(item.targetApps[TEST_APP_ID].minVersion, null);
+ equal(item.targetApps[TEST_APP_ID].maxVersion, null);
+});
+
+
+add_task(function* test_plugin_entry_from_json_without_blockid() {
+ const blocklist = Blocklist();
+ const data = Object.assign({}, SAMPLE_PLUGIN_RECORD);
+ delete data.blockID;
+
+ const entry = blocklist._handlePluginItemJSON(data);
+
+ equal(entry.blockID, SAMPLE_PLUGIN_RECORD.id);
+});
+
+
+add_task(function* test_plugin_entry_without_target_application() {
+ const blocklist = Blocklist();
+ const data = Object.assign({}, SAMPLE_PLUGIN_RECORD);
+ data.versionRange[0].targetApplication = [];
+
+ const entry = blocklist._handlePluginItemJSON(data);
+
+ const item = entry.versions[0];
+ equal(item.minVersion, "0");
+ equal(item.maxVersion, "14.2.0");
+ equal(item.severity, "1");
+ equal(item.targetApps[TEST_APP_ID].minVersion, null);
+ equal(item.targetApps[TEST_APP_ID].maxVersion, null);
+});
+
+
+add_task(function* test_is_loaded_synchronously() {
+ const blocklist = Blocklist();
+ strictEqual(blocklist._isBlocklistLoaded(), false);
+ // Calls synchronous method from Interface.
+ blocklist.isAddonBlocklisted("addon", "appVersion", "toolkitVersion");
+ strictEqual(blocklist._isBlocklistLoaded(), true);
+});
+
+
+add_task(function* test_notify_does_not_download_xml_file() {
+ const blocklist = Blocklist();
+ strictEqual(blocklist._isBlocklistLoaded(), false);
+ // When managed with Kinto, nothing is loaded/downloaded on notify.
+ blocklist.notify(null);
+ strictEqual(blocklist._isBlocklistLoaded(), false);
+});
+
+
+add_task(function* preload_json_async() {
+ const blocklist = Blocklist();
+
+ yield blocklist._preloadBlocklist();
+
+ // Preloaded content comes from app dir.
+ const addonsAppPath = FileUtils.getFile(KEY_APPDIR, ["defaults", "blocklists", "addons.json"]).path;
+ const gfxAppPath = FileUtils.getFile(KEY_APPDIR, ["defaults", "blocklists", "gfx.json"]).path;
+ const pluginsAppPath = FileUtils.getFile(KEY_APPDIR, ["defaults", "blocklists", "plugins.json"]).path;
+ ok(blocklist._preloadedBlocklistContent.get(addonsAppPath).length > 0);
+ ok(blocklist._preloadedBlocklistContent.get(gfxAppPath).length > 0);
+ ok(blocklist._preloadedBlocklistContent.get(pluginsAppPath).length > 0);
+});
+
+
+add_task(function* preload_json_reads_profile_data() {
+ const blocklist = Blocklist();
+
+ // Write some JSON content in profile dir.
+ copyToProfile(SAMPLE_FILE, "addons");
+
+ try {
+ yield blocklist._preloadBlocklist();
+ // Preloaded content comes from profile dir file.
+ const path = FileUtils.getFile(KEY_PROFILEDIR, ["blocklists", "addons.json"]).path;
+ let fileContent = blocklist._preloadedBlocklistContent.get(path);
+ strictEqual(fileContent.length > 0, true);
+ strictEqual(JSON.parse(fileContent).data[0].blockID, "i53923");
+ } finally {
+ // Clean-up: delete created file.
+ clearProfile("addons");
+ }
+});
+
+
+add_task(function* load_uses_preloaded_json_if_available() {
+ clearProfile("addons");
+ clearProfile("gfx");
+ clearProfile("plugins");
+
+ const blocklist = Blocklist();
+
+ // Simulate preload of data.
+ const addonsAppPath = FileUtils.getFile(KEY_APPDIR, ["defaults", "blocklists", "addons.json"]).path;
+ const gfxAppPath = FileUtils.getFile(KEY_APPDIR, ["defaults", "blocklists", "gfx.json"]).path;
+ const pluginsAppPath = FileUtils.getFile(KEY_APPDIR, ["defaults", "blocklists", "plugins.json"]).path;
+ blocklist._preloadedBlocklistContent.set(addonsAppPath, JSON.stringify({data: [SAMPLE_ADDON_RECORD]}));
+ blocklist._preloadedBlocklistContent.set(gfxAppPath, JSON.stringify({data: [SAMPLE_GFX_RECORD]}));
+ blocklist._preloadedBlocklistContent.set(pluginsAppPath, JSON.stringify({data: [SAMPLE_PLUGIN_RECORD]}));
+
+ blocklist._loadBlocklist();
+
+ // Data loaded comes from preloaded content.
+ equal(blocklist._addonEntries[0].blockID, SAMPLE_ADDON_RECORD.blockID);
+ equal(blocklist._gfxEntries[0].blockID, SAMPLE_GFX_RECORD.blockID);
+ equal(blocklist._pluginEntries[0].blockID, SAMPLE_PLUGIN_RECORD.blockID);
+});
+
+
+add_task(function* test_read_json_from_app_or_profile() {
+ const blocklist = Blocklist();
+
+ // Reads from app dir by default.
+ clearProfile("addons");
+ blocklist._loadBlocklist();
+ ok(blocklist._addonEntries.length >= 416);
+
+ // Reads from profile if present.
+ copyToProfile(SAMPLE_FILE, "addons");
+ try {
+ blocklist._loadBlocklist();
+ equal(blocklist._addonEntries.length, 3);
+ } finally {
+ clearProfile("addons");
+ }
+});
+
+
+add_task(function* test_invalid_preloaded_json() {
+ const blocklist = Blocklist();
+
+ // Simulate invalid json in preloaded data from app dir:
+ const addonsAppPath = FileUtils.getFile(KEY_APPDIR, ["defaults", "blocklists", "addons.json"]).path;
+ blocklist._preloadedBlocklistContent.set(addonsAppPath, "{>invalid}");
+
+ blocklist._loadBlocklist();
+
+ // TODO: Bug 1360576 fallback on release dumps instead of giving up.
+ deepEqual(blocklist._addonEntries, []);
+});
+
+
+add_task(function* test_invalid_json_file() {
+ // Write invalid JSON file in profile (as failed download or whatever)
+ const path = OS.Path.join(OS.Constants.Path.profileDir, "blocklists", "addons.json");
+ yield OS.File.writeAtomic(path, "{>invalid}", {tmpPath: path + ".tmp"});
+
+ const blocklist = Blocklist();
+ try {
+ blocklist._loadBlocklist();
+ // TODO: Bug 1360576 fallback on release dumps instead of giving up.
+ deepEqual(blocklist._addonEntries.length, 0);
+ } finally {
+ clearProfile("addons");
+ }
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -9,16 +9,18 @@ support-files =
xpcshell-shared.ini
[test_addon_path_service.js]
[test_addonStartup.js]
[test_asyncBlocklistLoad.js]
tags = blocklist
[test_blocklist_gfx.js]
tags = blocklist
+[test_blocklist_json.js]
+tags = blocklist
[test_cache_certdb.js]
run-if = addon_signing
[test_cacheflush.js]
[test_DeferredSave.js]
[test_gmpProvider.js]
skip-if = appname != "firefox"
[test_hotfix_cert.js]
[test_isReady.js]