diff --git a/apps/readest-app/.claude/memory/MEMORY.md b/apps/readest-app/.claude/memory/MEMORY.md index c685915d..6741cb20 100644 --- a/apps/readest-app/.claude/memory/MEMORY.md +++ b/apps/readest-app/.claude/memory/MEMORY.md @@ -42,6 +42,8 @@ ## Feature Notes - [OPDS Firefox strict-XML parse (#4479)](opds-firefox-strict-xml-4479.md) — MEK feed has junk after ``; Firefox DOMParser → `` (silent back-nav), Chrome lenient; `parseOPDSXML` slices root start→last close tag; jsdom mirrors Firefox; wired into page.tsx + validateOPDSURL + feedChecker (latter also #4181 `looksLikeXMLContent` swap) +- [OPDS 2.0 JSON search greyed out (#4502)](opds2-json-search-4502.md) — `isSearchLink` ignored templated `application/opds+json` links → `hasSearch` false → disabled navbar input; add `MIME.OPDS2`+`templated`, `expandOPDSSearchTemplate` (foliate `uri-template.js`), handleSearch OPDS2 branch. Gotcha: `resolveURL` mangles `{?query}` braces — expand template BEFORE resolving +- [OPDS HTML description (#4503)](opds-html-description-4503.md) — detail-view descriptions showed raw `

`/`"` tags; aggregator double-escapes `type="text"` summary + `PublicationView` dumped it into unsanitized `dangerouslySetInnerHTML`; fix = `getOPDSDescriptionHtml` (decode-one-level-iff-fully-escaped, then `sanitizeHtml`) - [Manage Cache + iOS container layout](manage-cache-ios-layout.md) — `'Cache'` base = `Library/Caches/` only (not all of Caches); iOS `Documents/Inbox` cleared too; WebKit cache + tmp out of reach; never touch App Support - [D-pad Navigation](dpad-navigation.md) — Android TV remote / keyboard arrow navigation design, key files, and pitfalls - [Cloudflare Workers WebSocket](cloudflare-workers-websocket.md) — use fetch() Upgrade pattern (not `ws` npm); CF delivers binary frames as Blob (must serialize async decodes) diff --git a/apps/readest-app/.claude/memory/opds-html-description-4503.md b/apps/readest-app/.claude/memory/opds-html-description-4503.md new file mode 100644 index 00000000..f6a7779d --- /dev/null +++ b/apps/readest-app/.claude/memory/opds-html-description-4503.md @@ -0,0 +1,46 @@ +--- +name: opds-html-description-4503 +description: "OPDS publication descriptions showed raw HTML tags; double-escaped type=\"text\" summaries + unsanitized innerHTML; fix decodes-if-fully-escaped then sanitizes" +metadata: + node_type: memory + type: project + originSessionId: 73fd2a21-ea89-4cdb-bbbd-23256a6ae5a2 +--- + +Issue #4503 (FR, but a real bug): OPDS publication detail descriptions rendered +raw HTML — literal `

`, `

`, `"`, `'` text — while Thorium renders +them. Reporter's feed served the same Gutenberg book (51726) via an aggregator. + +**Root cause** (confirmed empirically by running foliate `getPublication` in jsdom): +the aggregator serves the description as an Atom `type="text"` `` whose +HTML was escaped *twice*. foliate's `getContent` (`packages/foliate-js/opds.js`) +only un-escapes for `type="html"`/`"xhtml"`; for `text` it returns `textContent` +verbatim, so the value stays `"<p>...&quot;Wall&quot;..."`. +`PublicationView` then dumped `content.value` straight into +`dangerouslySetInnerHTML` (also **unsanitized — an XSS sink** for arbitrary +remote feeds). The browser decodes one entity level → shows the still-escaped +`

`/`"` as literal text. Single-escaped `type="text"` renders fine (value +already has real tags); only *double*-escaped breaks. type=html/xhtml render fine. + +**Fix:** new helper `src/app/opds/utils/opdsContent.ts` `getOPDSDescriptionHtml()`: +decode one extra entity level **only when the value is entirely escaped markup** +(`/<\/?[a-z]/i` present AND no real `/<[a-z]/i` tag — so mixed content like +`

