@ginger-ai/ginger-js 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/dist/ginger.cjs.js +2 -0
- package/dist/ginger.cjs.js.map +1 -0
- package/dist/ginger.esm.d.ts +294 -0
- package/dist/ginger.esm.js +2 -0
- package/dist/ginger.esm.js.map +1 -0
- package/dist/ginger.umd.js +2 -0
- package/dist/ginger.umd.js.map +1 -0
- package/dist/types/behaviour/index.d.ts +49 -0
- package/dist/types/behaviour/index.d.ts.map +1 -0
- package/dist/types/client/index.d.ts +35 -0
- package/dist/types/client/index.d.ts.map +1 -0
- package/dist/types/core/constants.d.ts +5 -0
- package/dist/types/core/constants.d.ts.map +1 -0
- package/dist/types/core/dto/bot-detector.dto.d.ts +30 -0
- package/dist/types/core/dto/bot-detector.dto.d.ts.map +1 -0
- package/dist/types/core/dto/device-detector.dto.d.ts +54 -0
- package/dist/types/core/dto/device-detector.dto.d.ts.map +1 -0
- package/dist/types/core/dto/fingerprint.dto.d.ts +29 -0
- package/dist/types/core/dto/fingerprint.dto.d.ts.map +1 -0
- package/dist/types/core/dto/ginger.dto.d.ts +73 -0
- package/dist/types/core/dto/ginger.dto.d.ts.map +1 -0
- package/dist/types/core/dto/incognito-detector.dto.d.ts +4 -0
- package/dist/types/core/dto/incognito-detector.dto.d.ts.map +1 -0
- package/dist/types/core/dto/index.d.ts +10 -0
- package/dist/types/core/dto/index.d.ts.map +1 -0
- package/dist/types/core/dto/metrics.dto.d.ts +19 -0
- package/dist/types/core/dto/metrics.dto.d.ts.map +1 -0
- package/dist/types/core/dto/os-detector.dto.d.ts +6 -0
- package/dist/types/core/dto/os-detector.dto.d.ts.map +1 -0
- package/dist/types/core/dto/tor-detector.dto.d.ts +5 -0
- package/dist/types/core/dto/tor-detector.dto.d.ts.map +1 -0
- package/dist/types/core/helpers.d.ts +8 -0
- package/dist/types/core/helpers.d.ts.map +1 -0
- package/dist/types/core/http/httpClient.d.ts +28 -0
- package/dist/types/core/http/httpClient.d.ts.map +1 -0
- package/dist/types/core/http/request.d.ts +4 -0
- package/dist/types/core/http/request.d.ts.map +1 -0
- package/dist/types/core/index.d.ts +6 -0
- package/dist/types/core/index.d.ts.map +1 -0
- package/dist/types/core/util/error.d.ts +25 -0
- package/dist/types/core/util/error.d.ts.map +1 -0
- package/dist/types/core/util/generate-requestid.d.ts +2 -0
- package/dist/types/core/util/generate-requestid.d.ts.map +1 -0
- package/dist/types/device/components/audio/audio.d.ts +2 -0
- package/dist/types/device/components/audio/audio.d.ts.map +1 -0
- package/dist/types/device/components/canvas/canvas.d.ts +3 -0
- package/dist/types/device/components/canvas/canvas.d.ts.map +1 -0
- package/dist/types/device/components/extra/extra.d.ts +19 -0
- package/dist/types/device/components/extra/extra.d.ts.map +1 -0
- package/dist/types/device/components/fonts/fonts.d.ts +3 -0
- package/dist/types/device/components/fonts/fonts.d.ts.map +1 -0
- package/dist/types/device/components/hardware/hardware.d.ts +2 -0
- package/dist/types/device/components/hardware/hardware.d.ts.map +1 -0
- package/dist/types/device/components/index.d.ts +15 -0
- package/dist/types/device/components/index.d.ts.map +1 -0
- package/dist/types/device/components/locales/locales.d.ts +2 -0
- package/dist/types/device/components/locales/locales.d.ts.map +1 -0
- package/dist/types/device/components/math/math.d.ts +2 -0
- package/dist/types/device/components/math/math.d.ts.map +1 -0
- package/dist/types/device/components/permissions/permissions.d.ts +3 -0
- package/dist/types/device/components/permissions/permissions.d.ts.map +1 -0
- package/dist/types/device/components/plugins/plugins.d.ts +3 -0
- package/dist/types/device/components/plugins/plugins.d.ts.map +1 -0
- package/dist/types/device/components/screen/screen.d.ts +2 -0
- package/dist/types/device/components/screen/screen.d.ts.map +1 -0
- package/dist/types/device/components/screen/screenResolution.d.ts +16 -0
- package/dist/types/device/components/screen/screenResolution.d.ts.map +1 -0
- package/dist/types/device/components/system/browser.d.ts +22 -0
- package/dist/types/device/components/system/browser.d.ts.map +1 -0
- package/dist/types/device/components/system/emoji.d.ts +2 -0
- package/dist/types/device/components/system/emoji.d.ts.map +1 -0
- package/dist/types/device/components/system/system.d.ts +2 -0
- package/dist/types/device/components/system/system.d.ts.map +1 -0
- package/dist/types/device/components/webgl/imageHash.d.ts +3 -0
- package/dist/types/device/components/webgl/imageHash.d.ts.map +1 -0
- package/dist/types/device/components/webgl/webgl.d.ts +54 -0
- package/dist/types/device/components/webgl/webgl.d.ts.map +1 -0
- package/dist/types/device/factory.d.ts +26 -0
- package/dist/types/device/factory.d.ts.map +1 -0
- package/dist/types/device/index.d.ts +61 -0
- package/dist/types/device/index.d.ts.map +1 -0
- package/dist/types/device/modules/bot.d.ts +9 -0
- package/dist/types/device/modules/bot.d.ts.map +1 -0
- package/dist/types/device/modules/browserDetails.d.ts +6 -0
- package/dist/types/device/modules/browserDetails.d.ts.map +1 -0
- package/dist/types/device/modules/device/analyze-data.d.ts +7 -0
- package/dist/types/device/modules/device/analyze-data.d.ts.map +1 -0
- package/dist/types/device/modules/device/gather-data.d.ts +6 -0
- package/dist/types/device/modules/device/gather-data.d.ts.map +1 -0
- package/dist/types/device/modules/device/helpers.d.ts +9 -0
- package/dist/types/device/modules/device/helpers.d.ts.map +1 -0
- package/dist/types/device/modules/device/index.d.ts +3 -0
- package/dist/types/device/modules/device/index.d.ts.map +1 -0
- package/dist/types/device/modules/fp.d.ts +14 -0
- package/dist/types/device/modules/fp.d.ts.map +1 -0
- package/dist/types/device/modules/incognito.d.ts +7 -0
- package/dist/types/device/modules/incognito.d.ts.map +1 -0
- package/dist/types/device/modules/options.d.ts +12 -0
- package/dist/types/device/modules/options.d.ts.map +1 -0
- package/dist/types/device/modules/os.d.ts +3 -0
- package/dist/types/device/modules/os.d.ts.map +1 -0
- package/dist/types/device/modules/tor.d.ts +4 -0
- package/dist/types/device/modules/tor.d.ts.map +1 -0
- package/dist/types/device/utils/async.d.ts +33 -0
- package/dist/types/device/utils/async.d.ts.map +1 -0
- package/dist/types/device/utils/browser_.d.ts +103 -0
- package/dist/types/device/utils/browser_.d.ts.map +1 -0
- package/dist/types/device/utils/commonPixels.d.ts +2 -0
- package/dist/types/device/utils/commonPixels.d.ts.map +1 -0
- package/dist/types/device/utils/data.d.ts +33 -0
- package/dist/types/device/utils/data.d.ts.map +1 -0
- package/dist/types/device/utils/dom.d.ts +26 -0
- package/dist/types/device/utils/dom.d.ts.map +1 -0
- package/dist/types/device/utils/ephemeralIFrame.d.ts +5 -0
- package/dist/types/device/utils/ephemeralIFrame.d.ts.map +1 -0
- package/dist/types/device/utils/getMostFrequent.d.ts +6 -0
- package/dist/types/device/utils/getMostFrequent.d.ts.map +1 -0
- package/dist/types/device/utils/hash.d.ts +6 -0
- package/dist/types/device/utils/hash.d.ts.map +1 -0
- package/dist/types/device/utils/misc.d.ts +7 -0
- package/dist/types/device/utils/misc.d.ts.map +1 -0
- package/dist/types/device/utils/raceAll.d.ts +9 -0
- package/dist/types/device/utils/raceAll.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +52 -0
- package/src/behaviour/index.ts +279 -0
- package/src/client/index.ts +132 -0
- package/src/core/constants.ts +4 -0
- package/src/core/dto/bot-detector.dto.ts +32 -0
- package/src/core/dto/device-detector.dto.ts +67 -0
- package/src/core/dto/fingerprint.dto.ts +38 -0
- package/src/core/dto/ginger.dto.ts +89 -0
- package/src/core/dto/incognito-detector.dto.ts +2 -0
- package/src/core/dto/index.ts +18 -0
- package/src/core/dto/metrics.dto.ts +20 -0
- package/src/core/dto/os-detector.dto.ts +5 -0
- package/src/core/dto/tor-detector.dto.ts +4 -0
- package/src/core/helpers.ts +33 -0
- package/src/core/http/httpClient.ts +52 -0
- package/src/core/http/request.ts +32 -0
- package/src/core/index.ts +5 -0
- package/src/core/util/error.ts +40 -0
- package/src/core/util/generate-requestid.ts +63 -0
- package/src/device/components/audio/audio.ts +58 -0
- package/src/device/components/canvas/canvas.ts +88 -0
- package/src/device/components/extra/extra.ts +581 -0
- package/src/device/components/fonts/fonts.ts +143 -0
- package/src/device/components/hardware/hardware.ts +66 -0
- package/src/device/components/index.ts +14 -0
- package/src/device/components/locales/locales.ts +21 -0
- package/src/device/components/math/math.ts +39 -0
- package/src/device/components/permissions/permissions.ts +60 -0
- package/src/device/components/plugins/plugins.ts +22 -0
- package/src/device/components/screen/screen.ts +13 -0
- package/src/device/components/screen/screenResolution.ts +45 -0
- package/src/device/components/system/browser.ts +838 -0
- package/src/device/components/system/emoji.ts +134 -0
- package/src/device/components/system/system.ts +76 -0
- package/src/device/components/webgl/imageHash.ts +144 -0
- package/src/device/components/webgl/webgl.ts +302 -0
- package/src/device/factory.ts +54 -0
- package/src/device/index.ts +60 -0
- package/src/device/modules/bot.ts +25 -0
- package/src/device/modules/browserDetails.ts +11 -0
- package/src/device/modules/device/analyze-data.ts +150 -0
- package/src/device/modules/device/gather-data.ts +92 -0
- package/src/device/modules/device/helpers.ts +123 -0
- package/src/device/modules/device/index.ts +64 -0
- package/src/device/modules/fp.ts +138 -0
- package/src/device/modules/incognito.ts +253 -0
- package/src/device/modules/options.ts +17 -0
- package/src/device/modules/os.ts +15 -0
- package/src/device/modules/tor.ts +41 -0
- package/src/device/utils/async.ts +106 -0
- package/src/device/utils/browser_.ts +347 -0
- package/src/device/utils/commonPixels.ts +38 -0
- package/src/device/utils/data.ts +161 -0
- package/src/device/utils/dom.ts +148 -0
- package/src/device/utils/ephemeralIFrame.ts +35 -0
- package/src/device/utils/getMostFrequent.ts +39 -0
- package/src/device/utils/hash.ts +202 -0
- package/src/device/utils/misc.ts +18 -0
- package/src/device/utils/raceAll.ts +19 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { countTruthy } from './data'
|
|
2
|
+
import { isFunctionNative } from './misc'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Functions to help with features that vary through browsers
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Checks whether the browser is based on Trident (the Internet Explorer engine) without using user-agent.
|
|
10
|
+
*
|
|
11
|
+
* Warning for package users:
|
|
12
|
+
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
|
|
13
|
+
*/
|
|
14
|
+
export function isTrident(): boolean {
|
|
15
|
+
const w = window
|
|
16
|
+
const n = navigator
|
|
17
|
+
|
|
18
|
+
// The properties are checked to be in IE 10, IE 11 and not to be in other browsers in October 2020
|
|
19
|
+
return (
|
|
20
|
+
countTruthy([
|
|
21
|
+
'MSCSSMatrix' in w,
|
|
22
|
+
'msSetImmediate' in w,
|
|
23
|
+
'msIndexedDB' in w,
|
|
24
|
+
'msMaxTouchPoints' in n,
|
|
25
|
+
'msPointerEnabled' in n,
|
|
26
|
+
]) >= 4
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Checks whether the browser is based on EdgeHTML (the pre-Chromium Edge engine) without using user-agent.
|
|
32
|
+
*
|
|
33
|
+
* Warning for package users:
|
|
34
|
+
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
|
|
35
|
+
*/
|
|
36
|
+
export function isEdgeHTML(): boolean {
|
|
37
|
+
// Based on research in October 2020
|
|
38
|
+
const w = window
|
|
39
|
+
const n = navigator
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
countTruthy(['msWriteProfilerMark' in w, 'MSStream' in w, 'msLaunchUri' in n, 'msSaveBlob' in n]) >= 3 &&
|
|
43
|
+
!isTrident()
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Checks whether the browser is based on Chromium without using user-agent.
|
|
49
|
+
*
|
|
50
|
+
* Warning for package users:
|
|
51
|
+
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
|
|
52
|
+
*/
|
|
53
|
+
export function isChromium(): boolean {
|
|
54
|
+
// Based on research in October 2020. Tested to detect Chromium 42-86.
|
|
55
|
+
const w = window
|
|
56
|
+
const n = navigator
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
countTruthy([
|
|
60
|
+
'webkitPersistentStorage' in n,
|
|
61
|
+
'webkitTemporaryStorage' in n,
|
|
62
|
+
(n.vendor || '').indexOf('Google') === 0,
|
|
63
|
+
'webkitResolveLocalFileSystemURL' in w,
|
|
64
|
+
'BatteryManager' in w,
|
|
65
|
+
'webkitMediaStream' in w,
|
|
66
|
+
'webkitSpeechGrammar' in w,
|
|
67
|
+
]) >= 5
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Checks whether the browser is based on mobile or desktop Safari without using user-agent.
|
|
73
|
+
* All iOS browsers use WebKit (the Safari engine).
|
|
74
|
+
*
|
|
75
|
+
* Warning for package users:
|
|
76
|
+
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
|
|
77
|
+
*/
|
|
78
|
+
export function isWebKit(): boolean {
|
|
79
|
+
// Based on research in August 2024
|
|
80
|
+
const w = window
|
|
81
|
+
const n = navigator
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
countTruthy([
|
|
85
|
+
'ApplePayError' in w,
|
|
86
|
+
'CSSPrimitiveValue' in w,
|
|
87
|
+
'Counter' in w,
|
|
88
|
+
n.vendor.indexOf('Apple') === 0,
|
|
89
|
+
'RGBColor' in w,
|
|
90
|
+
'WebKitMediaKeys' in w,
|
|
91
|
+
]) >= 4
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Checks whether this WebKit browser is a desktop browser.
|
|
97
|
+
* It doesn't check that the browser is based on WebKit, there is a separate function for this.
|
|
98
|
+
*
|
|
99
|
+
* Warning for package users:
|
|
100
|
+
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
|
|
101
|
+
*/
|
|
102
|
+
export function isDesktopWebKit(): boolean {
|
|
103
|
+
// Checked in Safari and DuckDuckGo
|
|
104
|
+
|
|
105
|
+
const w = window
|
|
106
|
+
const { HTMLElement, Document } = w
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
countTruthy([
|
|
110
|
+
'safari' in w, // Always false in Karma and BrowserStack Automate
|
|
111
|
+
!('ongestureend' in w),
|
|
112
|
+
!('TouchEvent' in w),
|
|
113
|
+
!('orientation' in w),
|
|
114
|
+
HTMLElement && !('autocapitalize' in HTMLElement.prototype),
|
|
115
|
+
Document && 'pointerLockElement' in Document.prototype,
|
|
116
|
+
]) >= 4
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Checks whether this WebKit browser is Safari.
|
|
122
|
+
* It doesn't check that the browser is based on WebKit, there is a separate function for this.
|
|
123
|
+
*
|
|
124
|
+
* Warning! The function works properly only for Safari version 15.4 and newer.
|
|
125
|
+
*/
|
|
126
|
+
export function isSafariWebKit(): boolean {
|
|
127
|
+
// Checked in Safari, Chrome, Firefox, Yandex, UC Browser, Opera, Edge and DuckDuckGo.
|
|
128
|
+
// iOS Safari and Chrome were checked on iOS 11-18. DuckDuckGo was checked on iOS 17-18 and macOS 14-15.
|
|
129
|
+
// Desktop Safari versions 12-18 were checked.
|
|
130
|
+
// The other browsers were checked on iOS 17 and 18; there was no chance to check them on the other OS versions.
|
|
131
|
+
|
|
132
|
+
const w = window
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
// Filters-out Chrome, Yandex, DuckDuckGo (macOS and iOS), Edge
|
|
136
|
+
isFunctionNative(w.print) &&
|
|
137
|
+
// Doesn't work in Safari < 15.4
|
|
138
|
+
String((w as unknown as Record<string, unknown>).browser) === '[object WebPageNamespace]'
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Checks whether the browser is based on Gecko (Firefox engine) without using user-agent.
|
|
144
|
+
*
|
|
145
|
+
* Warning for package users:
|
|
146
|
+
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
|
|
147
|
+
*/
|
|
148
|
+
export function isGecko(): boolean {
|
|
149
|
+
const w = window
|
|
150
|
+
|
|
151
|
+
// Based on research in September 2020
|
|
152
|
+
return (
|
|
153
|
+
countTruthy([
|
|
154
|
+
'buildID' in navigator,
|
|
155
|
+
'MozAppearance' in (document.documentElement?.style ?? {}),
|
|
156
|
+
'onmozfullscreenchange' in w,
|
|
157
|
+
'mozInnerScreenX' in w,
|
|
158
|
+
'CSSMozDocumentRule' in w,
|
|
159
|
+
'CanvasCaptureMediaStream' in w,
|
|
160
|
+
]) >= 4
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Checks whether the browser is based on Chromium version ≥86 without using user-agent.
|
|
166
|
+
* It doesn't check that the browser is based on Chromium, there is a separate function for this.
|
|
167
|
+
*/
|
|
168
|
+
export function isChromium86OrNewer(): boolean {
|
|
169
|
+
// Checked in Chrome 85 vs Chrome 86 both on desktop and Android. Checked in macOS Chrome 128, Android Chrome 127.
|
|
170
|
+
const w = window
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
countTruthy([
|
|
174
|
+
!('MediaSettingsRange' in w),
|
|
175
|
+
'RTCEncodedAudioFrame' in w,
|
|
176
|
+
'' + w.Intl === '[object Intl]',
|
|
177
|
+
'' + w.Reflect === '[object Reflect]',
|
|
178
|
+
]) >= 3
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Checks whether the browser is based on Chromium version ≥122 without using user-agent.
|
|
184
|
+
* It doesn't check that the browser is based on Chromium, there is a separate function for this.
|
|
185
|
+
*/
|
|
186
|
+
export function isChromium122OrNewer(): boolean {
|
|
187
|
+
// Checked in Chrome 121 vs Chrome 122 and 129 both on desktop and Android
|
|
188
|
+
const w: any = window
|
|
189
|
+
const { URLPattern } = w
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
countTruthy([
|
|
193
|
+
'union' in Set.prototype,
|
|
194
|
+
'Iterator' in w,
|
|
195
|
+
URLPattern && 'hasRegExpGroups' in URLPattern.prototype,
|
|
196
|
+
'RGB8' in WebGLRenderingContext.prototype,
|
|
197
|
+
]) >= 3
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Checks whether the browser is based on WebKit version ≥606 (Safari ≥12) without using user-agent.
|
|
203
|
+
* It doesn't check that the browser is based on WebKit, there is a separate function for this.
|
|
204
|
+
*
|
|
205
|
+
* @see https://en.wikipedia.org/wiki/Safari_version_history#Release_history Safari-WebKit versions map
|
|
206
|
+
*/
|
|
207
|
+
export function isWebKit606OrNewer(): boolean {
|
|
208
|
+
// Checked in Safari 9–18
|
|
209
|
+
const w = window
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
countTruthy([
|
|
213
|
+
'DOMRectList' in w,
|
|
214
|
+
'RTCPeerConnectionIceEvent' in w,
|
|
215
|
+
'SVGGeometryElement' in w,
|
|
216
|
+
'ontransitioncancel' in w,
|
|
217
|
+
]) >= 3
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Checks whether the browser is based on WebKit version ≥616 (Safari ≥17) without using user-agent.
|
|
223
|
+
* It doesn't check that the browser is based on WebKit, there is a separate function for this.
|
|
224
|
+
*
|
|
225
|
+
* @see https://developer.apple.com/documentation/safari-release-notes/safari-17-release-notes Safari 17 release notes
|
|
226
|
+
* @see https://tauri.app/v1/references/webview-versions/#webkit-versions-in-safari Safari-WebKit versions map
|
|
227
|
+
*/
|
|
228
|
+
export function isWebKit616OrNewer(): boolean {
|
|
229
|
+
const w = window
|
|
230
|
+
const n = navigator
|
|
231
|
+
const { CSS, HTMLButtonElement } = w
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
countTruthy([
|
|
235
|
+
!('getStorageUpdates' in n),
|
|
236
|
+
HTMLButtonElement && 'popover' in HTMLButtonElement.prototype,
|
|
237
|
+
'CSSCounterStyleRule' in w,
|
|
238
|
+
CSS.supports('font-size-adjust: ex-height 0.5'),
|
|
239
|
+
CSS.supports('text-transform: full-width'),
|
|
240
|
+
]) >= 4
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Checks whether the device is an iPad.
|
|
246
|
+
* It doesn't check that the engine is WebKit and that the WebKit isn't desktop.
|
|
247
|
+
*/
|
|
248
|
+
export function isIPad(): boolean {
|
|
249
|
+
// Checked on:
|
|
250
|
+
// Safari on iPadOS (both mobile and desktop modes): 8, 11-18
|
|
251
|
+
// Chrome on iPadOS (both mobile and desktop modes): 11-18
|
|
252
|
+
// Safari on iOS (both mobile and desktop modes): 9-18
|
|
253
|
+
// Chrome on iOS (both mobile and desktop modes): 9-18
|
|
254
|
+
|
|
255
|
+
// Before iOS 13. Safari tampers the value in "request desktop site" mode since iOS 13.
|
|
256
|
+
if (navigator.platform === 'iPad') {
|
|
257
|
+
return true
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const s = screen
|
|
261
|
+
const screenRatio = s.width / s.height
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
countTruthy([
|
|
265
|
+
// Since iOS 13. Doesn't work in Chrome on iPadOS <15, but works in desktop mode.
|
|
266
|
+
'MediaSource' in window,
|
|
267
|
+
// Since iOS 12. Doesn't work in Chrome on iPadOS.
|
|
268
|
+
!!(Element as any).prototype.webkitRequestFullscreen,
|
|
269
|
+
// iPhone 4S that runs iOS 9 matches this, but it is not supported
|
|
270
|
+
// Doesn't work in incognito mode of Safari ≥17 with split screen because of tracking prevention
|
|
271
|
+
screenRatio > 0.65 && screenRatio < 1.53,
|
|
272
|
+
]) >= 2
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Warning for package users:
|
|
278
|
+
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
|
|
279
|
+
*/
|
|
280
|
+
export function getFullscreenElement(): Element | null {
|
|
281
|
+
const d: any = document
|
|
282
|
+
return d.fullscreenElement || d.msFullscreenElement || d.mozFullScreenElement || d.webkitFullscreenElement || null
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function exitFullscreen(): Promise<void> {
|
|
286
|
+
const d: any = document
|
|
287
|
+
// `call` is required because the function throws an error without a proper "this" context
|
|
288
|
+
return (d.exitFullscreen || d.msExitFullscreen || d.mozCancelFullScreen || d.webkitExitFullscreen).call(d)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Checks whether the device runs on Android without using user-agent.
|
|
293
|
+
*
|
|
294
|
+
* Warning for package users:
|
|
295
|
+
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
|
|
296
|
+
*/
|
|
297
|
+
export function isAndroid(): boolean {
|
|
298
|
+
const isItChromium = isChromium()
|
|
299
|
+
const isItGecko = isGecko()
|
|
300
|
+
const w = window
|
|
301
|
+
const n = navigator
|
|
302
|
+
// Chrome removes all words "Android" from `navigator` when desktop version is requested
|
|
303
|
+
// Firefox keeps "Android" in `navigator.appVersion` when desktop version is requested
|
|
304
|
+
if (isItChromium) {
|
|
305
|
+
return (
|
|
306
|
+
countTruthy([
|
|
307
|
+
!('SharedWorker' in w),
|
|
308
|
+
// `typechange` is deprecated, but it's still present on Android (tested on Chrome Mobile 117)
|
|
309
|
+
// Removal proposal https://bugs.chromium.org/p/chromium/issues/detail?id=699892
|
|
310
|
+
// Note: this expression returns true on ChromeOS, so additional detectors are required to avoid false-positives
|
|
311
|
+
// n[c] && 'ontypechange' in n[c],
|
|
312
|
+
!('sinkId' in new Audio()),
|
|
313
|
+
]) >= 2
|
|
314
|
+
)
|
|
315
|
+
} else if (isItGecko) {
|
|
316
|
+
return countTruthy(['onorientationchange' in w, 'orientation' in w, /android/i.test(n.appVersion)]) >= 2
|
|
317
|
+
} else {
|
|
318
|
+
// Only 2 browser engines are presented on Android.
|
|
319
|
+
// Actually, there is also Android 4.1 browser, but it's not worth detecting it at the moment.
|
|
320
|
+
return false
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Checks whether the browser is Samsung Internet without using user-agent.
|
|
326
|
+
* It doesn't check that the browser is based on Chromium, please use `isChromium` before using this function.
|
|
327
|
+
*
|
|
328
|
+
* Warning for package users:
|
|
329
|
+
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
|
|
330
|
+
*/
|
|
331
|
+
export function isSamsungInternet(): boolean {
|
|
332
|
+
// Checked in Samsung Internet 21, 25 and 27
|
|
333
|
+
const n = navigator
|
|
334
|
+
const w = window
|
|
335
|
+
const audioPrototype = Audio.prototype
|
|
336
|
+
const { visualViewport } = w
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
countTruthy([
|
|
340
|
+
'srLatency' in audioPrototype,
|
|
341
|
+
'srChannelCount' in audioPrototype,
|
|
342
|
+
'devicePosture' in n, // Not available in HTTP
|
|
343
|
+
visualViewport && 'segments' in visualViewport,
|
|
344
|
+
'getTextInformation' in Image.prototype, // Not available in Samsung Internet 21
|
|
345
|
+
]) >= 3
|
|
346
|
+
)
|
|
347
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function getCommonPixels(images: ImageData[], width: number, height: number ): ImageData {
|
|
2
|
+
let finalData: number[] = [];
|
|
3
|
+
for (let i = 0; i < images[0].data.length; i++) {
|
|
4
|
+
let indice: number[] = [];
|
|
5
|
+
for (let u = 0; u < images.length; u++) {
|
|
6
|
+
indice.push(images[u].data[i]);
|
|
7
|
+
}
|
|
8
|
+
finalData.push(getMostFrequent(indice));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const pixelData = finalData;
|
|
12
|
+
const pixelArray = new Uint8ClampedArray(pixelData);
|
|
13
|
+
return new ImageData(pixelArray, width, height);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getMostFrequent(arr: number[]): number {
|
|
17
|
+
if (arr.length === 0) {
|
|
18
|
+
return 0; // Handle empty array case
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const frequencyMap: { [key: number]: number } = {};
|
|
22
|
+
|
|
23
|
+
// Count occurrences of each number in the array
|
|
24
|
+
for (const num of arr) {
|
|
25
|
+
frequencyMap[num] = (frequencyMap[num] || 0) + 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let mostFrequent: number = arr[0];
|
|
29
|
+
|
|
30
|
+
// Find the number with the highest frequency
|
|
31
|
+
for (const num in frequencyMap) {
|
|
32
|
+
if (frequencyMap[num] > frequencyMap[mostFrequent]) {
|
|
33
|
+
mostFrequent = parseInt(num, 10);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return mostFrequent;
|
|
38
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file contains functions to work with pure data only (no browser features, DOM, side effects, etc).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Does the same as Array.prototype.includes but has better typing
|
|
7
|
+
*/
|
|
8
|
+
export function includes<THaystack>(haystack: ArrayLike<THaystack>, needle: unknown): needle is THaystack {
|
|
9
|
+
for (let i = 0, l = haystack.length; i < l; ++i) {
|
|
10
|
+
if (haystack[i] === needle) {
|
|
11
|
+
return true
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Like `!includes()` but with proper typing
|
|
19
|
+
*/
|
|
20
|
+
export function excludes<THaystack, TNeedle>(
|
|
21
|
+
haystack: ArrayLike<THaystack>,
|
|
22
|
+
needle: TNeedle,
|
|
23
|
+
): needle is Exclude<TNeedle, THaystack> {
|
|
24
|
+
return !includes(haystack, needle)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Be careful, NaN can return
|
|
29
|
+
*/
|
|
30
|
+
export function toInt(value: unknown): number {
|
|
31
|
+
return parseInt(value as string)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Be careful, NaN can return
|
|
36
|
+
*/
|
|
37
|
+
export function toFloat(value: unknown): number {
|
|
38
|
+
return parseFloat(value as string)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function replaceNaN<T, U>(value: T, replacement: U): T | U {
|
|
42
|
+
return typeof value === 'number' && isNaN(value) ? replacement : value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function countTruthy(values: unknown[]): number {
|
|
46
|
+
return values.reduce<number>((sum, value) => sum + (value ? 1 : 0), 0)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function round(value: number, base = 1): number {
|
|
50
|
+
if (Math.abs(base) >= 1) {
|
|
51
|
+
return Math.round(value / base) * base
|
|
52
|
+
} else {
|
|
53
|
+
// Sometimes when a number is multiplied by a small number, precision is lost,
|
|
54
|
+
// for example 1234 * 0.0001 === 0.12340000000000001, and it's more precise divide: 1234 / (1 / 0.0001) === 0.1234.
|
|
55
|
+
const counterBase = 1 / base
|
|
56
|
+
return Math.round(value * counterBase) / counterBase
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parses a CSS selector into tag name with HTML attributes.
|
|
62
|
+
* Only single element selector are supported (without operators like space, +, >, etc).
|
|
63
|
+
*
|
|
64
|
+
* Multiple values can be returned for each attribute. You decide how to handle them.
|
|
65
|
+
*/
|
|
66
|
+
export function parseSimpleCssSelector(
|
|
67
|
+
selector: string,
|
|
68
|
+
): [tag: string | undefined, attributes: Record<string, string[]>] {
|
|
69
|
+
const errorMessage = `Unexpected syntax '${selector}'`
|
|
70
|
+
const tagMatch = /^\s*([a-z-]*)(.*)$/i.exec(selector) as RegExpExecArray
|
|
71
|
+
const tag = tagMatch[1] || undefined
|
|
72
|
+
const attributes: Record<string, string[]> = {}
|
|
73
|
+
const partsRegex = /([.:#][\w-]+|\[.+?\])/gi
|
|
74
|
+
|
|
75
|
+
const addAttribute = (name: string, value: string) => {
|
|
76
|
+
attributes[name] = attributes[name] || []
|
|
77
|
+
attributes[name].push(value)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (;;) {
|
|
81
|
+
const match = partsRegex.exec(tagMatch[2])
|
|
82
|
+
if (!match) {
|
|
83
|
+
break
|
|
84
|
+
}
|
|
85
|
+
const part = match[0]
|
|
86
|
+
switch (part[0]) {
|
|
87
|
+
case '.':
|
|
88
|
+
addAttribute('class', part.slice(1))
|
|
89
|
+
break
|
|
90
|
+
case '#':
|
|
91
|
+
addAttribute('id', part.slice(1))
|
|
92
|
+
break
|
|
93
|
+
case '[': {
|
|
94
|
+
const attributeMatch = /^\[([\w-]+)([~|^$*]?=("(.*?)"|([\w-]+)))?(\s+[is])?\]$/.exec(part)
|
|
95
|
+
if (attributeMatch) {
|
|
96
|
+
addAttribute(attributeMatch[1], attributeMatch[4] ?? attributeMatch[5] ?? '')
|
|
97
|
+
} else {
|
|
98
|
+
throw new Error(errorMessage)
|
|
99
|
+
}
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
default:
|
|
103
|
+
throw new Error(errorMessage)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return [tag, attributes]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function areSetsEqual(set1: Set<unknown>, set2: Set<unknown>): boolean {
|
|
111
|
+
if (set1 === set2) {
|
|
112
|
+
return true
|
|
113
|
+
}
|
|
114
|
+
if (set1.size !== set2.size) {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (let iter = set1.values(), step = iter.next(); !step.done; step = iter.next()) {
|
|
119
|
+
if (!set2.has(step.value)) {
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function maxInIterator<T>(iterator: Iterator<T>, getItemScore: (item: T) => number): T | undefined {
|
|
127
|
+
let maxItem: T | undefined
|
|
128
|
+
let maxItemScore: number | undefined
|
|
129
|
+
|
|
130
|
+
for (let step = iterator.next(); !step.done; step = iterator.next()) {
|
|
131
|
+
const item = step.value
|
|
132
|
+
const score = getItemScore(item)
|
|
133
|
+
if (maxItemScore === undefined || score > maxItemScore) {
|
|
134
|
+
maxItem = item
|
|
135
|
+
maxItemScore = score
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return maxItem
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Converts a string to UTF8 bytes
|
|
144
|
+
*/
|
|
145
|
+
export function getUTF8Bytes(input: string): Uint8Array {
|
|
146
|
+
// Benchmark: https://jsbench.me/b6klaaxgwq/1
|
|
147
|
+
// If you want to just count bytes, see solutions at https://jsbench.me/ehklab415e/1
|
|
148
|
+
const result = new Uint8Array(input.length)
|
|
149
|
+
for (let i = 0; i < input.length; i++) {
|
|
150
|
+
// `charCode` is faster than encoding, so we prefer that when it's possible
|
|
151
|
+
const charCode = input.charCodeAt(i)
|
|
152
|
+
|
|
153
|
+
// In case of non-ASCII symbols we use proper encoding
|
|
154
|
+
if (charCode > 127) {
|
|
155
|
+
return new TextEncoder().encode(input)
|
|
156
|
+
}
|
|
157
|
+
result[i] = charCode
|
|
158
|
+
}
|
|
159
|
+
return result
|
|
160
|
+
}
|
|
161
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { MaybePromise, wait } from './async'
|
|
2
|
+
import { parseSimpleCssSelector } from './data'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates and keeps an invisible iframe while the given function runs.
|
|
6
|
+
* The given function is called when the iframe is loaded and has a body.
|
|
7
|
+
* The iframe allows to measure DOM sizes inside itself.
|
|
8
|
+
*
|
|
9
|
+
* Notice: passing an initial HTML code doesn't work in IE.
|
|
10
|
+
*
|
|
11
|
+
* Warning for package users:
|
|
12
|
+
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
|
|
13
|
+
*/
|
|
14
|
+
export async function withIframe<T>(
|
|
15
|
+
action: (iframe: HTMLIFrameElement, iWindow: typeof window) => MaybePromise<T>,
|
|
16
|
+
initialHtml?: string,
|
|
17
|
+
domPollInterval = 50,
|
|
18
|
+
): Promise<T> {
|
|
19
|
+
const d = document
|
|
20
|
+
|
|
21
|
+
// document.body can be null while the page is loading
|
|
22
|
+
while (!d.body) {
|
|
23
|
+
await wait(domPollInterval)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const iframe = d.createElement('iframe')
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await new Promise<void>((_resolve, _reject) => {
|
|
30
|
+
let isComplete = false
|
|
31
|
+
const resolve = () => {
|
|
32
|
+
isComplete = true
|
|
33
|
+
_resolve()
|
|
34
|
+
}
|
|
35
|
+
const reject = (error: unknown) => {
|
|
36
|
+
isComplete = true
|
|
37
|
+
_reject(error)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
iframe.onload = resolve
|
|
41
|
+
iframe.onerror = reject
|
|
42
|
+
const { style } = iframe
|
|
43
|
+
style.setProperty('display', 'block', 'important') // Required for browsers to calculate the layout
|
|
44
|
+
style.position = 'absolute'
|
|
45
|
+
style.top = '0'
|
|
46
|
+
style.left = '0'
|
|
47
|
+
style.visibility = 'hidden'
|
|
48
|
+
if (initialHtml && 'srcdoc' in iframe) {
|
|
49
|
+
iframe.srcdoc = initialHtml
|
|
50
|
+
} else {
|
|
51
|
+
iframe.src = 'about:blank'
|
|
52
|
+
}
|
|
53
|
+
d.body.appendChild(iframe)
|
|
54
|
+
|
|
55
|
+
// WebKit in WeChat doesn't fire the iframe's `onload` for some reason.
|
|
56
|
+
// This code checks for the loading state manually.
|
|
57
|
+
// See https://github.com/fingerprintjs/fingerprintjs/issues/645
|
|
58
|
+
const checkReadyState = () => {
|
|
59
|
+
// The ready state may never become 'complete' in Firefox despite the 'load' event being fired.
|
|
60
|
+
// So an infinite setTimeout loop can happen without this check.
|
|
61
|
+
// See https://github.com/fingerprintjs/fingerprintjs/pull/716#issuecomment-986898796
|
|
62
|
+
if (isComplete) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Make sure iframe.contentWindow and iframe.contentWindow.document are both loaded
|
|
67
|
+
// The contentWindow.document can miss in JSDOM (https://github.com/jsdom/jsdom).
|
|
68
|
+
if (iframe.contentWindow?.document?.readyState === 'complete') {
|
|
69
|
+
resolve()
|
|
70
|
+
} else {
|
|
71
|
+
setTimeout(checkReadyState, 10)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
checkReadyState()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
while (!iframe.contentWindow?.document?.body) {
|
|
78
|
+
await wait(domPollInterval)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return await action(iframe, iframe.contentWindow as typeof window)
|
|
82
|
+
} finally {
|
|
83
|
+
iframe.parentNode?.removeChild(iframe)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Creates a DOM element that matches the given selector.
|
|
89
|
+
* Only single element selector are supported (without operators like space, +, >, etc).
|
|
90
|
+
*/
|
|
91
|
+
export function selectorToElement(selector: string): HTMLElement {
|
|
92
|
+
const [tag, attributes] = parseSimpleCssSelector(selector)
|
|
93
|
+
const element = document.createElement(tag ?? 'div')
|
|
94
|
+
for (const name of Object.keys(attributes)) {
|
|
95
|
+
const value = attributes[name].join(' ')
|
|
96
|
+
// Changing the `style` attribute can cause a CSP error, therefore we change the `style.cssText` property.
|
|
97
|
+
// https://github.com/fingerprintjs/fingerprintjs/issues/733
|
|
98
|
+
if (name === 'style') {
|
|
99
|
+
addStyleString(element.style, value)
|
|
100
|
+
} else {
|
|
101
|
+
element.setAttribute(name, value)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return element
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Adds CSS styles from a string in such a way that doesn't trigger a CSP warning (unsafe-inline or unsafe-eval)
|
|
109
|
+
*/
|
|
110
|
+
export function addStyleString(style: CSSStyleDeclaration, source: string): void {
|
|
111
|
+
// We don't use `style.cssText` because browsers must block it when no `unsafe-eval` CSP is presented: https://csplite.com/csp145/#w3c_note
|
|
112
|
+
// Even though the browsers ignore this standard, we don't use `cssText` just in case.
|
|
113
|
+
for (const property of source.split(';')) {
|
|
114
|
+
const match = /^\s*([\w-]+)\s*:\s*(.+?)(\s*!([\w-]+))?\s*$/.exec(property)
|
|
115
|
+
if (match) {
|
|
116
|
+
const [, name, value, , priority] = match
|
|
117
|
+
style.setProperty(name, value, priority || '') // The last argument can't be undefined in IE11
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns true if the code runs in an iframe, and any parent page's origin doesn't match the current origin
|
|
124
|
+
*/
|
|
125
|
+
export function isAnyParentCrossOrigin(): boolean {
|
|
126
|
+
let currentWindow: Window = window
|
|
127
|
+
|
|
128
|
+
for (;;) {
|
|
129
|
+
const parentWindow = currentWindow.parent
|
|
130
|
+
if (!parentWindow || parentWindow === currentWindow) {
|
|
131
|
+
return false // The top page is reached
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
if (parentWindow.location.origin !== currentWindow.location.origin) {
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// The error is thrown when `origin` is accessed on `parentWindow.location` when the parent is cross-origin
|
|
140
|
+
if (error instanceof Error && error.name === 'SecurityError') {
|
|
141
|
+
return true
|
|
142
|
+
}
|
|
143
|
+
throw error
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
currentWindow = parentWindow
|
|
147
|
+
}
|
|
148
|
+
}
|