feat(INDA-383): daily stats.
[ProtonMail-WebClient.git] / packages / shared / lib / sanitize / purify.ts
blobe7667f3ae569e1634ea4e5d995273f961ac27ace
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) => {
10         acc[key] = true;
11         return acc;
12     }, {});
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 } = {
24     default: {
25         ALLOWED_URI_REGEXP:
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 },
33     },
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 },
37     protonizer: {
38         FORBID_TAGS: ['input', 'form', 'video', 'audio'], // Override defaults to allow style (will be processed by juice afterward)
39         FORBID_ATTR: {},
40         ADD_ATTR: ['target', ...LIST_PROTON_ATTR.map((attr) => `proton-${attr}`)],
41         WHOLE_DOCUMENT: true,
42         RETURN_DOM: true,
43     },
44     content: {
45         ALLOW_UNKNOWN_PROTOCOLS: true,
46         WHOLE_DOCUMENT: false,
47         RETURN_DOM: true,
48         RETURN_DOM_FRAGMENT: true,
49     },
50     contentWithoutImg: {
51         ALLOW_UNKNOWN_PROTOCOLS: true,
52         WHOLE_DOCUMENT: false,
53         RETURN_DOM: true,
54         RETURN_DOM_FRAGMENT: true,
55         FORBID_TAGS: ['style', 'input', 'form', 'img'],
56     },
59 const getConfig = (type: string): Config => ({ ...CONFIG.default, ...(CONFIG[type] || {}) });
61 /**
62  * Rename some attributes adding the proton- prefix configured in LIST_PROTON_ATTR
63  * Also escape urls in style attributes
64  */
65 const beforeSanitizeElements = (node: Node) => {
66     // We only work on elements
67     if (node.nodeType !== 1) {
68         return node;
69     }
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;
77     }
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);
85         }
87         // Manage element styles tag
88         if (item === 'style') {
89             const escaped = escapeForbiddenStyle(escapeURLinStyle(element.getAttribute('style') || ''));
90             element.setAttribute('style', escaped);
91         }
92     });
94     return element;
97 const purifyHTMLHooks = (active: boolean) => {
98     if (active) {
99         DOMPurify.addHook('beforeSanitizeElements', beforeSanitizeElements);
100         return;
101     }
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.
115             return `${value}`;
116         }
117         return value;
118     };
122  * Custom config only for messages
123  */
124 export const message = clean('str') as (input: string) => string;
127  * Sanitize input with a config similar than Squire + ours
128  */
129 export const html = clean('raw') as (input: Node) => Element;
132  * Sanitize input with a config similar than Squire + ours
133  */
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
143  */
144 export const content = clean('content') as (input: string) => Node;
147  * Sanitize input without images and returns the whole document
149  */
150 export const contentWithoutImage = clean('contentWithoutImg') as (input: string) => Node;
153  * Default config we don't want any custom behaviour
154  */
155 export const input = (str: string) => {
156     const result = DOMPurify.sanitize(str, {});
157     return `${result}`;
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.
164  */
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'));