const {ejs, ElemJS} = require("../basic") const {HighlightedCode} = require("./components") const DOMPurify = require("dompurify") const {resolveMxc} = require("../functions") const {GroupableEvent} = require("./event") const purifier = DOMPurify() purifier.addHook("uponSanitizeAttribute", (node, hookevent, config) => { // If purifier already rejected an attribute there is no point in checking it if (hookevent.keepAttr === false) return; const allowedElementAttributes = { "FONT": ["data-mx-bg-color", "data-mx-color", "color"], "SPAN": ["data-mx-bg-color", "data-mx-color", "data-mx-spoiler"], "A": ["name", "target", "href"], "IMG": ["width", "height", "alt", "title", "src", "data-mx-emoticon"], "OL": ["start"], "CODE": ["class"], } const allowedAttributes = allowedElementAttributes[node.tagName] || [] hookevent.keepAttr = allowedAttributes.indexOf(hookevent.attrName) > -1 }) purifier.addHook("uponSanitizeElement", (node, hookevent, config) => { // Remove bad classes from our code element if (node.tagName === "CODE") { node.classList.forEach(c => { if (!c.startsWith("language-")) { node.classList.remove(c) } }) } if (node.tagName === "A") { node.setAttribute("rel", "noopener") // prevent the opening page from accessing carbon node.setAttribute("target", "_blank") // open in a new tab instead of replacing carbon } return node }) function cleanHTML(html) { const config = { ALLOWED_TAGS: [ "font", "del", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "a", "ul", "ol", "sup", "sub", "li", "b", "i", "u", "strong", "em", "strike", "code", "hr", "br", "div", "table", "thead", "tbody", "tr", "th", "td", "caption", "pre", "span", "img", // matrix tags "mx-reply" ], // In case we mess up in the uponSanitizeAttribute hook ALLOWED_ATTR: [ "color", "name", "target", "href", "width", "height", "alt", "title", "src", "start", "class", "noreferrer", "noopener", // matrix attrs "data-mx-emoticon", "data-mx-bg-color", "data-mx-color", "data-mx-spoiler" ], // Return a DOM fragment instead of a string, avoids potential future mutation XSS // should also be faster than the browser parsing HTML twice // https://research.securitum.com/mutation-xss-via-mathml-mutation-dompurify-2-0-17-bypass/ RETURN_DOM_FRAGMENT: true, RETURN_DOM_IMPORT: true } return purifier.sanitize(html, config) } // Here we put all the processing of the messages that isn't as likely to potentially lead to security issues function postProcessElements(element) { element.querySelectorAll("pre").forEach(n => { new HighlightedCode(n) }) element.querySelectorAll("img").forEach(n => { let src = n.getAttribute("src") if (src) src = resolveMxc(src) n.setAttribute("src", src) }) element.querySelectorAll("font, span").forEach(n => { const color = n.getAttribute("data-mx-color") || n.getAttribute("color") const bgColor = n.getAttribute("data-mx-bg-color") if (color) n.style.color = color if (bgColor) n.style.backgroundColor = bgColor }) element.querySelectorAll("[data-mx-spoiler]").forEach(spoiler => { spoiler.classList.add("mx-spoiler") spoiler.setAttribute("tabindex", 0) function toggle() { spoiler.classList.toggle("mx-spoiler--shown") } spoiler.addEventListener("click", toggle) spoiler.addEventListener("keydown", event => { if (event.key === "Enter") toggle() }) }) } class HTMLMessage extends GroupableEvent { render() { this.clearChildren() let html = this.data.content.formatted_body const fragment = cleanHTML(html) postProcessElements(fragment) this.child(ejs(fragment)) super.render() } static canRender(event) { const content = event.content return ( event.type === "m.room.message" && (content.msgtype === "m.text" || content.msgtype === "m.notice") && content.format === "org.matrix.custom.html" && content.formatted_body ) } } function autoLinkText(text) { const fragment = ejs(new DocumentFragment()) let lastIndex = 0 text.replace(/https?:\/\/(?:[A-Za-z-]+\.)+[A-Za-z]{1,10}(?::[0-9]{1,6})?(?:\/[^ ]*)?/g, (url, index) => { // add text before URL fragment.addText(text.slice(lastIndex, index)) // add URL fragment.child( ejs("a") .attribute("target", "_blank") .attribute("noopener", "") .attribute("href", url) .addText(url) ) // update state lastIndex = index + url.length }) // add final text fragment.addText(text.slice(lastIndex)) return fragment } class TextMessage extends GroupableEvent { render() { this.clearChildren() this.class("c-message--plain") const fragment = autoLinkText(this.data.content.body) this.child(fragment) super.render() } static canRender(event) { return event.type === "m.room.message" } } module.exports = [HTMLMessage, TextMessage]