Carbon/src/js/events/message.js

118 lines
3.3 KiB
JavaScript

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]