1 import type { Config } from 'dompurify';
2 import DOMPurify from 'dompurify';
4 import { parseStringToDOM } from '@proton/shared/lib/helpers/dom';
6 import { escapeForbiddenStyle, escapeURLinStyle } from './escape';
8 const toMap = (list: string[]) =>
9 list.reduce<{ [key: string]: true | undefined }>((acc, key) => {
14 const LIST_PROTON_ATTR = ['data-src', 'src', 'srcset', 'background', 'poster', 'xlink:href', 'href'];
15 const MAP_PROTON_ATTR = toMap(LIST_PROTON_ATTR);
16 const PROTON_ATTR_TAG_WHITELIST = ['a', 'base', 'area'];
17 const MAP_PROTON_ATTR_TAG_WHITELIST = toMap(PROTON_ATTR_TAG_WHITELIST.map((tag) => tag.toUpperCase()));
19 const shouldPrefix = (tagName: string, attributeName: string) => {
20 return !MAP_PROTON_ATTR_TAG_WHITELIST[tagName] && MAP_PROTON_ATTR[attributeName];
23 const CONFIG: { [key: string]: any } = {
26 /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|blob|xmpp|data):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i, // eslint-disable-line no-useless-escape
27 ADD_TAGS: ['proton-src', 'base'],
28 ADD_ATTR: ['target', 'proton-src'],
29 FORBID_TAGS: ['style', 'input', 'form'],
30 FORBID_ATTR: ['srcset', 'for'],
31 // Accept HTML (official) tags only and automatically excluding all SVG & MathML tags
32 USE_PROFILES: { html: true },
34 // When we display a message we need to be global and return more information
35 raw: { WHOLE_DOCUMENT: true, RETURN_DOM: true },
36 html: { WHOLE_DOCUMENT: false, RETURN_DOM: true },
38 FORBID_TAGS: ['input', 'form', 'video', 'audio'], // Override defaults to allow style (will be processed by juice afterward)
40 ADD_ATTR: ['target', ...LIST_PROTON_ATTR.map((attr) => `proton-${attr}`)],
45 ALLOW_UNKNOWN_PROTOCOLS: true,
46 WHOLE_DOCUMENT: false,
48 RETURN_DOM_FRAGMENT: true,
51 ALLOW_UNKNOWN_PROTOCOLS: true,
52 WHOLE_DOCUMENT: false,
54 RETURN_DOM_FRAGMENT: true,
55 FORBID_TAGS: ['style', 'input', 'form', 'img'],
59 const getConfig = (type: string): Config => ({ ...CONFIG.default, ...(CONFIG[type] || {}) });
62 * Rename some attributes adding the proton- prefix configured in LIST_PROTON_ATTR
63 * Also escape urls in style attributes
65 const beforeSanitizeElements = (node: Node) => {
66 // We only work on elements
67 if (node.nodeType !== 1) {
71 const element = node as HTMLElement;
73 // Manage styles element
74 if (element.tagName === 'STYLE') {
75 const escaped = escapeForbiddenStyle(escapeURLinStyle(element.innerHTML || ''));
76 element.innerHTML = escaped;
79 Array.from(element.attributes).forEach((type) => {
80 const item = type.name;
82 if (shouldPrefix(element.tagName, item)) {
83 element.setAttribute(`proton-${item}`, element.getAttribute(item) || '');
84 element.removeAttribute(item);
87 // Manage element styles tag
88 if (item === 'style') {
89 const escaped = escapeForbiddenStyle(escapeURLinStyle(element.getAttribute('style') || ''));
90 element.setAttribute('style', escaped);
97 const purifyHTMLHooks = (active: boolean) => {
99 DOMPurify.addHook('beforeSanitizeElements', beforeSanitizeElements);
103 DOMPurify.removeHook('beforeSanitizeElements');
106 const clean = (mode: string) => {
107 const config = getConfig(mode);
109 return (input: string | Node): string | Element => {
110 DOMPurify.clearConfig();
111 const value = DOMPurify.sanitize(input, config) as string | Element;
112 purifyHTMLHooks(false); // Always remove the hooks
113 if (mode === 'str') {
114 // When trusted types is available, DOMPurify returns a trustedHTML object and not a string, force cast it.
122 * Custom config only for messages
124 export const message = clean('str') as (input: string) => string;
127 * Sanitize input with a config similar than Squire + ours
129 export const html = clean('raw') as (input: Node) => Element;
132 * Sanitize input with a config similar than Squire + ours
134 export const protonizer = (input: string, attachHooks: boolean): Element => {
135 const process = clean('protonizer');
136 purifyHTMLHooks(attachHooks);
137 return process(input) as Element;
141 * Sanitize input and returns the whole document
144 export const content = clean('content') as (input: string) => Node;
147 * Sanitize input without images and returns the whole document
150 export const contentWithoutImage = clean('contentWithoutImg') as (input: string) => Node;
153 * Default config we don't want any custom behaviour
155 export const input = (str: string) => {
156 const result = DOMPurify.sanitize(str, {});
161 * We don't want to display images inside the autoreply composer.
162 * There is an issue on Firefox where images can still be added by drag&drop,
163 * and squire is not able to detect them. That's why we are removing them here.
165 export const removeImagesFromContent = (message: string) => {
166 const div = parseStringToDOM(message).body;
168 // Remove all images from the message
169 const allImages = div.querySelectorAll('img');
170 allImages.forEach((img) => img.remove());
172 return { message: div.innerHTML, containsImages: allImages.length > 0 };
175 export const sanitizeSignature = (input: string) => {
176 const process = clean('default');
177 return process(input.replace(/<a\s.*href="(.+?)".*>(.+?)<\/a>/, '[URL: $1] $2'));