File size: 1,775 Bytes
289daab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const ALLOWED_RICH_TAGS = new Set([
    'A', 'B', 'BLOCKQUOTE', 'BR', 'CODE', 'DEL', 'DIV', 'EM', 'H1', 'H2', 'H3',
    'H4', 'H5', 'H6', 'HR', 'I', 'LI', 'OL', 'P', 'PRE', 'S', 'SMALL', 'SPAN',
    'STRONG', 'SUB', 'SUP', 'U', 'UL'
]);

export function escapeHtml(value) {
    return String(value ?? '')
        .replaceAll('&', '&')
        .replaceAll('<', '&lt;')
        .replaceAll('>', '&gt;')
        .replaceAll('"', '&quot;')
        .replaceAll("'", '&#39;');
}

export function setSafeRichHtml(element, value) {
    const template = document.createElement('template');
    template.innerHTML = String(value ?? '');

    for (const node of Array.from(template.content.querySelectorAll('*'))) {
        if (!ALLOWED_RICH_TAGS.has(node.tagName)) {
            node.replaceWith(document.createTextNode(node.textContent || ''));
            continue;
        }

        for (const attribute of Array.from(node.attributes)) {
            const name = attribute.name.toLowerCase();
            const allowed = node.tagName === 'A' && (name === 'href' || name === 'title');
            if (!allowed) node.removeAttribute(attribute.name);
        }

        if (node.tagName === 'A') {
            const href = node.getAttribute('href');
            if (href) {
                try {
                    const url = new URL(href, window.location.href);
                    if (url.protocol !== 'http:' && url.protocol !== 'https:') node.removeAttribute('href');
                } catch (_) {
                    node.removeAttribute('href');
                }
            }
            node.setAttribute('target', '_blank');
            node.setAttribute('rel', 'noopener noreferrer');
        }
    }

    element.replaceChildren(template.content.cloneNode(true));
}