66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
const ALLOWED_TAGS = new Set([
|
|
'p', 'br', 'strong', 'b', 'em', 'i', 'u', 'ul', 'ol', 'li',
|
|
'img', 'a', 'h2', 'h3', 'blockquote', 'div', 'span',
|
|
]);
|
|
|
|
function sanitizeNode(node: Node): Node | null {
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
return node.cloneNode(false);
|
|
}
|
|
if (node.nodeType !== Node.ELEMENT_NODE) return null;
|
|
|
|
const el = node as HTMLElement;
|
|
const tag = el.tagName.toLowerCase();
|
|
if (!ALLOWED_TAGS.has(tag)) {
|
|
const frag = document.createDocumentFragment();
|
|
for (const child of Array.from(el.childNodes)) {
|
|
const safe = sanitizeNode(child);
|
|
if (safe) frag.appendChild(safe);
|
|
}
|
|
return frag;
|
|
}
|
|
|
|
const out = document.createElement(tag);
|
|
if (tag === 'img') {
|
|
const src = el.getAttribute('src')?.trim();
|
|
if (!src || /^javascript:/i.test(src)) return null;
|
|
out.setAttribute('src', src);
|
|
const alt = el.getAttribute('alt');
|
|
if (alt) out.setAttribute('alt', alt);
|
|
return out;
|
|
}
|
|
if (tag === 'a') {
|
|
const href = el.getAttribute('href')?.trim();
|
|
if (!href || /^javascript:/i.test(href)) return null;
|
|
out.setAttribute('href', href);
|
|
out.setAttribute('target', '_blank');
|
|
out.setAttribute('rel', 'noopener noreferrer');
|
|
}
|
|
for (const child of Array.from(el.childNodes)) {
|
|
const safe = sanitizeNode(child);
|
|
if (safe) out.appendChild(safe);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** 玩家端公告正文 HTML 白名单净化 */
|
|
export function sanitizeAnnouncementHtml(html: string): string {
|
|
if (!html?.trim()) return '';
|
|
if (!/[<>]/.test(html)) return html;
|
|
|
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
const container = document.createElement('div');
|
|
for (const child of Array.from(doc.body.childNodes)) {
|
|
const safe = sanitizeNode(child);
|
|
if (safe) container.appendChild(safe);
|
|
}
|
|
return container.innerHTML;
|
|
}
|
|
|
|
export function stripHtml(html: string): string {
|
|
if (!html) return '';
|
|
if (!/[<>]/.test(html)) return html.trim();
|
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
return (doc.body.textContent ?? '').replace(/\s+/g, ' ').trim();
|
|
}
|