@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.
Files changed (65) hide show
  1. package/dist/index.js +2743 -1748
  2. package/dist/index.umd.cjs +79 -86
  3. package/package.json +2 -3
  4. package/src/Navigator.ts +76 -0
  5. package/src/epub/EpubNavigator.ts +93 -10
  6. package/src/epub/css/Properties.ts +8 -8
  7. package/src/epub/css/ReadiumCSS.ts +7 -5
  8. package/src/epub/frame/FrameManager.ts +38 -2
  9. package/src/epub/frame/FramePoolManager.ts +9 -2
  10. package/src/epub/fxl/FXLFrameManager.ts +31 -2
  11. package/src/epub/fxl/FXLFramePoolManager.ts +9 -3
  12. package/src/epub/preferences/EpubDefaults.ts +6 -6
  13. package/src/epub/preferences/EpubPreferences.ts +6 -6
  14. package/src/epub/preferences/EpubPreferencesEditor.ts +1 -3
  15. package/src/epub/preferences/EpubSettings.ts +5 -7
  16. package/src/helpers/lineLength.ts +4 -6
  17. package/src/injection/epubInjectables.ts +11 -3
  18. package/src/injection/webpubInjectables.ts +12 -2
  19. package/src/peripherals/KeyboardPeripherals.ts +53 -0
  20. package/src/protection/AutomationDetector.ts +66 -0
  21. package/src/protection/ContextMenuProtector.ts +46 -0
  22. package/src/protection/DevToolsDetector.ts +290 -0
  23. package/src/protection/IframeEmbeddingDetector.ts +73 -0
  24. package/src/protection/NavigatorProtector.ts +95 -0
  25. package/src/protection/PrintProtector.ts +58 -0
  26. package/src/protection/utils/WorkerConsole.ts +84 -0
  27. package/src/protection/utils/console.ts +16 -0
  28. package/src/protection/utils/match.ts +18 -0
  29. package/src/protection/utils/platform.ts +22 -0
  30. package/src/webpub/WebPubFrameManager.ts +38 -5
  31. package/src/webpub/WebPubFramePoolManager.ts +9 -2
  32. package/src/webpub/WebPubNavigator.ts +87 -7
  33. package/types/src/Navigator.d.ts +14 -0
  34. package/types/src/epub/EpubNavigator.d.ts +14 -2
  35. package/types/src/epub/css/Properties.d.ts +4 -0
  36. package/types/src/epub/frame/FrameManager.d.ts +5 -1
  37. package/types/src/epub/frame/FramePoolManager.d.ts +4 -1
  38. package/types/src/epub/fxl/FXLFrameManager.d.ts +5 -1
  39. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +4 -2
  40. package/types/src/epub/preferences/EpubDefaults.d.ts +4 -0
  41. package/types/src/epub/preferences/EpubPreferences.d.ts +4 -0
  42. package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +2 -0
  43. package/types/src/epub/preferences/EpubSettings.d.ts +4 -0
  44. package/types/src/helpers/lineLength.d.ts +2 -3
  45. package/types/src/injection/epubInjectables.d.ts +2 -2
  46. package/types/src/injection/webpubInjectables.d.ts +2 -1
  47. package/types/src/peripherals/KeyboardPeripherals.d.ts +13 -0
  48. package/types/src/protection/AutomationDetector.d.ts +14 -0
  49. package/types/src/protection/ContextMenuProtector.d.ts +11 -0
  50. package/types/src/protection/DevToolsDetector.d.ts +75 -0
  51. package/types/src/protection/IframeEmbeddingDetector.d.ts +14 -0
  52. package/types/src/protection/NavigatorProtector.d.ts +12 -0
  53. package/types/src/protection/PrintProtector.d.ts +13 -0
  54. package/types/src/protection/utils/WorkerConsole.d.ts +15 -0
  55. package/types/src/protection/utils/console.d.ts +6 -0
  56. package/types/src/protection/utils/match.d.ts +8 -0
  57. package/types/src/protection/utils/platform.d.ts +4 -0
  58. package/types/src/webpub/WebPubFrameManager.d.ts +5 -1
  59. package/types/src/webpub/WebPubFramePoolManager.d.ts +4 -1
  60. package/types/src/webpub/WebPubNavigator.d.ts +13 -1
  61. package/dist/assets/AccessibleDfA-Bold.woff2 +0 -0
  62. package/dist/assets/AccessibleDfA-Italic.woff2 +0 -0
  63. package/dist/assets/AccessibleDfA-Regular.woff +0 -0
  64. package/dist/assets/AccessibleDfA-Regular.woff2 +0 -0
  65. 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(source: string) {
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
- // Check if currently loaded modules are equal
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 === 'function') {
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(this.container, cssProperties, this._injector);
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 === 'function') && 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 === 'function') && 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