1use ammonia::Builder;
21use rumtk_core::dependencies::maplit::{hashmap, hashset};
22use rumtk_core::strings::RUMString;
23use std::borrow::Borrow;
24use std::collections::{HashMap, HashSet};
25use std::iter::IntoIterator;
26use std::sync::LazyLock;
27
28type AllowedSet = LazyLock<HashSet<&'static str>>;
29type AllowedMap = LazyLock<HashMap<&'static str, HashSet<&'static str>>>;
30
31static CLEAN_CONTENT_TAGS: AllowedSet = LazyLock::new(|| hashset!["script", "style"]);
32static CLEAN_CONTENT_TAGS_RELAXED: AllowedSet = LazyLock::new(|| hashset![]);
33static ALLOWED_URL_SCHEMES: AllowedSet = LazyLock::new(|| hashset![
34 "https", "mailto", "data"
35]);
36static ALLOWED_URL_SCHEMES_RELAXED: AllowedSet = LazyLock::new(|| hashset![
37 "http", "https", "mailto", "data"
38]);
39const ALLOWED_LINK_POLICY: Option<&str> = Some("noopener noreferrer");
40static ALLOWED_TAGS: AllowedSet = LazyLock::new(|| hashset![
41 "animate", "video", "object", "img", "noscript", "meta", "link",
42 "title", "form", "input", "select", "option", "textarea",
43 "button", "label", "fieldset", "legend",
44
45 "svg", "circle", "path", "polygon"
46]);
47static ALLOWED_TAGS_RELAXED: AllowedSet = LazyLock::new(|| hashset![
48 "script", "html", "head", "body", "header", "main", "footer", "style"
49]);
50static ALLOWED_GENERIC_ATTR: AllowedSet = LazyLock::new(|| hashset![
51 "class", "open", "hidden", "alt", "type", "height", "width", "href", "id", "data",
52 "action", "formaction",
53 "name", "for", "value", "max", "placeholder", "accept", "alt", "pattern",
54 "maxlength", "minlength", "autocapitalize",
55 "autocomplete", "autocorrect", "autofocus", "disabled", "hidden", "required",
56 "content"
57]);
58static ALLOWED_GENERIC_ATTR_RELAXED: AllowedSet = LazyLock::new(|| hashset![
59 "onload", "onerror", "style", "src", "srcset", "sizes", "width", "height",
60 "fetchpriority", "defer", "role", "as", "rel", "lang",
61 "onload", "onerror", "onclick", "ondblclick", "onmouseover", "onmouseout",
62 "onmousedown", "onmouseup", "onwheel", "onkeydown", "onkeyup", "onkeypress",
63 "onchange", "onfocus", "onblur", "oninput", "onsubmit", "onreset", "onunload",
64 "onresize", "onhashchange", "onplay", "onpause", "onended", "onvolumechange",
65 "ontimeupdate"
66]);
67static ALLOWED_HTMX_ATTR: AllowedSet = LazyLock::new(|| hashset![
68 "hx-get", "hx-post", "hx-put", "hx-patch", "hx-delete", "hx-on", "hx-push-url",
70 "hx-select", "hx-select-oob", "hx-swap", "hx-swap-oob", "hx-target", "hx-trigger",
71 "hx-vals",
72 "hx-boost", "hx-confirm", "hx-disable", "hx-disable-elt", "hx-disinherit",
74 "hx-encoding", "hx-ext", "hx-headers", "hx-history", "hx-history-elt", "hx-include",
75 "hx-indicator", "hx-inherit", "hx-params", "hx-preserve", "hx-prompt", "hx-replace-url",
76 "hx-request", "hx-sync", "hx-validate", "hx-vars",
77]);
78static mut ALLOWED_ATTRS: AllowedMap = LazyLock::new(|| hashmap![
79 "a" => hashset![
80 "href", "hreflang"
81 ],
82 "bdo" => hashset![
83 "dir"
84 ],
85 "blockquote" => hashset![
86 "cite"
87 ],
88 "col" => hashset![
89 "align", "char", "charoff", "span"
90 ],
91 "colgroup" => hashset![
92 "align", "char", "charoff", "span"
93 ],
94 "del" => hashset![
95 "cite", "datetime"
96 ],
97 "hr" => hashset![
98 "align", "size", "width"
99 ],
100 "img" => hashset![
101 "align", "alt", "height", "src", "width"
102 ],
103 "ins" => hashset![
104 "cite", "datetime"
105 ],
106 "ol" => hashset![
107 "start"
108 ],
109 "q" => hashset![
110 "cite"
111 ],
112 "table" => hashset![
113 "align", "char", "charoff", "summary"
114 ],
115 "tbody" => hashset![
116 "align", "char", "charoff"
117 ],
118 "td" => hashset![
119 "align", "char", "charoff", "colspan", "headers", "rowspan"
120 ],
121 "tfoot" => hashset![
122 "align", "char", "charoff"
123 ],
124 "th" => hashset![
125 "align", "char", "charoff", "colspan", "headers", "rowspan", "scope"
126 ],
127 "thead" => hashset![
128 "align", "char", "charoff"
129 ],
130 "tr" => hashset![
131 "align", "char", "charoff"
132 ],
133
134
135 "object" => hashset![
136 "data", "type", "img"
137 ],
138 "path" => hashset![
139 "d", "stroke-linecap", "stroke-linejoin"
140 ],
141 "svg" => hashset![
142 "fill", "height", "opacity", "stroke", "stroke-width", "viewbox",
143 "width", "version", "baseProfile"
144 ],
145 "polygon" => hashset![
146 "points", "fill", "stroke", "stroke-width"
147 ],
148 "circle" => hashset![
149 "cx", "cy", "r", "fill", "stroke", "stroke-width"
150 ],
151 "video" => hashset![
152 "src", "controls", "autoplay", "loop", "muted", "width", "height"
153 ],
154 "animate" => hashset![
155 "attributeName", "from", "to", "dur", "fill", "begin", "repeatCount"
156 ],
157]);
158static mut ALLOWED_ATTRS_RELAXED: AllowedMap = LazyLock::new(|| hashmap![
159 "a" => hashset![
160 "href", "hreflang"
161 ],
162 "bdo" => hashset![
163 "dir"
164 ],
165 "blockquote" => hashset![
166 "cite"
167 ],
168 "col" => hashset![
169 "align", "char", "charoff", "span"
170 ],
171 "colgroup" => hashset![
172 "align", "char", "charoff", "span"
173 ],
174 "del" => hashset![
175 "cite", "datetime"
176 ],
177 "hr" => hashset![
178 "align", "size", "width"
179 ],
180 "img" => hashset![
181 "align", "alt", "height", "src", "width"
182 ],
183 "ins" => hashset![
184 "cite", "datetime"
185 ],
186 "ol" => hashset![
187 "start"
188 ],
189 "q" => hashset![
190 "cite"
191 ],
192 "table" => hashset![
193 "align", "char", "charoff", "summary"
194 ],
195 "tbody" => hashset![
196 "align", "char", "charoff"
197 ],
198 "td" => hashset![
199 "align", "char", "charoff", "colspan", "headers", "rowspan"
200 ],
201 "tfoot" => hashset![
202 "align", "char", "charoff"
203 ],
204 "th" => hashset![
205 "align", "char", "charoff", "colspan", "headers", "rowspan", "scope"
206 ],
207 "thead" => hashset![
208 "align", "char", "charoff"
209 ],
210 "tr" => hashset![
211 "align", "char", "charoff"
212 ],
213
214
215 "object" => hashset![
216 "data", "type", "img"
217 ],
218 "style" => hashset![
219 ],
220 "script" => hashset![
221 "src", "integrity", "crossorigin"
222 ],
223 "input" => hashset![
224 "id", "name", "for", "type", "value", "max", "placeholder",
225 "accept", "alt", "pattern", "maxlength", "minlength", "autocapitalize",
226 "autocomplete", "autocorrect", "autofocus", "disabled", "hidden", "required",
227 "onload", "onerror", "onclick", "ondblclick", "onmouseover", "onmouseout",
228 "onmousedown", "onmouseup", "onwheel", "onkeydown", "onkeyup", "onkeypress",
229 "onchange", "onfocus", "onblur", "oninput", "onsubmit", "onreset", "onunload",
230 "onresize", "onhashchange", "onplay", "onpause", "onended", "onvolumechange",
231 "ontimeupdate",
232 ],
233 "select" => hashset![
234 "id", "name", "for", "type", "value", "max", "placeholder",
235 "accept", "alt", "pattern", "maxlength", "minlength", "autocapitalize",
236 "autocomplete", "autocorrect", "autofocus", "disabled", "hidden", "required",
237 "onload", "onerror", "onclick", "ondblclick", "onmouseover", "onmouseout",
238 "onmousedown", "onmouseup", "onwheel", "onkeydown", "onkeyup", "onkeypress",
239 "onchange", "onfocus", "onblur", "oninput", "onsubmit", "onreset", "onunload",
240 "onresize", "onhashchange", "onplay", "onpause", "onended", "onvolumechange",
241 "ontimeupdate",
242 ],
243 "path" => hashset![
244 "d", "stroke-linecap", "stroke-linejoin"
245 ],
246 "svg" => hashset![
247 "fill", "height", "opacity", "stroke", "stroke-width", "viewbox",
248 "width", "version", "baseProfile"
249 ],
250 "polygon" => hashset![
251 "points", "fill", "stroke", "stroke-width"
252 ],
253 "circle" => hashset![
254 "cx", "cy", "r", "fill", "stroke", "stroke-width"
255 ],
256 "video" => hashset![
257 "src", "controls", "autoplay", "loop", "muted", "width", "height"
258 ],
259 "animate" => hashset![
260 "attributeName", "from", "to", "dur", "fill", "begin", "repeatCount"
261 ],
262]);
263static mut STRICT_SANITIZER: LazyLock<Builder> = LazyLock::new(|| unsafe {
264 let mut sanitizer = Builder::default();
265 default_init_sanitizer(&mut sanitizer, |sanitizer| { });
266 sanitizer
267 }
268);
269static mut RELAXED_SANITIZER: LazyLock<Builder> = LazyLock::new(|| unsafe {
270 let mut sanitizer = Builder::default();
271 default_init_sanitizer(&mut sanitizer, |sanitizer| {
272 sanitizer
273 .link_rel(None)
274 .add_tags((*ALLOWED_TAGS_RELAXED).clone())
275 .url_schemes((*ALLOWED_URL_SCHEMES_RELAXED).clone())
276 .add_generic_attributes((*ALLOWED_GENERIC_ATTR_RELAXED).clone())
277 .clean_content_tags(CLEAN_CONTENT_TAGS_RELAXED.clone())
278 ;
279 }
280 );
281 sanitizer
282 }
283);
284
285#[inline]
286unsafe fn default_init_sanitizer(builder: &mut Builder, init_closure: impl FnOnce(&mut Builder)) {
287 builder
288 .link_rel(ALLOWED_LINK_POLICY)
289 .url_schemes(ALLOWED_URL_SCHEMES.clone())
290 .add_tags((*ALLOWED_TAGS).clone())
291 .tag_attributes((*ALLOWED_ATTRS).clone())
292 .add_generic_attributes((*ALLOWED_GENERIC_ATTR).clone())
293 .add_generic_attributes((*ALLOWED_HTMX_ATTR).clone())
294 .clean_content_tags(CLEAN_CONTENT_TAGS.clone())
295 .strip_comments(true);
296 init_closure(builder);
297}
298
299#[inline]
300pub fn select_sanitizer<'a>(relaxed: bool) -> &'a Builder<'static> {
301 match relaxed {
302 true => unsafe {&*RELAXED_SANITIZER},
303 false => unsafe {&*STRICT_SANITIZER},
304 }
305}
306
307#[inline]
308pub fn select_sanitizer_mut<'a>(relaxed: bool) -> &'a mut Builder<'static> {
309 match relaxed {
310 true => unsafe {&mut *RELAXED_SANITIZER},
311 false => unsafe {&mut *STRICT_SANITIZER},
312 }
313}
314
315#[inline]
316pub fn sanitizer_update_attributes<T: 'static + ?Sized + Borrow<str>, I: IntoIterator<Item = &'static T>>(it: I, relaxed: bool) {
317 select_sanitizer_mut(relaxed).add_generic_attributes(it);
318}
319
320#[inline]
321pub fn sanitizer_update_tag_attributes<T: 'static + ?Sized + Borrow<str>, U: 'static + ?Sized + Borrow<str>, I: IntoIterator<Item = &'static T>>(tag: &'static U, it: I, relaxed: bool) {
322 select_sanitizer_mut(relaxed).add_tag_attributes(tag, it);
323}
324
325#[inline]
326pub fn sanitizers_update_attributes<T: 'static + ?Sized + Borrow<str>, I: IntoIterator<Item = &'static T> + Clone>(it: I) {
327 select_sanitizer_mut(false).add_generic_attributes(it.clone());
328 select_sanitizer_mut(true).add_generic_attributes(it);
329}
330
331#[inline]
332pub fn sanitizers_update_tag_attributes<T: 'static + ?Sized + Borrow<str>, I: IntoIterator<Item = &'static T> + Clone>(tag: &'static str, it: I) {
333 select_sanitizer_mut(false).add_tag_attributes(tag, it.clone());
334 select_sanitizer_mut(false).rm_clean_content_tags(&[tag]);
335 select_sanitizer_mut(true).add_tag_attributes(tag, it);
336 select_sanitizer_mut(true).rm_clean_content_tags(&[tag]);
337}
338
339#[inline]
340pub fn sanitize_html_strict(html: &str) -> RUMString {
341 select_sanitizer(false).clean(html).into()
342}
343
344#[inline]
345pub fn sanitize_html_relaxed(html: &str) -> RUMString {
346 select_sanitizer(true).clean(html).into()
347}
348
349#[inline]
350pub fn sanitize_html(html: &str, relaxed: bool) -> RUMString {
351 select_sanitizer(relaxed).clean(html).into()
352}