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:
@@ -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>`/`"` 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>`, `"`, `'` 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 `"<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
|
||||
`<p>`/`"` 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
|
||||
`<p>see <code></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 `©` literally); `dangerouslySetInnerHTML` decodes one level (shows
|
||||
literal `<p>` only if value is `<p>`). The screenshot showed literal `<p>`
|
||||
AND `"` → 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]].
|
||||
Reference in New Issue
Block a user