@readium/navigator 2.2.6 → 2.2.8
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/ar-DyHX_uy2-DyHX_uy2-DyHX_uy2.js +7 -0
- package/dist/ar-DyHX_uy2-DyHX_uy2.js +7 -0
- package/dist/da-Dct0PS3E-Dct0PS3E-Dct0PS3E.js +7 -0
- package/dist/da-Dct0PS3E-Dct0PS3E.js +7 -0
- package/dist/fr-C5HEel98-C5HEel98-C5HEel98.js +7 -0
- package/dist/fr-C5HEel98-C5HEel98.js +7 -0
- package/dist/index.js +4389 -2401
- package/dist/index.umd.cjs +1571 -39
- package/dist/it-DFOBoXGy-DFOBoXGy-DFOBoXGy.js +7 -0
- package/dist/it-DFOBoXGy-DFOBoXGy.js +7 -0
- package/dist/pt_PT-Di3sVjze-Di3sVjze-Di3sVjze.js +7 -0
- package/dist/pt_PT-Di3sVjze-Di3sVjze.js +7 -0
- package/dist/sv-BfzAFsVN-BfzAFsVN-BfzAFsVN.js +7 -0
- package/dist/sv-BfzAFsVN-BfzAFsVN.js +7 -0
- package/package.json +6 -5
- package/src/dom/_readium_cssSelectorGenerator.js +1 -0
- package/src/dom/_readium_executionCleanup.js +13 -0
- package/src/dom/_readium_executionPrevention.js +65 -0
- package/src/dom/_readium_webpubExecution.js +4 -0
- package/src/epub/EpubNavigator.ts +26 -2
- package/src/epub/frame/FrameBlobBuilder.ts +37 -130
- package/src/epub/frame/FramePoolManager.ts +34 -5
- package/src/epub/fxl/FXLFramePoolManager.ts +20 -2
- package/src/helpers/minify.ts +14 -0
- package/src/index.ts +2 -1
- package/src/injection/Injectable.ts +85 -0
- package/src/injection/Injector.ts +356 -0
- package/src/injection/epubInjectables.ts +90 -0
- package/src/injection/index.ts +2 -0
- package/src/injection/webpubInjectables.ts +59 -0
- package/src/webpub/WebPubBlobBuilder.ts +19 -76
- package/src/webpub/WebPubFramePoolManager.ts +29 -4
- package/src/webpub/WebPubNavigator.ts +15 -1
- package/types/src/epub/EpubNavigator.d.ts +3 -0
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +7 -4
- package/types/src/epub/frame/FramePoolManager.d.ts +3 -1
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +3 -1
- package/types/src/helpers/minify.d.ts +12 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/injection/Injectable.d.ts +68 -0
- package/types/src/injection/Injector.d.ts +22 -0
- package/types/src/injection/epubInjectables.d.ts +6 -0
- package/types/src/injection/index.d.ts +2 -0
- package/types/src/injection/webpubInjectables.d.ts +5 -0
- package/types/src/webpub/WebPubBlobBuilder.d.ts +7 -3
- package/types/src/webpub/WebPubFramePoolManager.d.ts +3 -1
- package/types/src/webpub/WebPubNavigator.d.ts +3 -0
|
@@ -1,85 +1,6 @@
|
|
|
1
1
|
import { MediaType } from "@readium/shared";
|
|
2
2
|
import { Link, Publication } from "@readium/shared";
|
|
3
|
-
|
|
4
|
-
// Readium CSS imports
|
|
5
|
-
// The "?inline" query is to prevent some bundlers from injecting these into the page (e.g. vite)
|
|
6
|
-
// @ts-ignore
|
|
7
|
-
import readiumCSSAfter from "@readium/css/css/dist/ReadiumCSS-after.css?inline";
|
|
8
|
-
// @ts-ignore
|
|
9
|
-
import readiumCSSBefore from "@readium/css/css/dist/ReadiumCSS-before.css?inline";
|
|
10
|
-
// @ts-ignore
|
|
11
|
-
import readiumCSSDefault from "@readium/css/css/dist/ReadiumCSS-default.css?inline";
|
|
12
|
-
|
|
13
|
-
// Utilities
|
|
14
|
-
const blobify = (source: string, type: string) => URL.createObjectURL(new Blob([source], { type }));
|
|
15
|
-
const stripJS = (source: string) => source.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\n/g, "").replace(/\s+/g, " ");
|
|
16
|
-
const stripCSS = (source: string) => source.replace(/\/\*(?:(?!\*\/)[\s\S])*\*\/|[\r\n\t]+/g, '').replace(/ {2,}/g, ' ')
|
|
17
|
-
// Fully resolve absolute local URLs created by bundlers since it's going into a blob
|
|
18
|
-
.replace(/url\((?!(https?:)?\/\/)("?)\/([^\)]+)/g, `url($2${window.location.origin}/$3`);
|
|
19
|
-
const scriptify = (doc: Document, source: string) => {
|
|
20
|
-
const s = doc.createElement("script");
|
|
21
|
-
s.dataset.readium = "true";
|
|
22
|
-
s.src = source.startsWith("blob:") ? source : blobify(source, "text/javascript");
|
|
23
|
-
return s;
|
|
24
|
-
}
|
|
25
|
-
const styleify = (doc: Document, source: string) => {
|
|
26
|
-
const s = doc.createElement("link");
|
|
27
|
-
s.dataset.readium = "true";
|
|
28
|
-
s.rel = "stylesheet";
|
|
29
|
-
s.type = "text/css";
|
|
30
|
-
s.href = source.startsWith("blob:") ? source : blobify(source, "text/css");
|
|
31
|
-
return s;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
type CacheFunction = () => string;
|
|
35
|
-
const resourceBlobCache = new Map<string, string>();
|
|
36
|
-
const cached = (key: string, cacher: CacheFunction) => {
|
|
37
|
-
if(resourceBlobCache.has(key)) return resourceBlobCache.get(key)!;
|
|
38
|
-
const value = cacher();
|
|
39
|
-
resourceBlobCache.set(key, value);
|
|
40
|
-
return value;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// https://unpkg.com/css-selector-generator@3.6.4/build/index.js
|
|
44
|
-
// CssSelectorGenerator --> _readium_cssSelectorGenerator
|
|
45
|
-
// This has to be injected because you need to be in the iframe's context for it to work properly
|
|
46
|
-
const cssSelectorGenerator = (doc: Document) => scriptify(doc, cached("css-selector-generator", () => blobify(
|
|
47
|
-
"!function(t,e){\"object\"==typeof exports&&\"object\"==typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define([],e):\"object\"==typeof exports?exports._readium_cssSelectorGenerator=e():t._readium_cssSelectorGenerator=e()}(self,(()=>(()=>{\"use strict\";var t,e,n={d:(t,e)=>{for(var o in e)n.o(e,o)&&!n.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(t,\"__esModule\",{value:!0})}},o={};function r(t){return t&&t instanceof Element}function i(t=\"unknown problem\",...e){console.warn(`CssSelectorGenerator: ${t}`,...e)}n.r(o),n.d(o,{default:()=>z,getCssSelector:()=>U}),function(t){t.NONE=\"none\",t.DESCENDANT=\"descendant\",t.CHILD=\"child\"}(t||(t={})),function(t){t.id=\"id\",t.class=\"class\",t.tag=\"tag\",t.attribute=\"attribute\",t.nthchild=\"nthchild\",t.nthoftype=\"nthoftype\"}(e||(e={}));const c={selectors:[e.id,e.class,e.tag,e.attribute],includeTag:!1,whitelist:[],blacklist:[],combineWithinSelector:!0,combineBetweenSelectors:!0,root:null,maxCombinations:Number.POSITIVE_INFINITY,maxCandidates:Number.POSITIVE_INFINITY};function u(t){return t instanceof RegExp}function s(t){return[\"string\",\"function\"].includes(typeof t)||u(t)}function l(t){return Array.isArray(t)?t.filter(s):[]}function a(t){const e=[Node.DOCUMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE,Node.ELEMENT_NODE];return function(t){return t instanceof Node}(t)&&e.includes(t.nodeType)}function f(t,e){if(a(t))return t.contains(e)||i(\"element root mismatch\",\"Provided root does not contain the element. This will most likely result in producing a fallback selector using element\'s real root node. If you plan to use the selector using provided root (e.g. `root.querySelector`), it will nto work as intended.\"),t;const n=e.getRootNode({composed:!1});return a(n)?(n!==document&&i(\"shadow root inferred\",\"You did not provide a root and the element is a child of Shadow DOM. This will produce a selector using ShadowRoot as a root. If you plan to use the selector using document as a root (e.g. `document.querySelector`), it will not work as intended.\"),n):e.ownerDocument.querySelector(\":root\")}function d(t){return\"number\"==typeof t?t:Number.POSITIVE_INFINITY}function m(t=[]){const[e=[],...n]=t;return 0===n.length?e:n.reduce(((t,e)=>t.filter((t=>e.includes(t)))),e)}function p(t){return[].concat(...t)}function h(t){const e=t.map((t=>{if(u(t))return e=>t.test(e);if(\"function\"==typeof t)return e=>{const n=t(e);return\"boolean\"!=typeof n?(i(\"pattern matcher function invalid\",\"Provided pattern matching function does not return boolean. It\'s result will be ignored.\",t),!1):n};if(\"string\"==typeof t){const e=new RegExp(\"^\"+t.replace(\/[|\\\\{}()[\\]^$+?.]\/g,\"\\\\$&\").replace(\/\\*\/g,\".+\")+\"$\");return t=>e.test(t)}return i(\"pattern matcher invalid\",\"Pattern matching only accepts strings, regular expressions and\/or functions. This item is invalid and will be ignored.\",t),()=>!1}));return t=>e.some((e=>e(t)))}function g(t,e,n){const o=Array.from(f(n,t[0]).querySelectorAll(e));return o.length===t.length&&t.every((t=>o.includes(t)))}function y(t,e){e=null!=e?e:function(t){return t.ownerDocument.querySelector(\":root\")}(t);const n=[];let o=t;for(;r(o)&&o!==e;)n.push(o),o=o.parentElement;return n}function b(t,e){return m(t.map((t=>y(t,e))))}const N={[t.NONE]:{type:t.NONE,value:\"\"},[t.DESCENDANT]:{type:t.DESCENDANT,value:\" > \"},[t.CHILD]:{type:t.CHILD,value:\" \"}},S=new RegExp([\"^$\",\"\\\\s\"].join(\"|\")),E=new RegExp([\"^$\"].join(\"|\")),w=[e.nthoftype,e.tag,e.id,e.class,e.attribute,e.nthchild],v=h([\"class\",\"id\",\"ng-*\"]);function C({nodeName:t}){return`[${t}]`}function O({nodeName:t,nodeValue:e}){return`[${t}=\'${L(e)}\']`}function T(t){const e=Array.from(t.attributes).filter((e=>function({nodeName:t},e){const n=e.tagName.toLowerCase();return!([\"input\",\"option\"].includes(n)&&\"value\"===t||v(t))}(e,t)));return[...e.map(C),...e.map(O)]}function I(t){return(t.getAttribute(\"class\")||\"\").trim().split(\/\\s+\/).filter((t=>!E.test(t))).map((t=>`.${L(t)}`))}function x(t){const e=t.getAttribute(\"id\")||\"\",n=`#${L(e)}`,o=t.getRootNode({composed:!1});return!S.test(e)&&g([t],n,o)?[n]:[]}function j(t){const e=t.parentNode;if(e){const n=Array.from(e.childNodes).filter(r).indexOf(t);if(n>-1)return[`:nth-child(${n+1})`]}return[]}function A(t){return[L(t.tagName.toLowerCase())]}function D(t){const e=[...new Set(p(t.map(A)))];return 0===e.length||e.length>1?[]:[e[0]]}function $(t){const e=D([t])[0],n=t.parentElement;if(n){const o=Array.from(n.children).filter((t=>t.tagName.toLowerCase()===e)),r=o.indexOf(t);if(r>-1)return[`${e}:nth-of-type(${r+1})`]}return[]}function R(t=[],{maxResults:e=Number.POSITIVE_INFINITY}={}){const n=[];let o=0,r=k(1);for(;r.length<=t.length&&o<e;)o+=1,n.push(r.map((e=>t[e]))),r=P(r,t.length-1);return n}function P(t=[],e=0){const n=t.length;if(0===n)return[];const o=[...t];o[n-1]+=1;for(let t=n-1;t>=0;t--)if(o[t]>e){if(0===t)return k(n+1);o[t-1]++,o[t]=o[t-1]+1}return o[n-1]>e?k(n+1):o}function k(t=1){return Array.from(Array(t).keys())}const _=\":\".charCodeAt(0).toString(16).toUpperCase(),M=\/[ !\"#$%&\'()\\[\\]{|}<>*+,.\/;=?@^`~\\\\]\/;function L(t=\"\"){var e,n;return null!==(n=null===(e=null===CSS||void 0===CSS?void 0:CSS.escape)||void 0===e?void 0:e.call(CSS,t))&&void 0!==n?n:function(t=\"\"){return t.split(\"\").map((t=>\":\"===t?`\\\\${_} `:M.test(t)?`\\\\${t}`:escape(t).replace(\/%\/g,\"\\\\\"))).join(\"\")}(t)}const q={tag:D,id:function(t){return 0===t.length||t.length>1?[]:x(t[0])},class:function(t){return m(t.map(I))},attribute:function(t){return m(t.map(T))},nthchild:function(t){return m(t.map(j))},nthoftype:function(t){return m(t.map($))}},F={tag:A,id:x,class:I,attribute:T,nthchild:j,nthoftype:$};function V(t){return t.includes(e.tag)||t.includes(e.nthoftype)?[...t]:[...t,e.tag]}function Y(t={}){const n=[...w];return t[e.tag]&&t[e.nthoftype]&&n.splice(n.indexOf(e.tag),1),n.map((e=>{return(o=t)[n=e]?o[n].join(\"\"):\"\";var n,o})).join(\"\")}function B(t,e,n=\"\",o){const r=function(t,e){return\"\"===e?t:function(t,e){return[...t.map((t=>e+\" \"+t)),...t.map((t=>e+\" > \"+t))]}(t,e)}(function(t,e,n){const o=function(t,e){const{blacklist:n,whitelist:o,combineWithinSelector:r,maxCombinations:i}=e,c=h(n),u=h(o);return function(t){const{selectors:e,includeTag:n}=t,o=[].concat(e);return n&&!o.includes(\"tag\")&&o.push(\"tag\"),o}(e).reduce(((e,n)=>{const o=function(t,e){var n;return(null!==(n=q[e])&&void 0!==n?n:()=>[])(t)}(t,n),s=function(t=[],e,n){return t.filter((t=>n(t)||!e(t)))}(o,c,u),l=function(t=[],e){return t.sort(((t,n)=>{const o=e(t),r=e(n);return o&&!r?-1:!o&&r?1:0}))}(s,u);return e[n]=r?R(l,{maxResults:i}):l.map((t=>[t])),e}),{})}(t,n),r=function(t,e){return function(t){const{selectors:e,combineBetweenSelectors:n,includeTag:o,maxCandidates:r}=t,i=n?R(e,{maxResults:r}):e.map((t=>[t]));return o?i.map(V):i}(e).map((e=>function(t,e){const n={};return t.forEach((t=>{const o=e[t];o.length>0&&(n[t]=o)})),function(t={}){let e=[];return Object.entries(t).forEach((([t,n])=>{e=n.flatMap((n=>0===e.length?[{[t]:n}]:e.map((e=>Object.assign(Object.assign({},e),{[t]:n})))))})),e}(n).map(Y)}(e,t))).filter((t=>t.length>0))}(o,n),i=p(r);return[...new Set(i)]}(t,o.root,o),n);for(const e of r)if(g(t,e,o.root))return e;return null}function G(t){return{value:t,include:!1}}function W({selectors:t,operator:n}){let o=[...w];t[e.tag]&&t[e.nthoftype]&&(o=o.filter((t=>t!==e.tag)));let r=\"\";return o.forEach((e=>{(t[e]||[]).forEach((({value:t,include:e})=>{e&&(r+=t)}))})),n.value+r}function H(n){return[\":root\",...y(n).reverse().map((n=>{const o=function(e,n,o=t.NONE){const r={};return n.forEach((t=>{Reflect.set(r,t,function(t,e){return F[e](t)}(e,t).map(G))})),{element:e,operator:N[o],selectors:r}}(n,[e.nthchild],t.DESCENDANT);return o.selectors.nthchild.forEach((t=>{t.include=!0})),o})).map(W)].join(\"\")}function U(t,n={}){const o=function(t){const e=(Array.isArray(t)?t:[t]).filter(r);return[...new Set(e)]}(t),i=function(t,n={}){const o=Object.assign(Object.assign({},c),n);return{selectors:(r=o.selectors,Array.isArray(r)?r.filter((t=>{return n=e,o=t,Object.values(n).includes(o);var n,o})):[]),whitelist:l(o.whitelist),blacklist:l(o.blacklist),root:f(o.root,t),combineWithinSelector:!!o.combineWithinSelector,combineBetweenSelectors:!!o.combineBetweenSelectors,includeTag:!!o.includeTag,maxCombinations:d(o.maxCombinations),maxCandidates:d(o.maxCandidates)};var r}(o[0],n);let u=\"\",s=i.root;function a(){return function(t,e,n=\"\",o){if(0===t.length)return null;const r=[t.length>1?t:[],...b(t,e).map((t=>[t]))];for(const t of r){const e=B(t,0,n,o);if(e)return{foundElements:t,selector:e}}return null}(o,s,u,i)}let m=a();for(;m;){const{foundElements:t,selector:e}=m;if(g(o,e,i.root))return e;s=t[0],u=e,m=a()}return o.length>1?o.map((t=>U(t,i))).join(\", \"):function(t){return t.map(H).join(\", \")}(o)}const z=U;return o})()));",
|
|
48
|
-
"text/javascript"
|
|
49
|
-
)));
|
|
50
|
-
|
|
51
|
-
// Note: we aren't blocking some of the events right now to try and be as nonintrusive as possible.
|
|
52
|
-
// For a more comprehensive implementation, see https://github.com/hackademix/noscript/blob/3a83c0e4a506f175e38b0342dad50cdca3eae836/src/content/syncFetchPolicy.js#L142
|
|
53
|
-
// The snippet of code at the beginning of this source is an attempt at defence against JS using persistent storage
|
|
54
|
-
const rBefore = (doc: Document) => scriptify(doc, cached("JS-Before", () => blobify(stripJS(`
|
|
55
|
-
const noop=()=>{},emptyObj={},emptyPromise=()=>Promise.resolve(void 0),fakeStorage={getItem:noop,setItem:noop,removeItem:noop,clear:noop,key:noop,length:0};["localStorage","sessionStorage"].forEach((e=>Object.defineProperty(window,e,{get:()=>fakeStorage,configurable:!0}))),Object.defineProperty(document,"cookie",{get:()=>"",set:noop,configurable:!0}),Object.defineProperty(window,"indexedDB",{get:()=>{},configurable:!0}),Object.defineProperty(window,"caches",{get:()=>emptyObj,configurable:!0}),Object.defineProperty(navigator,"storage",{get:()=>({persist:emptyPromise,persisted:emptyPromise,estimate:()=>Promise.resolve({quota:0,usage:0})}),configurable:!0}),Object.defineProperty(navigator,"serviceWorker",{get:()=>({register:emptyPromise,getRegistration:emptyPromise,ready:emptyPromise()}),configurable:!0});
|
|
56
|
-
|
|
57
|
-
window._readium_blockedEvents = [];
|
|
58
|
-
window._readium_blockEvents = true;
|
|
59
|
-
window._readium_eventBlocker = (e) => {
|
|
60
|
-
if(!window._readium_blockEvents) return;
|
|
61
|
-
e.preventDefault();
|
|
62
|
-
e.stopImmediatePropagation();
|
|
63
|
-
_readium_blockedEvents.push([
|
|
64
|
-
1, e, e.currentTarget || e.target
|
|
65
|
-
]);
|
|
66
|
-
};
|
|
67
|
-
window.addEventListener("DOMContentLoaded", window._readium_eventBlocker, true);
|
|
68
|
-
window.addEventListener("load", window._readium_eventBlocker, true);`
|
|
69
|
-
), "text/javascript")));
|
|
70
|
-
const rAfter = (doc: Document) => scriptify(doc, cached("JS-After", () => blobify(stripJS(`
|
|
71
|
-
if(window.onload) window.onload = new Proxy(window.onload, {
|
|
72
|
-
apply: function(target, receiver, args) {
|
|
73
|
-
if(!window._readium_blockEvents) {
|
|
74
|
-
Reflect.apply(target, receiver, args);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
_readium_blockedEvents.push([
|
|
78
|
-
0, target, receiver, args
|
|
79
|
-
]);
|
|
80
|
-
}
|
|
81
|
-
});`
|
|
82
|
-
), "text/javascript")));
|
|
3
|
+
import { Injector } from "../../injection/Injector";
|
|
83
4
|
|
|
84
5
|
const csp = (domains: string[]) => {
|
|
85
6
|
const d = domains.join(" ");
|
|
@@ -104,12 +25,22 @@ export default class FrameBlobBuider {
|
|
|
104
25
|
private readonly burl: string;
|
|
105
26
|
private readonly pub: Publication;
|
|
106
27
|
private readonly cssProperties?: { [key: string]: string };
|
|
107
|
-
|
|
108
|
-
|
|
28
|
+
private readonly injector: Injector | null = null;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
pub: Publication,
|
|
32
|
+
baseURL: string,
|
|
33
|
+
item: Link,
|
|
34
|
+
options: {
|
|
35
|
+
cssProperties?: { [key: string]: string };
|
|
36
|
+
injector?: Injector | null;
|
|
37
|
+
}
|
|
38
|
+
) {
|
|
109
39
|
this.pub = pub;
|
|
110
40
|
this.item = item;
|
|
111
41
|
this.burl = item.toURL(baseURL) || "";
|
|
112
|
-
this.cssProperties = cssProperties;
|
|
42
|
+
this.cssProperties = options.cssProperties;
|
|
43
|
+
this.injector = options.injector ?? null;
|
|
113
44
|
}
|
|
114
45
|
|
|
115
46
|
public async build(fxl = false): Promise<string> {
|
|
@@ -127,15 +58,23 @@ export default class FrameBlobBuider {
|
|
|
127
58
|
// Load the HTML resource
|
|
128
59
|
const txt = await this.pub.get(this.item).readAsString();
|
|
129
60
|
if(!txt) throw new Error(`Failed reading item ${this.item.href}`);
|
|
61
|
+
|
|
130
62
|
const doc = new DOMParser().parseFromString(
|
|
131
63
|
txt,
|
|
132
64
|
this.item.mediaType.string as DOMParserSupportedType
|
|
133
65
|
);
|
|
66
|
+
|
|
134
67
|
const perror = doc.querySelector("parsererror");
|
|
135
|
-
if(perror) {
|
|
68
|
+
if (perror) {
|
|
136
69
|
const details = perror.querySelector("div");
|
|
137
70
|
throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
|
|
138
71
|
}
|
|
72
|
+
|
|
73
|
+
// Apply resource injections if injection service is provided
|
|
74
|
+
if (this.injector) {
|
|
75
|
+
await this.injector.injectForDocument(doc, this.item);
|
|
76
|
+
}
|
|
77
|
+
|
|
139
78
|
return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, fxl, this.cssProperties);
|
|
140
79
|
}
|
|
141
80
|
|
|
@@ -150,29 +89,6 @@ export default class FrameBlobBuider {
|
|
|
150
89
|
return this.finalizeDOM(doc, this.pub.baseURL, this.burl, this.item.mediaType, true);
|
|
151
90
|
}
|
|
152
91
|
|
|
153
|
-
// Has JS that may have side-effects when the document is loaded, without any user interaction
|
|
154
|
-
private hasExecutable(doc: Document): boolean {
|
|
155
|
-
// This is not a 100% comprehensive check of all possibilities for JS execution,
|
|
156
|
-
// but it covers what the prevention scripts cover. Other possibilities include:
|
|
157
|
-
// - <iframe> src
|
|
158
|
-
// - <img> with onload/onerror
|
|
159
|
-
// - <meta http-equiv="refresh" content="xxx">
|
|
160
|
-
return (
|
|
161
|
-
!!doc.querySelector("script") || // Any <script> elements
|
|
162
|
-
!!doc.querySelector("body[onload]:not(body[onload=''])") // <body> that executes JS on load
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
private hasStyle(doc: Document): boolean {
|
|
167
|
-
if(
|
|
168
|
-
doc.querySelector("link[rel='stylesheet']") || // Any CSS link
|
|
169
|
-
doc.querySelector("style") || // Any <style> element
|
|
170
|
-
doc.querySelector("[style]:not([style=''])") // Any element with style attribute set
|
|
171
|
-
) return true;
|
|
172
|
-
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
92
|
private setProperties(cssProperties: { [key: string]: string }, doc: Document) {
|
|
177
93
|
for (const key in cssProperties) {
|
|
178
94
|
const value = cssProperties[key];
|
|
@@ -183,22 +99,20 @@ export default class FrameBlobBuider {
|
|
|
183
99
|
private finalizeDOM(doc: Document, root: string | undefined, base: string | undefined, mediaType: MediaType, fxl = false, cssProperties?: { [key: string]: string }): string {
|
|
184
100
|
if(!doc) return "";
|
|
185
101
|
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
rcssBefore.after(styleify(doc, cached("ReadiumCSS-default", () => blobify(stripCSS(readiumCSSDefault), "text/css"))))
|
|
102
|
+
// Get allowed domains from injector if it exists
|
|
103
|
+
const allowedDomains = this.injector?.getAllowedDomains?.() || [];
|
|
104
|
+
|
|
105
|
+
// Always include the root domain if provided
|
|
106
|
+
const domains = [...new Set([
|
|
107
|
+
...(root ? [root] : []),
|
|
108
|
+
...allowedDomains
|
|
109
|
+
])].filter(Boolean);
|
|
195
110
|
|
|
196
|
-
|
|
197
|
-
doc.head.appendChild(styleify(doc, cached("ReadiumCSS-after", () => blobify(stripCSS(readiumCSSAfter), "text/css"))));
|
|
111
|
+
// CSS and script injection is now handled by the Injector system
|
|
198
112
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
113
|
+
// Apply CSS properties if provided (only for reflowable)
|
|
114
|
+
if (cssProperties && !fxl) {
|
|
115
|
+
this.setProperties(cssProperties, doc);
|
|
202
116
|
}
|
|
203
117
|
|
|
204
118
|
// Set all <img> elements to high priority
|
|
@@ -263,20 +177,13 @@ export default class FrameBlobBuider {
|
|
|
263
177
|
doc.head.firstChild!.before(b);
|
|
264
178
|
}
|
|
265
179
|
|
|
266
|
-
//
|
|
267
|
-
const hasExecutable = this.hasExecutable(doc);
|
|
268
|
-
if (hasExecutable) doc.head.firstChild!.before(rBefore(doc));
|
|
269
|
-
doc.head.firstChild!.before(cssSelectorGenerator(doc)); // CSS selector utility
|
|
270
|
-
if (hasExecutable) doc.head.appendChild(rAfter(doc)); // Another execution prevention script
|
|
271
|
-
|
|
272
|
-
// Add CSP
|
|
180
|
+
// Add CSP with allowed domains
|
|
273
181
|
const meta = doc.createElement("meta");
|
|
274
182
|
meta.httpEquiv = "Content-Security-Policy";
|
|
275
|
-
meta.content = csp(
|
|
183
|
+
meta.content = csp(domains);
|
|
276
184
|
meta.dataset.readium = "true";
|
|
277
185
|
doc.head.firstChild!.before(meta);
|
|
278
186
|
|
|
279
|
-
|
|
280
187
|
// Make blob from doc
|
|
281
188
|
return URL.createObjectURL(
|
|
282
189
|
new Blob([new XMLSerializer().serializeToString(doc)], {
|
|
@@ -2,6 +2,7 @@ import { ModuleName } from "@readium/navigator-html-injectables";
|
|
|
2
2
|
import { Locator, Publication } from "@readium/shared";
|
|
3
3
|
import FrameBlobBuider from "./FrameBlobBuilder";
|
|
4
4
|
import { FrameManager } from "./FrameManager";
|
|
5
|
+
import { Injector } from "../../injection/Injector";
|
|
5
6
|
|
|
6
7
|
const UPPER_BOUNDARY = 5;
|
|
7
8
|
const LOWER_BOUNDARY = 3;
|
|
@@ -16,11 +17,18 @@ export class FramePoolManager {
|
|
|
16
17
|
private readonly inprogress: Map<string, Promise<void>> = new Map();
|
|
17
18
|
private pendingUpdates: Map<string, { inPool: boolean }> = new Map();
|
|
18
19
|
private currentBaseURL: string | undefined;
|
|
20
|
+
private readonly injector: Injector | null = null;
|
|
19
21
|
|
|
20
|
-
constructor(
|
|
22
|
+
constructor(
|
|
23
|
+
container: HTMLElement,
|
|
24
|
+
positions: Locator[],
|
|
25
|
+
cssProperties?: { [key: string]: string },
|
|
26
|
+
injector?: Injector | null
|
|
27
|
+
) {
|
|
21
28
|
this.container = container;
|
|
22
29
|
this.positions = positions;
|
|
23
30
|
this.currentCssProperties = cssProperties;
|
|
31
|
+
this.injector = injector ?? null;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
async destroy() {
|
|
@@ -47,7 +55,13 @@ export class FramePoolManager {
|
|
|
47
55
|
this.pool.clear();
|
|
48
56
|
|
|
49
57
|
// Revoke all blobs
|
|
50
|
-
this.blobs.forEach(v =>
|
|
58
|
+
this.blobs.forEach(v => {
|
|
59
|
+
this.injector?.releaseBlobUrl?.(v);
|
|
60
|
+
URL.revokeObjectURL(v);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Clean up injector if it exists
|
|
64
|
+
this.injector?.dispose();
|
|
51
65
|
|
|
52
66
|
// Empty container of elements
|
|
53
67
|
this.container.childNodes.forEach(v => {
|
|
@@ -90,7 +104,10 @@ export class FramePoolManager {
|
|
|
90
104
|
// Check if base URL of publication has changed
|
|
91
105
|
if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
|
|
92
106
|
// Revoke all blobs
|
|
93
|
-
this.blobs.forEach(v =>
|
|
107
|
+
this.blobs.forEach(v => {
|
|
108
|
+
this.injector?.releaseBlobUrl?.(v);
|
|
109
|
+
URL.revokeObjectURL(v);
|
|
110
|
+
});
|
|
94
111
|
this.blobs.clear();
|
|
95
112
|
}
|
|
96
113
|
this.currentBaseURL = pub.baseURL;
|
|
@@ -103,13 +120,17 @@ export class FramePoolManager {
|
|
|
103
120
|
// when navigating backwards, where paginated will go the
|
|
104
121
|
// start of the resource instead of the end due to the
|
|
105
122
|
// corrupted width ColumnSnapper (injectables) gets on init
|
|
106
|
-
this.blobs.forEach(v =>
|
|
123
|
+
this.blobs.forEach(v => {
|
|
124
|
+
this.injector?.releaseBlobUrl?.(v);
|
|
125
|
+
URL.revokeObjectURL(v);
|
|
126
|
+
});
|
|
107
127
|
this.blobs.clear();
|
|
108
128
|
this.pendingUpdates.clear();
|
|
109
129
|
}
|
|
110
130
|
if(this.pendingUpdates.has(href) && this.pendingUpdates.get(href)?.inPool === false) {
|
|
111
131
|
const url = this.blobs.get(href);
|
|
112
132
|
if(url) {
|
|
133
|
+
this.injector?.releaseBlobUrl?.(url);
|
|
113
134
|
URL.revokeObjectURL(url);
|
|
114
135
|
this.blobs.delete(href);
|
|
115
136
|
this.pendingUpdates.delete(href);
|
|
@@ -129,7 +150,15 @@ export class FramePoolManager {
|
|
|
129
150
|
const itm = pub.readingOrder.findWithHref(href);
|
|
130
151
|
if(!itm) return; // TODO throw?
|
|
131
152
|
if(!this.blobs.has(href)) {
|
|
132
|
-
const blobBuilder = new FrameBlobBuider(
|
|
153
|
+
const blobBuilder = new FrameBlobBuider(
|
|
154
|
+
pub,
|
|
155
|
+
this.currentBaseURL || "",
|
|
156
|
+
itm,
|
|
157
|
+
{
|
|
158
|
+
cssProperties: this.currentCssProperties,
|
|
159
|
+
injector: this.injector
|
|
160
|
+
}
|
|
161
|
+
);
|
|
133
162
|
const blobURL = await blobBuilder.build();
|
|
134
163
|
this.blobs.set(href, blobURL);
|
|
135
164
|
}
|
|
@@ -6,6 +6,7 @@ import { FXLFrameManager } from "./FXLFrameManager";
|
|
|
6
6
|
import { FXLPeripherals } from "./FXLPeripherals";
|
|
7
7
|
import { FXLSpreader, Orientation, Spread } from "./FXLSpreader";
|
|
8
8
|
import { VisualNavigatorViewport } from "../../Navigator";
|
|
9
|
+
import { Injector } from "../../injection/Injector";
|
|
9
10
|
|
|
10
11
|
const UPPER_BOUNDARY = 8;
|
|
11
12
|
const LOWER_BOUNDARY = 5;
|
|
@@ -26,6 +27,7 @@ export class FXLFramePoolManager {
|
|
|
26
27
|
private readonly delayedTimeout: Map<string, number> = new Map();
|
|
27
28
|
private currentBaseURL: string | undefined;
|
|
28
29
|
private previousFrames: FXLFrameManager[] = [];
|
|
30
|
+
private readonly injector: Injector | null = null;
|
|
29
31
|
|
|
30
32
|
// NEW
|
|
31
33
|
private readonly bookElement: HTMLDivElement;
|
|
@@ -44,10 +46,16 @@ export class FXLFramePoolManager {
|
|
|
44
46
|
// private readonly pages: FXLFrameManager[] = [];
|
|
45
47
|
public readonly peripherals: FXLPeripherals;
|
|
46
48
|
|
|
47
|
-
constructor(
|
|
49
|
+
constructor(
|
|
50
|
+
container: HTMLElement,
|
|
51
|
+
positions: Locator[],
|
|
52
|
+
pub: Publication,
|
|
53
|
+
injector?: Injector | null
|
|
54
|
+
) {
|
|
48
55
|
this.container = container;
|
|
49
56
|
this.positions = positions;
|
|
50
57
|
this.pub = pub;
|
|
58
|
+
this.injector = injector ?? null;
|
|
51
59
|
this.spreadPresentation = pub.metadata.otherMetadata?.spread || Spread.auto;
|
|
52
60
|
|
|
53
61
|
if(this.pub.metadata.effectiveReadingProgression !== ReadingProgression.rtl && this.pub.metadata.effectiveReadingProgression !== ReadingProgression.ltr)
|
|
@@ -393,6 +401,9 @@ export class FXLFramePoolManager {
|
|
|
393
401
|
// Revoke all blobs
|
|
394
402
|
this.blobs.forEach(v => URL.revokeObjectURL(v));
|
|
395
403
|
|
|
404
|
+
// Clean up injector if it exists
|
|
405
|
+
this.injector?.dispose();
|
|
406
|
+
|
|
396
407
|
// Empty container of elements
|
|
397
408
|
this.container.childNodes.forEach(v => {
|
|
398
409
|
if(v.nodeType === Node.ELEMENT_NODE || v.nodeType === Node.TEXT_NODE) v.remove();
|
|
@@ -495,7 +506,14 @@ export class FXLFramePoolManager {
|
|
|
495
506
|
const itm = pub.readingOrder.items[index];
|
|
496
507
|
if(!itm) return; // TODO throw?
|
|
497
508
|
if(!this.blobs.has(href)) {
|
|
498
|
-
const blobBuilder = new FrameBlobBuider(
|
|
509
|
+
const blobBuilder = new FrameBlobBuider(
|
|
510
|
+
pub,
|
|
511
|
+
this.currentBaseURL || "",
|
|
512
|
+
itm,
|
|
513
|
+
{
|
|
514
|
+
injector: this.injector
|
|
515
|
+
}
|
|
516
|
+
);
|
|
499
517
|
const blobURL = await blobBuilder.build(true);
|
|
500
518
|
this.blobs.set(href, blobURL);
|
|
501
519
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for processing CSS and JavaScript content
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minifies JavaScript by removing comments and normalizing whitespace
|
|
7
|
+
*/
|
|
8
|
+
export const stripJS = (source: string) => source.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\n/g, "").replace(/\s+/g, " ");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Minifies CSS by removing comments and normalizing whitespace
|
|
12
|
+
* Note: URL resolution should be handled by the caller with correct context
|
|
13
|
+
*/
|
|
14
|
+
export const stripCSS = (source: string) => source.replace(/\/\*(?:(?!\*\/)[\s\S])*\*\/|[\r\n\t]+/g, "").replace(/ {2,}/g, " ");
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Link } from "@readium/shared";
|
|
2
|
+
|
|
3
|
+
type ForbiddenAttributes = "type" | "rel" | "href" | "src";
|
|
4
|
+
type AllowedAttributes = {
|
|
5
|
+
[K in string]: K extends ForbiddenAttributes
|
|
6
|
+
? never
|
|
7
|
+
: (string | boolean | undefined);
|
|
8
|
+
} & {
|
|
9
|
+
[K in ForbiddenAttributes]?: never;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface IBaseInjectable {
|
|
13
|
+
id?: string;
|
|
14
|
+
target?: "head" | "body";
|
|
15
|
+
type?: string;
|
|
16
|
+
condition?: (doc: Document) => boolean;
|
|
17
|
+
|
|
18
|
+
// Extra attributes - type and rel are forbidden here since they are at root
|
|
19
|
+
attributes?: AllowedAttributes;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IScriptInjectable extends IBaseInjectable {
|
|
23
|
+
as: "script";
|
|
24
|
+
rel?: never; // Scripts don't have rel
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ILinkInjectable extends IBaseInjectable {
|
|
28
|
+
as: "link";
|
|
29
|
+
rel: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface IUrlInjectable {
|
|
33
|
+
url: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface IBlobInjectable {
|
|
37
|
+
blob: Blob;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type IInjectable = (IScriptInjectable | ILinkInjectable) & (IUrlInjectable | IBlobInjectable);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Defines a rule for resource injection, specifying which resources to inject into which documents.
|
|
44
|
+
*/
|
|
45
|
+
export interface IInjectableRule {
|
|
46
|
+
/**
|
|
47
|
+
* List of resource URLs or patterns that this rule applies to.
|
|
48
|
+
* Can be exact URLs or patterns with wildcards.
|
|
49
|
+
*/
|
|
50
|
+
resources: Array<string | RegExp>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resources to inject at the beginning of the target (in array order)
|
|
54
|
+
*/
|
|
55
|
+
prepend?: IInjectable[];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resources to inject at the end of the target (in array order)
|
|
59
|
+
*/
|
|
60
|
+
append?: IInjectable[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface IInjectablesConfig {
|
|
64
|
+
rules: IInjectableRule[];
|
|
65
|
+
allowedDomains?: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface IInjector {
|
|
69
|
+
/**
|
|
70
|
+
* Injects resources into a document based on matching rules
|
|
71
|
+
* @param doc The document to inject resources into
|
|
72
|
+
* @param link The link being loaded, used to match against injection rules
|
|
73
|
+
*/
|
|
74
|
+
injectForDocument(doc: Document, link: Link): Promise<void>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Cleans up any resources used by the injector
|
|
78
|
+
*/
|
|
79
|
+
dispose(): void;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the list of allowed domains
|
|
83
|
+
*/
|
|
84
|
+
getAllowedDomains(): string[]
|
|
85
|
+
}
|