import sanitizeHtml, { Transformer } from 'sanitize-html'; const MAX_TAG_NESTING = 100; const permittedHtmlTags = [ 'font', 'del', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 's', 'code', 'hr', 'br', 'div', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'caption', 'pre', 'span', 'img', 'details', 'summary', ]; const urlSchemes = ['https', 'http', 'ftp', 'mailto', 'magnet']; const permittedTagToAttributes = { font: ['style', 'data-mx-bg-color', 'data-mx-color', 'color'], span: [ 'style', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'data-mx-maths', 'data-mx-pill', 'data-mx-ping', 'data-md', ], div: ['data-mx-maths'], blockquote: ['data-md'], h1: ['data-md'], h2: ['data-md'], h3: ['data-md'], h4: ['data-md'], h5: ['data-md'], h6: ['data-md'], pre: ['data-md', 'class'], ol: ['start', 'type', 'data-md'], ul: ['data-md'], a: ['name', 'target', 'href', 'rel', 'data-md'], img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'], code: ['class', 'data-md'], strong: ['data-md'], i: ['data-md'], em: ['data-md'], u: ['data-md'], s: ['data-md'], del: ['data-md'], }; const transformFontTag: Transformer = (tagName, attribs) => ({ tagName, attribs: { ...attribs, style: `background-color: ${attribs['data-mx-bg-color']}; color: ${attribs['data-mx-color']}`, }, }); const transformSpanTag: Transformer = (tagName, attribs) => ({ tagName, attribs: { ...attribs, style: `background-color: ${attribs['data-mx-bg-color']}; color: ${attribs['data-mx-color']}`, }, }); const transformATag: Transformer = (tagName, attribs) => ({ tagName, attribs: { ...attribs, rel: 'noopener', target: '_blank', }, }); const transformImgTag: Transformer = (tagName, attribs) => { const { src } = attribs; if (typeof src === 'string' && src.startsWith('mxc://') === false) { return { tagName: 'a', attribs: { href: src, rel: 'noopener', target: '_blank', }, text: attribs.alt || src, }; } return { tagName, attribs: { ...attribs, }, }; }; export const sanitizeCustomHtml = (customHtml: string): string => sanitizeHtml(customHtml, { allowedTags: permittedHtmlTags, allowedAttributes: permittedTagToAttributes, disallowedTagsMode: 'discard', allowedSchemes: urlSchemes, allowedSchemesByTag: { a: urlSchemes, }, allowedSchemesAppliedToAttributes: ['href'], allowProtocolRelative: false, allowedClasses: { code: ['language-*'], }, allowedStyles: { '*': { color: [/^#(?:[0-9a-fA-F]{3}){1,2}$/], 'background-color': [/^#(?:[0-9a-fA-F]{3}){1,2}$/], }, }, transformTags: { font: transformFontTag, span: transformSpanTag, a: transformATag, img: transformImgTag, }, nonTextTags: ['style', 'script', 'textarea', 'option', 'noscript', 'mx-reply'], nestingLimit: MAX_TAG_NESTING, }); export const sanitizeText = (body: string) => { const tagsToReplace: Record = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }; return body.replace(/[&<>'"]/g, (tag) => tagsToReplace[tag] || tag); };