@readium/navigator-html-injectables 2.2.4 → 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/ar-DyHX_uy2.js +7 -0
- package/dist/da-Dct0PS3E.js +7 -0
- package/dist/fr-C5HEel98.js +7 -0
- package/dist/index.js +2121 -1352
- package/dist/index.umd.cjs +24 -10
- package/dist/it-DFOBoXGy.js +7 -0
- package/dist/pt_PT-Di3sVjze.js +7 -0
- package/dist/sv-BfzAFsVN.js +7 -0
- package/package.json +4 -2
- package/src/comms/keys.ts +22 -3
- package/src/index.ts +2 -1
- package/src/modules/ModuleLibrary.ts +10 -4
- package/src/modules/Peripherals.ts +458 -13
- package/src/modules/snapper/ColumnSnapper.ts +63 -2
- package/src/modules/snapper/ScrollSnapper.ts +59 -1
- package/src/modules/snapper/WebPubSnapper.ts +57 -3
- package/src/protection/BulkCopyProtector.ts +134 -0
- package/src/protection/KeyCombinationManager.ts +139 -0
- package/src/protection/KeyboardCombinations.ts +60 -0
- package/src/protection/PatternAnalyzer.ts +117 -0
- package/src/protection/PrintProtector.ts +87 -0
- package/src/protection/SelectionAnalyzer.ts +133 -0
- package/src/protection/config.ts +33 -0
- package/src/protection/index.ts +6 -0
- package/types/src/comms/keys.d.ts +3 -2
- package/types/src/index.d.ts +1 -0
- package/types/src/modules/ModuleLibrary.d.ts +1 -1
- package/types/src/modules/Peripherals.d.ts +102 -2
- package/types/src/modules/snapper/ColumnSnapper.d.ts +10 -0
- package/types/src/modules/snapper/ScrollSnapper.d.ts +13 -0
- package/types/src/modules/snapper/WebPubSnapper.d.ts +6 -0
- package/types/src/protection/BulkCopyProtector.d.ts +20 -0
- package/types/src/protection/KeyCombinationManager.d.ts +33 -0
- package/types/src/protection/KeyboardCombinations.d.ts +15 -0
- package/types/src/protection/PatternAnalyzer.d.ts +18 -0
- package/types/src/protection/PrintProtector.d.ts +20 -0
- package/types/src/protection/SelectionAnalyzer.d.ts +20 -0
- package/types/src/protection/config.d.ts +7 -0
- package/types/src/protection/index.d.ts +6 -0
- package/src/types/protection.ts +0 -16
- package/types/src/types/protection.d.ts +0 -7
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { Comms } from "../comms/comms";
|
|
2
2
|
import { Module } from "./Module";
|
|
3
3
|
import { ReadiumWindow, nearestInteractiveElement } from "../helpers/dom";
|
|
4
|
+
import { BulkCopyProtector, BulkCopyProtectionOptions } from "../protection/BulkCopyProtector";
|
|
5
|
+
import { SelectionAnalyzer, SelectionAnalyzerOptions } from "../protection/SelectionAnalyzer";
|
|
6
|
+
import {
|
|
7
|
+
KeyboardPeripheral
|
|
8
|
+
} from "../protection";
|
|
9
|
+
import { SuspiciousActivityType } from "../comms";
|
|
10
|
+
import { BULK_COPY_CONFIG, SELECTION_ANALYZER_CONFIG } from "../protection/config";
|
|
11
|
+
import {
|
|
12
|
+
KeyCombinationManager,
|
|
13
|
+
ActivityEventDispatcher,
|
|
14
|
+
KeyboardPeripheralEvent
|
|
15
|
+
} from "../protection/KeyCombinationManager";
|
|
16
|
+
import { SuspiciousScrollingEvent } from "./snapper/ScrollSnapper";
|
|
17
|
+
import { SuspiciousSnappingEvent } from "./snapper/ColumnSnapper";
|
|
4
18
|
|
|
5
19
|
export interface FrameClickEvent {
|
|
6
20
|
defaultPrevented: boolean;
|
|
@@ -22,12 +36,363 @@ export interface BasicTextSelection {
|
|
|
22
36
|
targetFrameSrc: string;
|
|
23
37
|
}
|
|
24
38
|
|
|
39
|
+
export interface BaseSuspiciousActivityEvent {
|
|
40
|
+
type: SuspiciousActivityType;
|
|
41
|
+
timestamp: number;
|
|
42
|
+
targetFrameSrc: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Common interface for all keyboard-related events
|
|
46
|
+
export interface KeyboardEventData {
|
|
47
|
+
key: string;
|
|
48
|
+
code: string;
|
|
49
|
+
keyCode: number;
|
|
50
|
+
ctrlKey: boolean;
|
|
51
|
+
altKey: boolean;
|
|
52
|
+
shiftKey: boolean;
|
|
53
|
+
metaKey: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Base interface for keyboard peripheral events
|
|
57
|
+
export interface BaseKeyboardPeripheralEvent {
|
|
58
|
+
type: string;
|
|
59
|
+
timestamp: number;
|
|
60
|
+
targetFrameSrc: string;
|
|
61
|
+
selectedText?: Omit<BasicTextSelection, "targetFrameSrc">;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DeveloperToolsEvent extends BaseSuspiciousActivityEvent, KeyboardEventData {
|
|
65
|
+
type: "developer_tools";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SelectAllEvent extends BaseSuspiciousActivityEvent, KeyboardEventData {
|
|
69
|
+
type: "select_all";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface PrintEvent extends BaseSuspiciousActivityEvent, KeyboardEventData {
|
|
73
|
+
type: "print";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SaveEvent extends BaseSuspiciousActivityEvent, KeyboardEventData {
|
|
77
|
+
type: "save";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface BulkCopyEvent extends BaseSuspiciousActivityEvent {
|
|
81
|
+
type: "bulk_copy";
|
|
82
|
+
clipboardTypes: readonly string[];
|
|
83
|
+
selectedText?: Omit<BasicTextSelection, "targetFrameSrc">;
|
|
84
|
+
selectionLength?: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface SuspiciousSelectionEvent extends BaseSuspiciousActivityEvent {
|
|
88
|
+
type: "suspicious_selection";
|
|
89
|
+
selectionLength: number;
|
|
90
|
+
selectedText: Omit<BasicTextSelection, "targetFrameSrc">;
|
|
91
|
+
eventType: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface DragDetectedEvent extends BaseSuspiciousActivityEvent {
|
|
95
|
+
type: "drag_detected";
|
|
96
|
+
dataTransferTypes: readonly string[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface DropDetectedEvent extends BaseSuspiciousActivityEvent {
|
|
100
|
+
type: "drop_detected";
|
|
101
|
+
dataTransferTypes: readonly string[];
|
|
102
|
+
fileCount: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ContextMenuEvent extends Omit<BaseSuspiciousActivityEvent, "type"> {
|
|
106
|
+
clientX: number;
|
|
107
|
+
clientY: number;
|
|
108
|
+
selectedText?: Omit<BasicTextSelection, "targetFrameSrc">;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type SuspiciousActivityEvent =
|
|
112
|
+
| DeveloperToolsEvent
|
|
113
|
+
| SelectAllEvent
|
|
114
|
+
| BulkCopyEvent
|
|
115
|
+
| SuspiciousSelectionEvent
|
|
116
|
+
| DragDetectedEvent
|
|
117
|
+
| DropDetectedEvent
|
|
118
|
+
| PrintEvent
|
|
119
|
+
| SaveEvent
|
|
120
|
+
| SuspiciousScrollingEvent
|
|
121
|
+
| SuspiciousSnappingEvent
|
|
122
|
+
| KeyboardPeripheralEvent;
|
|
123
|
+
|
|
124
|
+
export interface ContentProtectionConfig {
|
|
125
|
+
monitorSelection?: boolean | SelectionAnalyzerOptions;
|
|
126
|
+
protectCopy?: boolean | Omit<BulkCopyProtectionOptions, "enabled">;
|
|
127
|
+
disableContextMenu?: boolean;
|
|
128
|
+
disableDragAndDrop?: boolean;
|
|
129
|
+
disableSelectAll?: boolean;
|
|
130
|
+
disableSave?: boolean;
|
|
131
|
+
monitorDevTools?: boolean;
|
|
132
|
+
monitorScrollingExperimental?: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
25
135
|
export class Peripherals extends Module {
|
|
26
136
|
static readonly moduleName = "peripherals";
|
|
27
137
|
private wnd!: ReadiumWindow;
|
|
28
138
|
private comms!: Comms;
|
|
139
|
+
private configApplied = false; // Track if config has been applied
|
|
29
140
|
|
|
141
|
+
// State management
|
|
142
|
+
private cleanupCallbacks: (() => void)[] = [];
|
|
30
143
|
private pointerMoved = false;
|
|
144
|
+
|
|
145
|
+
// Feature flags
|
|
146
|
+
private isContextMenuEnabled = false;
|
|
147
|
+
private isDragAndDropEnabled = false;
|
|
148
|
+
private isSelectionMonitoringEnabled = false;
|
|
149
|
+
private isBulkCopyProtectionEnabled = false;
|
|
150
|
+
|
|
151
|
+
// Selection analysis
|
|
152
|
+
private selectionAnalyzer: SelectionAnalyzer | null = null;
|
|
153
|
+
private currentSelection: string | null = null;
|
|
154
|
+
|
|
155
|
+
// Bulk copy protection
|
|
156
|
+
private bulkCopyProtector: BulkCopyProtector | null = null;
|
|
157
|
+
|
|
158
|
+
// Key combination manager
|
|
159
|
+
private keyManager = new KeyCombinationManager();
|
|
160
|
+
|
|
161
|
+
private addContextMenuPrevention(): void {
|
|
162
|
+
if (this.isContextMenuEnabled || !this.wnd) return;
|
|
163
|
+
this.wnd.document.addEventListener("contextmenu", this.onContext);
|
|
164
|
+
this.isContextMenuEnabled = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private removeContextMenuPrevention(): void {
|
|
168
|
+
if (!this.isContextMenuEnabled || !this.wnd) return;
|
|
169
|
+
this.wnd.document.removeEventListener("contextmenu", this.onContext);
|
|
170
|
+
this.isContextMenuEnabled = false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private addDragAndDropPrevention(): void {
|
|
174
|
+
if (this.isDragAndDropEnabled || !this.wnd) return;
|
|
175
|
+
this.wnd.document.addEventListener("dragstart", this.onDragStart);
|
|
176
|
+
this.wnd.document.addEventListener("drop", this.onDrop);
|
|
177
|
+
this.isDragAndDropEnabled = true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private removeDragAndDropPrevention(): void {
|
|
181
|
+
if (!this.isDragAndDropEnabled || !this.wnd) return;
|
|
182
|
+
this.wnd.document.removeEventListener("dragstart", this.onDragStart);
|
|
183
|
+
this.wnd.document.removeEventListener("drop", this.onDrop);
|
|
184
|
+
this.isDragAndDropEnabled = false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private keyDownHandler: ((event: KeyboardEvent) => void) | null = null;
|
|
188
|
+
|
|
189
|
+
private enableKeyboardPeripherals(shortcuts: KeyboardPeripheral[] = []): void {
|
|
190
|
+
// Clear any existing state
|
|
191
|
+
this.disableKeyboardPeripherals();
|
|
192
|
+
|
|
193
|
+
// Create activity event dispatcher
|
|
194
|
+
const dispatcher: ActivityEventDispatcher = (activityEvent) => {
|
|
195
|
+
this.comms?.send("keyboard_peripherals", activityEvent);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Create unified handler using centralized KeyCombinationManager
|
|
199
|
+
this.keyDownHandler = this.keyManager.createUnifiedHandler(this.wnd.location.href, shortcuts, dispatcher, this.wnd);
|
|
200
|
+
|
|
201
|
+
// Add the event listener
|
|
202
|
+
if (this.wnd) {
|
|
203
|
+
this.wnd.document.addEventListener("keydown", this.keyDownHandler, {
|
|
204
|
+
capture: true
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private disableKeyboardPeripherals(): void {
|
|
210
|
+
if (this.wnd && this.keyDownHandler) {
|
|
211
|
+
this.wnd.document.removeEventListener("keydown", this.keyDownHandler, {
|
|
212
|
+
capture: true
|
|
213
|
+
});
|
|
214
|
+
this.keyDownHandler = null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private addBulkCopyProtection(options: Partial<BulkCopyProtectionOptions> = {}): void {
|
|
219
|
+
if (this.isBulkCopyProtectionEnabled || !this.wnd) return;
|
|
220
|
+
|
|
221
|
+
const defaultOptions = BULK_COPY_CONFIG;
|
|
222
|
+
|
|
223
|
+
const finalOptions = options ? { ...defaultOptions, ...options } : defaultOptions;
|
|
224
|
+
|
|
225
|
+
this.bulkCopyProtector = new BulkCopyProtector(this.wnd, finalOptions);
|
|
226
|
+
this.wnd.document.addEventListener("copy", this.preventBulkCopy, true);
|
|
227
|
+
this.wnd.document.addEventListener("cut", this.preventBulkCopy, true);
|
|
228
|
+
this.isBulkCopyProtectionEnabled = true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private removeBulkCopyProtection(): void {
|
|
232
|
+
if (!this.isBulkCopyProtectionEnabled || !this.wnd) return;
|
|
233
|
+
|
|
234
|
+
this.wnd.document.removeEventListener("copy", this.preventBulkCopy, true);
|
|
235
|
+
this.wnd.document.removeEventListener("cut", this.preventBulkCopy, true);
|
|
236
|
+
this.bulkCopyProtector?.destroy();
|
|
237
|
+
this.bulkCopyProtector = null;
|
|
238
|
+
this.isBulkCopyProtectionEnabled = false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private preventBulkCopy = (event: ClipboardEvent) => {
|
|
242
|
+
if (!this.isBulkCopyProtectionEnabled || !this.bulkCopyProtector) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!this.bulkCopyProtector.shouldAllowCopy(event)) {
|
|
247
|
+
event.preventDefault();
|
|
248
|
+
|
|
249
|
+
const selection = this.wnd.getSelection();
|
|
250
|
+
const selectedText = selection?.toString() || '';
|
|
251
|
+
const domRectList = selectedText ? selection?.getRangeAt(0)?.getClientRects() : null;
|
|
252
|
+
const rect = domRectList?.[0];
|
|
253
|
+
|
|
254
|
+
const activityEvent: BulkCopyEvent = {
|
|
255
|
+
type: "bulk_copy",
|
|
256
|
+
timestamp: Date.now(),
|
|
257
|
+
clipboardTypes: event.clipboardData?.types ? [...event.clipboardData.types] : [],
|
|
258
|
+
selectedText: rect ? {
|
|
259
|
+
text: selectedText,
|
|
260
|
+
x: rect.x,
|
|
261
|
+
y: rect.y,
|
|
262
|
+
width: rect.width,
|
|
263
|
+
height: rect.height
|
|
264
|
+
} : undefined,
|
|
265
|
+
selectionLength: selectedText.length,
|
|
266
|
+
targetFrameSrc: this.wnd.location.href
|
|
267
|
+
|
|
268
|
+
};
|
|
269
|
+
this.comms?.send("content_protection", activityEvent);
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
private handleSelection = (event?: Event) => {
|
|
276
|
+
if (!this.isSelectionMonitoringEnabled || !this.wnd || !this.selectionAnalyzer) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const selection = this.wnd.getSelection();
|
|
281
|
+
if (selection) {
|
|
282
|
+
this.currentSelection = selection.toString();
|
|
283
|
+
const isSuspicious = this.selectionAnalyzer.analyze(selection);
|
|
284
|
+
|
|
285
|
+
if (isSuspicious && this.currentSelection) {
|
|
286
|
+
const selection = this.wnd.getSelection();
|
|
287
|
+
const selectedText = selection?.toString() || '';
|
|
288
|
+
const domRectList = (selectedText && selection?.rangeCount) ? selection.getRangeAt(0)?.getClientRects() : null;
|
|
289
|
+
const rect = domRectList?.[0];
|
|
290
|
+
|
|
291
|
+
const activityEvent: SuspiciousSelectionEvent = {
|
|
292
|
+
type: "suspicious_selection",
|
|
293
|
+
timestamp: Date.now(),
|
|
294
|
+
selectionLength: selectedText.length,
|
|
295
|
+
selectedText: {
|
|
296
|
+
text: selectedText,
|
|
297
|
+
x: rect?.x ?? 0,
|
|
298
|
+
y: rect?.y ?? 0,
|
|
299
|
+
width: rect?.width ?? 0,
|
|
300
|
+
height: rect?.height ?? 0
|
|
301
|
+
},
|
|
302
|
+
eventType: event?.type || "selectionchange",
|
|
303
|
+
targetFrameSrc: this.wnd.location.href
|
|
304
|
+
};
|
|
305
|
+
this.comms?.send("content_protection", activityEvent);
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
this.currentSelection = null;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
private addSelectionMonitoring(options?: SelectionAnalyzerOptions): void {
|
|
313
|
+
if (this.isSelectionMonitoringEnabled || !this.wnd) return;
|
|
314
|
+
|
|
315
|
+
// Use provided options or fall back to default config
|
|
316
|
+
const analyzerOptions = options || SELECTION_ANALYZER_CONFIG;
|
|
317
|
+
this.selectionAnalyzer = new SelectionAnalyzer(analyzerOptions);
|
|
318
|
+
this.wnd.document.addEventListener("selectionchange", this.handleSelection);
|
|
319
|
+
this.isSelectionMonitoringEnabled = true;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private removeSelectionMonitoring(): void {
|
|
323
|
+
if (!this.isSelectionMonitoringEnabled || !this.wnd) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.wnd.document.removeEventListener("selectionchange", this.handleSelection);
|
|
328
|
+
this.selectionAnalyzer?.clear();
|
|
329
|
+
this.selectionAnalyzer = null;
|
|
330
|
+
this.isSelectionMonitoringEnabled = false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private onDragStart = (event: DragEvent) => {
|
|
334
|
+
if (this.isDragAndDropEnabled) {
|
|
335
|
+
// Drag and drop protection is enabled - prevent it
|
|
336
|
+
event.preventDefault();
|
|
337
|
+
const activityEvent: DragDetectedEvent = {
|
|
338
|
+
type: "drag_detected",
|
|
339
|
+
timestamp: Date.now(),
|
|
340
|
+
dataTransferTypes: event.dataTransfer?.types ? [...event.dataTransfer.types] : [],
|
|
341
|
+
targetFrameSrc: this.wnd.location.href
|
|
342
|
+
};
|
|
343
|
+
this.comms?.send("content_protection", activityEvent);
|
|
344
|
+
return false;
|
|
345
|
+
} else {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
private onDrop = (event: DragEvent) => {
|
|
351
|
+
if (this.isDragAndDropEnabled) {
|
|
352
|
+
// Drag and drop protection is enabled - prevent it
|
|
353
|
+
event.preventDefault();
|
|
354
|
+
const dataTransfer = event.dataTransfer;
|
|
355
|
+
const activityEvent: DropDetectedEvent = {
|
|
356
|
+
type: "drop_detected",
|
|
357
|
+
timestamp: Date.now(),
|
|
358
|
+
dataTransferTypes: dataTransfer?.types ? [...dataTransfer.types] : [],
|
|
359
|
+
fileCount: dataTransfer?.files?.length || 0,
|
|
360
|
+
targetFrameSrc: this.wnd.location.href
|
|
361
|
+
};
|
|
362
|
+
this.comms?.send("content_protection", activityEvent);
|
|
363
|
+
return false;
|
|
364
|
+
} else {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
private onContext = (event: MouseEvent) => {
|
|
370
|
+
if (this.isContextMenuEnabled) {
|
|
371
|
+
// Context menu protection is enabled - prevent it
|
|
372
|
+
event.preventDefault();
|
|
373
|
+
const selection = this.wnd.getSelection();
|
|
374
|
+
const selectedText = selection?.toString() || '';
|
|
375
|
+
const domRectList = (selectedText && selection?.rangeCount) ? selection.getRangeAt(0)?.getClientRects() : null;
|
|
376
|
+
const rect = domRectList?.[0];
|
|
377
|
+
|
|
378
|
+
const activityEvent: ContextMenuEvent = {
|
|
379
|
+
timestamp: Date.now(),
|
|
380
|
+
clientX: event.clientX,
|
|
381
|
+
clientY: event.clientY,
|
|
382
|
+
...(rect && {
|
|
383
|
+
selectedText: {
|
|
384
|
+
text: selectedText,
|
|
385
|
+
x: rect.x,
|
|
386
|
+
y: rect.y,
|
|
387
|
+
width: rect.width,
|
|
388
|
+
height: rect.height
|
|
389
|
+
}
|
|
390
|
+
}),
|
|
391
|
+
targetFrameSrc: this.wnd.location.href
|
|
392
|
+
};
|
|
393
|
+
this.comms?.send("context_menu", activityEvent);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
31
396
|
|
|
32
397
|
onPointUp(event: PointerEvent) {
|
|
33
398
|
const selection = this.wnd.getSelection();
|
|
@@ -44,7 +409,7 @@ export class Peripherals extends Module {
|
|
|
44
409
|
y: rect.y,
|
|
45
410
|
width: rect.width,
|
|
46
411
|
height: rect.height,
|
|
47
|
-
targetFrameSrc: this.wnd?.location?.href
|
|
412
|
+
targetFrameSrc: this.wnd?.location?.href
|
|
48
413
|
}
|
|
49
414
|
this.comms.send("text_selected", textSelection as BasicTextSelection);
|
|
50
415
|
}
|
|
@@ -95,13 +460,6 @@ export class Peripherals extends Module {
|
|
|
95
460
|
}
|
|
96
461
|
private readonly onPointerDown = this.onPointDown.bind(this);
|
|
97
462
|
|
|
98
|
-
onContext(event: MouseEvent) {
|
|
99
|
-
// If the context menu is triggered by a long press, we manage the event using the pointup event
|
|
100
|
-
this.onPointUp(event as PointerEvent);
|
|
101
|
-
this.pointerMoved = false;
|
|
102
|
-
}
|
|
103
|
-
private readonly onContextMenu = this.onContext.bind(this);
|
|
104
|
-
|
|
105
463
|
onClick(event: MouseEvent) {
|
|
106
464
|
event.preventDefault(); // To prevent certain browser actions. May have side-effects
|
|
107
465
|
if(!event.isTrusted) {
|
|
@@ -114,20 +472,90 @@ export class Peripherals extends Module {
|
|
|
114
472
|
clientY: event.clientY,
|
|
115
473
|
});
|
|
116
474
|
// Override properties we cannot set in the constructor
|
|
117
|
-
Object.defineProperty(synthEvent,
|
|
118
|
-
Object.defineProperty(synthEvent,
|
|
475
|
+
Object.defineProperty(synthEvent, "target", {writable: false, value: event.target});
|
|
476
|
+
Object.defineProperty(synthEvent, "defaultPrevented", {writable: false, value: event.defaultPrevented });
|
|
119
477
|
// Trigger `pointerup` event
|
|
120
478
|
this.onPointUp(synthEvent);
|
|
121
479
|
}
|
|
122
480
|
}
|
|
123
481
|
private readonly onClicker = this.onClick.bind(this);
|
|
124
482
|
|
|
483
|
+
private registerProtectionHandlers() {
|
|
484
|
+
// Single handler for all content protection features
|
|
485
|
+
this.comms?.register("peripherals_protection", Peripherals.moduleName, (data: unknown, ack) => {
|
|
486
|
+
const config = data as ContentProtectionConfig;
|
|
487
|
+
|
|
488
|
+
// Apply config only on first call, then ignore subsequent calls (immutable)
|
|
489
|
+
if (!this.configApplied) {
|
|
490
|
+
this.configApplied = true;
|
|
491
|
+
|
|
492
|
+
// Selection monitoring with optional configuration
|
|
493
|
+
if (config.monitorSelection) {
|
|
494
|
+
const options = typeof config.monitorSelection === "boolean"
|
|
495
|
+
? undefined
|
|
496
|
+
: config.monitorSelection;
|
|
497
|
+
this.addSelectionMonitoring(options);
|
|
498
|
+
this.comms?.log("Selection monitoring enabled");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Copy
|
|
502
|
+
if (typeof config.protectCopy === "object") {
|
|
503
|
+
// Limited copying with custom thresholds
|
|
504
|
+
this.addBulkCopyProtection({
|
|
505
|
+
enabled: true,
|
|
506
|
+
...config.protectCopy
|
|
507
|
+
});
|
|
508
|
+
this.comms?.log("Copy protection enabled (limited)");
|
|
509
|
+
} else if (config.protectCopy === true) {
|
|
510
|
+
// Block all copying
|
|
511
|
+
this.addBulkCopyProtection({
|
|
512
|
+
enabled: true,
|
|
513
|
+
maxSelectionPercent: 0,
|
|
514
|
+
minThreshold: 0,
|
|
515
|
+
absoluteMaxChars: 0
|
|
516
|
+
});
|
|
517
|
+
this.comms?.log("Copy protection enabled");
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Context menu
|
|
521
|
+
if (config.disableContextMenu) {
|
|
522
|
+
this.addContextMenuPrevention();
|
|
523
|
+
this.comms?.log("Context menu protection enabled");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Drag and drop
|
|
527
|
+
if (config.disableDragAndDrop) {
|
|
528
|
+
this.addDragAndDropPrevention();
|
|
529
|
+
this.comms?.log("Drag and drop protection enabled");
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
ack(true);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Separate handler for keyboard peripherals
|
|
537
|
+
this.comms?.register("keyboard_peripherals", Peripherals.moduleName, (data: unknown, ack) => {
|
|
538
|
+
const keyboardPeripherals = data as KeyboardPeripheral[];
|
|
539
|
+
|
|
540
|
+
if (keyboardPeripherals && keyboardPeripherals.length > 0) {
|
|
541
|
+
this.enableKeyboardPeripherals(keyboardPeripherals);
|
|
542
|
+
this.comms?.log(`Keyboard peripherals enabled: ${keyboardPeripherals.map(p => p.type).join(", ")}`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
ack(true);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
125
549
|
mount(wnd: ReadiumWindow, comms: Comms): boolean {
|
|
126
550
|
this.wnd = wnd;
|
|
127
551
|
this.comms = comms;
|
|
552
|
+
|
|
553
|
+
// Register protection handlers
|
|
554
|
+
this.registerProtectionHandlers();
|
|
555
|
+
|
|
556
|
+
// Core event listeners (always active)
|
|
128
557
|
wnd.document.addEventListener("pointerdown", this.onPointerDown);
|
|
129
558
|
wnd.document.addEventListener("pointerup", this.onPointerUp);
|
|
130
|
-
wnd.document.addEventListener("contextmenu", this.onContextMenu);
|
|
131
559
|
wnd.document.addEventListener("pointermove", this.onPointerMove);
|
|
132
560
|
wnd.document.addEventListener("click", this.onClicker);
|
|
133
561
|
|
|
@@ -136,12 +564,29 @@ export class Peripherals extends Module {
|
|
|
136
564
|
}
|
|
137
565
|
|
|
138
566
|
unmount(wnd: ReadiumWindow, comms: Comms): boolean {
|
|
567
|
+
// Clean up optional features
|
|
568
|
+
this.removeBulkCopyProtection();
|
|
569
|
+
this.removeSelectionMonitoring();
|
|
570
|
+
this.removeContextMenuPrevention();
|
|
571
|
+
this.removeDragAndDropPrevention();
|
|
572
|
+
this.disableKeyboardPeripherals();
|
|
573
|
+
|
|
574
|
+
// Clean up event listeners
|
|
575
|
+
this.cleanupCallbacks.forEach(cleanup => cleanup());
|
|
576
|
+
this.cleanupCallbacks = [];
|
|
577
|
+
|
|
578
|
+
// Remove core event listeners
|
|
139
579
|
wnd.document.removeEventListener("pointerdown", this.onPointerDown);
|
|
140
580
|
wnd.document.removeEventListener("pointerup", this.onPointerUp);
|
|
141
|
-
wnd.document.removeEventListener("contextmenu", this.onContextMenu);
|
|
142
581
|
wnd.document.removeEventListener("pointermove", this.onPointerMove);
|
|
143
582
|
wnd.document.removeEventListener("click", this.onClicker);
|
|
144
|
-
|
|
583
|
+
|
|
584
|
+
// Unregister all handlers
|
|
585
|
+
comms.unregisterAll(Peripherals.moduleName);
|
|
586
|
+
|
|
587
|
+
// Reset config applied flag for fresh instances
|
|
588
|
+
this.configApplied = false;
|
|
589
|
+
|
|
145
590
|
comms.log("Peripherals Unmounted");
|
|
146
591
|
return true;
|
|
147
592
|
}
|
|
@@ -6,8 +6,16 @@ import { ModuleName } from "../ModuleLibrary";
|
|
|
6
6
|
import { Locator, LocatorLocations, LocatorText } from "@readium/shared";
|
|
7
7
|
import { rangeFromLocator } from "../../helpers/locator";
|
|
8
8
|
import { ReadiumWindow, deselect, findFirstVisibleLocator } from "../../helpers/dom";
|
|
9
|
+
import { PatternAnalyzer } from "../../protection/PatternAnalyzer";
|
|
10
|
+
import { BaseSuspiciousActivityEvent } from "../Peripherals";
|
|
9
11
|
|
|
10
12
|
const COLUMN_SNAPPER_STYLE_ID = "readium-column-snapper-style";
|
|
13
|
+
|
|
14
|
+
export interface SuspiciousSnappingEvent extends BaseSuspiciousActivityEvent {
|
|
15
|
+
type: "suspicious_snapping";
|
|
16
|
+
event: null;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
const SNAP_DURATION = 200; // Milliseconds
|
|
12
20
|
|
|
13
21
|
enum ScrollTouchState {
|
|
@@ -21,8 +29,11 @@ enum ScrollTouchState {
|
|
|
21
29
|
*/
|
|
22
30
|
export class ColumnSnapper extends Snapper {
|
|
23
31
|
static readonly moduleName: ModuleName = "column_snapper";
|
|
32
|
+
private isSnapProtectionEnabled = false;
|
|
24
33
|
private resizeObserver!: ResizeObserver;
|
|
25
34
|
private mutationObserver!: MutationObserver;
|
|
35
|
+
private patternAnalyzer: PatternAnalyzer | null = null;
|
|
36
|
+
private lastTurnTime: number = 0;
|
|
26
37
|
private wnd!: ReadiumWindow;
|
|
27
38
|
private comms!: Comms;
|
|
28
39
|
private doc() { return this.wnd.document.scrollingElement as HTMLElement; }
|
|
@@ -72,6 +83,7 @@ export class ColumnSnapper extends Snapper {
|
|
|
72
83
|
private alreadyScrollLeft = 0;
|
|
73
84
|
private overscroll = 0;
|
|
74
85
|
private cachedScrollWidth = 0; // We have to cache this because during overscroll (transform, or left) the width is incorrect due to browser
|
|
86
|
+
|
|
75
87
|
private takeOverSnap() {
|
|
76
88
|
this.snappingCancelled = true;
|
|
77
89
|
this.clearTouches();
|
|
@@ -80,7 +92,7 @@ export class ColumnSnapper extends Snapper {
|
|
|
80
92
|
// translate3d(XXXpx, 0px, 0px) -> slice 12 -> XXXpx, 0px, 0px) -> split "px" [0] -> XXX
|
|
81
93
|
this.overscroll = doc.style.transform?.length > 12 ? parseFloat(doc.style.transform.slice(12).split("px")[0]) : 0;
|
|
82
94
|
}
|
|
83
|
-
|
|
95
|
+
|
|
84
96
|
// Snaps the current offset to the page width.
|
|
85
97
|
snapCurrentOffset(smooth=false, noprogress=false) {
|
|
86
98
|
const startX = this.wnd.scrollX > 0 ? this.wnd.scrollX : this.alreadyScrollLeft;
|
|
@@ -96,6 +108,11 @@ export class ColumnSnapper extends Snapper {
|
|
|
96
108
|
((factor * cdo) > 0 ? 2 : 1);
|
|
97
109
|
|
|
98
110
|
const so = this.snapOffset(currentOffset + hurdle);
|
|
111
|
+
|
|
112
|
+
const direction = so > this.scrollOffset() ? "right" : "left";
|
|
113
|
+
|
|
114
|
+
this.checkSuspiciousSnap(direction, Math.abs(so - this.scrollOffset()));
|
|
115
|
+
|
|
99
116
|
if(smooth && so !== this.scrollOffset()) { // Smooth snapping
|
|
100
117
|
this.snappingCancelled = false;
|
|
101
118
|
const position = (start: number, end: number, elapsed: number, period: number) => {
|
|
@@ -232,6 +249,37 @@ export class ColumnSnapper extends Snapper {
|
|
|
232
249
|
}
|
|
233
250
|
private readonly onTouchMover = this.onTouchMove.bind(this);
|
|
234
251
|
|
|
252
|
+
private enableSnapProtection() {
|
|
253
|
+
if (!this.patternAnalyzer) {
|
|
254
|
+
this.patternAnalyzer = new PatternAnalyzer({
|
|
255
|
+
maxVelocity: this.wnd.innerWidth, // page width
|
|
256
|
+
minVariance: 0.1, // Allow for some variation in swipe speed
|
|
257
|
+
historySize: 5, // Fewer samples needed for swipe detection
|
|
258
|
+
maxConsistentScrolls: 3, // Lower threshold for consistent scrolling
|
|
259
|
+
minDirectionChanges: 0.3 // Require more direction changes
|
|
260
|
+
});
|
|
261
|
+
this.isSnapProtectionEnabled = true;
|
|
262
|
+
this.comms?.log("Snap protection enabled");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private checkSuspiciousSnap(direction: "left" | "right", distance: number) {
|
|
267
|
+
if (!this.isSnapProtectionEnabled || !this.patternAnalyzer) return;
|
|
268
|
+
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
const timeDelta = now - (this.lastTurnTime || now);
|
|
271
|
+
this.lastTurnTime = now;
|
|
272
|
+
|
|
273
|
+
const isSuspicious = this.patternAnalyzer.analyze(direction, distance, timeDelta);
|
|
274
|
+
if (isSuspicious) {
|
|
275
|
+
this.comms?.send("content_protection", {
|
|
276
|
+
type: "suspicious_snapping",
|
|
277
|
+
timestamp: Date.now(),
|
|
278
|
+
event: null
|
|
279
|
+
} as SuspiciousSnappingEvent);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
235
283
|
mount(wnd: ReadiumWindow, comms: Comms): boolean {
|
|
236
284
|
this.wnd = wnd;
|
|
237
285
|
this.comms = comms;
|
|
@@ -317,6 +365,11 @@ export class ColumnSnapper extends Snapper {
|
|
|
317
365
|
// For cases the resizeObserver is not able to detect cos body is not resizing despite colCount,
|
|
318
366
|
// we need to check the syle attribute on the documentElement (ReadiumCSS props)
|
|
319
367
|
this.mutationObserver.observe(wnd.document.documentElement, {attributes: true, attributeFilter: ["style"]});
|
|
368
|
+
|
|
369
|
+
comms.register("scroll_protection", ColumnSnapper.moduleName, (_, ack) => {
|
|
370
|
+
this.enableSnapProtection();
|
|
371
|
+
ack(true);
|
|
372
|
+
});
|
|
320
373
|
|
|
321
374
|
const scrollToOffset = (offset: number): boolean => {
|
|
322
375
|
const oldScrollLeft = this.doc().scrollLeft;
|
|
@@ -427,6 +480,7 @@ export class ColumnSnapper extends Snapper {
|
|
|
427
480
|
if(change) {
|
|
428
481
|
this.reportProgress();
|
|
429
482
|
deselect(this.wnd);
|
|
483
|
+
this.checkSuspiciousSnap("left", this.wnd.innerWidth);
|
|
430
484
|
}
|
|
431
485
|
ack(change);
|
|
432
486
|
});
|
|
@@ -441,6 +495,7 @@ export class ColumnSnapper extends Snapper {
|
|
|
441
495
|
if(change) {
|
|
442
496
|
this.reportProgress();
|
|
443
497
|
deselect(this.wnd);
|
|
498
|
+
this.checkSuspiciousSnap("right", this.wnd.innerWidth);
|
|
444
499
|
}
|
|
445
500
|
ack(change);
|
|
446
501
|
});
|
|
@@ -478,7 +533,7 @@ export class ColumnSnapper extends Snapper {
|
|
|
478
533
|
wnd.addEventListener("touchmove", this.onTouchMover, { passive: true });
|
|
479
534
|
|
|
480
535
|
// Safari hack, otherwise other events won't register
|
|
481
|
-
wnd.document.addEventListener(
|
|
536
|
+
wnd.document.addEventListener("touchstart", () => {});
|
|
482
537
|
|
|
483
538
|
comms.log("ColumnSnapper Mounted");
|
|
484
539
|
return true;
|
|
@@ -490,6 +545,12 @@ export class ColumnSnapper extends Snapper {
|
|
|
490
545
|
this.resizeObserver.disconnect();
|
|
491
546
|
this.mutationObserver.disconnect();
|
|
492
547
|
|
|
548
|
+
if (this.patternAnalyzer) {
|
|
549
|
+
this.patternAnalyzer.clear();
|
|
550
|
+
this.patternAnalyzer = null;
|
|
551
|
+
this.isSnapProtectionEnabled = false;
|
|
552
|
+
}
|
|
553
|
+
|
|
493
554
|
wnd.removeEventListener("touchstart", this.onTouchStarter);
|
|
494
555
|
wnd.removeEventListener("touchend", this.onTouchEnder);
|
|
495
556
|
wnd.removeEventListener("touchmove", this.onTouchMover);
|