From f6eb226918b86ac833fa8b11d8a31f4bd9553aff Mon Sep 17 00:00:00 2001 From: Kato Date: Sat, 5 Sep 2026 19:21:31 +0000 Subject: [PATCH] Calendar + date cards: render webhook HTML (sanitize, links open in new tab) --- css/styles.css | 2 ++ js/main.js | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/css/styles.css b/css/styles.css index abb6355..a5fb5b1 100644 --- a/css/styles.css +++ b/css/styles.css @@ -1497,6 +1497,8 @@ TELEGRAM BANNER white-space: nowrap; } .calendar-event-headline { flex: 1; color: #1c1e21; } +.calendar-event-headline a { color: #1877f2; text-decoration: none; font-weight: 600; } +.calendar-event-headline a:hover { text-decoration: underline; } .calendar-event-time { color: #65676b; white-space: nowrap; } /* ======================================== diff --git a/js/main.js b/js/main.js index ba9a6c2..5b43f6c 100644 --- a/js/main.js +++ b/js/main.js @@ -981,7 +981,7 @@ function renderDates(events) { card.innerHTML = '
' + '
📅
' + - '

' + escapeHtml(event.headline || 'Event') + '

' + + '

' + sanitizeHtml(event.headline || 'Event') + '

' + '
' + '
' + '' + @@ -1004,6 +1004,34 @@ function renderDates(events) { // ======================================== function pad2(n) { return String(n).padStart(2, '0'); } +// Webhook text may contain simple HTML (e.g. links). Sanitize before injecting: +// strip script/iframe/... tags, on* handlers and javascript: URLs; force +// links to open in a new tab. +function sanitizeHtml(html) { + const div = document.createElement('div'); + div.innerHTML = String(html || ''); + div.querySelectorAll('script, style, iframe, object, embed, link, meta').forEach(n => n.remove()); + const walk = (node) => { + Array.from(node.childNodes || []).forEach(child => { + if (child.nodeType !== 1) return; + Array.from(child.attributes || []).forEach(attr => { + const name = attr.name.toLowerCase(); + const val = (attr.value || '').replace(/\s+/g, '').toLowerCase(); + if (name.startsWith('on') || ((name === 'href' || name === 'src') && val.startsWith('javascript:'))) { + child.removeAttribute(attr.name); + } + }); + if (child.tagName === 'A') { + child.setAttribute('target', '_blank'); + child.setAttribute('rel', 'noopener'); + } + walk(child); + }); + }; + walk(div); + return div.innerHTML; +} + function initCalendar() { renderCalendar(); } @@ -1028,7 +1056,7 @@ function renderCalendar() { const timeStr = pad2(sd.getHours()) + ':' + pad2(sd.getMinutes()) + (ed ? '–' + pad2(ed.getHours()) + ':' + pad2(ed.getMinutes()) : ''); list += '
  • ' + dateStr + '' + - '' + escapeHtml(ev.headline || 'Event') + '' + + '' + sanitizeHtml(ev.headline || 'Event') + '' + '' + timeStr + '
  • '; }); list += '';