feat(reader): upgrade epubjs & add restrictive iframe CSP

This commit is contained in:
Evan Reichard 2024-02-19 16:45:35 -05:00
parent 5865fe3c13
commit da1baeb4cd
3 changed files with 100 additions and 101 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />

View File

@ -97,16 +97,18 @@ class EBookReader {
flow: "paginated", flow: "paginated",
width: "100%", width: "100%",
height: "100%", height: "100%",
allowScriptedContent: true,
}); });
// Setup Reader // Setup Reader
this.book.ready.then(this.setupReader.bind(this)); this.book.ready.then(this.setupReader.bind(this));
// Initialize // Initialize
this.initCSP();
this.initDevice(); this.initDevice();
this.initWakeLock(); this.initWakeLock();
this.initThemes(); this.initThemes();
this.initRenditionListeners(); this.initViewerListeners();
this.initDocumentListeners(); this.initDocumentListeners();
} }
@ -279,6 +281,36 @@ class EBookReader {
); );
} }
/**
* EpubJS will set iframe sandbox when settings "allowScriptedContent: false".
* However, Safari completely blocks us from attaching listeners to the iframe
* document. So instead we just inject a restrictive CSP rule.
*
* This effectively blocks all script content within the iframe while still
* allowing us to attach listeners to the iframe document.
**/
initCSP() {
// Derive CSP Host
var protocol = document.location.protocol;
var host = document.location.host;
var cspURL = `${protocol}//${host}`;
// Add CSP Policy
this.book.spine.hooks.content.register((output, section) => {
let cspWrapper = document.createElement("div");
cspWrapper.innerHTML = `
<meta
http-equiv="Content-Security-Policy"
content="require-trusted-types-for 'script';
style-src 'self' blob: 'unsafe-inline' ${cspURL};
object-src 'none';
script-src 'none';"
>`;
let cspMeta = cspWrapper.children[0];
output.head.append(cspMeta);
});
}
/** /**
* Set theme & meta theme color * Set theme & meta theme color
**/ **/
@ -371,9 +403,9 @@ class EBookReader {
} }
/** /**
* Rendition hooks * Viewer Listeners
**/ **/
initRenditionListeners() { initViewerListeners() {
/** /**
* Initiate the debounce when the given function returns true. * Initiate the debounce when the given function returns true.
* Don't run it again until the timeout lapses. * Don't run it again until the timeout lapses.
@ -401,56 +433,17 @@ class EBookReader {
let bottomBar = document.querySelector("#bottom-bar"); let bottomBar = document.querySelector("#bottom-bar");
// Local Functions // Local Functions
let getCFIFromXPath = this.getCFIFromXPath.bind(this);
let setPosition = this.setPosition.bind(this);
let nextPage = this.nextPage.bind(this); let nextPage = this.nextPage.bind(this);
let prevPage = this.prevPage.bind(this); let prevPage = this.prevPage.bind(this);
let saveSettings = this.saveSettings.bind(this);
// Local Vars
let readerSettings = this.readerSettings;
let bookState = this.bookState;
this.rendition.hooks.render.register(function (doc, data) {
let renderDoc = doc.document;
// ------------------------------------------------ // // ------------------------------------------------ //
// ---------------- Wake Lock Hack ---------------- // // ----------------- Swipe Helpers ---------------- //
// ------------------------------------------------ //
let wakeLockListener = function () {
doc.window.parent.document.dispatchEvent(new CustomEvent("wakelock"));
};
renderDoc.addEventListener("click", wakeLockListener);
renderDoc.addEventListener("gesturechange", wakeLockListener);
renderDoc.addEventListener("touchstart", wakeLockListener);
// ------------------------------------------------ //
// --------------- Swipe Pagination --------------- //
// ------------------------------------------------ // // ------------------------------------------------ //
let touchStartX, let touchStartX,
touchStartY, touchStartY,
touchEndX, touchEndX,
touchEndY = undefined; touchEndY = undefined;
renderDoc.addEventListener(
"touchstart",
function (event) {
touchStartX = event.changedTouches[0].screenX;
touchStartY = event.changedTouches[0].screenY;
},
false,
);
renderDoc.addEventListener(
"touchend",
function (event) {
touchEndX = event.changedTouches[0].screenX;
touchEndY = event.changedTouches[0].screenY;
handleGesture(event);
},
false,
);
function handleGesture(event) { function handleGesture(event) {
let drasticity = 75; let drasticity = 75;
@ -476,8 +469,32 @@ class EBookReader {
} }
} }
function handleSwipeDown() {
if (bottomBar.classList.contains("bottom-0"))
bottomBar.classList.remove("bottom-0");
else topBar.classList.add("top-0");
}
function handleSwipeUp() {
if (topBar.classList.contains("top-0")) topBar.classList.remove("top-0");
else bottomBar.classList.add("bottom-0");
}
this.rendition.hooks.render.register(function (doc, data) {
let renderDoc = doc.document;
// ------------------------------------------------ // // ------------------------------------------------ //
// --------------- Bottom & Top Bar --------------- // // ---------------- Wake Lock Hack ---------------- //
// ------------------------------------------------ //
let wakeLockListener = function () {
renderDoc.dispatchEvent(new CustomEvent("wakelock"));
};
renderDoc.addEventListener("click", wakeLockListener);
renderDoc.addEventListener("gesturechange", wakeLockListener);
renderDoc.addEventListener("touchstart", wakeLockListener);
// ------------------------------------------------ //
// --------------- Bars & Page Turn --------------- //
// ------------------------------------------------ // // ------------------------------------------------ //
renderDoc.addEventListener( renderDoc.addEventListener(
"click", "click",
@ -529,45 +546,25 @@ class EBookReader {
}, 400), }, 400),
); );
function handleSwipeDown() {
if (bottomBar.classList.contains("bottom-0"))
bottomBar.classList.remove("bottom-0");
else topBar.classList.add("top-0");
}
function handleSwipeUp() {
if (topBar.classList.contains("top-0"))
topBar.classList.remove("top-0");
else bottomBar.classList.add("bottom-0");
}
// ------------------------------------------------ // // ------------------------------------------------ //
// -------------- Keyboard Shortcuts -------------- // // ------------------- Gestures ------------------- //
// ------------------------------------------------ // // ------------------------------------------------ //
renderDoc.addEventListener( renderDoc.addEventListener(
"keyup", "touchstart",
function (e) { function (event) {
// Left Key (Previous Page) touchStartX = event.changedTouches[0].screenX;
if ((e.keyCode || e.which) == 37) { touchStartY = event.changedTouches[0].screenY;
prevPage(); },
} false,
// Right Key (Next Page)
if ((e.keyCode || e.which) == 39) {
nextPage();
}
// "t" Key (Theme Cycle)
if ((e.keyCode || e.which) == 84) {
let currentThemeIdx = THEMES.indexOf(
readerSettings.theme.colorScheme,
); );
let colorScheme =
THEMES.length == currentThemeIdx + 1 renderDoc.addEventListener(
? THEMES[0] "touchend",
: THEMES[currentThemeIdx + 1]; function (event) {
setTheme({ colorScheme }); touchEndX = event.changedTouches[0].screenX;
} touchEndY = event.changedTouches[0].screenY;
handleGesture(event);
}, },
false, false,
); );
@ -584,7 +581,9 @@ class EBookReader {
let nextPage = this.nextPage.bind(this); let nextPage = this.nextPage.bind(this);
let prevPage = this.prevPage.bind(this); let prevPage = this.prevPage.bind(this);
// Keyboard Shortcuts // ------------------------------------------------ //
// -------------- Keyboard Shortcuts -------------- //
// ------------------------------------------------ //
document.addEventListener( document.addEventListener(
"keyup", "keyup",
function (e) { function (e) {