@readium/navigator 2.2.9 → 2.3.0
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/dist/index.js +2743 -1748
- package/dist/index.umd.cjs +79 -86
- package/package.json +2 -3
- package/src/Navigator.ts +76 -0
- package/src/epub/EpubNavigator.ts +93 -10
- package/src/epub/css/Properties.ts +8 -8
- package/src/epub/css/ReadiumCSS.ts +7 -5
- package/src/epub/frame/FrameManager.ts +38 -2
- package/src/epub/frame/FramePoolManager.ts +9 -2
- package/src/epub/fxl/FXLFrameManager.ts +31 -2
- package/src/epub/fxl/FXLFramePoolManager.ts +9 -3
- package/src/epub/preferences/EpubDefaults.ts +6 -6
- package/src/epub/preferences/EpubPreferences.ts +6 -6
- package/src/epub/preferences/EpubPreferencesEditor.ts +1 -3
- package/src/epub/preferences/EpubSettings.ts +5 -7
- package/src/helpers/lineLength.ts +4 -6
- package/src/injection/epubInjectables.ts +11 -3
- package/src/injection/webpubInjectables.ts +12 -2
- package/src/peripherals/KeyboardPeripherals.ts +53 -0
- package/src/protection/AutomationDetector.ts +66 -0
- package/src/protection/ContextMenuProtector.ts +46 -0
- package/src/protection/DevToolsDetector.ts +290 -0
- package/src/protection/IframeEmbeddingDetector.ts +73 -0
- package/src/protection/NavigatorProtector.ts +95 -0
- package/src/protection/PrintProtector.ts +58 -0
- package/src/protection/utils/WorkerConsole.ts +84 -0
- package/src/protection/utils/console.ts +16 -0
- package/src/protection/utils/match.ts +18 -0
- package/src/protection/utils/platform.ts +22 -0
- package/src/webpub/WebPubFrameManager.ts +38 -5
- package/src/webpub/WebPubFramePoolManager.ts +9 -2
- package/src/webpub/WebPubNavigator.ts +87 -7
- package/types/src/Navigator.d.ts +14 -0
- package/types/src/epub/EpubNavigator.d.ts +14 -2
- package/types/src/epub/css/Properties.d.ts +4 -0
- package/types/src/epub/frame/FrameManager.d.ts +5 -1
- package/types/src/epub/frame/FramePoolManager.d.ts +4 -1
- package/types/src/epub/fxl/FXLFrameManager.d.ts +5 -1
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +4 -2
- package/types/src/epub/preferences/EpubDefaults.d.ts +4 -0
- package/types/src/epub/preferences/EpubPreferences.d.ts +4 -0
- package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +2 -0
- package/types/src/epub/preferences/EpubSettings.d.ts +4 -0
- package/types/src/helpers/lineLength.d.ts +2 -3
- package/types/src/injection/epubInjectables.d.ts +2 -2
- package/types/src/injection/webpubInjectables.d.ts +2 -1
- package/types/src/peripherals/KeyboardPeripherals.d.ts +13 -0
- package/types/src/protection/AutomationDetector.d.ts +14 -0
- package/types/src/protection/ContextMenuProtector.d.ts +11 -0
- package/types/src/protection/DevToolsDetector.d.ts +75 -0
- package/types/src/protection/IframeEmbeddingDetector.d.ts +14 -0
- package/types/src/protection/NavigatorProtector.d.ts +12 -0
- package/types/src/protection/PrintProtector.d.ts +13 -0
- package/types/src/protection/utils/WorkerConsole.d.ts +15 -0
- package/types/src/protection/utils/console.d.ts +6 -0
- package/types/src/protection/utils/match.d.ts +8 -0
- package/types/src/protection/utils/platform.d.ts +4 -0
- package/types/src/webpub/WebPubFrameManager.d.ts +5 -1
- package/types/src/webpub/WebPubFramePoolManager.d.ts +4 -1
- package/types/src/webpub/WebPubNavigator.d.ts +13 -1
- package/dist/assets/AccessibleDfA-Bold.woff2 +0 -0
- package/dist/assets/AccessibleDfA-Italic.woff2 +0 -0
- package/dist/assets/AccessibleDfA-Regular.woff +0 -0
- package/dist/assets/AccessibleDfA-Regular.woff2 +0 -0
- package/dist/assets/iAWriterDuospace-Regular.ttf +0 -0
|
@@ -48,8 +48,8 @@ export interface IEpubPreferences {
|
|
|
48
48
|
scroll?: boolean | null,
|
|
49
49
|
scrollPaddingTop?: number | null,
|
|
50
50
|
scrollPaddingBottom?: number | null,
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
scrollPaddingLeft?: number | null,
|
|
52
|
+
scrollPaddingRight?: number | null,
|
|
53
53
|
selectionBackgroundColor?: string | null,
|
|
54
54
|
selectionTextColor?: string | null,
|
|
55
55
|
textAlign?: TextAlignment | null,
|
|
@@ -91,8 +91,8 @@ export class EpubPreferences implements ConfigurablePreferences {
|
|
|
91
91
|
scroll?: boolean | null;
|
|
92
92
|
scrollPaddingTop?: number | null;
|
|
93
93
|
scrollPaddingBottom?: number | null;
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
scrollPaddingLeft?: number | null;
|
|
95
|
+
scrollPaddingRight?: number | null;
|
|
96
96
|
selectionBackgroundColor?: string | null;
|
|
97
97
|
selectionTextColor?: string | null;
|
|
98
98
|
textAlign?: TextAlignment | null;
|
|
@@ -130,8 +130,8 @@ export class EpubPreferences implements ConfigurablePreferences {
|
|
|
130
130
|
this.scroll = ensureBoolean(preferences.scroll);
|
|
131
131
|
this.scrollPaddingTop = ensureNonNegative(preferences.scrollPaddingTop);
|
|
132
132
|
this.scrollPaddingBottom = ensureNonNegative(preferences.scrollPaddingBottom);
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
this.scrollPaddingLeft = ensureNonNegative(preferences.scrollPaddingLeft);
|
|
134
|
+
this.scrollPaddingRight = ensureNonNegative(preferences.scrollPaddingRight);
|
|
135
135
|
this.selectionBackgroundColor = ensureString(preferences.selectionBackgroundColor);
|
|
136
136
|
this.selectionTextColor = ensureString(preferences.selectionTextColor);
|
|
137
137
|
this.textAlign = ensureEnumValue<TextAlignment>(preferences.textAlign, TextAlignment);
|
|
@@ -364,7 +364,7 @@ export class EpubPreferencesEditor implements IPreferencesEditor {
|
|
|
364
364
|
return new Preference<number>({
|
|
365
365
|
initialValue: this.preferences.pageGutter,
|
|
366
366
|
effectiveValue: this.settings.pageGutter,
|
|
367
|
-
isEffective: this.layout !== Layout.fixed,
|
|
367
|
+
isEffective: this.layout !== Layout.fixed && !this.settings.scroll,
|
|
368
368
|
onChange: (newValue: number | null | undefined) => {
|
|
369
369
|
this.updatePreference("pageGutter", newValue || null);
|
|
370
370
|
}
|
|
@@ -430,7 +430,6 @@ export class EpubPreferencesEditor implements IPreferencesEditor {
|
|
|
430
430
|
});
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
-
/*
|
|
434
433
|
get scrollPaddingLeft(): Preference<number> {
|
|
435
434
|
return new Preference<number>({
|
|
436
435
|
initialValue: this.preferences.scrollPaddingLeft,
|
|
@@ -452,7 +451,6 @@ export class EpubPreferencesEditor implements IPreferencesEditor {
|
|
|
452
451
|
}
|
|
453
452
|
});
|
|
454
453
|
}
|
|
455
|
-
*/
|
|
456
454
|
|
|
457
455
|
get selectionBackgroundColor(): Preference<string> {
|
|
458
456
|
return new Preference<string>({
|
|
@@ -37,8 +37,8 @@ export interface IEpubSettings {
|
|
|
37
37
|
scroll?: boolean | null,
|
|
38
38
|
scrollPaddingTop?: number | null,
|
|
39
39
|
scrollPaddingBottom?: number | null,
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
scrollPaddingLeft?: number | null,
|
|
41
|
+
scrollPaddingRight?: number | null,
|
|
42
42
|
selectionBackgroundColor?: string | null,
|
|
43
43
|
selectionTextColor?: string | null,
|
|
44
44
|
textAlign?: TextAlignment | null,
|
|
@@ -81,8 +81,8 @@ export class EpubSettings implements ConfigurableSettings {
|
|
|
81
81
|
scroll: boolean | null;
|
|
82
82
|
scrollPaddingTop: number | null;
|
|
83
83
|
scrollPaddingBottom: number | null;
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
scrollPaddingLeft: number | null;
|
|
85
|
+
scrollPaddingRight: number | null;
|
|
86
86
|
selectionBackgroundColor: string | null;
|
|
87
87
|
selectionTextColor: string | null;
|
|
88
88
|
textAlign: TextAlignment | null;
|
|
@@ -205,8 +205,7 @@ export class EpubSettings implements ConfigurableSettings {
|
|
|
205
205
|
? preferences.scrollPaddingBottom
|
|
206
206
|
: defaults.scrollPaddingBottom !== undefined
|
|
207
207
|
? defaults.scrollPaddingBottom
|
|
208
|
-
: null;
|
|
209
|
-
/*
|
|
208
|
+
: null;
|
|
210
209
|
this.scrollPaddingLeft = preferences.scrollPaddingLeft !== undefined
|
|
211
210
|
? preferences.scrollPaddingLeft
|
|
212
211
|
: defaults.scrollPaddingLeft !== undefined
|
|
@@ -217,7 +216,6 @@ export class EpubSettings implements ConfigurableSettings {
|
|
|
217
216
|
: defaults.scrollPaddingRight !== undefined
|
|
218
217
|
? defaults.scrollPaddingRight
|
|
219
218
|
: null;
|
|
220
|
-
*/
|
|
221
219
|
this.selectionBackgroundColor = preferences.selectionBackgroundColor || defaults.selectionBackgroundColor || null;
|
|
222
220
|
this.selectionTextColor = preferences.selectionTextColor || defaults.selectionTextColor || null;
|
|
223
221
|
this.textAlign = preferences.textAlign || defaults.textAlign || null;
|
|
@@ -11,7 +11,7 @@ export interface ILineLengthsConfig {
|
|
|
11
11
|
maxChars?: number | null;
|
|
12
12
|
baseFontSize?: number | null;
|
|
13
13
|
sample?: string | null;
|
|
14
|
-
|
|
14
|
+
padding?: number | null;
|
|
15
15
|
fontFace?: string | ICustomFontFace | null;
|
|
16
16
|
letterSpacing?: number | null;
|
|
17
17
|
wordSpacing?: number | null;
|
|
@@ -51,13 +51,12 @@ export class LineLengths {
|
|
|
51
51
|
private _baseFontSize: number;
|
|
52
52
|
private _fontFace: string | ICustomFontFace;
|
|
53
53
|
private _sample: string | null;
|
|
54
|
-
private
|
|
54
|
+
private _padding: number;
|
|
55
55
|
private _letterSpacing: number;
|
|
56
56
|
private _wordSpacing: number;
|
|
57
57
|
private _isCJK: boolean;
|
|
58
58
|
private _getRelative: boolean;
|
|
59
59
|
|
|
60
|
-
private _padding: number;
|
|
61
60
|
private _minDivider: number | null;
|
|
62
61
|
private _maxMultiplier: number | null;
|
|
63
62
|
private _approximatedWordSpaces: number;
|
|
@@ -72,7 +71,7 @@ export class LineLengths {
|
|
|
72
71
|
this._baseFontSize = config.baseFontSize || DEFAULT_FONT_SIZE;
|
|
73
72
|
this._fontFace = config.fontFace || DEFAULT_FONT_FACE;
|
|
74
73
|
this._sample = config.sample || null;
|
|
75
|
-
this.
|
|
74
|
+
this._padding = config.padding ?? 0;
|
|
76
75
|
this._letterSpacing = config.letterSpacing
|
|
77
76
|
? Math.round(config.letterSpacing * this._baseFontSize)
|
|
78
77
|
: 0;
|
|
@@ -81,7 +80,6 @@ export class LineLengths {
|
|
|
81
80
|
: 0;
|
|
82
81
|
this._isCJK = config.isCJK || false;
|
|
83
82
|
this._getRelative = config.getRelative || false;
|
|
84
|
-
this._padding = this._pageGutter * 2;
|
|
85
83
|
this._minDivider = this._minChars && this._minChars < this._optimalChars
|
|
86
84
|
? this._optimalChars / this._minChars
|
|
87
85
|
: this._minChars === null
|
|
@@ -121,7 +119,7 @@ export class LineLengths {
|
|
|
121
119
|
if (props.letterSpacing) this._letterSpacing = props.letterSpacing;
|
|
122
120
|
if (props.wordSpacing) this._wordSpacing = props.wordSpacing;
|
|
123
121
|
if (props.isCJK != null) this._isCJK = props.isCJK;
|
|
124
|
-
if (props.
|
|
122
|
+
if (props.padding !== undefined) this._padding = props.padding ?? 0;
|
|
125
123
|
if (props.getRelative) this._getRelative = props.getRelative;
|
|
126
124
|
|
|
127
125
|
if (props.sample) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IInjectableRule, IInjectable } from "../injection/Injectable";
|
|
2
2
|
import { stripJS, stripCSS } from "../helpers/minify";
|
|
3
|
-
import { Metadata, Layout } from "@readium/shared";
|
|
3
|
+
import { Metadata, Layout, Link } from "@readium/shared";
|
|
4
4
|
|
|
5
5
|
import readiumCSSAfter from "@readium/css/css/dist/ReadiumCSS-after.css?raw";
|
|
6
6
|
import readiumCSSBefore from "@readium/css/css/dist/ReadiumCSS-before.css?raw";
|
|
@@ -13,9 +13,17 @@ import onloadProxyContent from "../dom/_readium_executionCleanup.js?raw";
|
|
|
13
13
|
/**
|
|
14
14
|
* Creates injectable rules for EPUB content documents
|
|
15
15
|
*/
|
|
16
|
-
export function createReadiumEpubRules(metadata: Metadata): IInjectableRule[] {
|
|
16
|
+
export function createReadiumEpubRules(metadata: Metadata, readingOrderItems: Link[]): IInjectableRule[] {
|
|
17
17
|
const isFixedLayout = metadata.effectiveLayout === Layout.fixed;
|
|
18
18
|
|
|
19
|
+
const htmlHrefs = readingOrderItems
|
|
20
|
+
.filter(item => item.mediaType.isHTML)
|
|
21
|
+
.map(item => item.href);
|
|
22
|
+
|
|
23
|
+
const resources = htmlHrefs.length > 0
|
|
24
|
+
? htmlHrefs
|
|
25
|
+
: [/\.xhtml$/, /\.html$/]; // fallback patterns
|
|
26
|
+
|
|
19
27
|
// Core injectables that should be prepended
|
|
20
28
|
const prependInjectables: IInjectable[] = [
|
|
21
29
|
// CSS Selector Generator - always injected
|
|
@@ -82,7 +90,7 @@ export function createReadiumEpubRules(metadata: Metadata): IInjectableRule[] {
|
|
|
82
90
|
|
|
83
91
|
return [
|
|
84
92
|
{
|
|
85
|
-
resources:
|
|
93
|
+
resources: resources,
|
|
86
94
|
prepend: prependInjectables,
|
|
87
95
|
append: appendInjectables
|
|
88
96
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IInjectableRule, IInjectable } from "../injection/Injectable";
|
|
2
2
|
import { stripJS, stripCSS } from "../helpers/minify";
|
|
3
|
+
import { Link } from "@readium/shared";
|
|
3
4
|
|
|
4
5
|
import readiumCSSWebPub from "@readium/css/css/dist/webPub/ReadiumCSS-webPub.css?raw";
|
|
5
6
|
|
|
@@ -10,7 +11,16 @@ import onloadProxyContent from "../dom/_readium_executionCleanup.js?raw";
|
|
|
10
11
|
/**
|
|
11
12
|
* Creates injectable rules for WebPub content documents
|
|
12
13
|
*/
|
|
13
|
-
export function createReadiumWebPubRules(): IInjectableRule[] {
|
|
14
|
+
export function createReadiumWebPubRules(readingOrderItems: Link[]): IInjectableRule[] {
|
|
15
|
+
// Create exact match patterns for manifest hrefs
|
|
16
|
+
const htmlHrefs = readingOrderItems
|
|
17
|
+
.filter(item => item.mediaType.isHTML)
|
|
18
|
+
.map(item => item.href);
|
|
19
|
+
|
|
20
|
+
const resources = htmlHrefs.length > 0
|
|
21
|
+
? htmlHrefs
|
|
22
|
+
: [/\.html$/, /\.xhtml$/, /\/$/]; // fallback patterns
|
|
23
|
+
|
|
14
24
|
// Core injectables that should be prepended
|
|
15
25
|
const prependInjectables: IInjectable[] = [
|
|
16
26
|
// CSS Selector Generator - always injected
|
|
@@ -51,7 +61,7 @@ export function createReadiumWebPubRules(): IInjectableRule[] {
|
|
|
51
61
|
|
|
52
62
|
return [
|
|
53
63
|
{
|
|
54
|
-
resources:
|
|
64
|
+
resources: resources,
|
|
55
65
|
prepend: prependInjectables,
|
|
56
66
|
append: appendInjectables
|
|
57
67
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
KeyCombinationManager,
|
|
3
|
+
ActivityEventDispatcher,
|
|
4
|
+
KeyboardPeripheral
|
|
5
|
+
} from "@readium/navigator-html-injectables";
|
|
6
|
+
|
|
7
|
+
export const NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT = "readium:navigator:keyboardPeripheral";
|
|
8
|
+
|
|
9
|
+
export interface KeyboardPeripheralOptions {
|
|
10
|
+
/** Array of keyboard peripherals to configure */
|
|
11
|
+
keyboardPeripherals?: KeyboardPeripheral[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class KeyboardPeripherals {
|
|
15
|
+
private keydownHandler?: (event: KeyboardEvent) => void;
|
|
16
|
+
private keyManager = new KeyCombinationManager();
|
|
17
|
+
|
|
18
|
+
constructor(options: KeyboardPeripheralOptions = {}) {
|
|
19
|
+
this.setupKeyboardPeripherals(options.keyboardPeripherals || []);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private setupKeyboardPeripherals(keyboardPeripherals: KeyboardPeripheral[]) {
|
|
23
|
+
if (keyboardPeripherals.length > 0) {
|
|
24
|
+
// Create activity event dispatcher
|
|
25
|
+
const dispatcher: ActivityEventDispatcher = (activityEvent) => {
|
|
26
|
+
const customEvent = new CustomEvent(NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT, {
|
|
27
|
+
detail: activityEvent
|
|
28
|
+
});
|
|
29
|
+
window.dispatchEvent(customEvent);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Create unified handler using the provided keyboard peripherals
|
|
33
|
+
this.keydownHandler = this.keyManager.createUnifiedHandler(
|
|
34
|
+
"", // Empty string as target frame source for main window
|
|
35
|
+
keyboardPeripherals,
|
|
36
|
+
dispatcher
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (this.keydownHandler) {
|
|
40
|
+
document.addEventListener("keydown", this.keydownHandler, true);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
window.addEventListener("unload", () => this.destroy());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public destroy() {
|
|
48
|
+
if (this.keydownHandler) {
|
|
49
|
+
document.removeEventListener("keydown", this.keydownHandler, true);
|
|
50
|
+
this.keydownHandler = undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface AutomationDetectorOptions {
|
|
2
|
+
/** Callback when an automation tool is detected */
|
|
3
|
+
onDetected?: (tool: string) => void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class AutomationDetector {
|
|
7
|
+
private options: AutomationDetectorOptions;
|
|
8
|
+
private detectedTools = new Set<string>();
|
|
9
|
+
private observer?: MutationObserver;
|
|
10
|
+
|
|
11
|
+
constructor(options: AutomationDetectorOptions) {
|
|
12
|
+
if (!options.onDetected) {
|
|
13
|
+
throw new Error('onDetected callback is required');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.setupDetection();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private isAutomationToolPresent(): string | null {
|
|
21
|
+
const win = window as any;
|
|
22
|
+
|
|
23
|
+
if (win.domAutomation || win.domAutomationController) return "Selenium";
|
|
24
|
+
if (navigator.webdriver === true) return "Puppeteer/Playwright";
|
|
25
|
+
if (win.__webdriver_evaluate || win.__selenium_evaluate) return "Chrome Automation";
|
|
26
|
+
if (win.callPhantom || win._phantom) return "PhantomJS";
|
|
27
|
+
if (win.__nightmare) return "Nightmare";
|
|
28
|
+
if (win.$testCafe) return "TestCafe";
|
|
29
|
+
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private setupDetection() {
|
|
34
|
+
const tool = this.isAutomationToolPresent();
|
|
35
|
+
if (tool) {
|
|
36
|
+
this.handleDetected(tool);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.observer = new MutationObserver(() => {
|
|
41
|
+
const tool = this.isAutomationToolPresent();
|
|
42
|
+
if (tool && !this.detectedTools.has(tool)) {
|
|
43
|
+
this.handleDetected(tool);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this.observer.observe(document.documentElement, {
|
|
48
|
+
childList: true,
|
|
49
|
+
subtree: true,
|
|
50
|
+
attributes: true
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
window.addEventListener("unload", () => this.destroy());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private handleDetected(tool: string) {
|
|
57
|
+
this.detectedTools.add(tool);
|
|
58
|
+
this.options.onDetected?.(tool);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public destroy() {
|
|
62
|
+
this.observer?.disconnect();
|
|
63
|
+
this.observer = undefined;
|
|
64
|
+
this.detectedTools.clear();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ContextMenuEvent } from "@readium/navigator-html-injectables";
|
|
2
|
+
|
|
3
|
+
export interface ContextMenuProtectionOptions {
|
|
4
|
+
onContextMenuBlocked?: (event: ContextMenuEvent) => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class ContextMenuProtector {
|
|
8
|
+
private contextMenuHandler?: (event: MouseEvent) => void;
|
|
9
|
+
private onContextMenuBlocked?: (event: ContextMenuEvent) => void;
|
|
10
|
+
|
|
11
|
+
constructor(options: ContextMenuProtectionOptions = {}) {
|
|
12
|
+
this.onContextMenuBlocked = options.onContextMenuBlocked;
|
|
13
|
+
this.contextMenuHandler = this.handleContextMenu.bind(this);
|
|
14
|
+
document.addEventListener("contextmenu", this.contextMenuHandler, true);
|
|
15
|
+
|
|
16
|
+
window.addEventListener("unload", () => this.destroy());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private handleContextMenu(event: MouseEvent) {
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
event.stopPropagation();
|
|
22
|
+
|
|
23
|
+
// Create context menu event
|
|
24
|
+
const activityEvent: ContextMenuEvent & { type: "context_menu" } = {
|
|
25
|
+
type: "context_menu",
|
|
26
|
+
timestamp: Date.now(),
|
|
27
|
+
clientX: event.clientX,
|
|
28
|
+
clientY: event.clientY,
|
|
29
|
+
targetFrameSrc: ''
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Call the callback if provided
|
|
33
|
+
if (this.onContextMenuBlocked) {
|
|
34
|
+
this.onContextMenuBlocked(activityEvent);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public destroy() {
|
|
41
|
+
if (this.contextMenuHandler) {
|
|
42
|
+
document.removeEventListener("contextmenu", this.contextMenuHandler, true);
|
|
43
|
+
this.contextMenuHandler = undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { sML } from "../helpers/sML";
|
|
2
|
+
import { WorkerConsole } from "./utils/WorkerConsole";
|
|
3
|
+
import { log, table, clear } from "./utils/console";
|
|
4
|
+
import { isBrave } from "./utils/platform";
|
|
5
|
+
import { match } from "./utils/match";
|
|
6
|
+
|
|
7
|
+
export interface DevToolsDetectorOptions {
|
|
8
|
+
/** Callback when Developer Tools are detected as open */
|
|
9
|
+
onDetected?: () => void;
|
|
10
|
+
/** Callback when Developer Tools are detected as closed */
|
|
11
|
+
onClosed?: () => void;
|
|
12
|
+
/** Detection interval in milliseconds (default: 1000) */
|
|
13
|
+
interval?: number;
|
|
14
|
+
/** Enable debugger-based detection (fallback, impacts UX) */
|
|
15
|
+
enableDebuggerDetection?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class DevToolsDetector {
|
|
19
|
+
private options: Required<DevToolsDetectorOptions>;
|
|
20
|
+
private isOpen = false;
|
|
21
|
+
private intervalId?: number;
|
|
22
|
+
private checkCount = 0;
|
|
23
|
+
private maxChecks = 10;
|
|
24
|
+
private maxPrintTime = 0;
|
|
25
|
+
private largeObjectArray: Record<string, string>[] | null = null;
|
|
26
|
+
private workerConsole?: WorkerConsole;
|
|
27
|
+
|
|
28
|
+
constructor(options: DevToolsDetectorOptions = {}) {
|
|
29
|
+
this.options = {
|
|
30
|
+
onDetected: options.onDetected || (() => {}),
|
|
31
|
+
onClosed: options.onClosed || (() => {}),
|
|
32
|
+
interval: options.interval || 1000,
|
|
33
|
+
enableDebuggerDetection: options.enableDebuggerDetection || false
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Initialize Web Worker for console operations (skip Firefox for now as it will fail)
|
|
37
|
+
if (!sML.UA.Firefox) {
|
|
38
|
+
try {
|
|
39
|
+
const blob = new Blob([WorkerConsole.workerScript], { type: 'application/javascript' });
|
|
40
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
41
|
+
const worker = new Worker(blobUrl);
|
|
42
|
+
this.workerConsole = new WorkerConsole(worker, blobUrl);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Fallback to regular console if Worker creation fails
|
|
45
|
+
console.warn('Failed to create Web Worker for DevTools detection:', error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.startDetection();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create large object array for performance testing
|
|
54
|
+
*/
|
|
55
|
+
private createLargeObjectArray(): Record<string, string>[] {
|
|
56
|
+
const largeObject: Record<string, string> = {};
|
|
57
|
+
for (let i = 0; i < 500; i++) {
|
|
58
|
+
largeObject[`${i}`] = `${i}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const largeObjectArray: Record<string, string>[] = [];
|
|
62
|
+
for (let i = 0; i < 50; i++) {
|
|
63
|
+
largeObjectArray.push(largeObject);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return largeObjectArray;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get cached large object array
|
|
71
|
+
*/
|
|
72
|
+
private getLargeObjectArray(): Record<string, string>[] {
|
|
73
|
+
if (this.largeObjectArray === null) {
|
|
74
|
+
this.largeObjectArray = this.createLargeObjectArray();
|
|
75
|
+
}
|
|
76
|
+
return this.largeObjectArray;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Performance-based detection using console.table timing
|
|
81
|
+
*/
|
|
82
|
+
private async calcTablePrintTime(): Promise<number> {
|
|
83
|
+
const largeObjectArray = this.getLargeObjectArray();
|
|
84
|
+
|
|
85
|
+
if (this.workerConsole) {
|
|
86
|
+
try {
|
|
87
|
+
const result = await this.workerConsole.table(largeObjectArray);
|
|
88
|
+
return result.time;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// Fallback to regular console
|
|
91
|
+
const start = performance.now();
|
|
92
|
+
table(largeObjectArray);
|
|
93
|
+
return performance.now() - start;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// Fallback to cached console methods
|
|
97
|
+
const start = performance.now();
|
|
98
|
+
table(largeObjectArray);
|
|
99
|
+
return performance.now() - start;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Performance-based detection using console.log timing
|
|
105
|
+
*/
|
|
106
|
+
private async calcLogPrintTime(): Promise<number> {
|
|
107
|
+
const largeObjectArray = this.getLargeObjectArray();
|
|
108
|
+
|
|
109
|
+
if (this.workerConsole) {
|
|
110
|
+
const result = await this.workerConsole.log(largeObjectArray);
|
|
111
|
+
return result.time;
|
|
112
|
+
} else {
|
|
113
|
+
// Fallback to cached console methods
|
|
114
|
+
const start = performance.now();
|
|
115
|
+
log(largeObjectArray);
|
|
116
|
+
return performance.now() - start;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if performance-based detection is enabled for current browser
|
|
122
|
+
*/
|
|
123
|
+
private isPerformanceDetectionEnabled(): boolean {
|
|
124
|
+
return match({
|
|
125
|
+
includes: [
|
|
126
|
+
() => !!sML.UA.Chrome,
|
|
127
|
+
() => !!sML.UA.Chromium,
|
|
128
|
+
() => !!sML.UA.Safari,
|
|
129
|
+
() => !!sML.UA.Firefox
|
|
130
|
+
],
|
|
131
|
+
excludes: []
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if debugger detection is enabled for current browser
|
|
137
|
+
*/
|
|
138
|
+
private isDebuggerDetectionEnabled(): boolean {
|
|
139
|
+
// Only enable debugger detection if explicitly enabled in options
|
|
140
|
+
// Note: We can't check for Brave here since isBrave() is async
|
|
141
|
+
// and this method needs to be synchronous
|
|
142
|
+
return this.options.enableDebuggerDetection;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Performance-based detection using large object timing differences
|
|
147
|
+
*/
|
|
148
|
+
private async checkPerformanceBased(): Promise<boolean> {
|
|
149
|
+
if (!this.isPerformanceDetectionEnabled()) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const tablePrintTime = await this.calcTablePrintTime();
|
|
154
|
+
const logPrintTime = Math.max(await this.calcLogPrintTime(), await this.calcLogPrintTime());
|
|
155
|
+
this.maxPrintTime = Math.max(this.maxPrintTime, logPrintTime);
|
|
156
|
+
|
|
157
|
+
if (this.workerConsole) {
|
|
158
|
+
await this.workerConsole.clear();
|
|
159
|
+
} else {
|
|
160
|
+
clear();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (tablePrintTime === 0) return false;
|
|
164
|
+
if (this.maxPrintTime === 0) {
|
|
165
|
+
if (await isBrave()) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return tablePrintTime > this.maxPrintTime * 10;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Debugger-based detection (fallback method)
|
|
176
|
+
* WARNING: This method impacts user experience
|
|
177
|
+
*/
|
|
178
|
+
private async checkDebuggerBased(): Promise<boolean> {
|
|
179
|
+
if (!this.isDebuggerDetectionEnabled()) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Skip debugger detection in Brave (has anti-fingerprinting measures)
|
|
184
|
+
if (await isBrave()) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const startTime = performance.now();
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
(() => {}).constructor('debugger')();
|
|
192
|
+
} catch {
|
|
193
|
+
debugger;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return performance.now() - startTime > 100;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Main detection method combining multiple approaches
|
|
201
|
+
* Prioritizes performance-based detection
|
|
202
|
+
*/
|
|
203
|
+
private async detectDevTools(): Promise<boolean> {
|
|
204
|
+
// Primary method: Performance-based detection (from original library)
|
|
205
|
+
const performanceResult = await this.checkPerformanceBased();
|
|
206
|
+
|
|
207
|
+
if (performanceResult) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Fallback method: Debugger-based (only if enabled)
|
|
212
|
+
if (this.options.enableDebuggerDetection && this.checkCount >= this.maxChecks) {
|
|
213
|
+
const debuggerResult = await this.checkDebuggerBased();
|
|
214
|
+
return debuggerResult;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Start continuous detection monitoring
|
|
222
|
+
*/
|
|
223
|
+
private startDetection() {
|
|
224
|
+
this.intervalId = window.setInterval(async () => {
|
|
225
|
+
this.checkCount++;
|
|
226
|
+
const currentlyOpen = await this.detectDevTools();
|
|
227
|
+
|
|
228
|
+
if (currentlyOpen !== this.isOpen) {
|
|
229
|
+
this.isOpen = currentlyOpen;
|
|
230
|
+
|
|
231
|
+
if (currentlyOpen) {
|
|
232
|
+
this.options.onDetected();
|
|
233
|
+
} else {
|
|
234
|
+
this.options.onClosed();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Reset check count periodically to avoid excessive debugger usage
|
|
239
|
+
if (this.checkCount > this.maxChecks * 2) {
|
|
240
|
+
this.checkCount = 0;
|
|
241
|
+
}
|
|
242
|
+
}, this.options.interval);
|
|
243
|
+
|
|
244
|
+
// Cleanup on page unload
|
|
245
|
+
window.addEventListener('beforeunload', () => this.destroy());
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get current DevTools state
|
|
250
|
+
*/
|
|
251
|
+
public isDevToolsOpen(): boolean {
|
|
252
|
+
return this.isOpen;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Force an immediate check
|
|
257
|
+
*/
|
|
258
|
+
public async checkNow(): Promise<boolean> {
|
|
259
|
+
const wasOpen = this.isOpen;
|
|
260
|
+
this.isOpen = await this.detectDevTools();
|
|
261
|
+
|
|
262
|
+
if (this.isOpen !== wasOpen) {
|
|
263
|
+
if (this.isOpen) {
|
|
264
|
+
this.options.onDetected();
|
|
265
|
+
} else {
|
|
266
|
+
this.options.onClosed();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return this.isOpen;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Stop detection and cleanup resources
|
|
275
|
+
*/
|
|
276
|
+
public destroy() {
|
|
277
|
+
if (this.intervalId) {
|
|
278
|
+
clearInterval(this.intervalId);
|
|
279
|
+
this.intervalId = undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Cleanup Web Worker
|
|
283
|
+
if (this.workerConsole) {
|
|
284
|
+
this.workerConsole.destroy();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.isOpen = false;
|
|
288
|
+
this.checkCount = 0;
|
|
289
|
+
}
|
|
290
|
+
}
|