feat(widget): enhance widget settings menu with improved styling and functionality

This commit is contained in:
2026-09-10 15:44:37 +02:00
parent 72ad1fb70e
commit a92b4d0f4c
6 changed files with 930 additions and 100 deletions
+115 -2
View File
@@ -535,7 +535,7 @@ async function selectMail(id) {
const res = await fetch(`/api/mail.php?action=get_message&id=${encodeURIComponent(id)}`);
const data = await res.json();
if (data.success && data.message) {
bodyEl.innerHTML = data.message.body_html || `<pre class="font-sans whitespace-pre-wrap">${escapeHtml(data.message.body_text || found.body_preview)}</pre>`;
renderMailBodyContent(bodyEl, data.message.body_html, data.message.body_text || found.body_preview);
found.is_read = true;
updateUnreadCount();
renderMailList(allMessages);
@@ -543,10 +543,123 @@ async function selectMail(id) {
bodyEl.innerHTML = `<div class="p-4 rounded-xl bg-red-50 text-red-700 text-xs">${data.error || 'Kon bericht niet inladen.'}</div>`;
}
} catch(err) {
bodyEl.innerHTML = `<p class="whitespace-pre-wrap">${escapeHtml(found.body_preview)}</p>`;
renderMailBodyContent(bodyEl, null, found.body_preview);
}
}
// Grondige HTML sanitization voor inkomende e-mails (XSS & exploit preventie)
function sanitizeEmailHtml(html) {
if (!html) return '';
// 1. Verwijder alle script tags en inhoud
let cleaned = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
// 2. Verwijder potentieel gevaarlijke tags
cleaned = cleaned.replace(/<\/?(object|embed|applet|meta|base|form|input|button|textarea|select)\b[^>]*>/gi, '');
// 3. Verwijder inline JavaScript event handlers (onload, onerror, onclick, onmouseover, etc.)
cleaned = cleaned.replace(/\s+on[a-z0-9_-]+\s*=\s*(?:'[^']*'|"[^"]*"|[^\s>]+)/gi, '');
// 4. Neutraliseer javascript: en data:text/html pseudo-protocollen in links en afbeeldingen
cleaned = cleaned.replace(/(href|src)\s*=\s*['"]\s*(?:javascript|data:text\/html):[^'"]*['"]/gi, '$1="#"');
return cleaned;
}
// Veilig renderen van e-mail HTML via een geïsoleerde sandboxed iframe
function renderMailBodyContent(container, htmlContent, plainContent) {
container.innerHTML = '';
if (!htmlContent) {
const pre = document.createElement('pre');
pre.className = 'font-sans whitespace-pre-wrap text-sm text-slate-800 dark:text-slate-200 leading-relaxed p-2';
pre.textContent = plainContent || '(Geen inhoud)';
container.appendChild(pre);
return;
}
const safeHtml = sanitizeEmailHtml(htmlContent);
const isDark = document.documentElement.classList.contains('dark');
// Sandboxed iframe: strikte isolatie (geen allow-scripts)
const iframe = document.createElement('iframe');
iframe.id = 'mailBodyIframe';
iframe.setAttribute('sandbox', 'allow-same-origin allow-popups');
iframe.className = 'w-full border-0 rounded-xl transition-all';
iframe.style.width = '100%';
iframe.style.minHeight = '350px';
iframe.style.border = 'none';
iframe.style.background = isDark ? '#0c0f17' : '#ffffff';
iframe.style.display = 'block';
container.appendChild(iframe);
const docHtml = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base target="_blank">
<style>
*, *::before, *::after {
box-sizing: border-box;
}
html {
color-scheme: ${isDark ? 'dark' : 'light'};
}
body {
margin: 0;
padding: 14px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
color: ${isDark ? '#f1f5f9' : '#0f172a'};
background-color: ${isDark ? '#0c0f17' : '#ffffff'};
word-break: break-word;
}
img {
max-width: 100% !important;
height: auto !important;
}
table {
max-width: 100% !important;
}
pre, code {
white-space: pre-wrap;
word-break: break-all;
}
a {
color: #4f46e5;
}
</style>
</head>
<body>
${safeHtml}
</body>
</html>`;
iframe.srcdoc = docHtml;
iframe.onload = () => {
try {
const innerDoc = iframe.contentDocument || iframe.contentWindow.document;
if (innerDoc) {
const updateHeight = () => {
const scrollH = innerDoc.documentElement.scrollHeight || innerDoc.body.scrollHeight;
iframe.style.height = Math.max(scrollH + 35, 280) + 'px';
};
updateHeight();
if (window.ResizeObserver) {
const observer = new ResizeObserver(updateHeight);
observer.observe(innerDoc.body);
}
}
} catch (e) {
iframe.style.height = '600px';
}
};
}
function updateUnreadCount() {
const unread = allMessages.filter(m => !m.is_read).length;
const el = document.getElementById('countUnread');