@readium/navigator 2.10.2 → 2.11.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 (86) hide show
  1. package/dist/audio/AudioNavigator.js +1 -1
  2. package/dist/audio/AudioPoolManager.js +1 -1
  3. package/dist/audio/engine/WebAudioEngine.js +1 -1
  4. package/dist/divina/DivinaNavigator.js +1 -0
  5. package/dist/divina/DivinaPageManager.js +1 -0
  6. package/dist/divina/DivinaPagedPresenter.js +1 -0
  7. package/dist/divina/DivinaPeripherals.js +1 -0
  8. package/dist/divina/DivinaScrolledPresenter.js +1 -0
  9. package/dist/divina/DivinaSpreader.js +1 -0
  10. package/dist/divina/DivinaVariantSelector.js +1 -0
  11. package/dist/divina/preferences/DivinaDefaults.js +1 -0
  12. package/dist/divina/preferences/DivinaPreferences.js +1 -0
  13. package/dist/divina/preferences/DivinaPreferencesEditor.js +1 -0
  14. package/dist/divina/preferences/DivinaSettings.js +1 -0
  15. package/dist/divina/protection/DivinaNavigatorProtector.js +1 -0
  16. package/dist/epub/EpubNavigator.js +1 -1
  17. package/dist/epub/frame/FrameBlobBuilder.js +3 -1
  18. package/dist/epub/frame/FrameManager.js +1 -1
  19. package/dist/epub/frame/FramePoolManager.js +1 -1
  20. package/dist/epub/fxl/FXLFrameManager.js +1 -1
  21. package/dist/epub/fxl/FXLFramePoolManager.js +1 -1
  22. package/dist/epub/fxl/FXLPeripherals.js +2 -2
  23. package/dist/index.js +1 -1
  24. package/dist/peripherals/KeyboardConditionBridge.js +1 -1
  25. package/dist/peripherals/KeyboardPeripherals.js +1 -1
  26. package/dist/webpub/WebPubFrameManager.js +1 -1
  27. package/dist/webpub/WebPubFramePoolManager.js +1 -1
  28. package/dist/webpub/WebPubNavigator.js +1 -1
  29. package/package.json +5 -5
  30. package/src/audio/AudioNavigator.ts +107 -5
  31. package/src/audio/AudioPoolManager.ts +14 -8
  32. package/src/audio/engine/AudioEngine.ts +34 -1
  33. package/src/audio/engine/WebAudioEngine.ts +48 -3
  34. package/src/divina/DivinaNavigator.ts +704 -0
  35. package/src/divina/DivinaPageManager.ts +322 -0
  36. package/src/divina/DivinaPagedPresenter.ts +508 -0
  37. package/src/divina/DivinaPeripherals.ts +630 -0
  38. package/src/divina/DivinaScrolledPresenter.ts +516 -0
  39. package/src/divina/DivinaSpreader.ts +202 -0
  40. package/src/divina/DivinaVariantSelector.ts +77 -0
  41. package/src/divina/index.ts +8 -0
  42. package/src/divina/preferences/DivinaDefaults.ts +37 -0
  43. package/src/divina/preferences/DivinaPreferences.ts +77 -0
  44. package/src/divina/preferences/DivinaPreferencesEditor.ts +96 -0
  45. package/src/divina/preferences/DivinaSettings.ts +38 -0
  46. package/src/divina/preferences/index.ts +4 -0
  47. package/src/divina/protection/DivinaNavigatorProtector.ts +44 -0
  48. package/src/epub/EpubNavigator.ts +73 -7
  49. package/src/epub/frame/FrameBlobBuilder.ts +144 -39
  50. package/src/epub/frame/FrameManager.ts +7 -9
  51. package/src/epub/frame/FramePoolManager.ts +106 -48
  52. package/src/epub/fxl/FXLFrameManager.ts +69 -26
  53. package/src/epub/fxl/FXLFramePoolManager.ts +98 -53
  54. package/src/epub/fxl/FXLPeripherals.ts +22 -1
  55. package/src/index.ts +1 -0
  56. package/src/peripherals/KeyboardConditionBridge.ts +15 -5
  57. package/src/peripherals/KeyboardPeripherals.ts +13 -7
  58. package/src/webpub/WebPubFrameManager.ts +7 -9
  59. package/src/webpub/WebPubFramePoolManager.ts +5 -2
  60. package/src/webpub/WebPubNavigator.ts +44 -0
  61. package/types/src/audio/AudioNavigator.d.ts +29 -0
  62. package/types/src/audio/engine/AudioEngine.d.ts +27 -1
  63. package/types/src/audio/engine/WebAudioEngine.d.ts +14 -2
  64. package/types/src/divina/DivinaNavigator.d.ts +119 -0
  65. package/types/src/divina/DivinaPageManager.d.ts +66 -0
  66. package/types/src/divina/DivinaPagedPresenter.d.ts +122 -0
  67. package/types/src/divina/DivinaPeripherals.d.ts +121 -0
  68. package/types/src/divina/DivinaScrolledPresenter.d.ts +125 -0
  69. package/types/src/divina/DivinaSpreader.d.ts +65 -0
  70. package/types/src/divina/DivinaVariantSelector.d.ts +30 -0
  71. package/types/src/divina/index.d.ts +8 -0
  72. package/types/src/divina/preferences/DivinaDefaults.d.ts +18 -0
  73. package/types/src/divina/preferences/DivinaPreferences.d.ts +33 -0
  74. package/types/src/divina/preferences/DivinaPreferencesEditor.d.ts +20 -0
  75. package/types/src/divina/preferences/DivinaSettings.d.ts +26 -0
  76. package/types/src/divina/preferences/index.d.ts +4 -0
  77. package/types/src/divina/protection/DivinaNavigatorProtector.d.ts +14 -0
  78. package/types/src/epub/EpubNavigator.d.ts +7 -1
  79. package/types/src/epub/frame/FrameBlobBuilder.d.ts +19 -3
  80. package/types/src/epub/frame/FramePoolManager.d.ts +5 -1
  81. package/types/src/epub/fxl/FXLFrameManager.d.ts +2 -0
  82. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +8 -2
  83. package/types/src/epub/fxl/FXLPeripherals.d.ts +3 -0
  84. package/types/src/index.d.ts +1 -0
  85. package/types/src/peripherals/KeyboardPeripherals.d.ts +1 -0
  86. package/types/src/webpub/WebPubNavigator.d.ts +2 -0
