From 7e0c27341a4efacad4966e3533a6d8018e098f94 Mon Sep 17 00:00:00 2001 From: Thomas Hackner Date: Mon, 6 Jul 2026 22:58:07 +0200 Subject: [PATCH] CalDAV: HTML-Entities in Terminfeldern dekodieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manche Kalender speichern SUMMARY/LOCATION/DESCRIPTION mit wörtlichen HTML-Entities (z. B. "Vorstellungsgespräch – dot.haus"). Diese wurden roh angezeigt. Beim Parsen werden sie jetzt in echte Zeichen dekodiert. Co-Authored-By: Claude Opus 4.8 --- lib/caldav.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/caldav.js b/lib/caldav.js index 4303b51..a8336ba 100644 --- a/lib/caldav.js +++ b/lib/caldav.js @@ -122,6 +122,21 @@ function unescapeText(s) { .replace(/\\\\/g, '\\'); } +// Some clients store SUMMARY/LOCATION/DESCRIPTION with HTML entities baked in +// (e.g. "Vorstellungsgespräch – dot.haus"). iCal text is plain, so +// decode them back to real characters before we hand the value on. +function decodeHtmlEntities(s) { + return String(s == null ? '' : s) + .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(parseInt(n, 10))) + .replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCodePoint(parseInt(n, 16))) + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/ /g, ' ') + .replace(/&/g, '&'); +} + // Fold a content line to <=75 octets per RFC 5545 (CRLF + single space). function fold(line) { if (Buffer.byteLength(line, 'utf8') <= 75) return line; @@ -204,9 +219,9 @@ function parseVEvent(ics) { paramParts.forEach((p) => { const [k, v] = p.split('='); if (k) params[k.toUpperCase()] = v; }); const key = name.toUpperCase(); if (key === 'UID') ev.uid = value.trim(); - else if (key === 'SUMMARY') ev.summary = unescapeText(value); - else if (key === 'LOCATION') ev.location = unescapeText(value); - else if (key === 'DESCRIPTION') ev.description = unescapeText(value); + else if (key === 'SUMMARY') ev.summary = decodeHtmlEntities(unescapeText(value)); + else if (key === 'LOCATION') ev.location = decodeHtmlEntities(unescapeText(value)); + else if (key === 'DESCRIPTION') ev.description = decodeHtmlEntities(unescapeText(value)); else if (key === 'DTSTART') { const r = icsToDate(value, params); ev.start = r.date; ev.allDay = r.allDay; } else if (key === 'DTEND') { const r = icsToDate(value, params); ev.end = r.date; } }