// # Bug 418986, part 2. const is_chrome_window = window.location.protocol === "chrome:"; const HTML_NS = "http://www.w3.org/1999/xhtml"; // 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-windows-compositor", "-moz-windows-default-theme", "-moz-windows-glass", "-moz-gtk-csd-available", "-moz-gtk-csd-minimize-button", "-moz-gtk-csd-maximize-button", "-moz-gtk-csd-close-button", "-moz-gtk-csd-reversed-placement", ]; var toggles_enabled_in_content = []; // 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 (!toggles_enabled_in_content.includes(key) && !is_chrome_window) { ok(!exists, key + " should not exist."); } else { ok(exists, key + " should exist."); if (resisting) { ok( keyValMatches(key, 0) && !keyValMatches(key, 1), "Should always match as false" ); } } }); }; // __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 fragment = document.createDocumentFragment(); expected_values.forEach(function ([key, offVal, onVal]) { let val = resisting ? onVal : offVal; if (val) { let div = document.createElementNS(HTML_NS, "div"); div.setAttribute("class", "spoof"); div.setAttribute("id", key); div.textContent = key; fragment.appendChild(div); } }); suppressed_toggles.forEach(function (key) { let div = document.createElementNS(HTML_NS, "div"); div.setAttribute("class", "suppress"); div.setAttribute("id", key); div.textContent = key; fragment.appendChild(div); }); return fragment; }; // __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) { if ( !toggles_enabled_in_content.includes(key) && !resisting && !is_chrome_window ) { lines += "#" + key + " { background-color: green; }\n"; } else { lines += suppressedMediaQueryCSSLine(key, "green"); } }); return lines; }; // __green__. // Returns the computed color style corresponding to green. var green = "rgb(0, 128, 0)"; // __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").appendChild(generateHtmlLines(resisting)); document.getElementById("test-css").textContent = 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.createElementNS(HTML_NS, "div"); div.style.MozOsxFontSmoothing = "unset"; document.documentElement.appendChild(div); let readBack = window.getComputedStyle(div).MozOsxFontSmoothing; div.remove(); 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. var testMediaQueriesInPictureElements = async function (resisting) { const MATCH = "/tests/layout/style/test/chrome/match.png"; let container = document.getElementById("pictures"); let testImages = []; for (let [key, offVal, onVal] of expected_values) { let expected = resisting ? onVal : offVal; if (expected) { let picture = document.createElementNS(HTML_NS, "picture"); let query = constructQuery(key, expected); ok(matchMedia(query).matches, `${query} should match`); let source = document.createElementNS(HTML_NS, "source"); source.setAttribute("srcset", MATCH); source.setAttribute("media", query); let image = document.createElementNS(HTML_NS, "img"); image.setAttribute("title", key + ":" + expected); image.setAttribute("class", "testImage"); image.setAttribute("src", "/tests/layout/style/test/chrome/mismatch.png"); image.setAttribute("alt", key); testImages.push(image); picture.appendChild(source); picture.appendChild(image); container.appendChild(picture); } } const matchURI = new URL(MATCH, document.baseURI).href; await sleep(0); for (let testImage of testImages) { is( testImage.currentSrc, matchURI, "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. var test = async function (isContent) { for (prefValue of [false, true]) { await pushPref("privacy.resistFingerprinting", prefValue); let resisting = prefValue && isContent; expected_values.forEach(function ([key, offVal, onVal]) { testMatch(key, resisting ? onVal : offVal); }); testToggles(resisting); testCSS(resisting); if (OS === "Darwin") { testOSXFontSmoothing(resisting); } await testMediaQueriesInPictureElements(resisting); } };