@@ -6,6 +6,7 @@ import { CommsEventKey, ContextMenuEvent, DecorationActivatedEvent, DecorationPo
6
6
  import { Decoration, OnDecorationActivatedEvent, OnDecorationPointerEnterEvent, OnDecorationPointerLeaveEvent, DecorationObserver, DecorableNavigator, DecoratorConfig, decorationsEqual, resolveDecorationForWire, supportsDecorationStyle as canRenderDecorationStyle, DecorationStyleType } from "../decorations/index.ts";
7
7
  import * as path from "path-browserify";
8
8
  import { FXLFrameManager } from "./fxl/FXLFrameManager.ts";
9
+ import { isTypedOMSupported } from "./fxl/FXLPeripherals.ts";
9
10
  import { FrameManager } from "./frame/FrameManager.ts";
10
11
  import { IEpubPreferences, EpubPreferences } from "./preferences/EpubPreferences.ts";
11
12
  import { IEpubDefaults, EpubDefaults } from "./preferences/EpubDefaults.ts";
@@ -21,6 +22,9 @@ import { IContentProtectionConfig, IKeyboardPeripheralsConfig, KeyboardPeriphera
21
22
  import { NavigatorProtector, NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT } from "../protection/NavigatorProtector.ts";
22
23
  import { KeyboardPeripherals, NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT } from "../peripherals/KeyboardPeripherals.ts";
23
24
  import { getScriptMode } from "../helpers/scriptMode.ts";
25
+ import { UpgradeInsecureRequests } from "./frame/FrameBlobBuilder.ts";
26
+
27
+ export type { UpgradeInsecureRequests };
24
28
 
25
29
  export type ManagerEventKey = "zoom";
26
30
 
@@ -31,6 +35,7 @@ export interface EpubNavigatorConfiguration {
31
35
  contentProtection?: IContentProtectionConfig;
32
36
  keyboardPeripherals?: IKeyboardPeripheralsConfig;
33
37
  decoratorConfig?: DecoratorConfig;
38
+ upgradeInsecureRequests?: UpgradeInsecureRequests;
34
39
  }
35
40
 
36
41
  export interface EpubNavigatorListeners {
@@ -77,6 +82,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
77
82
  private readonly container: HTMLElement;
78
83
  private readonly listeners: EpubNavigatorListeners;
79
84
  private framePool!: FramePoolManager | FXLFramePoolManager;
85
+ private _destroyed = false;
80
86
  private positions!: Locator[];
81
87
  private currentLocation!: Locator;
82
88
  private _currentTimelineItem: TimelineItem | undefined;
@@ -99,6 +105,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
99
105
  private readonly _injectablesConfig: IInjectablesConfig;
100
106
  private readonly _contentProtection: IContentProtectionConfig;
101
107
  private readonly _keyboardPeripherals: IKeyboardPeripheralsConfig;
108
+ private readonly _upgradeInsecureRequests: UpgradeInsecureRequests;
102
109
  private readonly _navigatorProtector: NavigatorProtector | null = null;
103
110
  private readonly _keyboardPeripheralsManager: KeyboardPeripherals | null = null;
104
111
  private readonly _suspiciousActivityListener: ((event: Event) => void) | null = null;
@@ -177,6 +184,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
177
184
  this._contentProtection = configuration.contentProtection || {};
178
185
  this._decoratorConfig = configuration.decoratorConfig || {};
179
186
  this._decorationResizeSelectors = new Set(this._decoratorConfig.resizeWatchSelectors ?? []);
187
+ this._upgradeInsecureRequests = configuration.upgradeInsecureRequests ?? 'always';
180
188
 
181
189
  // Merge keyboard peripherals
182
190
  this._keyboardPeripherals = this.mergeKeyboardPeripherals(
@@ -264,21 +272,26 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
264
272
  });
265
273
  }
266
274
 
275
+ if (this._destroyed) return;
276
+
267
277
  if(this._layout === Layout.fixed) {
268
278
  this.framePool = new FXLFramePoolManager(
269
279
  this.container,
270
280
  this.positions,
271
281
  this.pub,
282
+ this._layout,
272
283
  this._injector,
273
284
  this._contentProtection,
274
285
  this._keyboardPeripherals,
275
- [...this._decorationResizeSelectors]
286
+ [...this._decorationResizeSelectors],
287
+ this._upgradeInsecureRequests
276
288
  );
277
289
  this.framePool.listener = (key: CommsEventKey | ManagerEventKey, data: unknown) => {
278
290
  this.eventListener(key, data);
279
291
  }
280
292
  } else {
281
293
  await this.updateCSS(false);
294
+ if (this._destroyed) return;
282
295
  const cssProperties = this.compileCSSProperties(this._css);
283
296
  this.framePool = new FramePoolManager(
284
297
  this.container,
@@ -291,15 +304,22 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
291
304
  .flatMap(item => item.references)
292
305
  .map(ref => { const h = ref.indexOf('#'); return h >= 0 ? ref.slice(h + 1) : ''; })
293
306
  .filter(Boolean),
294
- [...this._decorationResizeSelectors]
307
+ [...this._decorationResizeSelectors],
308
+ this._upgradeInsecureRequests
295
309
  );
296
310
  }
297
311
 
298
312
  if(this.currentLocation === undefined)
299
313
  this.currentLocation = this.positions[0];
314
+ else
315
+ this.currentLocation = this.completeLocator(this.currentLocation);
300
316
 
301
317
  await this.resizeHandler();
302
- await this.apply();
318
+ if (this._destroyed) return;
319
+ return new Promise(res => {
320
+ if (this._destroyed) return res(false);
321
+ this.go(this.currentLocation, false, res);
322
+ });
303
323
  }
