Merge branch 'IDTEAM-1.26.0' into 'main'
[ProtonMail-WebClient.git] / packages / shared / lib / calendar / sanitize.ts
blob8b0fda75f6f02331157921c1134d44de0f050498
1 import DOMPurify from 'dompurify';
3 const addRelNoopenerAndTargetBlank = (node: Node) => {
4     if (node instanceof Element && node.tagName === 'A') {
5         node.setAttribute('rel', 'noopener noreferrer');
6         node.setAttribute('target', '_blank');
7     }
8 };
10 export const restrictedCalendarSanitize = (source: string) => {
11     DOMPurify.clearConfig();
12     DOMPurify.addHook('afterSanitizeAttributes', addRelNoopenerAndTargetBlank);
14     const sanitizedContent = DOMPurify.sanitize(source, {
15         ALLOWED_TAGS: ['a', 'b', 'em', 'br', 'i', 'u', 'ul', 'ol', 'li', 'span', 'p'],
16         ALLOWED_ATTR: ['href'],
17     });
19     DOMPurify.removeHook('afterSanitizeAttributes');
21     return sanitizedContent;
24 const validTags: Set<string> = new Set([
25     'a',
26     'abbr',
27     'acronym',
28     'address',
29     'applet',
30     'area',
31     'article',
32     'aside',
33     'audio',
34     'b',
35     'base',
36     'basefont',
37     'bdi',
38     'bdo',
39     'big',
40     'blockquote',
41     'body',
42     'br',
43     'button',
44     'canvas',
45     'caption',
46     // 'center', // Deprecated
47     'cite',
48     'code',
49     'col',
50     'colgroup',
51     'data',
52     'datalist',
53     'dd',
54     'del',
55     'details',
56     'dfn',
57     'dialog',
58     'dir',
59     'div',
60     'dl',
61     'dt',
62     'em',
63     'embed',
64     'fieldset',
65     'figcaption',
66     'figure',
67     'font',
68     'footer',
69     'form',
70     'frame',
71     'frameset',
72     'h1',
73     'h2',
74     'h3',
75     'h4',
76     'h5',
77     'h6',
78     'head',
79     'header',
80     'hr',
81     'html',
82     'i',
83     'iframe',
84     'img',
85     'input',
86     'ins',
87     'kbd',
88     'label',
89     'legend',
90     'li',
91     'link',
92     'main',
93     'map',
94     'mark',
95     'meta',
96     'meter',
97     'nav',
98     'noframes',
99     'noscript',
100     'object',
101     'ol',
102     'optgroup',
103     'option',
104     'output',
105     'p',
106     'param',
107     'picture',
108     'pre',
109     'progress',
110     'q',
111     'rp',
112     'rt',
113     'ruby',
114     's',
115     'samp',
116     'script',
117     'section',
118     'select',
119     'small',
120     'source',
121     'span',
122     'strike',
123     'strong',
124     'style',
125     'sub',
126     'summary',
127     'sup',
128     'svg',
129     'table',
130     'tbody',
131     'td',
132     'template',
133     'textarea',
134     'tfoot',
135     'th',
136     'thead',
137     'time',
138     'title',
139     'tr',
140     'track',
141     'tt',
142     'u',
143     'ul',
144     'var',
145     'video',
146     'wbr',
149 const escapeBrackets = (input: string): string => {
150     return input.replace(/</g, '&lt;').replace(/>/g, '&gt;');
153 const HTML_TAG_PATTERN: RegExp = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>/g;
155 export const escapeInvalidHtmlTags = (input: string): string => {
156     if (!input) {
157         return '';
158     }
160     let result = '';
161     let lastIndex = 0;
163     // Replace is only used to iterate over all matches
164     input.replace(HTML_TAG_PATTERN, (match, tagName, offset) => {
165         // Append the part of the string before the current match
166         result += escapeBrackets(input.slice(lastIndex, offset));
167         // Append the current match (HTML tag) if it's valid, otherwise escape it
168         result += validTags.has(tagName.toLowerCase()) ? match : escapeBrackets(match);
169         // Update lastIndex to the end of the current match
170         lastIndex = offset + match.length;
171         // Return the match as is
172         return match;
173     });
175     // Append any remaining part of the string after the last match
176     result += escapeBrackets(input.slice(lastIndex));
178     return result;
181 export const stripAllTags = (source: string) => {
182     const html = escapeInvalidHtmlTags(source);
183     const sanitized = restrictedCalendarSanitize(html);
184     const div = document.createElement('DIV');
185     div.style.whiteSpace = 'pre-wrap';
186     div.innerHTML = sanitized;
187     div.querySelectorAll('a').forEach((element) => {
188         element.innerText = element.href || element.innerText;
189     });
190     // Append it to force a layout pass so that innerText returns newlines
191     document.body.appendChild(div);
192     const result = div.innerText;
193     document.body.removeChild(div);
194     return result;