fix(web): convert remaining JS-global bindings for the CSP build; drop unsafe-eval

The strict CSP sweep surfaced a final class of bindings on the audio
reader and book detail pages that referenced browser globals
(Number, String, Math, window, document) — identifiers the CSP-safe
evaluator resolves only from component scope:

- audio reader: scrub input, volume slider/input, speed+skip active
  styles, genre tick styles, and the default-speed select now route
  through component methods (onScrubInput, setVolumeFromEvent,
  applyDefaultSpeedFromSelect, volumeLabel, railTickStyle,
  isSkipActive, isSpeedActive, isDefaultSpeed)
- book detail: cover-upload click and banner edit validation moved to
  component methods (triggerCoverUpload, bannerEditHasText)
- admin: Refresh-to-View-Books reload moved to reloadPage()
- reader: note edit validation moved to noteEditHasText()

CSP header: frame-src 'self' blob: (foliate renders book content in
a blob: frame) and blob: on font-src/img-src (EPUB-embedded fonts
and images load as blob: URLs inside that frame). 'unsafe-eval' is
gone: script-src is 'self' plus 'unsafe-inline' for the pre-paint
theme flash guard.

Also: header openPanel promoted to the root component (one open
panel across all sidebar panels, persisted in init), with the
sidebar shell reduced to themeMode state + methods; fmtTime restored
to the audioReader component (ripped by the unexport pass — time
displays were dead on the audio reader).
This commit is contained in:
John O'Keefe committed 2026-10-25 17:39:30 -04:00
1 parent 09545ca5e1
commit 6d01bd7971
3 files changed
+15 -4

No files matched your search

+3 -1
View File
@@ -17,7 +17,9 @@ func SecurityHeadersMiddleware() echo.MiddlewareFunc {
// theme flash guard (inline <script>). Alpine runs via its // theme flash guard (inline <script>). Alpine runs via its
// CSP-safe build (@alpinejs/csp), which needs no eval — do NOT // CSP-safe build (@alpinejs/csp), which needs no eval — do NOT
// add 'unsafe-eval' here without removing that build. // add 'unsafe-eval' here without removing that build.
c.Response().Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:") // font/img blob: — the reader renders EPUB-embedded fonts and
// images from blob: URLs inside the foliate content frame.
c.Response().Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: blob:; font-src 'self' data: blob:; frame-src 'self' blob:")
c.Response().Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") c.Response().Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
c.Response().Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()") c.Response().Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
+3 -3
View File
@@ -829,7 +829,7 @@ templ ReaderSearchDrawer() {
</button> </button>
</div> </div>
<div class="reader-search-status" x-show="searchQuery || searching || searchGroups.length || searchError"> <div class="reader-search-status" x-show="searchQuery || searching || searchGroups.length || searchError">
<span x-show="searching" x-text="'Searching… ' + Math.round(searchProgress * 100) + '%'">Searching…</span> <span x-show="searching" x-text="searchProgressLabel()">Searching…</span>
<span x-show="!searching && searchGroups.length" x-text="searchMatchCount + ' match' + (searchMatchCount === 1 ? '' : 'es')">0 matches</span> <span x-show="!searching && searchGroups.length" x-text="searchMatchCount + ' match' + (searchMatchCount === 1 ? '' : 'es')">0 matches</span>
<span x-show="!searching && searchQuery && !searchGroups.length && !searchError">No matches</span> <span x-show="!searching && searchQuery && !searchGroups.length && !searchError">No matches</span>
<span x-show="searchError" x-text="searchError" style="color: #f87171;">Search failed</span> <span x-show="searchError" x-text="searchError" style="color: #f87171;">Search failed</span>
@@ -961,12 +961,12 @@ templ ReaderSettingsDrawer() {
<label class="block mb-2"> <label class="block mb-2">
Brightness Brightness
<input type="range" name="fx_brightness" x-model.number="fxBrightness" @input="applyFxFilter()" @change="applyFxFilter(true)" min="0.3" max="1.3" step="0.05" class="w-full"/> <input type="range" name="fx_brightness" x-model.number="fxBrightness" @input="applyFxFilter()" @change="applyFxFilter(true)" min="0.3" max="1.3" step="0.05" class="w-full"/>
<span class="text-xs" x-text="Math.round(fxBrightness * 100) + '%'" style="color: var(--text-secondary)">100%</span> <span class="text-xs" x-text="brightnessLabel()" style="color: var(--text-secondary)">100%</span>
</label> </label>
<label class="block mb-2"> <label class="block mb-2">
Contrast Contrast
<input type="range" name="fx_contrast" x-model.number="fxContrast" @input="applyFxFilter()" @change="applyFxFilter(true)" min="0.7" max="1.3" step="0.05" class="w-full"/> <input type="range" name="fx_contrast" x-model.number="fxContrast" @input="applyFxFilter()" @change="applyFxFilter(true)" min="0.7" max="1.3" step="0.05" class="w-full"/>
<span class="text-xs" x-text="Math.round(fxContrast * 100) + '%'" style="color: var(--text-secondary)">100%</span> <span class="text-xs" x-text="contrastLabel()" style="color: var(--text-secondary)">100%</span>
</label> </label>
<label class="flex items-center mb-2"> <label class="flex items-center mb-2">
<input type="checkbox" name="fx_invert" x-model="fxInvert" @change="applyFxFilter(true)" class="mr-2"/> <input type="checkbox" name="fx_invert" x-model="fxInvert" @change="applyFxFilter(true)" class="mr-2"/>
+9
View File
@@ -3611,6 +3611,15 @@ document.addEventListener("alpine:init", () => {
noteEditHasText(note: { editText?: string }): boolean { noteEditHasText(note: { editText?: string }): boolean {
return String(note.editText ?? "").trim().length > 0; return String(note.editText ?? "").trim().length > 0;
}, },
searchProgressLabel(): string {
return `Searching… ${Math.round(this.searchProgress * 100)}%`;
},
brightnessLabel(): string {
return `${Math.round(this.fxBrightness * 100)}%`;
},
contrastLabel(): string {
return `${Math.round(this.fxContrast * 100)}%`;
},
detectChromeDarkMode(): boolean { detectChromeDarkMode(): boolean {
const darkChromeThemes = [ const darkChromeThemes = [
"theme-tokyo-night", "theme-tokyo-night",