304
324
 
305
325
  public get settings(): Readonly<EpubSettings> {
@@ -404,7 +424,12 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
404
424
  const parentEl = this.container.parentElement || document.documentElement;
405
425
 
406
426
  if (this._layout === Layout.fixed) {
407
- this.container.style.width = `${ getContentWidth(parentEl) - this._settings.constraint }px`;
427
+ const width = getContentWidth(parentEl) - this._settings.constraint;
428
+ if (isTypedOMSupported()) {
429
+ this.container.attributeStyleMap.set("width", CSS.px(width));
430
+ } else {
431
+ this.container.style.width = `${ width }px`;
432
+ }
408
433
  if (!this.framePool) return;
409
434
  (this.framePool as FXLFramePoolManager).resizeHandler();
410
435
  } else {
@@ -633,7 +658,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
633
658
  if(this._layout === Layout.fixed) {
634
659
  return modules.filter((m) => FXLModules.includes(m));
635
660
  } else modules = modules.filter((m) => ReflowableModules.includes(m));
636
-
661
+
637
662
  // CJK/Mongolian vertical: uses the X-axis snapper, never column or scroll snappers
638
663
  const mode = getScriptMode(this.pub.metadata);
639
664
  if (mode === 'cjk-vertical' || mode === 'mongolian-vertical') {
@@ -874,6 +899,8 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
874
899
  // End of Decoration
875
900
 
876
901
  public async destroy() {
902
+ // Flag synchronously so an in-flight load() bails before attaching frames.
903
+ this._destroyed = true;
877
904
  if (this._suspiciousActivityListener) {
878
905
  window.removeEventListener(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, this._suspiciousActivityListener);
879
906
  }
@@ -1158,7 +1185,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
1158
1185
  const entries = positions.filter(p => p.href === bare);
1159
1186
  if (!entries.length) return {};
1160
1187
  const atFragment = fragment
1161
- ? entries.find(p => p.locations.fragments[0] === fragment)
1188
+ ? entries.find(p => p.locations.fragments?.[0] === fragment)
1162
1189
  : undefined;
1163
1190
  let candidate: Locator | undefined;
1164
1191
  if (atFragment) {
@@ -1268,7 +1295,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
1268
1295
  return;
1269
1296
  }
1270
1297
 
1271
- const progression = locator?.locations?.progression;
1298
+ const progression = locator.locations?.progression;
1272
1299
  const hasProgression = progression && progression > 0;
1273
1300
  if(hasProgression)
1274
1301
  done = await new Promise<boolean>((res, _) => {
@@ -1279,7 +1306,42 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
1279
1306
  cb(done);
1280
1307
  }
1281
1308
 
1309
+ private completeLocator(locator: Locator): Locator {
1310
+ if(!locator.href) {
1311
+ let fellback = false;
1312
+ if(typeof locator.locations.position === "number") {
1313
+ const match = this.positions.find(p => p.locations.position === locator.locations.position);
1314
+ if (match) {
1315
+ locator = match.copyWithLocations(locator.locations);
1316
+ fellback = true;
1317
+ }
1318
+ }
1319
+ if(!fellback && typeof locator.locations?.totalProgression === "number") {
1320
+ // If locator has no href, but it does have a totalProgression,
1321
+ // we can attempt to find the right resource from the positions list.
1322
+ // This is here to help with conversion from OPDS locators which only
1323
+ // require the total progression in the publication.
1324
+ const targetProgression = locator.locations.totalProgression;
1325
+ let closestIdx = 0;
1326
+ let closestDist = Infinity;
1327
+ for (let i = 0; i < this.positions.length; i++) {
1328
+ const pos = this.positions[i];
1329
+ // Use totalProgression if available, otherwise estimate from index
1330
+ const posProg = pos.locations.totalProgression ?? (i / this.positions.length);
1331
+ const dist = Math.abs(posProg - targetProgression);
1332
+ if (dist < closestDist) {
1333
+ closestDist = dist;
1334
+ closestIdx = i;
1335
+ }
1336
+ }
1337
+ locator = this.positions[closestIdx].copyWithLocations(locator.locations);
1338
+ }
1339
+ }
1340
+ return locator;
1341
+ }
1342
+
1282
1343
  public go(locator: Locator, _: boolean, cb: (ok: boolean) => void): void {
1344
+ locator = this.completeLocator(locator);
1283
1345
  const href = locator.href.split("#")[0];
1284
1346
  let link = this.pub.readingOrder.findWithHref(href);
1285
1347
  if(!link) {
@@ -1297,6 +1359,10 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
1297
1359
  // Now that we've gone to the right locator, we can attach the listeners.
1298
1360
  // Doing this only at this stage reduces janky UI with multiple locator updates.
1299
1361
  this.attachListener();
1362
+ }).catch((error) => {
1363
+ console.error(error);
1364
+ this._isNavigating = false;
1365
+ cb(false);
1300
1366
  });
1301
1367
  }
1302
1368
 
@@ -1,12 +1,50 @@
1
- import { Link, MediaType, Publication, ReadingProgression } from "@readium/shared";
1
+ import { Link, MediaType, Publication, ReadingProgression, Resource } from "@readium/shared";
2
2
  import { Injector } from "../../injection/Injector.ts";
3
3
  import { getScriptMode } from "../../helpers/scriptMode.ts";
4
4
 
5
- const csp = (domains: string[]) => {
5
+ /** CSP source expressions cannot carry a query or fragment */
6
+ const cspSource = (url: string) => url.split(/[?#]/)[0];
7
+
8
+ /** A CSP path only prefix-matches when it ends in a slash; otherwise it must
9
+ * match exactly, which would allow the manifest and block every resource.
10
+ * See https://w3c.github.io/webappsec-csp/#match-paths */
11
+ const cspRootSource = (url: string) => {
12
+ const source = cspSource(url);
13
+ if (source.endsWith("/")) return source;
14
+ try {
15
+ const parsed = new URL(source);
16
+ parsed.pathname = parsed.pathname.substring(
17
+ 0,
18
+ parsed.pathname.lastIndexOf("/") + 1
19
+ );
20
+ return parsed.href;
21
+ } catch {
22
+ // Relative root, not parseable by `URL`
23
+ return source.replace(/[^/]*$/, "");
24
+ }
25
+ };
26
+
27
+ /**
28
+ * A flag to control whether `upgrade-insecure-requests` is included in the Content
29
+ * Security Policy of each spine-item blob frame:
30
+ *
31
+ * - `always` will always upgrade insecure requests
32
+ * - `never` will never upgrade insecure requests
33
+ * - `auto` will upgrade insecure requests only when the publication root URL is HTTPS
34
+ */
35
+ export type UpgradeInsecureRequests = 'always' | 'never' | 'auto';
36
+
37
+ const csp = (domains: string[], upgrade: UpgradeInsecureRequests = 'always', root?: string) => {
38
+ const includeUpgrade =
39
+ upgrade === "always" ||
40
+ (upgrade === "never"
41
+ ? false
42
+ : typeof root === "string" && root.startsWith("https:"));
43
+
6
44
  const d = domains.join(" ");
7
45
  return [
8
46
  // 'self' is useless because the document is loaded from a blob: URL
9
- `upgrade-insecure-requests`,
47
+ ...(includeUpgrade ? [`upgrade-insecure-requests`] : []),
10
48
  `default-src ${d} blob:`,
11
49
  `connect-src 'none'`, // No fetches to anywhere. TODO: change?
12
50
  `script-src ${d} blob: 'unsafe-inline'`, // JS scripts
@@ -20,73 +58,135 @@ const csp = (domains: string[]) => {
20
58
  ].join("; ");
21
59
  };
22
60
 
23
- export default class FrameBlobBuider {
24
- private readonly item: Link;
25
- private readonly burl: string;
26
- private readonly pub: Publication;
61
+ export default class FrameBlobBuilder {
27
62
  private readonly cssProperties?: { [key: string]: string };
28
63
  private readonly injector: Injector | null = null;
64
+ private readonly upgradeInsecureRequests: UpgradeInsecureRequests;
65
+
66
+ private currentUrl?: string;
67
+ private currentResource?: Resource;
68
+ private pendingBuild?: Promise<string>;
29
69
 
30
70
  constructor(
31
- pub: Publication,
32
- baseURL: string,
33
- item: Link,
71
+ private readonly pub: Publication,
72
+ private readonly baseURL: string,
73
+ private readonly item: Link,
34
74
  options: {
35
75
  cssProperties?: { [key: string]: string };
36
76
  injector?: Injector | null;
77
+ upgradeInsecureRequests?: UpgradeInsecureRequests;
37
78
  }
38
79
  ) {
39
- this.pub = pub;
40
80
  this.item = item;
41
- this.burl = item.toURL(baseURL) || "";
42
81
  this.cssProperties = options.cssProperties;
43
82
  this.injector = options.injector ?? null;
83
+ this.upgradeInsecureRequests = options.upgradeInsecureRequests ?? 'always';
84
+ }
85
+
86
+ public reset() {
87
+ this.currentUrl && URL.revokeObjectURL(this.currentUrl);
88
+ this.currentUrl = undefined;
89
+ this.currentResource?.close();
90
+ this.currentResource = undefined;
91
+ this.pendingBuild = undefined;
44
92
  }
45
93
 
46
94
  public async build(fxl = false): Promise<string> {
47
- if(!this.item.mediaType.isHTML) {
48
- if(this.item.mediaType.isBitmap || this.item.mediaType.equals(MediaType.SVG)) {
49
- return this.buildImageFrame();
95
+ if(this.currentUrl) return this.currentUrl;
96
+ if(this.pendingBuild) return this.pendingBuild;
97
+
98
+ const p = this.doBuild(fxl).finally(() => {
99
+ if(this.pendingBuild === p) this.pendingBuild = undefined;
100
+ });
101
+ this.pendingBuild = p;
102
+ return p;
103
+ }
104
+
105
+ private async doBuild(fxl: boolean): Promise<string> {
106
+ this.currentResource = this.pub.get(this.item);
107
+ const link = await this.currentResource.link();
108
+ if(!this.currentResource) {
109
+ // Reset has occured in the meantime
110
+ return "about:blank";
111
+ }
112
+ if(!link.mediaType.isHTML) {
113
+ if(link.mediaType.isBitmap || link.mediaType.equals(MediaType.SVG)) {
114
+ const blobUrl = await this.buildImageFrame();
115
+ this.currentUrl = blobUrl;
116
+ return blobUrl;
50
117
  } else
51
- throw Error("Unsupported frame mediatype " + this.item.mediaType.string);
118
+ throw Error("Unsupported frame mediatype " + link.mediaType.string);
52
119
  } else {
53
- return await this.buildHtmlFrame(fxl);
120
+ const blobUrl = await this.buildHtmlFrame(fxl);
121
+ this.currentUrl = blobUrl;
122
+ return blobUrl;
54
123
  }
55
124
  }
56
125
 
57
126
  private async buildHtmlFrame(fxl = false): Promise<string> {
58
- // Load the HTML resource
59
- const txt = await this.pub.get(this.item).readAsString();
60
- if(!txt) throw new Error(`Failed reading item ${this.item.href}`);
127
+ if(!this.currentResource) throw new Error("No resource loaded");
61
128
 
62
- const doc = new DOMParser().parseFromString(
63
- txt,
64
- this.item.mediaType.string as DOMParserSupportedType
65
- );
129
+ // Load the HTML resource
130
+ const link = await this.currentResource.link();
131
+ const doc = await this.currentResource.readAsXML() as HTMLDocument;
132
+ if(!doc) throw new Error(`Failed reading item ${link.href}`);
66
133
 
67
134
  const perror = doc.querySelector("parsererror");
68
135
  if (perror) {
69
136
  const details = perror.querySelector("div");
70
- throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
137
+ throw new Error(`Failed parsing item ${link.href}: ${details?.textContent || perror.textContent}`);
71
138
  }
72
139
 
73
140
  // Apply resource injections if injection service is provided
74
141
  if (this.injector) {
75
- await this.injector.injectForDocument(doc, this.item);
142
+ await this.injector.injectForDocument(doc, link);
76
143
  }
77
144
 
78
- return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, fxl, this.cssProperties);
145
+ return this.finalizeDOM(doc, this.pub.baseURL, link.toURL(this.baseURL) || "", link.mediaType, fxl, this.cssProperties, this.upgradeInsecureRequests);
79
146
  }
80
147
 
81
- private buildImageFrame(): string {
82
- // Rudimentary image display
83
- const doc = document.implementation.createHTMLDocument(this.item.title || this.item.href);
148
+ private async buildImageFrame(): Promise<string> {
149
+ if(!this.currentResource) throw new Error("No resource loaded");
150
+ const link = await this.currentResource.link();
151
+ const burl = link.toURL(this.baseURL) || ""
152
+
153
+ // Rudimentary image display in an HTML doc
154
+ const doc = document.implementation.createHTMLDocument(link.title || link.href);
155
+
156
+ // Add viewport if available
157
+ if((link?.height || 0) > 0 && (link?.width || 0) > 0) {
158
+ const viewportMeta = doc.createElement("meta");
159
+ viewportMeta.name = "viewport";
160
+ viewportMeta.content = `width=${link.width}, height=${link.height}`;
161
+ viewportMeta.dataset.readium = "true";
162
+ doc.head.appendChild(viewportMeta);
163
+ }
164
+
84
165
  const simg = document.createElement("img");
85
- simg.src = this.burl || "";
86
- simg.alt = this.item.title || "";
87
- simg.decoding = "async";
166
+ simg.src = burl || "";
167
+ simg.alt = link.title || "";
168
+ await simg.decode(); // Reduce repaints
88
169
  doc.body.appendChild(simg);
89
- return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, true);
170
+
171
+ // Apply resource injections if injection service is provided
172
+ if (this.injector) {
173
+ await this.injector.injectForDocument(doc, new Link({
174
+ // Temporary solution to address injector only expecting (X)HTML
175
+ // documents for injection, which we are technically providing
176
+ href: "readium-image-frame.xhtml",
177
+ type: MediaType.XHTML.string
178
+ }));
179
+ }
180
+
181
+ // Add image style
182
+ const sstyle = doc.createElement("style");
183
+ sstyle.dataset.readium = "true";
184
+ sstyle.textContent = `
185
+ html, body { width: 100%; height: 100%; margin: 0; padding: 0; font-size: 0; }
186
+ img { margin: 0; padding: 0; border: 0; }`;
187
+ doc.head.appendChild(sstyle);
188
+
189
+ return this.finalizeDOM(doc, this.pub.baseURL, burl, link.mediaType, true, undefined, this.upgradeInsecureRequests);
90
190
  }
91
191
 
92
192
  private setProperties(cssProperties: { [key: string]: string }, doc: Document) {
@@ -96,15 +196,19 @@ export default class FrameBlobBuider {
96
196
  }
97
197
  }
98
198
 
99
- private finalizeDOM(doc: Document, root: string | undefined, base: string | undefined, mediaType: MediaType, fxl = false, cssProperties?: { [key: string]: string }): string {
199
+ private finalizeDOM(doc: Document, root: string | undefined, base: string | undefined, mediaType: MediaType, fxl = false, cssProperties?: { [key: string]: string }, upgradeInsecureRequests: UpgradeInsecureRequests = 'always'): string {
100
200
  if(!doc) return "";
101
201
 
102
- // Get allowed domains from injector if it exists
103
- const allowedDomains = this.injector?.getAllowedDomains?.() || [];
202
+ // Get allowed domains from injector if it exists. Host-configured, so
203
+ // only strip what CSP cannot express and leave their paths as given
204
+ const allowedDomains = (this.injector?.getAllowedDomains?.() || []).map(cspSource);
205
+
206
+ // The root must be a directory so the policy covers the whole publication
207
+ const rootSource = root ? cspRootSource(root) : undefined;
104
208
 
105
209
  // Always include the root domain if provided
106
210
  const domains = [...new Set([
107
- ...(root ? [root] : []),
211
+ ...(rootSource ? [rootSource] : []),
108
212
  ...allowedDomains
109
213
  ])].filter(Boolean);
110
214
 
@@ -124,6 +228,7 @@ export default class FrameBlobBuider {
124
228
  // loaded in parallel, greatly increasing overall speed.
125
229
  doc.body.querySelectorAll("img").forEach((img) => {
126
230
  img.setAttribute("fetchpriority", "high");
231
+ img.setAttribute("referrerpolicy", "origin");
127
232
  });
128
233
 
129
234
  // We need to ensure that lang is set on the root element
@@ -180,7 +285,7 @@ export default class FrameBlobBuider {
180
285
  // Add CSP with allowed domains
181
286
  const meta = doc.createElement("meta");
182
287
  meta.httpEquiv = "Content-Security-Policy";
183
- meta.content = csp(domains);
288
+ meta.content = csp(domains, upgradeInsecureRequests, root);
184
289
  meta.dataset.readium = "true";
185
290
  doc.head.firstChild!.before(meta);
186
291
 
@@ -31,7 +31,7 @@ export class FrameManager {
31
31
  this.frame.sandbox.value = "allow-same-origin allow-scripts";
32
32
  this.frame.classList.add("readium-navigator-iframe");
33
33
  this.frame.style.visibility = "hidden";
34
- this.frame.style.setProperty("aria-hidden", "true");
34
+ this.frame.ariaHidden = "true";
35
35
  this.frame.style.opacity = "0";
36
36
  this.frame.style.position = "absolute";
37
37
  this.frame.style.pointerEvents = "none";
@@ -142,7 +142,7 @@ export class FrameManager {
142
142
  async hide(): Promise<void> {
143
143
  if(this.destroyed) return;
144
144
  this.frame.style.visibility = "hidden";
145
- this.frame.style.setProperty("aria-hidden", "true");
145
+ this.frame.ariaHidden = "true";
146
146
  this.frame.style.opacity = "0";
147
147
  this.frame.style.pointerEvents = "none";
148
148
  this.hidden = true;
@@ -178,9 +178,9 @@ export class FrameManager {
178
178
 
179
179
  const remove = () => {
180
180
  this.frame.style.removeProperty("visibility");
181
- this.frame.style.removeProperty("aria-hidden");
182
181
  this.frame.style.removeProperty("opacity");
183
182
  this.frame.style.removeProperty("pointer-events");
183
+ this.frame.ariaHidden = null;
184
184
  this.hidden = false;
185
185
 
186
186
  if (sML.UA.WebKit) {
@@ -202,14 +202,12 @@ export class FrameManager {
202
202
  setCSSProperties(properties: { [key: string]: string }) {
203
203
  if(this.destroyed || !this.frame.contentWindow) return;
204
204
 
205
- // We need to resume and halt postMessage to update the properties
206
- // if the frame is hidden since it’s been halted in hide()
207
- if (this.hidden) {
208
- if (this.comms) this.comms?.resume();
209
- else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
205
+ // A halted comms can still send, so only a never-shown hidden frame needs one created
206
+ if (this.hidden && !this.comms) {
207
+ this.comms = new FrameComms(this.frame.contentWindow, this.source);
208
+ this.comms.halt();
210
209
  }
211
210
  this.comms?.send("update_properties", properties);
212
- if (this.hidden) this.comms?.halt();
213
211
  }
214
212
 
215
213
  get iframe() {