// # Bug 418986, part 2.
/* jshint esnext:true */
/* jshint loopfunc:true */
/* global window, screen, ok, SpecialPowers, matchMedia */
// Expected values. Format: [name, pref_off_value, pref_on_value]
// If pref_*_value is an array with two values, then we will match
// any value in between those two values. If a value is null, then
// we skip the media query.
var expected_values = [
["color", null, 8],
["color-index", null, 0],
["aspect-ratio", null, window.innerWidth + "/" + window.innerHeight],
["device-aspect-ratio", screen.width + "/" + screen.height,
window.innerWidth + "/" + window.innerHeight],
["device-height", screen.height + "px", window.innerHeight + "px"],
["device-width", screen.width + "px", window.innerWidth + "px"],
["grid", null, 0],
["height", window.innerHeight + "px", window.innerHeight + "px"],
["monochrome", null, 0],
// Square is defined as portrait:
["orientation", null,
window.innerWidth > window.innerHeight ?
"landscape" : "portrait"],
["resolution", null, "96dpi"],
["resolution", [0.999 * window.devicePixelRatio + "dppx",
1.001 * window.devicePixelRatio + "dppx"], "1dppx"],
["width", window.innerWidth + "px", window.innerWidth + "px"],
["-moz-device-pixel-ratio", window.devicePixelRatio, 1],
["-moz-device-orientation", screen.width > screen.height ?
"landscape" : "portrait",
window.innerWidth > window.innerHeight ?
"landscape" : "portrait"]
];
// These media queries return value 0 or 1 when the pref is off.
// When the pref is on, they should not match.
var suppressed_toggles = [
"-moz-mac-graphite-theme",
// Not available on most OSs.
// "-moz-maemo-classic",
"-moz-scrollbar-end-backward",
"-moz-scrollbar-end-forward",
"-moz-scrollbar-start-backward",
"-moz-scrollbar-start-forward",
"-moz-scrollbar-thumb-proportional",
"-moz-touch-enabled",
"-moz-windows-compositor",
"-moz-windows-default-theme",
"-moz-windows-glass",
];
// Possible values for '-moz-os-version'
var windows_versions = [
"windows-win7",
"windows-win8",
"windows-win10",
];
// Possible values for '-moz-windows-theme'
var windows_themes = [
"aero",
"aero-lite",
"luna-blue",
"luna-olive",
"luna-silver",
"royale",
"generic",
"zune"
];
// Read the current OS.
var OS = SpecialPowers.Services.appinfo.OS;
// If we are using Windows, add an extra toggle only
// available on that OS.
if (OS === "WINNT") {
suppressed_toggles.push("-moz-windows-classic");
}
// __keyValMatches(key, val)__.
// Runs a media query and returns true if key matches to val.
var keyValMatches = (key, val) => matchMedia("(" + key + ":" + val +")").matches;
// __testMatch(key, val)__.
// Attempts to run a media query match for the given key and value.
// If value is an array of two elements [min max], then matches any
// value in-between.
var testMatch = function (key, val) {
if (val === null) {
return;
} else if (Array.isArray(val)) {
ok(keyValMatches("min-" + key, val[0]) && keyValMatches("max-" + key, val[1]),
"Expected " + key + " between " + val[0] + " and " + val[1]);
} else {
ok(keyValMatches(key, val), "Expected " + key + ":" + val);
}
};
// __testToggles(resisting)__.
// Test whether we are able to match the "toggle" media queries.
var testToggles = function (resisting) {
suppressed_toggles.forEach(
function (key) {
var exists = keyValMatches(key, 0) || keyValMatches(key, 1);
if (resisting) {
ok(!exists, key + " should not exist.");
} else {
ok(exists, key + " should exist.");
}
});
};
// __testWindowsSpecific__.
// Runs a media query on the queryName with the given possible matching values.
var testWindowsSpecific = function (resisting, queryName, possibleValues) {
let foundValue = null;
possibleValues.forEach(function (val) {
if (keyValMatches(queryName, val)) {
foundValue = val;
}
});
if (resisting) {
ok(!foundValue, queryName + " should have no match");
} else {
ok(foundValue, foundValue ? ("Match found: '" + queryName + ":" + foundValue + "'")
: "Should have a match for '" + queryName + "'");
}
};
// __generateHtmlLines(resisting)__.
// Create a series of div elements that look like:
// `
resolution
`,
// where each line corresponds to a different media query.
var generateHtmlLines = function (resisting) {
let lines = "";
expected_values.forEach(
function ([key, offVal, onVal]) {
let val = resisting ? onVal : offVal;
if (val) {
lines += "
" + key + "
\n";
}
});
suppressed_toggles.forEach(
function (key) {
lines += "
" + key + "
\n";
});
if (OS === "WINNT") {
lines += "
-moz-os-version
";
lines += "
-moz-windows-theme
";
}
return lines;
};
// __cssLine__.
// Creates a line of css that looks something like
// `@media (resolution: 1ppx) { .spoof#resolution { background-color: green; } }`.
var cssLine = function (query, clazz, id, color) {
return "@media " + query + " { ." + clazz + "#" + id +
" { background-color: " + color + "; } }\n";
};
// __constructQuery(key, val)__.
// Creates a CSS media query from key and val. If key is an array of
// two elements, constructs a range query (using min- and max-).
var constructQuery = function (key, val) {
return Array.isArray(val) ?
"(min-" + key + ": " + val[0] + ") and (max-" + key + ": " + val[1] + ")" :
"(" + key + ": " + val + ")";
};
// __mediaQueryCSSLine(key, val, color)__.
// Creates a line containing a CSS media query and a CSS expression.
var mediaQueryCSSLine = function (key, val, color) {
if (val === null) {
return "";
}
return cssLine(constructQuery(key, val), "spoof", key, color);
};
// __suppressedMediaQueryCSSLine(key, color)__.
// Creates a CSS line that matches the existence of a
// media query that is supposed to be suppressed.
var suppressedMediaQueryCSSLine = function (key, color, suppressed) {
let query = "(" + key + ": 0), (" + key + ": 1)";
return cssLine(query, "suppress", key, color);
};
// __generateCSSLines(resisting)__.
// Creates a series of lines of CSS, each of which corresponds to
// a different media query. If the query produces a match to the
// expected value, then the element will be colored green.
var generateCSSLines = function (resisting) {
let lines = ".spoof { background-color: red;}\n";
expected_values.forEach(
function ([key, offVal, onVal]) {
lines += mediaQueryCSSLine(key, resisting ? onVal : offVal, "green");
});
lines += ".suppress { background-color: " + (resisting ? "green" : "red") + ";}\n";
suppressed_toggles.forEach(
function (key) {
lines += suppressedMediaQueryCSSLine(key, resisting ? "red" : "green");
});
if (OS === "WINNT") {
lines += ".windows { background-color: " + (resisting ? "green" : "red") + ";}\n";
lines += windows_versions.map(val => "(-moz-os-version: " + val + ")").join(", ") +
" { #-moz-os-version { background-color: " + (resisting ? "red" : "green") + ";} }\n";
lines += windows_themes.map(val => "(-moz-windows-theme: " + val + ")").join(",") +
" { #-moz-windows-theme { background-color: " + (resisting ? "red" : "green") + ";} }\n";
}
return lines;
};
// __green__.
// Returns the computed color style corresponding to green.
var green = (function () {
let temp = document.createElement("span");
temp.style.backgroundColor = "green";
return getComputedStyle(temp).backgroundColor;
})();
// __testCSS(resisting)__.
// Creates a series of divs and CSS using media queries to set their
// background color. If all media queries match as expected, then
// all divs should have a green background color.
var testCSS = function (resisting) {
document.getElementById("display").innerHTML = generateHtmlLines(resisting);
document.getElementById("test-css").innerHTML = generateCSSLines(resisting);
let cssTestDivs = document.querySelectorAll(".spoof,.suppress");
for (let div of cssTestDivs) {
let color = window.getComputedStyle(div).backgroundColor;
ok(color === green, "CSS for '" + div.id + "'");
}
};
// __testOSXFontSmoothing(resisting)__.
// When fingerprinting resistance is enabled, the `getComputedStyle`
// should always return `undefined` for `MozOSXFontSmoothing`.
var testOSXFontSmoothing = function (resisting) {
let div = document.createElement("div");
div.style.MozOsxFontSmoothing = "unset";
let readBack = window.getComputedStyle(div).MozOsxFontSmoothing;
let smoothingPref = SpecialPowers.getBoolPref("layout.css.osx-font-smoothing.enabled", false);
is(readBack, resisting ? "" : (smoothingPref ? "auto" : ""),
"-moz-osx-font-smoothing");
};
// __sleep(timeoutMs)__.
// Returns a promise that resolves after the given timeout.
var sleep = function (timeoutMs) {
return new Promise(function(resolve, reject) {
window.setTimeout(resolve);
});
};
// __testMediaQueriesInPictureElements(resisting)__.
// Test to see if media queries are properly spoofed in picture elements
// when we are resisting fingerprinting. A generator function
// to be used with SpawnTask.js.
var testMediaQueriesInPictureElements = function* (resisting) {
let lines = "";
for (let [key, offVal, onVal] of expected_values) {
let expected = resisting ? onVal : offVal;
if (expected) {
let query = constructQuery(key, expected);
lines += " \n";
}
}
document.getElementById("pictures").innerHTML = lines;
var testImages = document.getElementsByClassName("testImage");
yield sleep(0);
for (let testImage of testImages) {
ok(testImage.currentSrc.endsWith("/match.png"), "Media query '" + testImage.title + "' in picture should match.");
}
};
// __pushPref(key, value)__.
// Set a pref value asynchronously, returning a promise that resolves
// when it succeeds.
var pushPref = function (key, value) {
return new Promise(function(resolve, reject) {
SpecialPowers.pushPrefEnv({"set": [[key, value]]}, resolve);
});
};
// __test(isContent)__.
// Run all tests. A generator function to be used
// with SpawnTask.js.
var test = function* (isContent) {
for (prefValue of [false, true]) {
yield pushPref("privacy.resistFingerprinting", prefValue);
let resisting = prefValue && isContent;
expected_values.forEach(
function ([key, offVal, onVal]) {
testMatch(key, resisting ? onVal : offVal);
});
testToggles(resisting);
if (OS === "WINNT") {
testWindowsSpecific(resisting, "-moz-os-version", windows_versions);
testWindowsSpecific(resisting, "-moz-windows-theme", windows_themes);
}
testCSS(resisting);
if (OS === "Darwin") {
testOSXFontSmoothing(resisting);
}
yield testMediaQueriesInPictureElements(resisting);
}
};