1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
|
// # 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:
// `<div class='spoof' id='resolution'>resolution</div>`,
// 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);
}
};
|