chore(memory): record OPDS HTML description and JSON search notes (#4516)

Persist pending agent memory notes for the recently merged OPDS fixes:
- OPDS HTML description rendering (#4503 / PR #4510), including the
  sanitizeHtml consolidation into @/utils/sanitize.
- OPDS 2.0 JSON catalog search (#4502 / PR #4509).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-10 13:17:11 +08:00
committed by GitHub
parent 31ebf4b586
commit 7e5c74f5ef
3 changed files with 71 additions and 0 deletions
@@ -42,6 +42,8 @@
## Feature Notes
- [OPDS Firefox strict-XML parse (#4479)](opds-firefox-strict-xml-4479.md) — MEK feed has junk after `</feed>`; Firefox DOMParser → `<parsererror>` (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 `<p>`/`&quot;` 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/<bundle>` 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)
@@ -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 `<p>`, `</p>`, `&quot;`, `&#x27;` 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"` `<summary>` 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 `"&lt;p&gt;...&amp;quot;Wall&amp;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
`<p>`/`&quot;` 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**
(`/&lt;\/?[a-z]/i` present AND no real `/<[a-z]/i` tag — so mixed content like
`<p>see &lt;code&gt;</p>` is left literal), then `sanitizeHtml()` (the shared
DOMPurify sanitizer in `@/utils/sanitize` — generic, reused; note its
ALLOWED_TAGS has no `div`, so xhtml's `<div xmlns>` 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 `&copy;` literally); `dangerouslySetInnerHTML` decodes one level (shows
literal `<p>` only if value is `&lt;p&gt;`). The screenshot showed literal `<p>`
AND `&quot;` → double-escaped innerHTML path, not the dead `description` path
(foliate's `getPublication` never sets `metadata.description`). Related:
[[opds-firefox-strict-xml-4479]].
@@ -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 → `<input disabled={!hasSearch}>`. `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]].