@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.
- package/dist/audio/AudioNavigator.js +1 -1
- package/dist/audio/AudioPoolManager.js +1 -1
- package/dist/audio/engine/WebAudioEngine.js +1 -1
- package/dist/divina/DivinaNavigator.js +1 -0
- package/dist/divina/DivinaPageManager.js +1 -0
- package/dist/divina/DivinaPagedPresenter.js +1 -0
- package/dist/divina/DivinaPeripherals.js +1 -0
- package/dist/divina/DivinaScrolledPresenter.js +1 -0
- package/dist/divina/DivinaSpreader.js +1 -0
- package/dist/divina/DivinaVariantSelector.js +1 -0
- package/dist/divina/preferences/DivinaDefaults.js +1 -0
- package/dist/divina/preferences/DivinaPreferences.js +1 -0
- package/dist/divina/preferences/DivinaPreferencesEditor.js +1 -0
- package/dist/divina/preferences/DivinaSettings.js +1 -0
- package/dist/divina/protection/DivinaNavigatorProtector.js +1 -0
- package/dist/epub/EpubNavigator.js +1 -1
- package/dist/epub/frame/FrameBlobBuilder.js +3 -1
- package/dist/epub/frame/FrameManager.js +1 -1
- package/dist/epub/frame/FramePoolManager.js +1 -1
- package/dist/epub/fxl/FXLFrameManager.js +1 -1
- package/dist/epub/fxl/FXLFramePoolManager.js +1 -1
- package/dist/epub/fxl/FXLPeripherals.js +2 -2
- package/dist/index.js +1 -1
- package/dist/peripherals/KeyboardConditionBridge.js +1 -1
- package/dist/peripherals/KeyboardPeripherals.js +1 -1
- package/dist/webpub/WebPubFrameManager.js +1 -1
- package/dist/webpub/WebPubFramePoolManager.js +1 -1
- package/dist/webpub/WebPubNavigator.js +1 -1
- package/package.json +5 -5
- package/src/audio/AudioNavigator.ts +107 -5
- package/src/audio/AudioPoolManager.ts +14 -8
- package/src/audio/engine/AudioEngine.ts +34 -1
- package/src/audio/engine/WebAudioEngine.ts +48 -3
- package/src/divina/DivinaNavigator.ts +704 -0
- package/src/divina/DivinaPageManager.ts +322 -0
- package/src/divina/DivinaPagedPresenter.ts +508 -0
- package/src/divina/DivinaPeripherals.ts +630 -0
- package/src/divina/DivinaScrolledPresenter.ts +516 -0
- package/src/divina/DivinaSpreader.ts +202 -0
- package/src/divina/DivinaVariantSelector.ts +77 -0
- package/src/divina/index.ts +8 -0
- package/src/divina/preferences/DivinaDefaults.ts +37 -0
- package/src/divina/preferences/DivinaPreferences.ts +77 -0
- package/src/divina/preferences/DivinaPreferencesEditor.ts +96 -0
- package/src/divina/preferences/DivinaSettings.ts +38 -0
- package/src/divina/preferences/index.ts +4 -0
- package/src/divina/protection/DivinaNavigatorProtector.ts +44 -0
- package/src/epub/EpubNavigator.ts +73 -7
- package/src/epub/frame/FrameBlobBuilder.ts +144 -39
- package/src/epub/frame/FrameManager.ts +7 -9
- package/src/epub/frame/FramePoolManager.ts +106 -48
- package/src/epub/fxl/FXLFrameManager.ts +69 -26
- package/src/epub/fxl/FXLFramePoolManager.ts +98 -53
- package/src/epub/fxl/FXLPeripherals.ts +22 -1
- package/src/index.ts +1 -0
- package/src/peripherals/KeyboardConditionBridge.ts +15 -5
- package/src/peripherals/KeyboardPeripherals.ts +13 -7
- package/src/webpub/WebPubFrameManager.ts +7 -9
- package/src/webpub/WebPubFramePoolManager.ts +5 -2
- package/src/webpub/WebPubNavigator.ts +44 -0
- package/types/src/audio/AudioNavigator.d.ts +29 -0
- package/types/src/audio/engine/AudioEngine.d.ts +27 -1
- package/types/src/audio/engine/WebAudioEngine.d.ts +14 -2
- package/types/src/divina/DivinaNavigator.d.ts +119 -0
- package/types/src/divina/DivinaPageManager.d.ts +66 -0
- package/types/src/divina/DivinaPagedPresenter.d.ts +122 -0
- package/types/src/divina/DivinaPeripherals.d.ts +121 -0
- package/types/src/divina/DivinaScrolledPresenter.d.ts +125 -0
- package/types/src/divina/DivinaSpreader.d.ts +65 -0
- package/types/src/divina/DivinaVariantSelector.d.ts +30 -0
- package/types/src/divina/index.d.ts +8 -0
- package/types/src/divina/preferences/DivinaDefaults.d.ts +18 -0
- package/types/src/divina/preferences/DivinaPreferences.d.ts +33 -0
- package/types/src/divina/preferences/DivinaPreferencesEditor.d.ts +20 -0
- package/types/src/divina/preferences/DivinaSettings.d.ts +26 -0
- package/types/src/divina/preferences/index.d.ts +4 -0
- package/types/src/divina/protection/DivinaNavigatorProtector.d.ts +14 -0
- package/types/src/epub/EpubNavigator.d.ts +7 -1
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +19 -3
- package/types/src/epub/frame/FramePoolManager.d.ts +5 -1
- package/types/src/epub/fxl/FXLFrameManager.d.ts +2 -0
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +8 -2
- package/types/src/epub/fxl/FXLPeripherals.d.ts +3 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/peripherals/KeyboardPeripherals.d.ts +1 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
48
|
-
|
|
49
|
-
|
|
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 " +
|
|
118
|
+
throw Error("Unsupported frame mediatype " + link.mediaType.string);
|
|
52
119
|
} else {
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 ${
|
|
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,
|
|
142
|
+
await this.injector.injectForDocument(doc, link);
|
|
76
143
|
}
|
|
77
144
|
|
|
78
|
-
return this.finalizeDOM(doc, this.pub.baseURL, this.
|
|
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
|
-
|
|
83
|
-
const
|
|
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 =
|
|
86
|
-
simg.alt =
|
|
87
|
-
simg.
|
|
166
|
+
simg.src = burl || "";
|
|
167
|
+
simg.alt = link.title || "";
|
|
168
|
+
await simg.decode(); // Reduce repaints
|
|
88
169
|
doc.body.appendChild(simg);
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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() {
|