const {ejs, ElemJS} = require("../basic") const {HighlightedCode} = require("./components") const DOMPurify = require("dompurify") const {resolveMxc} = require("../functions") const {MatrixEvent} = 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", "color"], "A": ["name", "target", "href"], "IMG": ["width", "height", "alt", "title", "src", "data-mx-emoticon"], "OL": ["start"], "CODE": ["class"], } const allowed_attributes = allowedElementAttributes[node.tagName] || [] hookevent.keepAttr = allowed_attributes.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") } return node }) function cleanHTML(html) { const config = { ALLOWED_URI_REGEXP: /^mxc:\/\/[a-zA-Z0-9\.]+\/[a-zA-Z0-9]+$/, // As per the spec we only allow mxc uris 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'], // In case we mess up in the uponSanitizeAttribute hook ALLOWED_ATTR: ["data-mx-bg-color", "data-mx-color", "color", "name", "target", "href", "width", "height", "alt", "title", "src", "data-mx-emoticon", "start", "class"], } return purifier.sanitize(html) } // Here we put all the processing of the messages that isn't as likely to potentially lead to security issues function postProcessElements(rootNode) { const element = rootNode.element element.querySelectorAll("code").forEach((n) => rootNode.child(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; }) } class HTMLMessage extends MatrixEvent { render() { this.clearChildren() let html = this.data.content.formatted_body const content = ejs("div") html = cleanHTML(html) content.html(html) postProcessElements(content) this.child(content) super.render() } static canRender(event) { const content = event.content return event.type == "m.room.message" && content.msgtype == "m.text" && content.format == "org.matrix.custom.html" && content.formatted_body } canGroup() { return true } } class TextMessage extends MatrixEvent { render() { this.text(this.data.content.body) return super.render() } static canRender(event) { return event.type == "m.room.message" } canGroup() { return true } } module.exports = [HTMLMessage, TextMessage]