]>
Commit | Line | Data |
---|---|---|
51a2f11c TL |
1 | // NOTE: just relays parsing to markedjs parser |
2 | Ext.define('Proxmox.Markdown', { | |
3 | alternateClassName: 'Px.Markdown', // just trying out something, do NOT copy this line | |
4 | singleton: true, | |
5 | ||
71bc0913 | 6 | // transforms HTML to a DOM tree and recursively descends and HTML-encodes every branch with a |
51a2f11c TL |
7 | // "bad" node.type and drops "bad" attributes from the remaining nodes. |
8 | // "bad" means anything which can do XSS or break the layout of the outer page | |
9 | sanitizeHTML: function(input) { | |
10 | if (!input) { | |
11 | return input; | |
12 | } | |
f2c4f9bd | 13 | let _isHTTPLike = value => value.match(/^\s*https?:/i); // URL's protocol ends with : |
51a2f11c TL |
14 | let _sanitize; |
15 | _sanitize = (node) => { | |
16 | if (node.nodeType === 3) return; | |
1d3d61ea TL |
17 | if (node.nodeType !== 1 || |
18 | /^(script|style|form|select|option|optgroup|map|area|canvas|textarea|applet|font|iframe|audio|video|object|embed|svg)$/i.test(node.tagName) | |
19 | ) { | |
71bc0913 TL |
20 | // could do node.remove() instead, but it's nicer UX if we keep the (encoded!) html |
21 | node.outerHTML = Ext.String.htmlEncode(node.outerHTML); | |
51a2f11c TL |
22 | return; |
23 | } | |
24 | for (let i=node.attributes.length; i--;) { | |
25 | const name = node.attributes[i].name; | |
f2c4f9bd | 26 | const value = node.attributes[i].value; |
51a2f11c | 27 | // TODO: we may want to also disallow class and id attrs |
f2c4f9bd TL |
28 | if ( |
29 | !/^(class|id|name|href|src|alt|align|valign|disabled|checked|start|type)$/i.test(name) | |
30 | ) { | |
51a2f11c | 31 | node.attributes.removeNamedItem(name); |
f2c4f9bd TL |
32 | } else if ((name === 'href' || name === 'src') && !_isHTTPLike(value)) { |
33 | try { | |
34 | let url = new URL(value, window.location.origin); | |
e1fc4744 | 35 | if (_isHTTPLike(url.protocol) || (node.tagName === 'img' && url.protocol === 'data:')) { |
f2c4f9bd TL |
36 | node.attributes[i].value = url.href; |
37 | } else { | |
38 | node.attributes.removeNamedItem(name); | |
39 | } | |
40 | } catch (e) { | |
41 | node.attributes[i].removeNamedItem(name); | |
42 | } | |
51a2f11c TL |
43 | } |
44 | } | |
45 | for (let i=node.childNodes.length; i--;) _sanitize(node.childNodes[i]); | |
46 | }; | |
47 | ||
48 | const doc = new DOMParser().parseFromString(`<!DOCTYPE html><html><body>${input}`, 'text/html'); | |
49 | doc.normalize(); | |
50 | ||
51 | _sanitize(doc.body); | |
52 | ||
53 | return doc.body.innerHTML; | |
54 | }, | |
55 | ||
56 | parse: function(markdown) { | |
57 | /*global marked*/ | |
58 | let unsafeHTML = marked(markdown); | |
59 | ||
60 | return `<div class="pmx-md">${this.sanitizeHTML(unsafeHTML)}</div>`; | |
61 | }, | |
62 | ||
63 | }); |