see <code>

` is left literal), then `sanitizeHtml()` (the shared +DOMPurify sanitizer in `@/utils/sanitize` — generic, reused; note its +ALLOWED_TAGS has no `div`, so xhtml's `
` wrapper is unwrapped, +harmless). Wired into `PublicationView` via `useMemo`. Decode-then-sanitize +order matters: scripts hidden behind double-escaping are still stripped. +(PR #4510 also moved `sanitizeHtml`/`sanitizeForParsing` out of +`services/send/conversion/` into `@/utils/sanitize`, alongside `sanitizeString`.) +Exported `OPDSContent` from `types/opds.ts` for the helper's param. + +Scope: only the detail view renders description HTML. `PublicationCard` shows no +summary; `NavigationCard` renders `SYMBOL.SUMMARY` as React-escaped plain text +(getSummary only returns `type==='text'` values) — both correct, untouched. + +Verifying which render path produces "raw tags": React `{value}` text-escapes +(shows `©` literally); `dangerouslySetInnerHTML` decodes one level (shows +literal `

` only if value is `<p>`). The screenshot showed literal `

` +AND `"` → double-escaped innerHTML path, not the dead `description` path +(foliate's `getPublication` never sets `metadata.description`). Related: +[[opds-firefox-strict-xml-4479]]. diff --git a/apps/readest-app/.claude/memory/opds2-json-search-4502.md b/apps/readest-app/.claude/memory/opds2-json-search-4502.md new file mode 100644 index 00000000..718cd6d2 --- /dev/null +++ b/apps/readest-app/.claude/memory/opds2-json-search-4502.md @@ -0,0 +1,23 @@ +--- +name: opds2-json-search-4502 +description: "OPDS 2.0 JSON catalog search bar greyed out; isSearchLink didn't recognize templated application/opds+json links" +metadata: + node_type: memory + type: project + originSessionId: 9eb835a8-ce7a-4f80-ae3d-94e330935585 +--- + +#4502 — OPDS 2.0 JSON catalogs (e.g. `type: "application/opds+json"`, `templated: true`, href `/opds/search{?query}`) showed a **greyed-out** navbar search input that rejected queries. + +**Root cause:** `isSearchLink` (`src/app/opds/utils/opdsUtils.ts`) only matched `MIME.OPENSEARCH`/`MIME.ATOM`, so `hasSearch` (page.tsx) was false → ``. `handleSearch` also only handled those two types. + +**Fix:** +- Add `MIME.OPDS2 = 'application/opds+json'` + `templated?: boolean` on `OPDSBaseLink`; `isSearchLink` now also accepts `type === OPDS2 && !!templated`. +- New `expandOPDSSearchTemplate(templateHref, queryTerm)` in opdsUtils expands the RFC 6570 template, placing the term in the primary text var (`query`/`searchTerms`/`q`, else first var). Reuses `foliate-js/uri-template.js` (`replace`, `getVariables`) — do NOT reinvent RFC 6570. +- page.tsx `handleSearch` adds an `OPDS2` branch. + +**Gotcha (key):** `resolveURL` mangles `{?query}` template braces (`/opds/search%7B?query}`) — it treats `?` as the query start. ALWAYS `expandOPDSSearchTemplate` FIRST, THEN `resolveURL`. For OPENSEARCH/ATOM the order is reversed (resolve then `.replace('{searchTerms}', ...)`), which is why the new branch can't share the top-level `searchURL`. + +**Foliate has `getSearch(link)`** (async, OPDS 2.0 JSON → OPDSSearch via uri-template) but readest's page.tsx never wired it; the JSON path just `JSON.parse`s the feed, preserving raw `templated`/`type`. OPDS 2.0 is JSON-only (XML `getFeed` links never carry `templated`). + +Related: [[opds-firefox-strict-xml-4479]].