forked from fingerprintjs/fingerprintjs
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscreen_frame.ts
More file actions
127 lines (109 loc) · 4.31 KB
/
screen_frame.ts
File metadata and controls
127 lines (109 loc) · 4.31 KB
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
import { replaceNaN, round, toFloat } from '../utils/data'
import { exitFullscreen, getFullscreenElement } from '../utils/browser'
/**
* The order matches the CSS side order: top, right, bottom, left.
*
* @ignore Named array elements aren't used because of multiple TypeScript compatibility complaints from users
*/
export type FrameSize = [number | null, number | null, number | null, number | null]
export const screenFrameCheckInterval = 2500
const roundingPrecision = 10
// The type is readonly to protect from unwanted mutations
let screenFrameBackup: Readonly<FrameSize> | undefined
let screenFrameSizeTimeoutId: number | undefined
/**
* Starts watching the screen frame size. When a non-zero size appears, the size is saved and the watch is stopped.
* Later, when `getScreenFrame` runs, it will return the saved non-zero size if the current size is null.
*
* This trick is required to mitigate the fact that the screen frame turns null in some cases.
* See more on this at https://github.com/fingerprintjs/fingerprintjs/issues/568
*/
function watchScreenFrame() {
if (screenFrameSizeTimeoutId !== undefined) {
return
}
const checkScreenFrame = () => {
const frameSize = getCurrentScreenFrame()
if (isFrameSizeNull(frameSize)) {
screenFrameSizeTimeoutId = (setTimeout as typeof window.setTimeout)(checkScreenFrame, screenFrameCheckInterval)
} else {
screenFrameBackup = frameSize
screenFrameSizeTimeoutId = undefined
}
}
checkScreenFrame()
}
/**
* For tests only
*/
export function resetScreenFrameWatch(): void {
if (screenFrameSizeTimeoutId !== undefined) {
clearTimeout(screenFrameSizeTimeoutId)
screenFrameSizeTimeoutId = undefined
}
screenFrameBackup = undefined
}
/**
* For tests only
*/
export function hasScreenFrameBackup(): boolean {
return !!screenFrameBackup
}
export function getScreenFrame(): () => Promise<FrameSize> {
watchScreenFrame()
return async () => {
let frameSize = getCurrentScreenFrame()
if (isFrameSizeNull(frameSize)) {
if (screenFrameBackup) {
return [...screenFrameBackup]
}
if (getFullscreenElement()) {
// Some browsers set the screen frame to zero when programmatic fullscreen is on.
// There is a chance of getting a non-zero frame after exiting the fullscreen.
// See more on this at https://github.com/fingerprintjs/fingerprintjs/issues/568
await exitFullscreen()
frameSize = getCurrentScreenFrame()
}
}
if (!isFrameSizeNull(frameSize)) {
screenFrameBackup = frameSize
}
return frameSize
}
}
/**
* Sometimes the available screen resolution changes a bit, e.g. 1900x1440 → 1900x1439. A possible reason: macOS Dock
* shrinks to fit more icons when there is too little space. The rounding is used to mitigate the difference.
*/
export function getRoundedScreenFrame(): () => Promise<FrameSize> {
const screenFrameGetter = getScreenFrame()
return async () => {
const frameSize = await screenFrameGetter()
const processSize = (sideSize: FrameSize[number]) => (sideSize === null ? null : round(sideSize, roundingPrecision))
// It might look like I don't know about `for` and `map`.
// In fact, such code is used to avoid TypeScript issues without using `as`.
return [processSize(frameSize[0]), processSize(frameSize[1]), processSize(frameSize[2]), processSize(frameSize[3])]
}
}
function getCurrentScreenFrame(): FrameSize {
const s = screen
// Some browsers return screen resolution as strings, e.g. "1200", instead of a number, e.g. 1200.
// I suspect it's done by certain plugins that randomize browser properties to prevent fingerprinting.
//
// Some browsers (IE, Edge ≤18) don't provide `screen.availLeft` and `screen.availTop`. The property values are
// replaced with 0 in such cases to not lose the entropy from `screen.availWidth` and `screen.availHeight`.
return [
replaceNaN(toFloat(s.availTop), null),
replaceNaN(toFloat(s.width) - toFloat(s.availWidth) - replaceNaN(toFloat(s.availLeft), 0), null),
replaceNaN(toFloat(s.height) - toFloat(s.availHeight) - replaceNaN(toFloat(s.availTop), 0), null),
replaceNaN(toFloat(s.availLeft), null),
]
}
function isFrameSizeNull(frameSize: FrameSize) {
for (let i = 0; i < 4; ++i) {
if (frameSize[i]) {
return false
}
}
return true
}