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 = '
' +
'
' +
'' +
@@ -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 += '';