@readium/navigator-html-injectables 2.2.5 → 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 +1107 -548
- package/dist/index.umd.cjs +23 -9
- package/package.json +1 -1
- 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
package/package.json
CHANGED
package/src/comms/keys.ts
CHANGED
|
@@ -16,8 +16,11 @@ export type CommsEventKey =
|
|
|
16
16
|
"progress" |
|
|
17
17
|
"first_visible_locator" |
|
|
18
18
|
"text_selected" |
|
|
19
|
+
"context_menu" |
|
|
19
20
|
"media_play" |
|
|
20
|
-
"media_pause"
|
|
21
|
+
"media_pause" |
|
|
22
|
+
"content_protection" |
|
|
23
|
+
"keyboard_peripherals";
|
|
21
24
|
;
|
|
22
25
|
|
|
23
26
|
export type CommsCommandKey =
|
|
@@ -42,5 +45,21 @@ export type CommsCommandKey =
|
|
|
42
45
|
"focus" |
|
|
43
46
|
"activate" |
|
|
44
47
|
"shake" |
|
|
45
|
-
"force_webkit_recalc"
|
|
46
|
-
|
|
48
|
+
"force_webkit_recalc" |
|
|
49
|
+
"peripherals_protection" |
|
|
50
|
+
"keyboard_peripherals" |
|
|
51
|
+
"scroll_protection" |
|
|
52
|
+
"print_protection";
|
|
53
|
+
;
|
|
54
|
+
|
|
55
|
+
export type SuspiciousActivityType =
|
|
56
|
+
"developer_tools" |
|
|
57
|
+
"select_all" |
|
|
58
|
+
"save" |
|
|
59
|
+
"suspicious_selection" |
|
|
60
|
+
"bulk_copy" |
|
|
61
|
+
"suspicious_scrolling" |
|
|
62
|
+
"suspicious_snapping" |
|
|
63
|
+
"drag_detected" |
|
|
64
|
+
"drop_detected" |
|
|
65
|
+
"print";
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { ReflowableSetup } from "./setup/ReflowableSetup";
|
|
|
7
7
|
import { FixedSetup } from "./setup/FixedSetup";
|
|
8
8
|
import { Decorator } from "./Decorator";
|
|
9
9
|
import { WebPubSetup } from "./setup/WebPubSetup";
|
|
10
|
+
import { PrintProtector } from "../protection/PrintProtector";
|
|
10
11
|
|
|
11
12
|
// All the module names. TODO: Come up with a better way of collecting these in a way TS will recognize
|
|
12
13
|
export type ModuleName =
|
|
@@ -19,13 +20,15 @@ export type ModuleName =
|
|
|
19
20
|
"decorator" |
|
|
20
21
|
"reflowable_setup" |
|
|
21
22
|
"peripherals" |
|
|
22
|
-
"webpub_setup"
|
|
23
|
+
"webpub_setup" |
|
|
24
|
+
"print_protection";
|
|
23
25
|
|
|
24
26
|
// Modules that are valid for FXL publications
|
|
25
27
|
export const FXLModules: ModuleName[] = [
|
|
26
28
|
"fixed_setup",
|
|
27
29
|
"decorator",
|
|
28
|
-
"peripherals"
|
|
30
|
+
"peripherals",
|
|
31
|
+
"print_protection"
|
|
29
32
|
];
|
|
30
33
|
|
|
31
34
|
// Modules that are valid for reflowable publications
|
|
@@ -34,7 +37,8 @@ export const ReflowableModules: ModuleName[] = [
|
|
|
34
37
|
"decorator",
|
|
35
38
|
"peripherals",
|
|
36
39
|
"column_snapper",
|
|
37
|
-
"scroll_snapper"
|
|
40
|
+
"scroll_snapper",
|
|
41
|
+
"print_protection"
|
|
38
42
|
];
|
|
39
43
|
|
|
40
44
|
// Modules that are valid for WebPub publications (simple scroll-based)
|
|
@@ -42,7 +46,8 @@ export const WebPubModules: ModuleName[] = [
|
|
|
42
46
|
"webpub_setup",
|
|
43
47
|
"webpub_snapper",
|
|
44
48
|
"decorator",
|
|
45
|
-
"peripherals"
|
|
49
|
+
"peripherals",
|
|
50
|
+
"print_protection"
|
|
46
51
|
];
|
|
47
52
|
|
|
48
53
|
export const ModuleLibrary = new Map<string, ModuleDerived>([
|
|
@@ -55,4 +60,5 @@ export const ModuleLibrary = new Map<string, ModuleDerived>([
|
|
|
55
60
|
Decorator,
|
|
56
61
|
ColumnSnapper,
|
|
57
62
|
ScrollSnapper,
|
|
63
|
+
PrintProtector
|
|
58
64
|
].map(m => [m.moduleName, m])); // Turn module list into K/V list for quick access by name
|
|
@@ -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
|
}
|