@readium/navigator 2.2.9 → 2.3.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/dist/index.js +2786 -1791
- 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 +291 -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 +91 -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
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export interface IframeEmbeddingDetectorOptions {
|
|
2
|
+
/** Callback when iframe embedding is detected */
|
|
3
|
+
onDetected?: (isCrossOrigin: boolean) => void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class IframeEmbeddingDetector {
|
|
7
|
+
private options: IframeEmbeddingDetectorOptions;
|
|
8
|
+
private observer?: MutationObserver;
|
|
9
|
+
private detected = false;
|
|
10
|
+
|
|
11
|
+
constructor(options: IframeEmbeddingDetectorOptions) {
|
|
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 isIframed(): { isEmbedded: boolean; isCrossOrigin: boolean } {
|
|
21
|
+
try {
|
|
22
|
+
// If we can access top, check if we're in an iframe
|
|
23
|
+
const isEmbedded = window.self !== window.top;
|
|
24
|
+
if (!isEmbedded) {
|
|
25
|
+
return { isEmbedded: false, isCrossOrigin: false };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Try to access top's location - will throw if cross-origin
|
|
29
|
+
// @ts-ignore - We know this might throw
|
|
30
|
+
const isCrossOrigin = !window.top.location.href;
|
|
31
|
+
return { isEmbedded: true, isCrossOrigin };
|
|
32
|
+
} catch (e) {
|
|
33
|
+
// If we can't access top due to same-origin policy, it's cross-origin
|
|
34
|
+
return { isEmbedded: true, isCrossOrigin: true };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private setupDetection() {
|
|
39
|
+
const { isEmbedded, isCrossOrigin } = this.isIframed();
|
|
40
|
+
if (isEmbedded) {
|
|
41
|
+
this.handleDetected(isCrossOrigin);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Set up a listener for future checks in case the page is embedded later
|
|
46
|
+
this.observer = new MutationObserver(() => {
|
|
47
|
+
const { isEmbedded, isCrossOrigin } = this.isIframed();
|
|
48
|
+
if (isEmbedded && !this.detected) {
|
|
49
|
+
this.handleDetected(isCrossOrigin);
|
|
50
|
+
this.observer?.disconnect(); // No need to observe further
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.observer.observe(document.documentElement, {
|
|
55
|
+
childList: true,
|
|
56
|
+
subtree: true,
|
|
57
|
+
attributes: true
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
window.addEventListener("unload", () => this.destroy());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private handleDetected(isCrossOrigin: boolean) {
|
|
64
|
+
this.detected = true;
|
|
65
|
+
this.options.onDetected?.(isCrossOrigin);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public destroy() {
|
|
69
|
+
this.observer?.disconnect();
|
|
70
|
+
this.observer = undefined;
|
|
71
|
+
this.detected = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { AutomationDetector } from "./AutomationDetector";
|
|
2
|
+
import { DevToolsDetector } from "./DevToolsDetector";
|
|
3
|
+
import { IframeEmbeddingDetector } from "./IframeEmbeddingDetector";
|
|
4
|
+
import { PrintProtector } from "./PrintProtector";
|
|
5
|
+
import { ContextMenuProtector } from "./ContextMenuProtector";
|
|
6
|
+
import { ContextMenuEvent } from "@readium/navigator-html-injectables";
|
|
7
|
+
import { IContentProtectionConfig } from "../Navigator";
|
|
8
|
+
|
|
9
|
+
export const NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT = "readium:navigator:suspiciousActivity";
|
|
10
|
+
|
|
11
|
+
export class NavigatorProtector {
|
|
12
|
+
private automationDetector?: AutomationDetector;
|
|
13
|
+
private devToolsDetector?: DevToolsDetector;
|
|
14
|
+
private iframeEmbeddingDetector?: IframeEmbeddingDetector;
|
|
15
|
+
private printProtector?: PrintProtector;
|
|
16
|
+
private contextMenuProtector?: ContextMenuProtector;
|
|
17
|
+
|
|
18
|
+
private dispatchSuspiciousActivity(type: string, detail: Record<string, unknown>) {
|
|
19
|
+
const event = new CustomEvent(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, {
|
|
20
|
+
detail: {
|
|
21
|
+
type,
|
|
22
|
+
timestamp: Date.now(),
|
|
23
|
+
...detail
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
window.dispatchEvent(event);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
config: IContentProtectionConfig = {}
|
|
31
|
+
) {
|
|
32
|
+
// Enable DevTools detection if explicitly enabled in config
|
|
33
|
+
if (config.monitorDevTools) {
|
|
34
|
+
this.devToolsDetector = new DevToolsDetector({
|
|
35
|
+
onDetected: () => {
|
|
36
|
+
this.dispatchSuspiciousActivity("developer_tools", {
|
|
37
|
+
targetFrameSrc: "",
|
|
38
|
+
key: "",
|
|
39
|
+
code: "",
|
|
40
|
+
keyCode: -1,
|
|
41
|
+
ctrlKey: false,
|
|
42
|
+
altKey: false,
|
|
43
|
+
shiftKey: false,
|
|
44
|
+
metaKey: false
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Enable automation detection if explicitly enabled in config
|
|
51
|
+
if (config.checkAutomation) {
|
|
52
|
+
this.automationDetector = new AutomationDetector({
|
|
53
|
+
onDetected: (tool: string) => {
|
|
54
|
+
this.dispatchSuspiciousActivity("automation_detected", { tool });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Enable iframe embedding detection if explicitly enabled in config
|
|
60
|
+
if (config.checkIFrameEmbedding) {
|
|
61
|
+
this.iframeEmbeddingDetector = new IframeEmbeddingDetector({
|
|
62
|
+
onDetected: (isCrossOrigin: boolean) => {
|
|
63
|
+
this.dispatchSuspiciousActivity("iframe_embedding_detected", { isCrossOrigin });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Enable print protection if configured
|
|
69
|
+
if (config.protectPrinting?.disable) {
|
|
70
|
+
this.printProtector = new PrintProtector({
|
|
71
|
+
...config.protectPrinting,
|
|
72
|
+
onPrintAttempt: () => {
|
|
73
|
+
this.dispatchSuspiciousActivity("print", {});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Enable context menu protection if configured
|
|
79
|
+
if (config.disableContextMenu) {
|
|
80
|
+
this.contextMenuProtector = new ContextMenuProtector({
|
|
81
|
+
onContextMenuBlocked: (event: ContextMenuEvent) => {
|
|
82
|
+
this.dispatchSuspiciousActivity("context_menu", event as unknown as Record<string, unknown>);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public destroy() {
|
|
89
|
+
this.automationDetector?.destroy();
|
|
90
|
+
this.devToolsDetector?.destroy();
|
|
91
|
+
this.iframeEmbeddingDetector?.destroy();
|
|
92
|
+
this.printProtector?.destroy();
|
|
93
|
+
this.contextMenuProtector?.destroy();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface NavigatorPrintProtectionConfig {
|
|
2
|
+
disable?: boolean;
|
|
3
|
+
watermark?: string;
|
|
4
|
+
onPrintAttempt?: () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class PrintProtector {
|
|
8
|
+
private styleElement: HTMLStyleElement | null = null;
|
|
9
|
+
private beforePrintHandler: ((e: Event) => void) | null = null;
|
|
10
|
+
private onPrintAttempt?: () => void;
|
|
11
|
+
|
|
12
|
+
constructor(config: NavigatorPrintProtectionConfig = {}) {
|
|
13
|
+
this.onPrintAttempt = config.onPrintAttempt;
|
|
14
|
+
if (config.disable) {
|
|
15
|
+
this.setupPrintProtection(config.watermark);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private setupPrintProtection(watermark?: string) {
|
|
20
|
+
const style = document.createElement("style");
|
|
21
|
+
style.textContent = `
|
|
22
|
+
@media print {
|
|
23
|
+
body * {
|
|
24
|
+
display: none !important;
|
|
25
|
+
}
|
|
26
|
+
body::after {
|
|
27
|
+
content: "${watermark || 'Printing has been disabled'}";
|
|
28
|
+
font-size: 200%;
|
|
29
|
+
display: block;
|
|
30
|
+
text-align: center;
|
|
31
|
+
margin-top: 50vh;
|
|
32
|
+
transform: translateY(-50%);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
document.head.appendChild(style);
|
|
37
|
+
this.styleElement = style;
|
|
38
|
+
|
|
39
|
+
this.beforePrintHandler = (e: Event) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
this.onPrintAttempt?.();
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
window.addEventListener("beforeprint", this.beforePrintHandler);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public destroy() {
|
|
48
|
+
if (this.beforePrintHandler) {
|
|
49
|
+
window.removeEventListener("beforeprint", this.beforePrintHandler);
|
|
50
|
+
this.beforePrintHandler = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (this.styleElement?.parentNode) {
|
|
54
|
+
this.styleElement.parentNode.removeChild(this.styleElement);
|
|
55
|
+
this.styleElement = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
let idCounter = 0;
|
|
2
|
+
|
|
3
|
+
function getId(): number {
|
|
4
|
+
return ++idCounter;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const workerScript = `
|
|
8
|
+
onmessage = function(event) {
|
|
9
|
+
var action = event.data;
|
|
10
|
+
var startTime = performance.now()
|
|
11
|
+
|
|
12
|
+
console[action.type](...action.payload);
|
|
13
|
+
postMessage({
|
|
14
|
+
id: action.id,
|
|
15
|
+
time: performance.now() - startTime
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
export type WorkerConsoleMethod<Args extends any[]> = (
|
|
21
|
+
...args: Args
|
|
22
|
+
) => Promise<{ time: number }>;
|
|
23
|
+
|
|
24
|
+
export class WorkerConsole {
|
|
25
|
+
static workerScript = workerScript;
|
|
26
|
+
|
|
27
|
+
private readonly worker: Worker;
|
|
28
|
+
private readonly blobUrl: string;
|
|
29
|
+
private callbacks: Map<number, (data: { time: number }) => void> = new Map();
|
|
30
|
+
|
|
31
|
+
readonly log: WorkerConsoleMethod<Parameters<Console['log']>>;
|
|
32
|
+
readonly table: WorkerConsoleMethod<Parameters<Console['table']>>;
|
|
33
|
+
readonly clear: WorkerConsoleMethod<Parameters<Console['clear']>>;
|
|
34
|
+
|
|
35
|
+
constructor(worker: Worker, blobUrl: string) {
|
|
36
|
+
this.worker = worker;
|
|
37
|
+
this.blobUrl = blobUrl;
|
|
38
|
+
|
|
39
|
+
this.worker.onmessage = (event) => {
|
|
40
|
+
const action = event.data;
|
|
41
|
+
const id = action.id;
|
|
42
|
+
const callback = this.callbacks.get(action.id);
|
|
43
|
+
if (callback) {
|
|
44
|
+
callback({
|
|
45
|
+
time: action.time,
|
|
46
|
+
});
|
|
47
|
+
this.callbacks.delete(id);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
this.log = (...args) => {
|
|
52
|
+
return this.send('log', ...args);
|
|
53
|
+
};
|
|
54
|
+
this.table = (...args) => {
|
|
55
|
+
return this.send('table', ...args);
|
|
56
|
+
};
|
|
57
|
+
this.clear = (...args) => {
|
|
58
|
+
return this.send('clear', ...args);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async send(type: string, ...messages: any[]) {
|
|
63
|
+
const id = getId();
|
|
64
|
+
return new Promise<{ time: number }>((resolve, reject) => {
|
|
65
|
+
this.callbacks.set(id, resolve);
|
|
66
|
+
|
|
67
|
+
this.worker.postMessage({
|
|
68
|
+
id,
|
|
69
|
+
type,
|
|
70
|
+
payload: messages,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
reject(new Error('timeout'));
|
|
75
|
+
this.callbacks.delete(id);
|
|
76
|
+
}, 2000);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public destroy() {
|
|
81
|
+
this.worker.terminate();
|
|
82
|
+
URL.revokeObjectURL(this.blobUrl);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache console methods to prevent third-party code from hooking them
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function cacheConsoleMethod<K extends keyof Console>(name: K): Console[K] {
|
|
6
|
+
if (typeof window !== 'undefined' && console) {
|
|
7
|
+
return console[name];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Fallback for non-browser environments
|
|
11
|
+
return (..._args: any[]) => {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const log = cacheConsoleMethod('log');
|
|
15
|
+
export const table = cacheConsoleMethod('table');
|
|
16
|
+
export const clear = cacheConsoleMethod('clear');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match utilities for browser compatibility checking
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface MatchOptions {
|
|
6
|
+
includes: (() => boolean)[];
|
|
7
|
+
excludes: (() => boolean)[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function match(options: MatchOptions): boolean {
|
|
11
|
+
// Check if any exclude condition is true
|
|
12
|
+
if (options.excludes.some(condition => condition())) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check if any include condition is true
|
|
17
|
+
return options.includes.some(condition => condition());
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export async function isBrave(): Promise<boolean> {
|
|
6
|
+
// Use native Brave API if available (most reliable)
|
|
7
|
+
if (typeof navigator !== 'undefined' && (navigator as any).brave && (navigator as any).brave.isBrave) {
|
|
8
|
+
try {
|
|
9
|
+
return await Promise.race([
|
|
10
|
+
(navigator as any).brave.isBrave(),
|
|
11
|
+
new Promise(resolve => setTimeout(() => resolve(false), 1000))
|
|
12
|
+
]);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
// API call failed, but we know Brave is available
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Fallback: only when native API doesn't exist at all
|
|
20
|
+
// If we're not sure, return false to avoid false positives
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Loader, ModuleName } from "@readium/navigator-html-injectables";
|
|
|
2
2
|
import { FrameComms } from "../epub/frame/FrameComms";
|
|
3
3
|
import { ReadiumWindow } from "../../../navigator-html-injectables/types/src/helpers/dom";
|
|
4
4
|
import { sML } from "../helpers";
|
|
5
|
+
import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../Navigator";
|
|
5
6
|
|
|
6
7
|
export class WebPubFrameManager {
|
|
7
8
|
private frame: HTMLIFrameElement;
|
|
@@ -10,10 +11,15 @@ export class WebPubFrameManager {
|
|
|
10
11
|
private comms: FrameComms | undefined;
|
|
11
12
|
private hidden: boolean = true;
|
|
12
13
|
private destroyed: boolean = false;
|
|
13
|
-
|
|
14
|
+
private readonly contentProtectionConfig: IContentProtectionConfig;
|
|
15
|
+
private readonly keyboardPeripheralsConfig: IKeyboardPeripheralsConfig;
|
|
14
16
|
private currModules: ModuleName[] = [];
|
|
15
17
|
|
|
16
|
-
constructor(
|
|
18
|
+
constructor(
|
|
19
|
+
source: string,
|
|
20
|
+
contentProtectionConfig: IContentProtectionConfig = {},
|
|
21
|
+
keyboardPeripheralsConfig: IKeyboardPeripheralsConfig = []
|
|
22
|
+
) {
|
|
17
23
|
this.frame = document.createElement("iframe");
|
|
18
24
|
this.frame.classList.add("readium-navigator-iframe");
|
|
19
25
|
this.frame.style.visibility = "hidden";
|
|
@@ -25,14 +31,17 @@ export class WebPubFrameManager {
|
|
|
25
31
|
// Protect against background color bleeding
|
|
26
32
|
this.frame.style.backgroundColor = "#FFFFFF";
|
|
27
33
|
this.source = source;
|
|
34
|
+
|
|
35
|
+
// Use the provided content protection config directly without overriding defaults
|
|
36
|
+
this.contentProtectionConfig = { ...contentProtectionConfig };
|
|
37
|
+
this.keyboardPeripheralsConfig = [...keyboardPeripheralsConfig];
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
async load(modules: ModuleName[] = []): Promise<Window> {
|
|
31
41
|
return new Promise((res, rej) => {
|
|
32
|
-
if(this.loader) {
|
|
42
|
+
if (this.loader) {
|
|
33
43
|
const wnd = this.frame.contentWindow!;
|
|
34
|
-
|
|
35
|
-
if([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
|
|
44
|
+
if ([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
|
|
36
45
|
try { res(wnd); } catch (error) {};
|
|
37
46
|
return;
|
|
38
47
|
}
|
|
@@ -57,6 +66,28 @@ export class WebPubFrameManager {
|
|
|
57
66
|
});
|
|
58
67
|
}
|
|
59
68
|
|
|
69
|
+
private applyContentProtection() {
|
|
70
|
+
if (!this.comms) this.comms!.resume();
|
|
71
|
+
|
|
72
|
+
// Send content protection config
|
|
73
|
+
this.comms!.send("peripherals_protection", this.contentProtectionConfig);
|
|
74
|
+
|
|
75
|
+
// Send keyboard peripherals separately
|
|
76
|
+
if (this.keyboardPeripheralsConfig && this.keyboardPeripheralsConfig.length > 0) {
|
|
77
|
+
this.comms!.send("keyboard_peripherals", this.keyboardPeripheralsConfig);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Apply scroll protection if enabled
|
|
81
|
+
if (this.contentProtectionConfig.monitorScrollingExperimental) {
|
|
82
|
+
this.comms!.send("scroll_protection", {});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Apply print protection if configured
|
|
86
|
+
if (this.contentProtectionConfig.protectPrinting) {
|
|
87
|
+
this.comms!.send("print_protection", this.contentProtectionConfig.protectPrinting);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
60
91
|
async destroy() {
|
|
61
92
|
await this.hide();
|
|
62
93
|
this.loader?.destroy();
|
|
@@ -94,6 +125,8 @@ export class WebPubFrameManager {
|
|
|
94
125
|
return new Promise((res, _) => {
|
|
95
126
|
this.comms?.send("activate", undefined, () => {
|
|
96
127
|
this.comms?.send("focus", undefined, () => {
|
|
128
|
+
// Apply content protection synchronously
|
|
129
|
+
this.applyContentProtection();
|
|
97
130
|
const remove = () => {
|
|
98
131
|
this.frame.style.removeProperty("visibility");
|
|
99
132
|
this.frame.style.removeProperty("aria-hidden");
|
|
@@ -3,6 +3,7 @@ import { Locator, Publication } from "@readium/shared";
|
|
|
3
3
|
import { WebPubBlobBuilder } from "./WebPubBlobBuilder";
|
|
4
4
|
import { WebPubFrameManager } from "./WebPubFrameManager";
|
|
5
5
|
import { Injector } from "../injection/Injector";
|
|
6
|
+
import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../Navigator";
|
|
6
7
|
|
|
7
8
|
export class WebPubFramePoolManager {
|
|
8
9
|
private readonly container: HTMLElement;
|
|
@@ -14,15 +15,21 @@ export class WebPubFramePoolManager {
|
|
|
14
15
|
private pendingUpdates: Map<string, { inPool: boolean }> = new Map();
|
|
15
16
|
private currentBaseURL: string | undefined;
|
|
16
17
|
private readonly injector?: Injector | null = null;
|
|
18
|
+
private readonly contentProtectionConfig: IContentProtectionConfig;
|
|
19
|
+
private readonly keyboardPeripheralsConfig: IKeyboardPeripheralsConfig;
|
|
17
20
|
|
|
18
21
|
constructor(
|
|
19
22
|
container: HTMLElement,
|
|
20
23
|
cssProperties?: { [key: string]: string },
|
|
21
|
-
injector?: Injector | null
|
|
24
|
+
injector?: Injector | null,
|
|
25
|
+
contentProtectionConfig: IContentProtectionConfig = {},
|
|
26
|
+
keyboardPeripheralsConfig: IKeyboardPeripheralsConfig = []
|
|
22
27
|
) {
|
|
23
28
|
this.container = container;
|
|
24
29
|
this.currentCssProperties = cssProperties;
|
|
25
30
|
this.injector = injector;
|
|
31
|
+
this.contentProtectionConfig = contentProtectionConfig;
|
|
32
|
+
this.keyboardPeripheralsConfig = [...keyboardPeripheralsConfig];
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
async destroy() {
|
|
@@ -147,7 +154,7 @@ export class WebPubFramePoolManager {
|
|
|
147
154
|
this.blobs.set(href, blobURL);
|
|
148
155
|
}
|
|
149
156
|
|
|
150
|
-
const fm = new WebPubFrameManager(this.blobs.get(href)
|
|
157
|
+
const fm = new WebPubFrameManager(this.blobs.get(href)!, this.contentProtectionConfig, this.keyboardPeripheralsConfig);
|
|
151
158
|
if(href !== newHref) await fm.hide();
|
|
152
159
|
this.container.appendChild(fm.iframe);
|
|
153
160
|
await fm.load(modules);
|
|
@@ -2,7 +2,7 @@ import { Feature, Link, Locator, Publication, ReadingProgression, LocatorLocatio
|
|
|
2
2
|
import { VisualNavigator, VisualNavigatorViewport, ProgressionRange } from "../Navigator";
|
|
3
3
|
import { Configurable } from "../preferences/Configurable";
|
|
4
4
|
import { WebPubFramePoolManager } from "./WebPubFramePoolManager";
|
|
5
|
-
import { BasicTextSelection, CommsEventKey, FrameClickEvent, ModuleLibrary, ModuleName, WebPubModules } from "@readium/navigator-html-injectables";
|
|
5
|
+
import { BasicTextSelection, CommsEventKey, ContextMenuEvent, FrameClickEvent, KeyboardEventData, ModuleLibrary, ModuleName, SuspiciousActivityEvent, WebPubModules } from "@readium/navigator-html-injectables";
|
|
6
6
|
import * as path from "path-browserify";
|
|
7
7
|
import { WebPubFrameManager } from "./WebPubFrameManager";
|
|
8
8
|
|
|
@@ -17,11 +17,16 @@ import { WebPubPreferencesEditor } from "./preferences/WebPubPreferencesEditor";
|
|
|
17
17
|
import { Injector } from "../injection/Injector";
|
|
18
18
|
import { createReadiumWebPubRules } from "../injection/webpubInjectables";
|
|
19
19
|
import { IInjectablesConfig } from "../injection/Injectable";
|
|
20
|
+
import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../Navigator";
|
|
21
|
+
import { NavigatorProtector, NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT } from "../protection/NavigatorProtector";
|
|
22
|
+
import { KeyboardPeripherals, NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT } from "../peripherals/KeyboardPeripherals";
|
|
20
23
|
|
|
21
24
|
export interface WebPubNavigatorConfiguration {
|
|
22
25
|
preferences: IWebPubPreferences;
|
|
23
26
|
defaults: IWebPubDefaults;
|
|
24
27
|
injectables?: IInjectablesConfig;
|
|
28
|
+
contentProtection?: IContentProtectionConfig;
|
|
29
|
+
keyboardPeripherals?: IKeyboardPeripheralsConfig;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export interface WebPubNavigatorListeners {
|
|
@@ -34,6 +39,9 @@ export interface WebPubNavigatorListeners {
|
|
|
34
39
|
customEvent: (key: string, data: unknown) => void;
|
|
35
40
|
handleLocator: (locator: Locator) => boolean;
|
|
36
41
|
textSelected: (selection: BasicTextSelection) => void;
|
|
42
|
+
contentProtection: (type: string, data: SuspiciousActivityEvent) => void;
|
|
43
|
+
contextMenu: (data: ContextMenuEvent) => void;
|
|
44
|
+
peripheral: (data: KeyboardEventData) => void;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
const defaultListeners = (listeners: WebPubNavigatorListeners): WebPubNavigatorListeners => ({
|
|
@@ -45,7 +53,10 @@ const defaultListeners = (listeners: WebPubNavigatorListeners): WebPubNavigatorL
|
|
|
45
53
|
scroll: listeners.scroll || (() => {}),
|
|
46
54
|
customEvent: listeners.customEvent || (() => {}),
|
|
47
55
|
handleLocator: listeners.handleLocator || (() => false),
|
|
48
|
-
textSelected: listeners.textSelected || (() => {})
|
|
56
|
+
textSelected: listeners.textSelected || (() => {}),
|
|
57
|
+
contentProtection: listeners.contentProtection || (() => {}),
|
|
58
|
+
contextMenu: listeners.contextMenu || (() => {}),
|
|
59
|
+
peripheral: listeners.peripheral || (() => {})
|
|
49
60
|
})
|
|
50
61
|
|
|
51
62
|
export class WebPubNavigator extends VisualNavigator implements Configurable<WebPubSettings, WebPubPreferences> {
|
|
@@ -62,6 +73,12 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
62
73
|
private _css: WebPubCSS;
|
|
63
74
|
private _preferencesEditor: WebPubPreferencesEditor | null = null;
|
|
64
75
|
private readonly _injector: Injector | null = null;
|
|
76
|
+
private readonly _contentProtection: IContentProtectionConfig;
|
|
77
|
+
private readonly _keyboardPeripherals: IKeyboardPeripheralsConfig;
|
|
78
|
+
private readonly _navigatorProtector: NavigatorProtector | null = null;
|
|
79
|
+
private readonly _keyboardPeripheralsManager: KeyboardPeripherals | null = null;
|
|
80
|
+
private readonly _suspiciousActivityListener: ((event: Event) => void) | null = null;
|
|
81
|
+
private readonly _keyboardPeripheralListener: ((event: Event) => void) | null = null;
|
|
65
82
|
|
|
66
83
|
private webViewport: VisualNavigatorViewport = {
|
|
67
84
|
readingOrder: [],
|
|
@@ -85,7 +102,7 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
85
102
|
});
|
|
86
103
|
|
|
87
104
|
// Combine WebPub rules with user-provided injectables
|
|
88
|
-
const webpubRules = createReadiumWebPubRules();
|
|
105
|
+
const webpubRules = createReadiumWebPubRules(pub.readingOrder.items);
|
|
89
106
|
const userConfig = configuration.injectables || { rules: [], allowedDomains: [] };
|
|
90
107
|
|
|
91
108
|
this._injector = new Injector({
|
|
@@ -93,8 +110,51 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
93
110
|
allowedDomains: userConfig.allowedDomains
|
|
94
111
|
});
|
|
95
112
|
|
|
113
|
+
// Initialize content protection with provided config or default values
|
|
114
|
+
this._contentProtection = configuration.contentProtection || {};
|
|
115
|
+
|
|
116
|
+
// Merge keyboard peripherals
|
|
117
|
+
this._keyboardPeripherals = this.mergeKeyboardPeripherals(
|
|
118
|
+
this._contentProtection,
|
|
119
|
+
configuration.keyboardPeripherals || []
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Initialize navigator protection if any protection is configured
|
|
123
|
+
if (this._contentProtection.disableContextMenu ||
|
|
124
|
+
this._contentProtection.checkAutomation ||
|
|
125
|
+
this._contentProtection.checkIFrameEmbedding ||
|
|
126
|
+
this._contentProtection.monitorDevTools ||
|
|
127
|
+
this._contentProtection.protectPrinting?.disable) {
|
|
128
|
+
this._navigatorProtector = new NavigatorProtector(this._contentProtection);
|
|
129
|
+
|
|
130
|
+
// Listen for custom events from NavigatorProtector
|
|
131
|
+
this._suspiciousActivityListener = (event: Event) => {
|
|
132
|
+
const { type, ...activity } = (event as CustomEvent).detail;
|
|
133
|
+
if (type === "context_menu") {
|
|
134
|
+
this.listeners.contextMenu(activity as ContextMenuEvent);
|
|
135
|
+
} else {
|
|
136
|
+
this.listeners.contentProtection(type, activity);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
window.addEventListener(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, this._suspiciousActivityListener);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Initialize keyboard peripherals separately (works independently of protection)
|
|
143
|
+
if (this._keyboardPeripherals.length > 0) {
|
|
144
|
+
this._keyboardPeripheralsManager = new KeyboardPeripherals({
|
|
145
|
+
keyboardPeripherals: this._keyboardPeripherals
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Listen for keyboard peripheral events from main window
|
|
149
|
+
this._keyboardPeripheralListener = (event: Event) => {
|
|
150
|
+
const activity = (event as CustomEvent).detail;
|
|
151
|
+
this.listeners.peripheral(activity);
|
|
152
|
+
};
|
|
153
|
+
window.addEventListener(NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT, this._keyboardPeripheralListener);
|
|
154
|
+
}
|
|
155
|
+
|
|
96
156
|
// Initialize current location
|
|
97
|
-
if (initialPosition && typeof initialPosition.copyWithLocations ===
|
|
157
|
+
if (initialPosition && typeof initialPosition.copyWithLocations === "function") {
|
|
98
158
|
this.currentLocation = initialPosition;
|
|
99
159
|
// Update currentIndex to match the initial position
|
|
100
160
|
const index = this.pub.readingOrder.findIndexWithHref(initialPosition.href);
|
|
@@ -109,7 +169,13 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
109
169
|
public async load() {
|
|
110
170
|
await this.updateCSS(false);
|
|
111
171
|
const cssProperties = this.compileCSSProperties(this._css);
|
|
112
|
-
this.framePool = new WebPubFramePoolManager(
|
|
172
|
+
this.framePool = new WebPubFramePoolManager(
|
|
173
|
+
this.container,
|
|
174
|
+
cssProperties,
|
|
175
|
+
this._injector,
|
|
176
|
+
this._contentProtection,
|
|
177
|
+
this._keyboardPeripherals
|
|
178
|
+
);
|
|
113
179
|
|
|
114
180
|
await this.apply();
|
|
115
181
|
}
|
|
@@ -282,6 +348,16 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
282
348
|
case "progress":
|
|
283
349
|
this.syncLocation(data as ProgressionRange);
|
|
284
350
|
break;
|
|
351
|
+
case "content_protection":
|
|
352
|
+
const activity = data as SuspiciousActivityEvent;
|
|
353
|
+
this.listeners.contentProtection(activity.type, activity);
|
|
354
|
+
break;
|
|
355
|
+
case "context_menu":
|
|
356
|
+
this.listeners.contextMenu(data as ContextMenuEvent);
|
|
357
|
+
break;
|
|
358
|
+
case "keyboard_peripherals":
|
|
359
|
+
this.listeners.peripheral(data as KeyboardEventData);
|
|
360
|
+
break;
|
|
285
361
|
case "log":
|
|
286
362
|
console.log(this.framePool.currentFrames[0]?.source?.split("/")[3], ...(data as any[]));
|
|
287
363
|
break;
|
|
@@ -317,6 +393,14 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
317
393
|
}
|
|
318
394
|
|
|
319
395
|
public async destroy() {
|
|
396
|
+
if (this._suspiciousActivityListener) {
|
|
397
|
+
window.removeEventListener(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, this._suspiciousActivityListener);
|
|
398
|
+
}
|
|
399
|
+
if (this._keyboardPeripheralListener) {
|
|
400
|
+
window.removeEventListener(NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT, this._keyboardPeripheralListener);
|
|
401
|
+
}
|
|
402
|
+
this._navigatorProtector?.destroy();
|
|
403
|
+
this._keyboardPeripheralsManager?.destroy();
|
|
320
404
|
await this.framePool?.destroy();
|
|
321
405
|
}
|
|
322
406
|
|
|
@@ -419,7 +503,7 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
419
503
|
|
|
420
504
|
private async loadLocator(locator: Locator, cb: (ok: boolean) => void) {
|
|
421
505
|
let done = false;
|
|
422
|
-
let cssSelector = (typeof locator.locations.getCssSelector ===
|
|
506
|
+
let cssSelector = (typeof locator.locations.getCssSelector === "function") && locator.locations.getCssSelector();
|
|
423
507
|
if(locator.text?.highlight) {
|
|
424
508
|
done = await new Promise<boolean>((res, _) => {
|
|
425
509
|
// Attempt to go to a highlighted piece of text in the resource
|
|
@@ -451,7 +535,7 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
451
535
|
// This sanity check has to be performed because we're still passing non-locator class
|
|
452
536
|
// locator objects to this function. This is not good and should eventually be forbidden
|
|
453
537
|
// or the locator should be deserialized sometime before this function.
|
|
454
|
-
const hid = (typeof locator.locations.htmlId ===
|
|
538
|
+
const hid = (typeof locator.locations.htmlId === "function") && locator.locations.htmlId();
|
|
455
539
|
if(hid)
|
|
456
540
|
done = await new Promise<boolean>((res, _) => {
|
|
457
541
|
// Attempt to go to an HTML ID in the resource
|