@readium/navigator 2.2.9 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2743 -1748
- package/dist/index.umd.cjs +79 -86
- package/package.json +2 -3
- package/src/Navigator.ts +76 -0
- package/src/epub/EpubNavigator.ts +93 -10
- package/src/epub/css/Properties.ts +8 -8
- package/src/epub/css/ReadiumCSS.ts +7 -5
- package/src/epub/frame/FrameManager.ts +38 -2
- package/src/epub/frame/FramePoolManager.ts +9 -2
- package/src/epub/fxl/FXLFrameManager.ts +31 -2
- package/src/epub/fxl/FXLFramePoolManager.ts +9 -3
- package/src/epub/preferences/EpubDefaults.ts +6 -6
- package/src/epub/preferences/EpubPreferences.ts +6 -6
- package/src/epub/preferences/EpubPreferencesEditor.ts +1 -3
- package/src/epub/preferences/EpubSettings.ts +5 -7
- package/src/helpers/lineLength.ts +4 -6
- package/src/injection/epubInjectables.ts +11 -3
- package/src/injection/webpubInjectables.ts +12 -2
- package/src/peripherals/KeyboardPeripherals.ts +53 -0
- package/src/protection/AutomationDetector.ts +66 -0
- package/src/protection/ContextMenuProtector.ts +46 -0
- package/src/protection/DevToolsDetector.ts +290 -0
- package/src/protection/IframeEmbeddingDetector.ts +73 -0
- package/src/protection/NavigatorProtector.ts +95 -0
- package/src/protection/PrintProtector.ts +58 -0
- package/src/protection/utils/WorkerConsole.ts +84 -0
- package/src/protection/utils/console.ts +16 -0
- package/src/protection/utils/match.ts +18 -0
- package/src/protection/utils/platform.ts +22 -0
- package/src/webpub/WebPubFrameManager.ts +38 -5
- package/src/webpub/WebPubFramePoolManager.ts +9 -2
- package/src/webpub/WebPubNavigator.ts +87 -7
- package/types/src/Navigator.d.ts +14 -0
- package/types/src/epub/EpubNavigator.d.ts +14 -2
- package/types/src/epub/css/Properties.d.ts +4 -0
- package/types/src/epub/frame/FrameManager.d.ts +5 -1
- package/types/src/epub/frame/FramePoolManager.d.ts +4 -1
- package/types/src/epub/fxl/FXLFrameManager.d.ts +5 -1
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +4 -2
- package/types/src/epub/preferences/EpubDefaults.d.ts +4 -0
- package/types/src/epub/preferences/EpubPreferences.d.ts +4 -0
- package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +2 -0
- package/types/src/epub/preferences/EpubSettings.d.ts +4 -0
- package/types/src/helpers/lineLength.d.ts +2 -3
- package/types/src/injection/epubInjectables.d.ts +2 -2
- package/types/src/injection/webpubInjectables.d.ts +2 -1
- package/types/src/peripherals/KeyboardPeripherals.d.ts +13 -0
- package/types/src/protection/AutomationDetector.d.ts +14 -0
- package/types/src/protection/ContextMenuProtector.d.ts +11 -0
- package/types/src/protection/DevToolsDetector.d.ts +75 -0
- package/types/src/protection/IframeEmbeddingDetector.d.ts +14 -0
- package/types/src/protection/NavigatorProtector.d.ts +12 -0
- package/types/src/protection/PrintProtector.d.ts +13 -0
- package/types/src/protection/utils/WorkerConsole.d.ts +15 -0
- package/types/src/protection/utils/console.d.ts +6 -0
- package/types/src/protection/utils/match.d.ts +8 -0
- package/types/src/protection/utils/platform.d.ts +4 -0
- package/types/src/webpub/WebPubFrameManager.d.ts +5 -1
- package/types/src/webpub/WebPubFramePoolManager.d.ts +4 -1
- package/types/src/webpub/WebPubNavigator.d.ts +13 -1
- package/dist/assets/AccessibleDfA-Bold.woff2 +0 -0
- package/dist/assets/AccessibleDfA-Italic.woff2 +0 -0
- package/dist/assets/AccessibleDfA-Regular.woff +0 -0
- package/dist/assets/AccessibleDfA-Regular.woff2 +0 -0
- package/dist/assets/iAWriterDuospace-Regular.ttf +0 -0
|
@@ -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,47 @@ 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 customEvent = event as CustomEvent;
|
|
133
|
+
this.listeners.contentProtection(customEvent.detail.type, customEvent.detail);
|
|
134
|
+
};
|
|
135
|
+
window.addEventListener(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, this._suspiciousActivityListener);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Initialize keyboard peripherals separately (works independently of protection)
|
|
139
|
+
if (this._keyboardPeripherals.length > 0) {
|
|
140
|
+
this._keyboardPeripheralsManager = new KeyboardPeripherals({
|
|
141
|
+
keyboardPeripherals: this._keyboardPeripherals
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Listen for keyboard peripheral events from main window
|
|
145
|
+
this._keyboardPeripheralListener = (event: Event) => {
|
|
146
|
+
const activity = (event as CustomEvent).detail;
|
|
147
|
+
this.listeners.peripheral(activity);
|
|
148
|
+
};
|
|
149
|
+
window.addEventListener(NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT, this._keyboardPeripheralListener);
|
|
150
|
+
}
|
|
151
|
+
|
|
96
152
|
// Initialize current location
|
|
97
|
-
if (initialPosition && typeof initialPosition.copyWithLocations ===
|
|
153
|
+
if (initialPosition && typeof initialPosition.copyWithLocations === "function") {
|
|
98
154
|
this.currentLocation = initialPosition;
|
|
99
155
|
// Update currentIndex to match the initial position
|
|
100
156
|
const index = this.pub.readingOrder.findIndexWithHref(initialPosition.href);
|
|
@@ -109,7 +165,13 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
109
165
|
public async load() {
|
|
110
166
|
await this.updateCSS(false);
|
|
111
167
|
const cssProperties = this.compileCSSProperties(this._css);
|
|
112
|
-
this.framePool = new WebPubFramePoolManager(
|
|
168
|
+
this.framePool = new WebPubFramePoolManager(
|
|
169
|
+
this.container,
|
|
170
|
+
cssProperties,
|
|
171
|
+
this._injector,
|
|
172
|
+
this._contentProtection,
|
|
173
|
+
this._keyboardPeripherals
|
|
174
|
+
);
|
|
113
175
|
|
|
114
176
|
await this.apply();
|
|
115
177
|
}
|
|
@@ -282,6 +344,16 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
282
344
|
case "progress":
|
|
283
345
|
this.syncLocation(data as ProgressionRange);
|
|
284
346
|
break;
|
|
347
|
+
case "content_protection":
|
|
348
|
+
const activity = data as SuspiciousActivityEvent;
|
|
349
|
+
this.listeners.contentProtection(activity.type, activity);
|
|
350
|
+
break;
|
|
351
|
+
case "context_menu":
|
|
352
|
+
this.listeners.contextMenu(data as ContextMenuEvent);
|
|
353
|
+
break;
|
|
354
|
+
case "keyboard_peripherals":
|
|
355
|
+
this.listeners.peripheral(data as KeyboardEventData);
|
|
356
|
+
break;
|
|
285
357
|
case "log":
|
|
286
358
|
console.log(this.framePool.currentFrames[0]?.source?.split("/")[3], ...(data as any[]));
|
|
287
359
|
break;
|
|
@@ -317,6 +389,14 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
317
389
|
}
|
|
318
390
|
|
|
319
391
|
public async destroy() {
|
|
392
|
+
if (this._suspiciousActivityListener) {
|
|
393
|
+
window.removeEventListener(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, this._suspiciousActivityListener);
|
|
394
|
+
}
|
|
395
|
+
if (this._keyboardPeripheralListener) {
|
|
396
|
+
window.removeEventListener(NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT, this._keyboardPeripheralListener);
|
|
397
|
+
}
|
|
398
|
+
this._navigatorProtector?.destroy();
|
|
399
|
+
this._keyboardPeripheralsManager?.destroy();
|
|
320
400
|
await this.framePool?.destroy();
|
|
321
401
|
}
|
|
322
402
|
|
|
@@ -419,7 +499,7 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
419
499
|
|
|
420
500
|
private async loadLocator(locator: Locator, cb: (ok: boolean) => void) {
|
|
421
501
|
let done = false;
|
|
422
|
-
let cssSelector = (typeof locator.locations.getCssSelector ===
|
|
502
|
+
let cssSelector = (typeof locator.locations.getCssSelector === "function") && locator.locations.getCssSelector();
|
|
423
503
|
if(locator.text?.highlight) {
|
|
424
504
|
done = await new Promise<boolean>((res, _) => {
|
|
425
505
|
// Attempt to go to a highlighted piece of text in the resource
|
|
@@ -451,7 +531,7 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
451
531
|
// This sanity check has to be performed because we're still passing non-locator class
|
|
452
532
|
// locator objects to this function. This is not good and should eventually be forbidden
|
|
453
533
|
// or the locator should be deserialized sometime before this function.
|
|
454
|
-
const hid = (typeof locator.locations.htmlId ===
|
|
534
|
+
const hid = (typeof locator.locations.htmlId === "function") && locator.locations.htmlId();
|
|
455
535
|
if(hid)
|
|
456
536
|
done = await new Promise<boolean>((res, _) => {
|
|
457
537
|
// Attempt to go to an HTML ID in the resource
|