bug418986-2.js (10348B)
1 // # Bug 418986, part 2. 2 3 /* eslint-disable mozilla/no-comparison-or-assignment-inside-ok */ 4 5 const is_chrome_window = window.location.protocol === "chrome:"; 6 7 const HTML_NS = "http://www.w3.org/1999/xhtml"; 8 9 // Expected values. Format: [name, pref_off_value, pref_on_value] 10 // If pref_*_value is an array with two values, then we will match 11 // any value in between those two values. If a value is null, then 12 // we skip the media query. 13 var expected_values = [ 14 ["color", null, 8], 15 ["color-index", null, 0], 16 ["aspect-ratio", null, window.innerWidth + "/" + window.innerHeight], 17 [ 18 "device-aspect-ratio", 19 screen.width + "/" + screen.height, 20 window.innerWidth + "/" + window.innerHeight, 21 ], 22 ["device-height", screen.height + "px", window.innerHeight + "px"], 23 ["device-width", screen.width + "px", window.innerWidth + "px"], 24 ["grid", null, 0], 25 ["height", window.innerHeight + "px", window.innerHeight + "px"], 26 ["monochrome", null, 0], 27 // Square is defined as portrait: 28 [ 29 "orientation", 30 null, 31 window.innerWidth > window.innerHeight ? "landscape" : "portrait", 32 ], 33 ["resolution", null, "192dpi"], 34 [ 35 "resolution", 36 [ 37 0.999 * window.devicePixelRatio + "dppx", 38 1.001 * window.devicePixelRatio + "dppx", 39 ], 40 "2dppx", 41 ], 42 ["width", window.innerWidth + "px", window.innerWidth + "px"], 43 ["-moz-device-pixel-ratio", window.devicePixelRatio, 2], 44 [ 45 "-moz-device-orientation", 46 screen.width > screen.height ? "landscape" : "portrait", 47 window.innerWidth > window.innerHeight ? "landscape" : "portrait", 48 ], 49 ]; 50 51 // These media queries return value 0 or 1 when the pref is off. 52 // When the pref is on, they should not match. 53 var suppressed_toggles = [ 54 "-moz-gtk-csd-available", 55 "-moz-gtk-csd-minimize-button", 56 "-moz-gtk-csd-maximize-button", 57 "-moz-gtk-csd-close-button", 58 "-moz-gtk-csd-reversed-placement", 59 ]; 60 61 var toggles_enabled_in_content = []; 62 63 // Read the current OS. 64 var OS = SpecialPowers.Services.appinfo.OS; 65 66 // __keyValMatches(key, val)__. 67 // Runs a media query and returns true if key matches to val. 68 var keyValMatches = (key, val) => 69 matchMedia("(" + key + ":" + val + ")").matches; 70 71 // __testMatch(key, val)__. 72 // Attempts to run a media query match for the given key and value. 73 // If value is an array of two elements [min max], then matches any 74 // value in-between. 75 var testMatch = function (key, val) { 76 if (val === null) { 77 return; 78 } else if (Array.isArray(val)) { 79 ok( 80 keyValMatches("min-" + key, val[0]) && 81 keyValMatches("max-" + key, val[1]), 82 "Expected " + key + " between " + val[0] + " and " + val[1] 83 ); 84 } else { 85 ok(keyValMatches(key, val), "Expected " + key + ":" + val); 86 } 87 }; 88 89 // __testToggles(resisting)__. 90 // Test whether we are able to match the "toggle" media queries. 91 var testToggles = function (resisting) { 92 suppressed_toggles.forEach(function (key) { 93 var exists = keyValMatches(key, 0) || keyValMatches(key, 1); 94 if (!toggles_enabled_in_content.includes(key) && !is_chrome_window) { 95 ok(!exists, key + " should not exist."); 96 } else { 97 ok(exists, key + " should exist."); 98 if (resisting) { 99 ok( 100 keyValMatches(key, 0) && !keyValMatches(key, 1), 101 "Should always match as false" 102 ); 103 } 104 } 105 }); 106 }; 107 108 // __generateHtmlLines(resisting)__. 109 // Create a series of div elements that look like: 110 // `<div class='spoof' id='resolution'>resolution</div>`, 111 // where each line corresponds to a different media query. 112 var generateHtmlLines = function (resisting) { 113 let fragment = document.createDocumentFragment(); 114 expected_values.forEach(function ([key, offVal, onVal]) { 115 let val = resisting ? onVal : offVal; 116 if (val) { 117 let div = document.createElementNS(HTML_NS, "div"); 118 div.setAttribute("class", "spoof"); 119 div.setAttribute("id", key); 120 div.textContent = key; 121 fragment.appendChild(div); 122 } 123 }); 124 suppressed_toggles.forEach(function (key) { 125 let div = document.createElementNS(HTML_NS, "div"); 126 div.setAttribute("class", "suppress"); 127 div.setAttribute("id", key); 128 div.textContent = key; 129 fragment.appendChild(div); 130 }); 131 return fragment; 132 }; 133 134 // __cssLine__. 135 // Creates a line of css that looks something like 136 // `@media (resolution: 1ppx) { .spoof#resolution { background-color: green; } }`. 137 var cssLine = function (query, clazz, id, color) { 138 return ( 139 "@media " + 140 query + 141 " { ." + 142 clazz + 143 "#" + 144 id + 145 " { background-color: " + 146 color + 147 "; } }\n" 148 ); 149 }; 150 151 // __constructQuery(key, val)__. 152 // Creates a CSS media query from key and val. If key is an array of 153 // two elements, constructs a range query (using min- and max-). 154 var constructQuery = function (key, val) { 155 return Array.isArray(val) 156 ? "(min-" + key + ": " + val[0] + ") and (max-" + key + ": " + val[1] + ")" 157 : "(" + key + ": " + val + ")"; 158 }; 159 160 // __mediaQueryCSSLine(key, val, color)__. 161 // Creates a line containing a CSS media query and a CSS expression. 162 var mediaQueryCSSLine = function (key, val, color) { 163 if (val === null) { 164 return ""; 165 } 166 return cssLine(constructQuery(key, val), "spoof", key, color); 167 }; 168 169 // __suppressedMediaQueryCSSLine(key, color)__. 170 // Creates a CSS line that matches the existence of a 171 // media query that is supposed to be suppressed. 172 var suppressedMediaQueryCSSLine = function (key, color, suppressed) { 173 let query = "(" + key + ": 0), (" + key + ": 1)"; 174 return cssLine(query, "suppress", key, color); 175 }; 176 177 // __generateCSSLines(resisting)__. 178 // Creates a series of lines of CSS, each of which corresponds to 179 // a different media query. If the query produces a match to the 180 // expected value, then the element will be colored green. 181 var generateCSSLines = function (resisting) { 182 let lines = ".spoof { background-color: red;}\n"; 183 expected_values.forEach(function ([key, offVal, onVal]) { 184 lines += mediaQueryCSSLine(key, resisting ? onVal : offVal, "green"); 185 }); 186 lines += 187 ".suppress { background-color: " + (resisting ? "green" : "red") + ";}\n"; 188 suppressed_toggles.forEach(function (key) { 189 if ( 190 !toggles_enabled_in_content.includes(key) && 191 !resisting && 192 !is_chrome_window 193 ) { 194 lines += "#" + key + " { background-color: green; }\n"; 195 } else { 196 lines += suppressedMediaQueryCSSLine(key, "green"); 197 } 198 }); 199 return lines; 200 }; 201 202 // __green__. 203 // Returns the computed color style corresponding to green. 204 var green = "rgb(0, 128, 0)"; 205 206 // __testCSS(resisting)__. 207 // Creates a series of divs and CSS using media queries to set their 208 // background color. If all media queries match as expected, then 209 // all divs should have a green background color. 210 var testCSS = function (resisting) { 211 document.getElementById("display").appendChild(generateHtmlLines(resisting)); 212 document.getElementById("test-css").textContent = generateCSSLines(resisting); 213 let cssTestDivs = document.querySelectorAll(".spoof,.suppress"); 214 for (let div of cssTestDivs) { 215 let color = window.getComputedStyle(div).backgroundColor; 216 ok(color === green, "CSS for '" + div.id + "'"); 217 } 218 }; 219 220 // __testOSXFontSmoothing(resisting)__. 221 // When fingerprinting resistance is enabled, the `getComputedStyle` 222 // should always return `undefined` for `MozOSXFontSmoothing`. 223 var testOSXFontSmoothing = function (resisting) { 224 let div = document.createElementNS(HTML_NS, "div"); 225 div.style.MozOsxFontSmoothing = "unset"; 226 document.documentElement.appendChild(div); 227 let readBack = window.getComputedStyle(div).MozOsxFontSmoothing; 228 div.remove(); 229 let smoothingPref = SpecialPowers.getBoolPref( 230 "layout.css.osx-font-smoothing.enabled", 231 false 232 ); 233 is( 234 readBack, 235 resisting ? "" : smoothingPref ? "auto" : "", 236 "-moz-osx-font-smoothing" 237 ); 238 }; 239 240 // __sleep(timeoutMs)__. 241 // Returns a promise that resolves after the given timeout. 242 var sleep = function (timeoutMs) { 243 return new Promise(function (resolve, reject) { 244 window.setTimeout(resolve); 245 }); 246 }; 247 248 // __testMediaQueriesInPictureElements(resisting)__. 249 // Test to see if media queries are properly spoofed in picture elements 250 // when we are resisting fingerprinting. 251 var testMediaQueriesInPictureElements = async function (resisting) { 252 const MATCH = "/tests/layout/style/test/chrome/match.png"; 253 let container = document.getElementById("pictures"); 254 let testImages = []; 255 for (let [key, offVal, onVal] of expected_values) { 256 let expected = resisting ? onVal : offVal; 257 if (expected) { 258 let picture = document.createElementNS(HTML_NS, "picture"); 259 let query = constructQuery(key, expected); 260 ok(matchMedia(query).matches, `${query} should match`); 261 262 let source = document.createElementNS(HTML_NS, "source"); 263 source.setAttribute("srcset", MATCH); 264 source.setAttribute("media", query); 265 266 let image = document.createElementNS(HTML_NS, "img"); 267 image.setAttribute("title", key + ":" + expected); 268 image.setAttribute("class", "testImage"); 269 image.setAttribute("src", "/tests/layout/style/test/chrome/mismatch.png"); 270 image.setAttribute("alt", key); 271 272 testImages.push(image); 273 274 picture.appendChild(source); 275 picture.appendChild(image); 276 container.appendChild(picture); 277 } 278 } 279 const matchURI = new URL(MATCH, document.baseURI).href; 280 await sleep(0); 281 for (let testImage of testImages) { 282 is( 283 testImage.currentSrc, 284 matchURI, 285 "Media query '" + testImage.title + "' in picture should match." 286 ); 287 } 288 }; 289 290 // __pushPref(key, value)__. 291 // Set a pref value asynchronously, returning a promise that resolves 292 // when it succeeds. 293 var pushPref = function (key, value) { 294 return new Promise(function (resolve, reject) { 295 SpecialPowers.pushPrefEnv({ set: [[key, value]] }, resolve); 296 }); 297 }; 298 299 // __test(isContent)__. 300 // Run all tests. 301 var test = async function (isContent) { 302 for (prefValue of [false, true]) { 303 await pushPref("privacy.resistFingerprinting", prefValue); 304 let resisting = prefValue && isContent; 305 expected_values.forEach(function ([key, offVal, onVal]) { 306 testMatch(key, resisting ? onVal : offVal); 307 }); 308 testToggles(resisting); 309 testCSS(resisting); 310 if (OS === "Darwin") { 311 testOSXFontSmoothing(resisting); 312 } 313 await testMediaQueriesInPictureElements(resisting); 314 } 315 };