Skip to main content

ammonia/
lib.rs

1// Copyright (C) Michael Howell and others
2// this library is released under the same terms as Rust itself.
3
4#![deny(unsafe_code)]
5#![deny(missing_docs)]
6
7//! Ammonia is a whitelist-based HTML sanitization library. It is designed to
8//! prevent cross-site scripting, layout breaking, and clickjacking caused
9//! by untrusted user-provided HTML being mixed into a larger web page.
10//!
11//! Ammonia uses [html5ever] to parse and serialize document fragments the same way browsers do,
12//! so it is extremely resilient to syntactic obfuscation.
13//!
14//! Ammonia parses its input exactly according to the HTML5 specification;
15//! it will not linkify bare URLs, insert line or paragraph breaks, or convert `(C)` into ©.
16//! If you want that, use a markup processor before running the sanitizer, like [pulldown-cmark].
17//!
18//! # Examples
19//!
20//! ```
21//! let result = ammonia::clean(
22//!     "<b><img src='' onerror=alert('hax')>I'm not trying to XSS you</b>"
23//! );
24//! assert_eq!(result, "<b><img src=\"\">I'm not trying to XSS you</b>");
25//! ```
26//!
27//! [html5ever]: https://github.com/servo/html5ever "The HTML parser in Servo"
28//! [pulldown-cmark]: https://github.com/google/pulldown-cmark "CommonMark parser"
29
30#[cfg(ammonia_unstable)]
31pub mod rcdom;
32
33#[cfg(not(ammonia_unstable))]
34mod rcdom;
35
36mod style;
37
38use html5ever::interface::Attribute;
39use html5ever::serialize::{serialize, SerializeOpts};
40use html5ever::tree_builder::{NodeOrText, TreeSink};
41use html5ever::{driver as html, local_name, ns, Namespace, QualName};
42use maplit::{hashmap, hashset};
43use std::sync::LazyLock;
44use rcdom::{Handle, NodeData, RcDom, SerializableHandle};
45use std::borrow::{Borrow, Cow};
46use std::cell::Cell;
47use std::cmp::max;
48use std::collections::{HashMap, HashSet};
49use std::fmt::{self, Display};
50use std::io;
51use std::iter::IntoIterator as IntoIter;
52use std::mem;
53use std::rc::Rc;
54use std::str::FromStr;
55use html5ever::tendril::stream::TendrilSink;
56use html5ever::tendril::StrTendril;
57use html5ever::tendril::{format_tendril, ByteTendril};
58pub use url::Url;
59
60use html5ever::buffer_queue::BufferQueue;
61use html5ever::tokenizer::{Token, TokenSink, TokenSinkResult, Tokenizer};
62pub use url;
63
64static AMMONIA: LazyLock<Builder<'static>> = LazyLock::new(Builder::default);
65
66/// Clean HTML with a conservative set of defaults.
67///
68/// * [tags](struct.Builder.html#defaults)
69/// * [`script` and `style` have their contents stripped](struct.Builder.html#defaults-1)
70/// * [attributes on specific tags](struct.Builder.html#defaults-2)
71/// * [attributes on all tags](struct.Builder.html#defaults-6)
72/// * [url schemes](struct.Builder.html#defaults-7)
73/// * [relative URLs are passed through, unchanged, by default](struct.Builder.html#defaults-8)
74/// * [links are marked `noopener noreferrer` by default](struct.Builder.html#defaults-9)
75/// * all `class=""` settings are blocked by default
76/// * comments are stripped by default
77/// * no generic attribute prefixes are turned on by default
78/// * no specific tag-attribute-value settings are configured by default
79///
80/// [opener]: https://mathiasbynens.github.io/rel-noopener/
81/// [referrer]: https://en.wikipedia.org/wiki/HTTP_referer
82///
83/// # Examples
84///
85///     assert_eq!(ammonia::clean("XSS<script>attack</script>"), "XSS")
86pub fn clean(src: &str) -> String {
87    AMMONIA.clean(src).to_string()
88}
89
90/// Turn an arbitrary string into unformatted HTML.
91///
92/// This function is roughly equivalent to PHP's `htmlspecialchars` and `htmlentities`.
93/// It is as strict as possible, encoding every character that has special meaning to the
94/// HTML parser.
95///
96/// # Warnings
97///
98/// This function cannot be used to package strings into a `<script>` or `<style>` tag;
99/// you need a JavaScript or CSS escaper to do that.
100///
101///     // DO NOT DO THIS
102///     # use ammonia::clean_text;
103///     let untrusted = "Robert\"); abuse();//";
104///     let html = format!("<script>invoke(\"{}\")</script>", clean_text(untrusted));
105///
106/// `<textarea>` tags will strip the first newline, if present, even if that newline is encoded.
107/// If you want to build an editor that works the way most folks expect them to, you should put a
108/// newline at the beginning of the tag, like this:
109///
110///     # use ammonia::{Builder, clean_text};
111///     let untrusted = "\n\nhi!";
112///     let mut b = Builder::new();
113///     b.add_tags(&["textarea"]);
114///     // This is the bad version
115///     // The user put two newlines at the beginning, but the first one was removed
116///     let sanitized = b.clean(&format!("<textarea>{}</textarea>", clean_text(untrusted))).to_string();
117///     assert_eq!("<textarea>\nhi!</textarea>", sanitized);
118///     // This is a good version
119///     // The user put two newlines at the beginning, and we add a third one,
120///     // so the result still has two
121///     let sanitized = b.clean(&format!("<textarea>\n{}</textarea>", clean_text(untrusted))).to_string();
122///     assert_eq!("<textarea>\n\nhi!</textarea>", sanitized);
123///     // This version is also often considered good
124///     // For many applications, leading and trailing whitespace is probably unwanted
125///     let sanitized = b.clean(&format!("<textarea>{}</textarea>", clean_text(untrusted.trim()))).to_string();
126///     assert_eq!("<textarea>hi!</textarea>", sanitized);
127///
128/// It also does not make user text safe for HTML attribute microsyntaxes such as `class` or `id`.
129/// Only use this function for places where HTML accepts unrestricted text such as `title` attributes
130/// and paragraph contents.
131pub fn clean_text(src: &str) -> String {
132    let mut ret_val = String::with_capacity(max(4, src.len()));
133    for c in src.chars() {
134        let replacement = match c {
135            // this character, when confronted, will start a tag
136            '<' => "&lt;",
137            // in an unquoted attribute, will end the attribute value
138            '>' => "&gt;",
139            // in an attribute surrounded by double quotes, this character will end the attribute value
140            '\"' => "&quot;",
141            // in an attribute surrounded by single quotes, this character will end the attribute value
142            '\'' => "&apos;",
143            // in HTML5, returns a bogus parse error in an unquoted attribute, while in SGML/HTML, it will end an attribute value surrounded by backquotes
144            '`' => "&grave;",
145            // in an unquoted attribute, this character will end the attribute
146            '/' => "&#47;",
147            // starts an entity reference
148            '&' => "&amp;",
149            // if at the beginning of an unquoted attribute, will get ignored
150            '=' => "&#61;",
151            // will end an unquoted attribute
152            ' ' => "&#32;",
153            '\t' => "&#9;",
154            '\n' => "&#10;",
155            '\x0c' => "&#12;",
156            '\r' => "&#13;",
157            // a spec-compliant browser will perform this replacement anyway, but the middleware might not
158            '\0' => "&#65533;",
159            // ALL OTHER CHARACTERS ARE PASSED THROUGH VERBATIM
160            _ => {
161                ret_val.push(c);
162                continue;
163            }
164        };
165        ret_val.push_str(replacement);
166    }
167    ret_val
168}
169
170/// Determine if a given string contains HTML
171///
172/// This function is parses the full string into HTML and checks if the input contained any
173/// HTML syntax.
174///
175/// # Note
176/// This function will return positively for strings that contain invalid HTML syntax like
177/// `<g>` and even `Vec::<u8>::new()`.
178pub fn is_html(input: &str) -> bool {
179    let santok = SanitizationTokenizer::new();
180    let mut chunk = ByteTendril::new();
181    chunk.push_slice(input.as_bytes());
182    let mut input = BufferQueue::default();
183    input.push_back(chunk.try_reinterpret().unwrap());
184
185    let tok = Tokenizer::new(santok, Default::default());
186    let _ = tok.feed(&mut input);
187    tok.end();
188    tok.sink.was_sanitized.get()
189}
190
191#[derive(Clone)]
192struct SanitizationTokenizer {
193    was_sanitized: Cell<bool>,
194}
195
196impl SanitizationTokenizer {
197    pub fn new() -> SanitizationTokenizer {
198        SanitizationTokenizer {
199            was_sanitized: false.into(),
200        }
201    }
202}
203
204impl TokenSink for SanitizationTokenizer {
205    type Handle = ();
206    fn process_token(&self, token: Token, _line_number: u64) -> TokenSinkResult<()> {
207        match token {
208            Token::CharacterTokens(_) | Token::EOFToken | Token::ParseError(_) => {}
209            _ => {
210                self.was_sanitized.set(true);
211            }
212        }
213        TokenSinkResult::Continue
214    }
215    fn end(&self) {}
216}
217
218/// An HTML sanitizer.
219///
220/// Given a fragment of HTML, Ammonia will parse it according to the HTML5
221/// parsing algorithm and sanitize any disallowed tags or attributes. This
222/// algorithm also takes care of things like unclosed and (some) misnested
223/// tags.
224///
225/// # Examples
226///
227///     use ammonia::{Builder, UrlRelative};
228///
229///     let a = Builder::default()
230///         .link_rel(None)
231///         .url_relative(UrlRelative::PassThrough)
232///         .clean("<a href=/>test")
233///         .to_string();
234///     assert_eq!(
235///         a,
236///         "<a href=\"/\">test</a>");
237///
238/// # Panics
239///
240/// Running [`clean`] or [`clean_from_reader`] may cause a panic if the builder is
241/// configured with any of these (contradictory) settings:
242///
243///  * The `rel` attribute is added to [`generic_attributes`] or the
244///    [`tag_attributes`] for the `<a>` tag, and [`link_rel`] is not set to `None`.
245///
246///    For example, this is going to panic, since [`link_rel`] is set  to
247///    `Some("noopener noreferrer")` by default,
248///    and it makes no sense to simultaneously say that the user is allowed to
249///    set their own `rel` attribute while saying that every link shall be set to
250///    a particular value:
251///
252///    ```should_panic
253///    use ammonia::Builder;
254///    use maplit::hashset;
255///
256///    # fn main() {
257///    Builder::default()
258///        .generic_attributes(hashset!["rel"])
259///        .clean("");
260///    # }
261///    ```
262///
263///    This, however, is perfectly valid:
264///
265///    ```
266///    use ammonia::Builder;
267///    use maplit::hashset;
268///
269///    # fn main() {
270///    Builder::default()
271///        .generic_attributes(hashset!["rel"])
272///        .link_rel(None)
273///        .clean("");
274///    # }
275///    ```
276///
277///  * The `class` attribute is in [`allowed_classes`] and is in the
278///    corresponding [`tag_attributes`] or in [`generic_attributes`].
279///
280///    This is done both to line up with the treatment of `rel`,
281///    and to prevent people from accidentally allowing arbitrary
282///    classes on a particular element.
283///
284///    This will panic:
285///
286///    ```should_panic
287///    use ammonia::Builder;
288///    use maplit::{hashmap, hashset};
289///
290///    # fn main() {
291///    Builder::default()
292///        .generic_attributes(hashset!["class"])
293///        .allowed_classes(hashmap!["span" => hashset!["hidden"]])
294///        .clean("");
295///    # }
296///    ```
297///
298///    This, however, is perfectly valid:
299///
300///    ```
301///    use ammonia::Builder;
302///    use maplit::{hashmap, hashset};
303///
304///    # fn main() {
305///    Builder::default()
306///        .allowed_classes(hashmap!["span" => hashset!["hidden"]])
307///        .clean("");
308///    # }
309///    ```
310///
311///  * A tag is in either [`tags`] or [`tag_attributes`] while also
312///    being in [`clean_content_tags`].
313///
314///    Both [`tags`] and [`tag_attributes`] are whitelists but
315///    [`clean_content_tags`] is a blacklist, so it doesn't make sense
316///    to have the same tag in both.
317///
318///    For example, this will panic, since the `aside` tag is in
319///    [`tags`] by default:
320///
321///    ```should_panic
322///    use ammonia::Builder;
323///    use maplit::hashset;
324///
325///    # fn main() {
326///    Builder::default()
327///        .clean_content_tags(hashset!["aside"])
328///        .clean("");
329///    # }
330///    ```
331///
332///    This, however, is valid:
333///
334///    ```
335///    use ammonia::Builder;
336///    use maplit::hashset;
337///
338///    # fn main() {
339///    Builder::default()
340///        .rm_tags(&["aside"])
341///        .clean_content_tags(hashset!["aside"])
342///        .clean("");
343///    # }
344///    ```
345///
346/// [`clean`]: #method.clean
347/// [`clean_from_reader`]: #method.clean_from_reader
348/// [`generic_attributes`]: #method.generic_attributes
349/// [`tag_attributes`]: #method.tag_attributes
350/// [`generic_attributes`]: #method.generic_attributes
351/// [`link_rel`]: #method.link_rel
352/// [`allowed_classes`]: #method.allowed_classes
353/// [`id_prefix`]: #method.id_prefix
354/// [`tags`]: #method.tags
355/// [`clean_content_tags`]: #method.clean_content_tags
356#[derive(Debug)]
357pub struct Builder<'a> {
358    tags: HashSet<&'a str>,
359    clean_content_tags: HashSet<&'a str>,
360    tag_attributes: HashMap<&'a str, HashSet<&'a str>>,
361    tag_attribute_values: HashMap<&'a str, HashMap<&'a str, HashSet<&'a str>>>,
362    set_tag_attribute_values: HashMap<&'a str, HashMap<&'a str, &'a str>>,
363    generic_attributes: HashSet<&'a str>,
364    url_schemes: HashSet<&'a str>,
365    url_relative: UrlRelative<'a>,
366    attribute_filter: Option<Box<dyn AttributeFilter>>,
367    link_rel: Option<&'a str>,
368    allowed_classes: HashMap<&'a str, HashSet<&'a str>>,
369    strip_comments: bool,
370    id_prefix: Option<&'a str>,
371    generic_attribute_prefixes: Option<HashSet<&'a str>>,
372    style_properties: Option<HashSet<&'a str>>,
373}
374
375impl<'a> Default for Builder<'a> {
376    fn default() -> Self {
377        #[rustfmt::skip]
378        let tags = hashset![
379            "a", "abbr", "acronym", "area", "article", "aside", "b", "bdi",
380            "bdo", "blockquote", "br", "caption", "center", "cite", "code",
381            "col", "colgroup", "data", "dd", "del", "details", "dfn", "div",
382            "dl", "dt", "em", "figcaption", "figure", "footer", "h1", "h2",
383            "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "i", "img",
384            "ins", "kbd", "li", "map", "mark", "nav", "ol", "p", "pre",
385            "q", "rp", "rt", "rtc", "ruby", "s", "samp", "small", "span",
386            "strike", "strong", "sub", "summary", "sup", "table", "tbody",
387            "td", "th", "thead", "time", "tr", "tt", "u", "ul", "var", "wbr"
388        ];
389        let clean_content_tags = hashset!["script", "style"];
390        let generic_attributes = hashset!["lang", "title"];
391        let tag_attributes = hashmap![
392            "a" => hashset![
393                "href", "hreflang"
394            ],
395            "bdo" => hashset![
396                "dir"
397            ],
398            "blockquote" => hashset![
399                "cite"
400            ],
401            "col" => hashset![
402                "align", "char", "charoff", "span"
403            ],
404            "colgroup" => hashset![
405                "align", "char", "charoff", "span"
406            ],
407            "del" => hashset![
408                "cite", "datetime"
409            ],
410            "hr" => hashset![
411                "align", "size", "width"
412            ],
413            "img" => hashset![
414                "align", "alt", "height", "src", "width"
415            ],
416            "ins" => hashset![
417                "cite", "datetime"
418            ],
419            "ol" => hashset![
420                "start"
421            ],
422            "q" => hashset![
423                "cite"
424            ],
425            "table" => hashset![
426                "align", "char", "charoff", "summary"
427            ],
428            "tbody" => hashset![
429                "align", "char", "charoff"
430            ],
431            "td" => hashset![
432                "align", "char", "charoff", "colspan", "headers", "rowspan"
433            ],
434            "tfoot" => hashset![
435                "align", "char", "charoff"
436            ],
437            "th" => hashset![
438                "align", "char", "charoff", "colspan", "headers", "rowspan", "scope"
439            ],
440            "thead" => hashset![
441                "align", "char", "charoff"
442            ],
443            "tr" => hashset![
444                "align", "char", "charoff"
445            ],
446        ];
447        let tag_attribute_values = hashmap![];
448        let set_tag_attribute_values = hashmap![];
449        let url_schemes = hashset![
450            "bitcoin",
451            "ftp",
452            "ftps",
453            "geo",
454            "http",
455            "https",
456            "im",
457            "irc",
458            "ircs",
459            "magnet",
460            "mailto",
461            "mms",
462            "mx",
463            "news",
464            "nntp",
465            "openpgp4fpr",
466            "sip",
467            "sms",
468            "smsto",
469            "ssh",
470            "tel",
471            "url",
472            "webcal",
473            "wtai",
474            "xmpp"
475        ];
476        let allowed_classes = hashmap![];
477
478        Builder {
479            tags,
480            clean_content_tags,
481            tag_attributes,
482            tag_attribute_values,
483            set_tag_attribute_values,
484            generic_attributes,
485            url_schemes,
486            url_relative: UrlRelative::PassThrough,
487            attribute_filter: None,
488            link_rel: Some("noopener noreferrer"),
489            allowed_classes,
490            strip_comments: true,
491            id_prefix: None,
492            generic_attribute_prefixes: None,
493            style_properties: None,
494        }
495    }
496}
497
498impl<'a> Builder<'a> {
499    /// Sets the tags that are allowed.
500    ///
501    /// Note that the document-level tags `<html>`, `<head>`, and `<body>` cannot
502    /// be allowed here. Ammonia parses its input as a fragment (as if it were
503    /// the contents of a `<div>`), so these tags are stripped by the parser
504    /// before they reach the sanitizer.
505    ///
506    /// # Examples
507    ///
508    ///     use ammonia::Builder;
509    ///     use maplit::hashset;
510    ///
511    ///     # fn main() {
512    ///     let tags = hashset!["my-tag"];
513    ///     let a = Builder::new()
514    ///         .tags(tags)
515    ///         .clean("<my-tag>")
516    ///         .to_string();
517    ///     assert_eq!(a, "<my-tag></my-tag>");
518    ///     # }
519    ///
520    /// # Defaults
521    ///
522    /// ```notest
523    /// a, abbr, acronym, area, article, aside, b, bdi,
524    /// bdo, blockquote, br, caption, center, cite, code,
525    /// col, colgroup, data, dd, del, details, dfn, div,
526    /// dl, dt, em, figcaption, figure, footer, h1, h2,
527    /// h3, h4, h5, h6, header, hgroup, hr, i, img,
528    /// ins, kbd, li, map, mark, nav, ol, p, pre,
529    /// q, rp, rt, rtc, ruby, s, samp, small, span,
530    /// strike, strong, sub, summary, sup, table, tbody,
531    /// td, th, thead, time, tr, tt, u, ul, var, wbr
532    /// ```
533    pub fn tags(&mut self, value: HashSet<&'a str>) -> &mut Self {
534        self.tags = value;
535        self
536    }
537
538    /// Add additonal whitelisted tags without overwriting old ones.
539    ///
540    /// Does nothing if the tag is already there.
541    ///
542    /// # Examples
543    ///
544    ///     let a = ammonia::Builder::default()
545    ///         .add_tags(&["my-tag"])
546    ///         .clean("<my-tag>test</my-tag> <span>mess</span>").to_string();
547    ///     assert_eq!("<my-tag>test</my-tag> <span>mess</span>", a);
548    pub fn add_tags<T: 'a + ?Sized + Borrow<str>, I: IntoIter<Item = &'a T>>(
549        &mut self,
550        it: I,
551    ) -> &mut Self {
552        self.tags.extend(it.into_iter().map(Borrow::borrow));
553        self
554    }
555
556    /// Remove already-whitelisted tags.
557    ///
558    /// Does nothing if the tags is already gone.
559    ///
560    /// # Examples
561    ///
562    ///     let a = ammonia::Builder::default()
563    ///         .rm_tags(&["span"])
564    ///         .clean("<span></span>").to_string();
565    ///     assert_eq!("", a);
566    pub fn rm_tags<'b, T: 'b + ?Sized + Borrow<str>, I: IntoIter<Item = &'b T>>(
567        &mut self,
568        it: I,
569    ) -> &mut Self {
570        for i in it {
571            self.tags.remove(i.borrow());
572        }
573        self
574    }
575
576    /// Returns a copy of the set of whitelisted tags.
577    ///
578    /// # Examples
579    ///
580    ///     use maplit::hashset;
581    ///
582    ///     let tags = hashset!["my-tag-1", "my-tag-2"];
583    ///
584    ///     let mut b = ammonia::Builder::default();
585    ///     b.tags(Clone::clone(&tags));
586    ///     assert_eq!(tags, b.clone_tags());
587    pub fn clone_tags(&self) -> HashSet<&'a str> {
588        self.tags.clone()
589    }
590
591    /// Sets the tags whose contents will be completely removed from the output.
592    ///
593    /// Adding tags which are whitelisted in `tags` or `tag_attributes` will cause
594    /// a panic.
595    ///
596    /// # Examples
597    ///
598    ///     use ammonia::Builder;
599    ///     use maplit::hashset;
600    ///
601    ///     # fn main() {
602    ///     let tag_blacklist = hashset!["script", "style"];
603    ///     let a = Builder::new()
604    ///         .clean_content_tags(tag_blacklist)
605    ///         .clean("<script>alert('hello')</script><style>a { background: #fff }</style>")
606    ///         .to_string();
607    ///     assert_eq!(a, "");
608    ///     # }
609    ///
610    /// # Defaults
611    ///
612    /// ```notest
613    /// script, style
614    /// ```
615    pub fn clean_content_tags(&mut self, value: HashSet<&'a str>) -> &mut Self {
616        self.clean_content_tags = value;
617        self
618    }
619
620    /// Add additonal blacklisted clean-content tags without overwriting old ones.
621    ///
622    /// Does nothing if the tag is already there.
623    ///
624    /// Adding tags which are whitelisted in `tags` or `tag_attributes` will cause
625    /// a panic.
626    ///
627    /// # Examples
628    ///
629    ///     let a = ammonia::Builder::default()
630    ///         .add_clean_content_tags(&["my-tag"])
631    ///         .clean("<my-tag>test</my-tag><span>mess</span>").to_string();
632    ///     assert_eq!("<span>mess</span>", a);
633    pub fn add_clean_content_tags<T: 'a + ?Sized + Borrow<str>, I: IntoIter<Item = &'a T>>(
634        &mut self,
635        it: I,
636    ) -> &mut Self {
637        self.clean_content_tags
638            .extend(it.into_iter().map(Borrow::borrow));
639        self
640    }
641
642    /// Remove already-blacklisted clean-content tags.
643    ///
644    /// Does nothing if the tags aren't blacklisted.
645    ///
646    /// # Examples
647    ///     use ammonia::Builder;
648    ///     use maplit::hashset;
649    ///
650    ///     # fn main() {
651    ///     let tag_blacklist = hashset!["script"];
652    ///     let a = ammonia::Builder::default()
653    ///         .clean_content_tags(tag_blacklist)
654    ///         .rm_clean_content_tags(&["script"])
655    ///         .clean("<script>XSS</script>").to_string();
656    ///     assert_eq!("XSS", a);
657    ///     # }
658    pub fn rm_clean_content_tags<'b, T: 'b + ?Sized + Borrow<str>, I: IntoIter<Item = &'b T>>(
659        &mut self,
660        it: I,
661    ) -> &mut Self {
662        for i in it {
663            self.clean_content_tags.remove(i.borrow());
664        }
665        self
666    }
667
668    /// Returns a copy of the set of blacklisted clean-content tags.
669    ///
670    /// # Examples
671    ///     # use maplit::hashset;
672    ///
673    ///     let tags = hashset!["my-tag-1", "my-tag-2"];
674    ///
675    ///     let mut b = ammonia::Builder::default();
676    ///     b.clean_content_tags(Clone::clone(&tags));
677    ///     assert_eq!(tags, b.clone_clean_content_tags());
678    pub fn clone_clean_content_tags(&self) -> HashSet<&'a str> {
679        self.clean_content_tags.clone()
680    }
681
682    /// Sets the HTML attributes that are allowed on specific tags.
683    ///
684    /// The value is structured as a map from tag names to a set of attribute names.
685    ///
686    /// If a tag is not itself whitelisted, adding entries to this map will do nothing.
687    ///
688    /// # Examples
689    ///
690    ///     use ammonia::Builder;
691    ///     use maplit::{hashmap, hashset};
692    ///
693    ///     # fn main() {
694    ///     let tags = hashset!["my-tag"];
695    ///     let tag_attributes = hashmap![
696    ///         "my-tag" => hashset!["val"]
697    ///     ];
698    ///     let a = Builder::new().tags(tags).tag_attributes(tag_attributes)
699    ///         .clean("<my-tag val=1>")
700    ///         .to_string();
701    ///     assert_eq!(a, "<my-tag val=\"1\"></my-tag>");
702    ///     # }
703    ///
704    /// # Defaults
705    ///
706    /// ```notest
707    /// a =>
708    ///     href, hreflang
709    /// bdo =>
710    ///     dir
711    /// blockquote =>
712    ///     cite
713    /// col =>
714    ///     align, char, charoff, span
715    /// colgroup =>
716    ///     align, char, charoff, span
717    /// del =>
718    ///     cite, datetime
719    /// hr =>
720    ///     align, size, width
721    /// img =>
722    ///     align, alt, height, src, width
723    /// ins =>
724    ///     cite, datetime
725    /// ol =>
726    ///     start
727    /// q =>
728    ///     cite
729    /// table =>
730    ///     align, char, charoff, summary
731    /// tbody =>
732    ///     align, char, charoff
733    /// td =>
734    ///     align, char, charoff, colspan, headers, rowspan
735    /// tfoot =>
736    ///     align, char, charoff
737    /// th =>
738    ///     align, char, charoff, colspan, headers, rowspan, scope
739    /// thead =>
740    ///     align, char, charoff
741    /// tr =>
742    ///     align, char, charoff
743    /// ```
744    pub fn tag_attributes(&mut self, value: HashMap<&'a str, HashSet<&'a str>>) -> &mut Self {
745        self.tag_attributes = value;
746        self
747    }
748
749    /// Add additonal whitelisted tag-specific attributes without overwriting old ones.
750    ///
751    /// # Examples
752    ///
753    ///     let a = ammonia::Builder::default()
754    ///         .add_tags(&["my-tag"])
755    ///         .add_tag_attributes("my-tag", &["my-attr"])
756    ///         .clean("<my-tag my-attr>test</my-tag> <span>mess</span>").to_string();
757    ///     assert_eq!("<my-tag my-attr=\"\">test</my-tag> <span>mess</span>", a);
758    pub fn add_tag_attributes<
759        T: 'a + ?Sized + Borrow<str>,
760        U: 'a + ?Sized + Borrow<str>,
761        I: IntoIter<Item = &'a T>,
762    >(
763        &mut self,
764        tag: &'a U,
765        it: I,
766    ) -> &mut Self {
767        self.tag_attributes
768            .entry(tag.borrow())
769            .or_default()
770            .extend(it.into_iter().map(Borrow::borrow));
771        self
772    }
773
774    /// Remove already-whitelisted tag-specific attributes.
775    ///
776    /// Does nothing if the attribute is already gone.
777    ///
778    /// # Examples
779    ///
780    ///     let a = ammonia::Builder::default()
781    ///         .rm_tag_attributes("a", &["href"])
782    ///         .clean("<a href=\"/\"></a>").to_string();
783    ///     assert_eq!("<a rel=\"noopener noreferrer\"></a>", a);
784    pub fn rm_tag_attributes<
785        'b,
786        'c,
787        T: 'b + ?Sized + Borrow<str>,
788        U: 'c + ?Sized + Borrow<str>,
789        I: IntoIter<Item = &'b T>,
790    >(
791        &mut self,
792        tag: &'c U,
793        it: I,
794    ) -> &mut Self {
795        if let Some(tag) = self.tag_attributes.get_mut(tag.borrow()) {
796            for i in it {
797                tag.remove(i.borrow());
798            }
799        }
800        self
801    }
802
803    /// Returns a copy of the set of whitelisted tag-specific attributes.
804    ///
805    /// # Examples
806    ///     use maplit::{hashmap, hashset};
807    ///
808    ///     let tag_attributes = hashmap![
809    ///         "my-tag" => hashset!["my-attr-1", "my-attr-2"]
810    ///     ];
811    ///
812    ///     let mut b = ammonia::Builder::default();
813    ///     b.tag_attributes(Clone::clone(&tag_attributes));
814    ///     assert_eq!(tag_attributes, b.clone_tag_attributes());
815    pub fn clone_tag_attributes(&self) -> HashMap<&'a str, HashSet<&'a str>> {
816        self.tag_attributes.clone()
817    }
818
819    /// Sets the values of HTML attributes that are allowed on specific tags.
820    ///
821    /// The value is structured as a map from tag names to a map from attribute names to a set of
822    /// attribute values.
823    ///
824    /// If a tag is not itself whitelisted, adding entries to this map will do nothing.
825    ///
826    /// # Examples
827    ///
828    ///     use ammonia::Builder;
829    ///     use maplit::{hashmap, hashset};
830    ///
831    ///     # fn main() {
832    ///     let tags = hashset!["my-tag"];
833    ///     let tag_attribute_values = hashmap![
834    ///         "my-tag" => hashmap![
835    ///             "my-attr" => hashset!["val"],
836    ///         ],
837    ///     ];
838    ///     let a = Builder::new().tags(tags).tag_attribute_values(tag_attribute_values)
839    ///         .clean("<my-tag my-attr=val>")
840    ///         .to_string();
841    ///     assert_eq!(a, "<my-tag my-attr=\"val\"></my-tag>");
842    ///     # }
843    ///
844    /// # Defaults
845    ///
846    /// None.
847    pub fn tag_attribute_values(
848        &mut self,
849        value: HashMap<&'a str, HashMap<&'a str, HashSet<&'a str>>>,
850    ) -> &mut Self {
851        self.tag_attribute_values = value;
852        self
853    }
854
855    /// Add additonal whitelisted tag-specific attribute values without overwriting old ones.
856    ///
857    /// # Examples
858    ///
859    ///     let a = ammonia::Builder::default()
860    ///         .add_tags(&["my-tag"])
861    ///         .add_tag_attribute_values("my-tag", "my-attr", &[""])
862    ///         .clean("<my-tag my-attr>test</my-tag> <span>mess</span>").to_string();
863    ///     assert_eq!("<my-tag my-attr=\"\">test</my-tag> <span>mess</span>", a);
864    pub fn add_tag_attribute_values<
865        T: 'a + ?Sized + Borrow<str>,
866        U: 'a + ?Sized + Borrow<str>,
867        V: 'a + ?Sized + Borrow<str>,
868        I: IntoIter<Item = &'a T>,
869    >(
870        &mut self,
871        tag: &'a U,
872        attribute: &'a V,
873        it: I,
874    ) -> &mut Self {
875        self.tag_attribute_values
876            .entry(tag.borrow())
877            .or_default()
878            .entry(attribute.borrow())
879            .or_default()
880            .extend(it.into_iter().map(Borrow::borrow));
881
882        self
883    }
884
885    /// Remove already-whitelisted tag-specific attribute values.
886    ///
887    /// Does nothing if the attribute or the value is already gone.
888    ///
889    /// # Examples
890    ///
891    ///     let a = ammonia::Builder::default()
892    ///         .rm_tag_attributes("a", &["href"])
893    ///         .add_tag_attribute_values("a", "href", &["/"])
894    ///         .rm_tag_attribute_values("a", "href", &["/"])
895    ///         .clean("<a href=\"/\"></a>").to_string();
896    ///     assert_eq!("<a rel=\"noopener noreferrer\"></a>", a);
897    pub fn rm_tag_attribute_values<
898        'b,
899        'c,
900        T: 'b + ?Sized + Borrow<str>,
901        U: 'c + ?Sized + Borrow<str>,
902        V: 'c + ?Sized + Borrow<str>,
903        I: IntoIter<Item = &'b T>,
904    >(
905        &mut self,
906        tag: &'c U,
907        attribute: &'c V,
908        it: I,
909    ) -> &mut Self {
910        if let Some(attrs) = self
911            .tag_attribute_values
912            .get_mut(tag.borrow())
913            .and_then(|map| map.get_mut(attribute.borrow()))
914        {
915            for i in it {
916                attrs.remove(i.borrow());
917            }
918        }
919        self
920    }
921
922    /// Returns a copy of the set of whitelisted tag-specific attribute values.
923    ///
924    /// # Examples
925    ///
926    ///     use maplit::{hashmap, hashset};
927    ///
928    ///     let attribute_values = hashmap![
929    ///         "my-attr-1" => hashset!["foo"],
930    ///         "my-attr-2" => hashset!["baz", "bar"],
931    ///     ];
932    ///     let tag_attribute_values = hashmap![
933    ///         "my-tag" => attribute_values
934    ///     ];
935    ///
936    ///     let mut b = ammonia::Builder::default();
937    ///     b.tag_attribute_values(Clone::clone(&tag_attribute_values));
938    ///     assert_eq!(tag_attribute_values, b.clone_tag_attribute_values());
939    pub fn clone_tag_attribute_values(
940        &self,
941    ) -> HashMap<&'a str, HashMap<&'a str, HashSet<&'a str>>> {
942        self.tag_attribute_values.clone()
943    }
944
945    /// Sets the values of HTML attributes that are to be set on specific tags.
946    ///
947    /// The value is structured as a map from tag names to a map from attribute names to an
948    /// attribute value.
949    ///
950    /// If a tag is not itself whitelisted, adding entries to this map will do nothing.
951    ///
952    /// # Examples
953    ///
954    ///     use ammonia::Builder;
955    ///     use maplit::{hashmap, hashset};
956    ///
957    ///     # fn main() {
958    ///     let tags = hashset!["my-tag"];
959    ///     let set_tag_attribute_values = hashmap![
960    ///         "my-tag" => hashmap![
961    ///             "my-attr" => "val",
962    ///         ],
963    ///     ];
964    ///     let a = Builder::new().tags(tags).set_tag_attribute_values(set_tag_attribute_values)
965    ///         .clean("<my-tag>")
966    ///         .to_string();
967    ///     assert_eq!(a, "<my-tag my-attr=\"val\"></my-tag>");
968    ///     # }
969    ///
970    /// # Defaults
971    ///
972    /// None.
973    pub fn set_tag_attribute_values(
974        &mut self,
975        value: HashMap<&'a str, HashMap<&'a str, &'a str>>,
976    ) -> &mut Self {
977        self.set_tag_attribute_values = value;
978        self
979    }
980
981    /// Add an attribute value to set on a specific element.
982    ///
983    /// # Examples
984    ///
985    ///     let a = ammonia::Builder::default()
986    ///         .add_tags(&["my-tag"])
987    ///         .set_tag_attribute_value("my-tag", "my-attr", "val")
988    ///         .clean("<my-tag>test</my-tag> <span>mess</span>").to_string();
989    ///     assert_eq!("<my-tag my-attr=\"val\">test</my-tag> <span>mess</span>", a);
990    pub fn set_tag_attribute_value<
991        T: 'a + ?Sized + Borrow<str>,
992        A: 'a + ?Sized + Borrow<str>,
993        V: 'a + ?Sized + Borrow<str>,
994    >(
995        &mut self,
996        tag: &'a T,
997        attribute: &'a A,
998        value: &'a V,
999    ) -> &mut Self {
1000        self.set_tag_attribute_values
1001            .entry(tag.borrow())
1002            .or_default()
1003            .insert(attribute.borrow(), value.borrow());
1004        self
1005    }
1006
1007    /// Remove existing tag-specific attribute values to be set.
1008    ///
1009    /// Does nothing if the attribute is already gone.
1010    ///
1011    /// # Examples
1012    ///
1013    ///     let a = ammonia::Builder::default()
1014    ///         // this does nothing, since no value is set for this tag attribute yet
1015    ///         .rm_set_tag_attribute_value("a", "target")
1016    ///         .set_tag_attribute_value("a", "target", "_blank")
1017    ///         .rm_set_tag_attribute_value("a", "target")
1018    ///         .clean("<a href=\"/\"></a>").to_string();
1019    ///     assert_eq!("<a href=\"/\" rel=\"noopener noreferrer\"></a>", a);
1020    pub fn rm_set_tag_attribute_value<
1021        T: 'a + ?Sized + Borrow<str>,
1022        A: 'a + ?Sized + Borrow<str>,
1023    >(
1024        &mut self,
1025        tag: &'a T,
1026        attribute: &'a A,
1027    ) -> &mut Self {
1028        if let Some(attributes) = self.set_tag_attribute_values.get_mut(tag.borrow()) {
1029            attributes.remove(attribute.borrow());
1030        }
1031        self
1032    }
1033
1034    /// Returns the value that will be set for the attribute on the element, if any.
1035    ///
1036    /// # Examples
1037    ///
1038    ///     let mut b = ammonia::Builder::default();
1039    ///     b.set_tag_attribute_value("a", "target", "_blank");
1040    ///     let value = b.get_set_tag_attribute_value("a", "target");
1041    ///     assert_eq!(value, Some("_blank"));
1042    pub fn get_set_tag_attribute_value<
1043        T: 'a + ?Sized + Borrow<str>,
1044        A: 'a + ?Sized + Borrow<str>,
1045    >(
1046        &self,
1047        tag: &'a T,
1048        attribute: &'a A,
1049    ) -> Option<&'a str> {
1050        self.set_tag_attribute_values
1051            .get(tag.borrow())
1052            .and_then(|map| map.get(attribute.borrow()))
1053            .copied()
1054    }
1055
1056    /// Returns a copy of the set of tag-specific attribute values to be set.
1057    ///
1058    /// # Examples
1059    ///
1060    ///     use maplit::{hashmap, hashset};
1061    ///
1062    ///     let attribute_values = hashmap![
1063    ///         "my-attr-1" => "foo",
1064    ///         "my-attr-2" => "bar",
1065    ///     ];
1066    ///     let set_tag_attribute_values = hashmap![
1067    ///         "my-tag" => attribute_values,
1068    ///     ];
1069    ///
1070    ///     let mut b = ammonia::Builder::default();
1071    ///     b.set_tag_attribute_values(Clone::clone(&set_tag_attribute_values));
1072    ///     assert_eq!(set_tag_attribute_values, b.clone_set_tag_attribute_values());
1073    pub fn clone_set_tag_attribute_values(&self) -> HashMap<&'a str, HashMap<&'a str, &'a str>> {
1074        self.set_tag_attribute_values.clone()
1075    }
1076
1077    /// Sets the prefix of attributes that are allowed on any tag.
1078    ///
1079    /// # Examples
1080    ///
1081    ///     use ammonia::Builder;
1082    ///     use maplit::hashset;
1083    ///
1084    ///     # fn main() {
1085    ///     let prefixes = hashset!["data-"];
1086    ///     let a = Builder::new()
1087    ///         .generic_attribute_prefixes(prefixes)
1088    ///         .clean("<b data-val=1>")
1089    ///         .to_string();
1090    ///     assert_eq!(a, "<b data-val=\"1\"></b>");
1091    ///     # }
1092    ///
1093    /// # Defaults
1094    ///
1095    /// No attribute prefixes are allowed by default.
1096    pub fn generic_attribute_prefixes(&mut self, value: HashSet<&'a str>) -> &mut Self {
1097        self.generic_attribute_prefixes = Some(value);
1098        self
1099    }
1100
1101    /// Add additional whitelisted attribute prefix without overwriting old ones.
1102    ///
1103    /// # Examples
1104    ///
1105    ///     let a = ammonia::Builder::default()
1106    ///         .add_generic_attribute_prefixes(&["my-"])
1107    ///         .clean("<span my-attr>mess</span>").to_string();
1108    ///     assert_eq!("<span my-attr=\"\">mess</span>", a);
1109    pub fn add_generic_attribute_prefixes<
1110        T: 'a + ?Sized + Borrow<str>,
1111        I: IntoIter<Item = &'a T>,
1112    >(
1113        &mut self,
1114        it: I,
1115    ) -> &mut Self {
1116        self.generic_attribute_prefixes
1117            .get_or_insert_with(HashSet::new)
1118            .extend(it.into_iter().map(Borrow::borrow));
1119        self
1120    }
1121
1122    /// Remove already-whitelisted attribute prefixes.
1123    ///
1124    /// Does nothing if the attribute prefix is already gone.
1125    ///
1126    /// # Examples
1127    ///
1128    ///     let a = ammonia::Builder::default()
1129    ///         .add_generic_attribute_prefixes(&["data-", "code-"])
1130    ///         .rm_generic_attribute_prefixes(&["data-"])
1131    ///         .clean("<span code-test=\"foo\" data-test=\"cool\"></span>").to_string();
1132    ///     assert_eq!("<span code-test=\"foo\"></span>", a);
1133    pub fn rm_generic_attribute_prefixes<
1134        'b,
1135        T: 'b + ?Sized + Borrow<str>,
1136        I: IntoIter<Item = &'b T>,
1137    >(
1138        &mut self,
1139        it: I,
1140    ) -> &mut Self {
1141        if let Some(true) = self.generic_attribute_prefixes.as_mut().map(|prefixes| {
1142            for i in it {
1143                let _ = prefixes.remove(i.borrow());
1144            }
1145            prefixes.is_empty()
1146        }) {
1147            self.generic_attribute_prefixes = None;
1148        }
1149        self
1150    }
1151
1152    /// Returns a copy of the set of whitelisted attribute prefixes.
1153    ///
1154    /// # Examples
1155    ///
1156    ///     use maplit::hashset;
1157    ///
1158    ///     let generic_attribute_prefixes = hashset!["my-prfx-1-", "my-prfx-2-"];
1159    ///
1160    ///     let mut b = ammonia::Builder::default();
1161    ///     b.generic_attribute_prefixes(Clone::clone(&generic_attribute_prefixes));
1162    ///     assert_eq!(Some(generic_attribute_prefixes), b.clone_generic_attribute_prefixes());
1163    pub fn clone_generic_attribute_prefixes(&self) -> Option<HashSet<&'a str>> {
1164        self.generic_attribute_prefixes.clone()
1165    }
1166
1167    /// Sets the attributes that are allowed on any tag.
1168    ///
1169    /// # Examples
1170    ///
1171    ///     use ammonia::Builder;
1172    ///     use maplit::hashset;
1173    ///
1174    ///     # fn main() {
1175    ///     let attributes = hashset!["data-val"];
1176    ///     let a = Builder::new()
1177    ///         .generic_attributes(attributes)
1178    ///         .clean("<b data-val=1>")
1179    ///         .to_string();
1180    ///     assert_eq!(a, "<b data-val=\"1\"></b>");
1181    ///     # }
1182    ///
1183    /// # Defaults
1184    ///
1185    /// ```notest
1186    /// lang, title
1187    /// ```
1188    pub fn generic_attributes(&mut self, value: HashSet<&'a str>) -> &mut Self {
1189        self.generic_attributes = value;
1190        self
1191    }
1192
1193    /// Add additonal whitelisted attributes without overwriting old ones.
1194    ///
1195    /// # Examples
1196    ///
1197    ///     let a = ammonia::Builder::default()
1198    ///         .add_generic_attributes(&["my-attr"])
1199    ///         .clean("<span my-attr>mess</span>").to_string();
1200    ///     assert_eq!("<span my-attr=\"\">mess</span>", a);
1201    pub fn add_generic_attributes<T: 'a + ?Sized + Borrow<str>, I: IntoIter<Item = &'a T>>(
1202        &mut self,
1203        it: I,
1204    ) -> &mut Self {
1205        self.generic_attributes
1206            .extend(it.into_iter().map(Borrow::borrow));
1207        self
1208    }
1209
1210    /// Remove already-whitelisted attributes.
1211    ///
1212    /// Does nothing if the attribute is already gone.
1213    ///
1214    /// # Examples
1215    ///
1216    ///     let a = ammonia::Builder::default()
1217    ///         .rm_generic_attributes(&["title"])
1218    ///         .clean("<span title=\"cool\"></span>").to_string();
1219    ///     assert_eq!("<span></span>", a);
1220    pub fn rm_generic_attributes<'b, T: 'b + ?Sized + Borrow<str>, I: IntoIter<Item = &'b T>>(
1221        &mut self,
1222        it: I,
1223    ) -> &mut Self {
1224        for i in it {
1225            self.generic_attributes.remove(i.borrow());
1226        }
1227        self
1228    }
1229
1230    /// Returns a copy of the set of whitelisted attributes.
1231    ///
1232    /// # Examples
1233    ///
1234    ///     use maplit::hashset;
1235    ///
1236    ///     let generic_attributes = hashset!["my-attr-1", "my-attr-2"];
1237    ///
1238    ///     let mut b = ammonia::Builder::default();
1239    ///     b.generic_attributes(Clone::clone(&generic_attributes));
1240    ///     assert_eq!(generic_attributes, b.clone_generic_attributes());
1241    pub fn clone_generic_attributes(&self) -> HashSet<&'a str> {
1242        self.generic_attributes.clone()
1243    }
1244
1245    /// Sets the URL schemes permitted on `href` and `src` attributes.
1246    ///
1247    /// # Examples
1248    ///
1249    ///     use ammonia::Builder;
1250    ///     use maplit::hashset;
1251    ///
1252    ///     # fn main() {
1253    ///     let url_schemes = hashset![
1254    ///         "http", "https", "mailto", "magnet"
1255    ///     ];
1256    ///     let a = Builder::new().url_schemes(url_schemes)
1257    ///         .clean("<a href=\"magnet:?xt=urn:ed2k:31D6CFE0D16AE931B73C59D7E0C089C0&xl=0&dn=zero_len.fil&xt=urn:bitprint:3I42H3S6NNFQ2MSVX7XZKYAYSCX5QBYJ.LWPNACQDBZRYXW3VHJVCJ64QBZNGHOHHHZWCLNQ&xt=urn:md5:D41D8CD98F00B204E9800998ECF8427E\">zero-length file</a>")
1258    ///         .to_string();
1259    ///
1260    ///     // See `link_rel` for information on the rel="noopener noreferrer" attribute
1261    ///     // in the cleaned HTML.
1262    ///     assert_eq!(a,
1263    ///       "<a href=\"magnet:?xt=urn:ed2k:31D6CFE0D16AE931B73C59D7E0C089C0&amp;xl=0&amp;dn=zero_len.fil&amp;xt=urn:bitprint:3I42H3S6NNFQ2MSVX7XZKYAYSCX5QBYJ.LWPNACQDBZRYXW3VHJVCJ64QBZNGHOHHHZWCLNQ&amp;xt=urn:md5:D41D8CD98F00B204E9800998ECF8427E\" rel=\"noopener noreferrer\">zero-length file</a>");
1264    ///     # }
1265    ///
1266    /// # Defaults
1267    ///
1268    /// ```notest
1269    /// bitcoin, ftp, ftps, geo, http, https, im, irc,
1270    /// ircs, magnet, mailto, mms, mx, news, nntp,
1271    /// openpgp4fpr, sip, sms, smsto, ssh, tel, url,
1272    /// webcal, wtai, xmpp
1273    /// ```
1274    pub fn url_schemes(&mut self, value: HashSet<&'a str>) -> &mut Self {
1275        self.url_schemes = value;
1276        self
1277    }
1278
1279    /// Add additonal whitelisted URL schemes without overwriting old ones.
1280    ///
1281    /// # Examples
1282    ///
1283    ///     let a = ammonia::Builder::default()
1284    ///         .add_url_schemes(&["my-scheme"])
1285    ///         .clean("<a href=my-scheme:home>mess</span>").to_string();
1286    ///     assert_eq!("<a href=\"my-scheme:home\" rel=\"noopener noreferrer\">mess</a>", a);
1287    pub fn add_url_schemes<T: 'a + ?Sized + Borrow<str>, I: IntoIter<Item = &'a T>>(
1288        &mut self,
1289        it: I,
1290    ) -> &mut Self {
1291        self.url_schemes.extend(it.into_iter().map(Borrow::borrow));
1292        self
1293    }
1294
1295    /// Remove already-whitelisted attributes.
1296    ///
1297    /// Does nothing if the attribute is already gone.
1298    ///
1299    /// # Examples
1300    ///
1301    ///     let a = ammonia::Builder::default()
1302    ///         .rm_url_schemes(&["ftp"])
1303    ///         .clean("<a href=\"ftp://ftp.mozilla.org/\"></a>").to_string();
1304    ///     assert_eq!("<a rel=\"noopener noreferrer\"></a>", a);
1305    pub fn rm_url_schemes<'b, T: 'b + ?Sized + Borrow<str>, I: IntoIter<Item = &'b T>>(
1306        &mut self,
1307        it: I,
1308    ) -> &mut Self {
1309        for i in it {
1310            self.url_schemes.remove(i.borrow());
1311        }
1312        self
1313    }
1314
1315    /// Returns a copy of the set of whitelisted URL schemes.
1316    ///
1317    /// # Examples
1318    ///     use maplit::hashset;
1319    ///
1320    ///     let url_schemes = hashset!["my-scheme-1", "my-scheme-2"];
1321    ///
1322    ///     let mut b = ammonia::Builder::default();
1323    ///     b.url_schemes(Clone::clone(&url_schemes));
1324    ///     assert_eq!(url_schemes, b.clone_url_schemes());
1325    pub fn clone_url_schemes(&self) -> HashSet<&'a str> {
1326        self.url_schemes.clone()
1327    }
1328
1329    /// Configures the behavior for relative URLs: pass-through, resolve-with-base, or deny.
1330    ///
1331    /// # Examples
1332    ///
1333    ///     use ammonia::{Builder, UrlRelative};
1334    ///
1335    ///     let a = Builder::new().url_relative(UrlRelative::PassThrough)
1336    ///         .clean("<a href=/>Home</a>")
1337    ///         .to_string();
1338    ///
1339    ///     // See `link_rel` for information on the rel="noopener noreferrer" attribute
1340    ///     // in the cleaned HTML.
1341    ///     assert_eq!(
1342    ///       a,
1343    ///       "<a href=\"/\" rel=\"noopener noreferrer\">Home</a>");
1344    ///
1345    /// # Defaults
1346    ///
1347    /// ```notest
1348    /// UrlRelative::PassThrough
1349    /// ```
1350    pub fn url_relative(&mut self, value: UrlRelative<'a>) -> &mut Self {
1351        self.url_relative = value;
1352        self
1353    }
1354
1355    /// Allows rewriting of all attributes using a callback.
1356    ///
1357    /// The callback takes name of the element, attribute and its value.
1358    /// Returns `None` to remove the attribute, or a value to use.
1359    ///
1360    /// Rewriting of attributes with URLs is done before `url_relative()`.
1361    ///
1362    /// # Panics
1363    ///
1364    /// If more than one callback is set.
1365    ///
1366    /// # Examples
1367    ///
1368    /// ```rust
1369    /// use ammonia::Builder;
1370    /// let a = Builder::new()
1371    ///     .attribute_filter(|element, attribute, value| {
1372    ///         match (element, attribute) {
1373    ///             ("img", "src") => None,
1374    ///             _ => Some(value.into())
1375    ///         }
1376    ///     })
1377    ///     .link_rel(None)
1378    ///     .clean("<a href=/><img alt=Home src=foo></a>")
1379    ///     .to_string();
1380    /// assert_eq!(a,
1381    ///     r#"<a href="/"><img alt="Home"></a>"#);
1382    /// ```
1383    pub fn attribute_filter<'cb, CallbackFn>(&mut self, callback: CallbackFn) -> &mut Self
1384    where
1385        CallbackFn: for<'u> Fn(&str, &str, &'u str) -> Option<Cow<'u, str>> + Send + Sync + 'static,
1386    {
1387        assert!(
1388            self.attribute_filter.is_none(),
1389            "attribute_filter can be set only once"
1390        );
1391        self.attribute_filter = Some(Box::new(callback));
1392        self
1393    }
1394
1395    /// Returns `true` if the relative URL resolver is set to `Deny`.
1396    ///
1397    /// # Examples
1398    ///
1399    ///     use ammonia::{Builder, UrlRelative};
1400    ///     let mut a = Builder::default();
1401    ///     a.url_relative(UrlRelative::Deny);
1402    ///     assert!(a.is_url_relative_deny());
1403    ///     a.url_relative(UrlRelative::PassThrough);
1404    ///     assert!(!a.is_url_relative_deny());
1405    pub fn is_url_relative_deny(&self) -> bool {
1406        matches!(self.url_relative, UrlRelative::Deny)
1407    }
1408
1409    /// Returns `true` if the relative URL resolver is set to `PassThrough`.
1410    ///
1411    /// # Examples
1412    ///
1413    ///     use ammonia::{Builder, UrlRelative};
1414    ///     let mut a = Builder::default();
1415    ///     a.url_relative(UrlRelative::Deny);
1416    ///     assert!(!a.is_url_relative_pass_through());
1417    ///     a.url_relative(UrlRelative::PassThrough);
1418    ///     assert!(a.is_url_relative_pass_through());
1419    pub fn is_url_relative_pass_through(&self) -> bool {
1420        matches!(self.url_relative, UrlRelative::PassThrough)
1421    }
1422
1423    /// Returns `true` if the relative URL resolver is set to `Custom`.
1424    ///
1425    /// # Examples
1426    ///
1427    ///     use ammonia::{Builder, UrlRelative};
1428    ///     use std::borrow::Cow;
1429    ///     fn test(a: &str) -> Option<Cow<str>> { None }
1430    ///     # fn main() {
1431    ///     let mut a = Builder::default();
1432    ///     a.url_relative(UrlRelative::Custom(Box::new(test)));
1433    ///     assert!(a.is_url_relative_custom());
1434    ///     a.url_relative(UrlRelative::PassThrough);
1435    ///     assert!(!a.is_url_relative_custom());
1436    ///     a.url_relative(UrlRelative::Deny);
1437    ///     assert!(!a.is_url_relative_custom());
1438    ///     # }
1439    pub fn is_url_relative_custom(&self) -> bool {
1440        matches!(self.url_relative, UrlRelative::Custom(_))
1441    }
1442
1443    /// Returns `true` if the relative URL resolver is set to `RewriteWithBase`.
1444    ///
1445    /// # Examples
1446    ///
1447    ///     use ammonia::{Builder, Url, UrlRelative};
1448    ///     # fn main() -> Result<(), url::ParseError> {
1449    ///     let mut a = Builder::default();
1450    ///     a.url_relative(UrlRelative::RewriteWithBase(Url::parse("https://example.com/")?));
1451    ///     assert!(a.is_url_relative_rewrite_with_base());
1452    ///     a.url_relative(UrlRelative::PassThrough);
1453    ///     assert!(!a.is_url_relative_rewrite_with_base());
1454    ///     # Ok(())
1455    ///     # }
1456    pub fn is_url_relative_rewrite_with_base(&self) -> bool {
1457        matches!(self.url_relative, UrlRelative::RewriteWithBase(_))
1458    }
1459
1460    /// Returns `true` if the relative URL resolver is set to `RewriteWithRoot`.
1461    ///
1462    /// # Examples
1463    ///
1464    ///     use ammonia::{Builder, Url, UrlRelative};
1465    ///     # fn main() -> Result<(), url::ParseError> {
1466    ///     let mut a = Builder::default();
1467    ///     a.url_relative(UrlRelative::RewriteWithRoot {
1468    ///         root: Url::parse("https://example.com/")?,
1469    ///         path: "index.html".to_string(),
1470    ///     });
1471    ///     assert!(a.is_url_relative_rewrite_with_root());
1472    ///     a.url_relative(UrlRelative::PassThrough);
1473    ///     assert!(!a.is_url_relative_rewrite_with_root());
1474    ///     # Ok(())
1475    ///     # }
1476    pub fn is_url_relative_rewrite_with_root(&self) -> bool {
1477        matches!(self.url_relative, UrlRelative::RewriteWithRoot { .. })
1478    }
1479
1480    /// Returns the base [`Url`] when the relative URL resolver is set to
1481    /// [`UrlRelative::RewriteWithBase`], or `None` otherwise.
1482    ///
1483    /// # Examples
1484    ///
1485    ///     use ammonia::{Builder, Url, UrlRelative};
1486    ///     # fn main() -> Result<(), url::ParseError> {
1487    ///     let base = Url::parse("https://example.com/")?;
1488    ///     let mut a = Builder::default();
1489    ///     a.url_relative(UrlRelative::RewriteWithBase(base.clone()));
1490    ///     assert_eq!(a.url_relative_base(), Some(&base));
1491    ///     a.url_relative(UrlRelative::PassThrough);
1492    ///     assert_eq!(a.url_relative_base(), None);
1493    ///     # Ok(())
1494    ///     # }
1495    pub fn url_relative_base(&self) -> Option<&Url> {
1496        match self.url_relative {
1497            UrlRelative::RewriteWithBase(ref base) => Some(base),
1498            _ => None,
1499        }
1500    }
1501
1502    /// Configures a `rel` attribute that will be added on links.
1503    ///
1504    /// If `rel` is in the generic or tag attributes, this must be set to `None`.
1505    /// Common `rel` values to include:
1506    ///
1507    /// * `noopener`: This prevents [a particular type of XSS attack],
1508    ///   and should usually be turned on for untrusted HTML.
1509    /// * `noreferrer`: This prevents the browser from [sending the source URL]
1510    ///   to the website that is linked to.
1511    /// * `nofollow`: This prevents search engines from [using this link for
1512    ///   ranking], which disincentivizes spammers.
1513    ///
1514    /// To turn on rel-insertion, call this function with a space-separated list.
1515    /// Ammonia does not parse rel-attributes;
1516    /// it just puts the given string into the attribute directly.
1517    ///
1518    /// [a particular type of XSS attack]: https://mathiasbynens.github.io/rel-noopener/
1519    /// [sending the source URL]: https://en.wikipedia.org/wiki/HTTP_referer
1520    /// [using this link for ranking]: https://en.wikipedia.org/wiki/Nofollow
1521    ///
1522    /// # Examples
1523    ///
1524    ///     use ammonia::Builder;
1525    ///
1526    ///     let a = Builder::new().link_rel(None)
1527    ///         .clean("<a href=https://rust-lang.org/>Rust</a>")
1528    ///         .to_string();
1529    ///     assert_eq!(
1530    ///       a,
1531    ///       "<a href=\"https://rust-lang.org/\">Rust</a>");
1532    ///
1533    /// # Defaults
1534    ///
1535    /// ```notest
1536    /// Some("noopener noreferrer")
1537    /// ```
1538    pub fn link_rel(&mut self, value: Option<&'a str>) -> &mut Self {
1539        self.link_rel = value;
1540        self
1541    }
1542
1543    /// Returns the settings for links' `rel` attribute, if one is set.
1544    ///
1545    /// # Examples
1546    ///
1547    ///     use ammonia::{Builder, UrlRelative};
1548    ///     let mut a = Builder::default();
1549    ///     a.link_rel(Some("a b"));
1550    ///     assert_eq!(a.get_link_rel(), Some("a b"));
1551    pub fn get_link_rel(&self) -> Option<&str> {
1552        self.link_rel
1553    }
1554
1555    /// Sets the CSS classes that are allowed on specific tags.
1556    ///
1557    /// The values is structured as a map from tag names to a set of class names.
1558    ///
1559    /// If the `class` attribute is itself whitelisted for a tag, then adding entries to
1560    /// this map will cause a panic.
1561    ///
1562    /// # Examples
1563    ///
1564    ///     use ammonia::Builder;
1565    ///     use maplit::{hashmap, hashset};
1566    ///
1567    ///     # fn main() {
1568    ///     let allowed_classes = hashmap![
1569    ///         "code" => hashset!["rs", "ex", "c", "cxx", "js"]
1570    ///     ];
1571    ///     let a = Builder::new()
1572    ///         .allowed_classes(allowed_classes)
1573    ///         .clean("<code class=rs>fn main() {}</code>")
1574    ///         .to_string();
1575    ///     assert_eq!(
1576    ///       a,
1577    ///       "<code class=\"rs\">fn main() {}</code>");
1578    ///     # }
1579    ///
1580    /// # Defaults
1581    ///
1582    /// The set of allowed classes is empty by default.
1583    pub fn allowed_classes(&mut self, value: HashMap<&'a str, HashSet<&'a str>>) -> &mut Self {
1584        self.allowed_classes = value;
1585        self
1586    }
1587
1588    /// Add additonal whitelisted classes without overwriting old ones.
1589    ///
1590    /// # Examples
1591    ///
1592    ///     let a = ammonia::Builder::default()
1593    ///         .add_allowed_classes("a", &["onebox"])
1594    ///         .clean("<a href=/ class=onebox>mess</span>").to_string();
1595    ///     assert_eq!("<a href=\"/\" class=\"onebox\" rel=\"noopener noreferrer\">mess</a>", a);
1596    pub fn add_allowed_classes<
1597        T: 'a + ?Sized + Borrow<str>,
1598        U: 'a + ?Sized + Borrow<str>,
1599        I: IntoIter<Item = &'a T>,
1600    >(
1601        &mut self,
1602        tag: &'a U,
1603        it: I,
1604    ) -> &mut Self {
1605        self.allowed_classes
1606            .entry(tag.borrow())
1607            .or_default()
1608            .extend(it.into_iter().map(Borrow::borrow));
1609        self
1610    }
1611
1612    /// Remove already-whitelisted attributes.
1613    ///
1614    /// Does nothing if the attribute is already gone.
1615    ///
1616    /// # Examples
1617    ///
1618    ///     let a = ammonia::Builder::default()
1619    ///         .add_allowed_classes("span", &["active"])
1620    ///         .rm_allowed_classes("span", &["active"])
1621    ///         .clean("<span class=active>").to_string();
1622    ///     assert_eq!("<span class=\"\"></span>", a);
1623    pub fn rm_allowed_classes<
1624        'b,
1625        'c,
1626        T: 'b + ?Sized + Borrow<str>,
1627        U: 'c + ?Sized + Borrow<str>,
1628        I: IntoIter<Item = &'b T>,
1629    >(
1630        &mut self,
1631        tag: &'c U,
1632        it: I,
1633    ) -> &mut Self {
1634        if let Some(tag) = self.allowed_classes.get_mut(tag.borrow()) {
1635            for i in it {
1636                tag.remove(i.borrow());
1637            }
1638        }
1639        self
1640    }
1641
1642    /// Returns a copy of the set of whitelisted class attributes.
1643    ///
1644    /// # Examples
1645    ///
1646    ///     use maplit::{hashmap, hashset};
1647    ///
1648    ///     let allowed_classes = hashmap![
1649    ///         "my-tag" => hashset!["my-class-1", "my-class-2"]
1650    ///     ];
1651    ///
1652    ///     let mut b = ammonia::Builder::default();
1653    ///     b.allowed_classes(Clone::clone(&allowed_classes));
1654    ///     assert_eq!(allowed_classes, b.clone_allowed_classes());
1655    pub fn clone_allowed_classes(&self) -> HashMap<&'a str, HashSet<&'a str>> {
1656        self.allowed_classes.clone()
1657    }
1658
1659    /// Configures the handling of HTML comments.
1660    ///
1661    /// If this option is false, comments will be preserved.
1662    ///
1663    /// # Examples
1664    ///
1665    ///     use ammonia::Builder;
1666    ///
1667    ///     let a = Builder::new().strip_comments(false)
1668    ///         .clean("<!-- yes -->")
1669    ///         .to_string();
1670    ///     assert_eq!(
1671    ///       a,
1672    ///       "<!-- yes -->");
1673    ///
1674    /// # Defaults
1675    ///
1676    /// `true`
1677    pub fn strip_comments(&mut self, value: bool) -> &mut Self {
1678        self.strip_comments = value;
1679        self
1680    }
1681
1682    /// Returns `true` if comment stripping is turned on.
1683    ///
1684    /// # Examples
1685    ///
1686    ///     let mut a = ammonia::Builder::new();
1687    ///     a.strip_comments(true);
1688    ///     assert!(a.will_strip_comments());
1689    ///     a.strip_comments(false);
1690    ///     assert!(!a.will_strip_comments());
1691    pub fn will_strip_comments(&self) -> bool {
1692        self.strip_comments
1693    }
1694
1695    /// Prefixes all "id" attribute values with a given string.  Note that the tag and
1696    /// attribute themselves must still be whitelisted.
1697    ///
1698    /// # Examples
1699    ///
1700    ///     use ammonia::Builder;
1701    ///     use maplit::hashset;
1702    ///
1703    ///     # fn main() {
1704    ///     let attributes = hashset!["id"];
1705    ///     let a = Builder::new()
1706    ///         .generic_attributes(attributes)
1707    ///         .id_prefix(Some("safe-"))
1708    ///         .clean("<b id=42>")
1709    ///         .to_string();
1710    ///     assert_eq!(a, "<b id=\"safe-42\"></b>");
1711    ///     # }
1712
1713    ///
1714    /// # Defaults
1715    ///
1716    /// `None`
1717    pub fn id_prefix(&mut self, value: Option<&'a str>) -> &mut Self {
1718        self.id_prefix = value;
1719        self
1720    }
1721
1722    /// Only allows the specified properties in `style` attributes.
1723    ///
1724    /// Irrelevant if `style` is not an allowed attribute.
1725    ///
1726    /// Note that if style filtering is enabled style properties will be normalised e.g.
1727    /// invalid declarations and @rules will be removed, with only syntactically valid
1728    /// declarations kept.
1729    ///
1730    /// # Examples
1731    ///
1732    ///     use ammonia::Builder;
1733    ///     use maplit::hashset;
1734    ///
1735    ///     # fn main() {
1736    ///     let attributes = hashset!["style"];
1737    ///     let properties = hashset!["color"];
1738    ///     let a = Builder::new()
1739    ///         .generic_attributes(attributes)
1740    ///         .filter_style_properties(properties)
1741    ///         .clean("<p style=\"font-weight: heavy; color: red\">my html</p>")
1742    ///         .to_string();
1743    ///     assert_eq!(a, "<p style=\"color:red\">my html</p>");
1744    ///     # }
1745    pub fn filter_style_properties(&mut self, value: HashSet<&'a str>) -> &mut Self {
1746        self.style_properties = Some(value);
1747        self
1748    }
1749
1750    /// Constructs a [`Builder`] instance configured with the [default options].
1751    ///
1752    /// # Examples
1753    ///
1754    ///     use ammonia::{Builder, Url, UrlRelative};
1755    ///     # use std::error::Error;
1756    ///
1757    ///     # fn do_main() -> Result<(), Box<dyn Error>> {
1758    ///     let input = "<!-- comments will be stripped -->This is an <a href=.>Ammonia</a> example using <a href=struct.Builder.html#method.new onclick=xss>the <code onmouseover=xss>new()</code> function</a>.";
1759    ///     let output = "This is an <a href=\"https://docs.rs/ammonia/1.0/ammonia/\" rel=\"noopener noreferrer\">Ammonia</a> example using <a href=\"https://docs.rs/ammonia/1.0/ammonia/struct.Builder.html#method.new\" rel=\"noopener noreferrer\">the <code>new()</code> function</a>.";
1760    ///
1761    ///     let result = Builder::new() // <--
1762    ///         .url_relative(UrlRelative::RewriteWithBase(Url::parse("https://docs.rs/ammonia/1.0/ammonia/")?))
1763    ///         .clean(input)
1764    ///         .to_string();
1765    ///     assert_eq!(result, output);
1766    ///     # Ok(())
1767    ///     # }
1768    ///     # fn main() { do_main().unwrap() }
1769    ///
1770    /// [default options]: fn.clean.html
1771    /// [`Builder`]: struct.Builder.html
1772    pub fn new() -> Self {
1773        Self::default()
1774    }
1775
1776    /// Constructs a [`Builder`] instance configured with no allowed tags.
1777    ///
1778    /// # Examples
1779    ///
1780    ///     use ammonia::{Builder, Url, UrlRelative};
1781    ///     # use std::error::Error;
1782    ///
1783    ///     # fn do_main() -> Result<(), Box<dyn Error>> {
1784    ///     let input = "<!-- comments will be stripped -->This is an <a href=.>Ammonia</a> example using <a href=struct.Builder.html#method.new onclick=xss>the <code onmouseover=xss>empty()</code> function</a>.";
1785    ///     let output = "This is an Ammonia example using the empty() function.";
1786    ///
1787    ///     let result = Builder::empty() // <--
1788    ///         .url_relative(UrlRelative::RewriteWithBase(Url::parse("https://docs.rs/ammonia/1.0/ammonia/")?))
1789    ///         .clean(input)
1790    ///         .to_string();
1791    ///     assert_eq!(result, output);
1792    ///     # Ok(())
1793    ///     # }
1794    ///     # fn main() { do_main().unwrap() }
1795    ///
1796    /// [default options]: fn.clean.html
1797    /// [`Builder`]: struct.Builder.html
1798    pub fn empty() -> Self {
1799        Self {
1800            tags: hashset![],
1801            ..Self::default()
1802        }
1803    }
1804
1805    /// Sanitizes an HTML fragment in a string according to the configured options.
1806    ///
1807    /// # Examples
1808    ///
1809    ///     use ammonia::{Builder, Url, UrlRelative};
1810    ///     # use std::error::Error;
1811    ///
1812    ///     # fn do_main() -> Result<(), Box<dyn Error>> {
1813    ///     let input = "<!-- comments will be stripped -->This is an <a href=.>Ammonia</a> example using <a href=struct.Builder.html#method.new onclick=xss>the <code onmouseover=xss>new()</code> function</a>.";
1814    ///     let output = "This is an <a href=\"https://docs.rs/ammonia/1.0/ammonia/\" rel=\"noopener noreferrer\">Ammonia</a> example using <a href=\"https://docs.rs/ammonia/1.0/ammonia/struct.Builder.html#method.new\" rel=\"noopener noreferrer\">the <code>new()</code> function</a>.";
1815    ///
1816    ///     let result = Builder::new()
1817    ///         .url_relative(UrlRelative::RewriteWithBase(Url::parse("https://docs.rs/ammonia/1.0/ammonia/")?))
1818    ///         .clean(input)
1819    ///         .to_string(); // <--
1820    ///     assert_eq!(result, output);
1821    ///     # Ok(())
1822    ///     # }
1823    ///     # fn main() { do_main().unwrap() }
1824    pub fn clean(&self, src: &str) -> Document {
1825        let parser = Self::make_parser();
1826        let dom = parser.one(src);
1827        self.clean_dom(dom)
1828    }
1829
1830    /// Sanitizes an HTML fragment from a reader according to the configured options.
1831    ///
1832    /// The input should be in UTF-8 encoding, otherwise the decoding is lossy, just
1833    /// like when using [`String::from_utf8_lossy`].
1834    ///
1835    /// To avoid consuming the reader, a mutable reference can be passed to this method.
1836    ///
1837    /// # Examples
1838    ///
1839    ///     use ammonia::Builder;
1840    ///     # use std::error::Error;
1841    ///
1842    ///     # fn do_main() -> Result<(), Box<dyn Error>> {
1843    ///     let a = Builder::new()
1844    ///         .clean_from_reader(&b"<!-- no -->"[..])? // notice the `b`
1845    ///         .to_string();
1846    ///     assert_eq!(a, "");
1847    ///     # Ok(()) }
1848    ///     # fn main() { do_main().unwrap() }
1849    ///
1850    /// [`String::from_utf8_lossy`]: https://doc.rust-lang.org/std/string/struct.String.html#method.from_utf8_lossy
1851    pub fn clean_from_reader<R>(&self, mut src: R) -> io::Result<Document>
1852    where
1853        R: io::Read,
1854    {
1855        let parser = Self::make_parser().from_utf8();
1856        let dom = parser.read_from(&mut src)?;
1857        Ok(self.clean_dom(dom))
1858    }
1859
1860    /// Clean a post-parsing DOM.
1861    ///
1862    /// This is not a public API because RcDom isn't really stable.
1863    /// We want to be able to take breaking changes to html5ever itself
1864    /// without having to break Ammonia's API.
1865    fn clean_dom(&self, dom: RcDom) -> Document {
1866        let mut id_to_tag_name_map = HashMap::new();
1867        let mut id_to_tag_name_stack = vec![{
1868            let children = dom.document.children.borrow();
1869            children[0].clone()
1870        }];
1871        while let Some(tag) = id_to_tag_name_stack.pop() {
1872            if let NodeData::Element { name, attrs, .. } = &tag.data {
1873                let attrs = attrs.borrow();
1874                for attr in &attrs[..] {
1875                    if &*attr.name.local == "id" {
1876                        id_to_tag_name_map.entry(attr.value.to_string()).and_modify(|ent| *ent = None).or_insert_with(|| Some(name.local.to_string()));
1877                    }
1878                }
1879            }
1880            id_to_tag_name_stack.extend(tag.children.borrow().iter().map(|x| x.clone()));
1881        }
1882
1883        let mut stack = Vec::new();
1884        let mut removed = Vec::new();
1885        let link_rel = self
1886            .link_rel
1887            .map(|link_rel| format_tendril!("{}", link_rel));
1888        if link_rel.is_some() {
1889            assert!(self.generic_attributes.get("rel").is_none());
1890            assert!(self
1891                .tag_attributes
1892                .get("a")
1893                .and_then(|a| a.get("rel"))
1894                .is_none());
1895        }
1896        assert!(self.allowed_classes.is_empty() || !self.generic_attributes.contains("class"));
1897        for tag_name in self.allowed_classes.keys() {
1898            assert!(self
1899                .tag_attributes
1900                .get(tag_name)
1901                .and_then(|a| a.get("class"))
1902                .is_none());
1903        }
1904        for tag_name in &self.clean_content_tags {
1905            assert!(!self.tags.contains(tag_name), "`{tag_name}` appears in `clean_content_tags` and in `tags` at the same time");
1906            assert!(!self.tag_attributes.contains_key(tag_name), "`{tag_name}` appears in `clean_content_tags` and in `tag_attributes` at the same time");
1907        }
1908        let body = {
1909            let children = dom.document.children.borrow();
1910            children[0].clone()
1911        };
1912        stack.extend(
1913            mem::take(&mut *body.children.borrow_mut())
1914                .into_iter()
1915                .rev(),
1916        );
1917        // This design approach is used to prevent pathological content from producing
1918        // a stack overflow. The `stack` contains to-be-cleaned nodes, while `remove`,
1919        // of course, contains nodes that need to be dropped (we can't just drop them,
1920        // because they could have a very deep child tree).
1921        while let Some(mut node) = stack.pop() {
1922            if matches!(node.data, NodeData::Element { ref name, .. } if &*name.local == "selectedcontent" && name.ns == ns!(html)) &&
1923                self.is_within(node.clone(), ns!(html), "select")
1924            {
1925                for sub in node.children.borrow_mut().iter_mut() {
1926                    sub.parent.replace(None);
1927                }
1928                *node.children.borrow_mut() = Vec::new();
1929            }
1930            let parent = node.parent
1931                .replace(None).expect("a node in the DOM will have a parent, except the root, which is not processed")
1932                .upgrade().expect("a node's parent will be pointed to by its parent (or the root pointer), and will not be dropped");
1933            let pass = self.clean_child(&mut node, &parent, &id_to_tag_name_map);
1934            self.adjust_node_attributes(&mut node, &link_rel, self.id_prefix, &parent, &id_to_tag_name_map);
1935            if self.clean_node_content(&node) || !self.check_expected_namespace(&parent, &node) {
1936                removed.push(node);
1937                continue;
1938            }
1939            if pass {
1940                dom.append(&parent.clone(), NodeOrText::AppendNode(node.clone()));
1941            } else {
1942                for sub in node.children.borrow_mut().iter_mut() {
1943                    sub.parent.replace(Some(Rc::downgrade(&parent)));
1944                }
1945            }
1946            stack.extend(
1947                mem::take(&mut *node.children.borrow_mut())
1948                    .into_iter()
1949                    .rev(),
1950            );
1951            if !pass {
1952                removed.push(node);
1953            }
1954        }
1955        // Now, imperatively clean up all of the child nodes.
1956        // Otherwise, we could wind up with a DoS, either caused by a memory leak,
1957        // or caused by a stack overflow.
1958        while let Some(node) = removed.pop() {
1959            removed.extend_from_slice(&mem::take(&mut *node.children.borrow_mut())[..]);
1960        }
1961        Document(dom)
1962    }
1963
1964    fn is_within(&self, mut child: Handle, ns: Namespace, tag: &str) -> bool {
1965        while let Some(parent) = child.parent.take() {
1966            child.parent.set(Some(parent.clone()));
1967            match child.data {
1968                NodeData::Element { ref name, .. } if name.ns == ns && &*name.local == tag => return true,
1969                _ => {
1970                    if let Some(parent) = parent.upgrade() {
1971                        child = parent;
1972                    } else {
1973                        return false;
1974                    }
1975                }
1976            }
1977        }
1978        false
1979    }
1980
1981    /// Returns `true` if a node and all its content should be removed.
1982    fn clean_node_content(&self, node: &Handle) -> bool {
1983        match node.data {
1984            NodeData::Text { .. }
1985            | NodeData::Comment { .. }
1986            | NodeData::Doctype { .. }
1987            | NodeData::Document
1988            | NodeData::ProcessingInstruction { .. } => false,
1989            NodeData::Element { ref name, .. } => self.clean_content_tags.contains(&*name.local),
1990        }
1991    }
1992
1993    /// Remove unwanted attributes, and check if the node should be kept or not.
1994    ///
1995    /// The root node doesn't need cleaning because we create the root node ourselves,
1996    /// and it doesn't get serialized, and ... it just exists to give the parser
1997    /// a context (in this case, a div-like block context).
1998    fn clean_child(&self, child: &mut Handle, parent: &Handle, id_to_tag_name_map: &HashMap<String, Option<String>>) -> bool {
1999        match child.data {
2000            NodeData::Text { .. } => true,
2001            NodeData::Comment { .. } => !self.strip_comments,
2002            NodeData::Doctype { .. }
2003            | NodeData::Document
2004            | NodeData::ProcessingInstruction { .. } => false,
2005            NodeData::Element {
2006                ref name,
2007                ref attrs,
2008                ..
2009            } => {
2010                if self.tags.contains(&*name.local) {
2011                    let whitelisted = |tag_name: &str, attr_name: &str, attr_val: &str|
2012                        self.generic_attributes.contains(attr_name)
2013                            || self.generic_attribute_prefixes.as_ref().map(|prefixes| {
2014                                prefixes.iter().any(|&p| attr_name.starts_with(p))
2015                            }) == Some(true)
2016                            || self
2017                                .tag_attributes
2018                                .get(tag_name)
2019                                .map(|ta| ta.contains(attr_name))
2020                                == Some(true)
2021                            || self
2022                                .tag_attribute_values
2023                                .get(tag_name)
2024                                .and_then(|tav| tav.get(attr_name))
2025                                .map(|vs| {
2026                                    vs.iter().any(|v| v.to_lowercase() == attr_val.to_lowercase())
2027                                })
2028                                == Some(true);
2029                    let attr_filter = |tag_name: &str, attr_name: &str, attr_val: &str| {
2030                        if !whitelisted(tag_name, attr_name, attr_val) {
2031                            // If the class attribute is not whitelisted,
2032                            // but there is a whitelisted set of allowed_classes,
2033                            // do not strip out the class attribute.
2034                            // Banned classes will be filtered later.
2035                            attr_name == "class" && self.allowed_classes.contains_key(tag_name)
2036                        } else if is_url_attr(tag_name, attr_name) {
2037                            let url = Url::parse(attr_val);
2038                            if let Ok(url) = url {
2039                                self.url_schemes.contains(url.scheme())
2040                            } else if url == Err(url::ParseError::RelativeUrlWithoutBase) {
2041                                !matches!(self.url_relative, UrlRelative::Deny)
2042                            } else {
2043                                false
2044                            }
2045                        } else {
2046                            true
2047                        }
2048                    };
2049                    attrs.borrow_mut().retain(|attr| attr_filter(&*name.local, &*attr.name.local, &*attr.value));
2050                    if
2051                        // https://svgwg.org/specs/animations/#AnimateElement
2052                        name.ns == ns!(svg) &&
2053                        (&*name.local == "animate" || &*name.local == "set")
2054                    {
2055                        let animate_name = attrs.borrow()
2056                            .iter()
2057                            .find(|attr| &*attr.name.local == "attributeName")
2058                            .map(|attr| attr.value.clone());
2059                        let animate_values = attrs.borrow()
2060                            .iter()
2061                            .find(|attr| &*attr.name.local == "values")
2062                            .map(|attr| attr.value.clone());
2063                        let animate_from = attrs.borrow()
2064                            .iter()
2065                            .find(|attr| &*attr.name.local == "from")
2066                            .map(|attr| attr.value.clone());
2067                        let animate_to = attrs.borrow()
2068                            .iter()
2069                            .find(|attr| &*attr.name.local == "to")
2070                            .map(|attr| attr.value.clone());
2071                        let animate_href = attrs.borrow()
2072                            .iter()
2073                            .find(|attr| &*attr.name.local == "href")
2074                            .map(|attr| attr.value.clone());
2075                        let animate_tag_name = animate_href
2076                            .map(|href| {
2077                                if href.starts_with("#") {
2078                                    id_to_tag_name_map.get(&href[1..]).and_then(|inner| Some(&inner.as_ref()?[..]))
2079                                } else {
2080                                    None
2081                                }
2082                            })
2083                            .unwrap_or_else(|| {
2084                                if let &NodeData::Element { name: ref parent_name, .. } = &parent.data {
2085                                    Some(&*parent_name.local)
2086                                } else {
2087                                    None
2088                                }
2089                            });
2090                        match (animate_name, animate_values, animate_from, animate_to, animate_tag_name) {
2091                            (Some(animate_name), _, _, _, Some(animate_tag_name)) if self.set_tag_attribute_values.get(animate_tag_name).map_or(false, |attribute_values| attribute_values.contains_key(&*animate_name)) => false,
2092                            (Some(animate_name), Some(animate_values), None, None, Some(animate_tag_name)) => {
2093                                // https://svgwg.org/specs/animations/#ValuesAttribute
2094                                animate_values.split(';').all(|attr_val| attr_filter(animate_tag_name, &*animate_name, attr_val))
2095                            }
2096                            (Some(animate_name), None, Some(animate_from), Some(animate_to), Some(animate_tag_name)) => {
2097                                // https://svgwg.org/specs/animations/#FromAttribute
2098                                attr_filter(animate_tag_name, &*animate_name, &*animate_from) &&
2099                                    attr_filter(animate_tag_name, &*animate_name, &*animate_to)
2100                            }
2101                            (Some(animate_name), None, Some(animate_from), None, Some(animate_tag_name)) => {
2102                                // https://svgwg.org/specs/animations/#FromAttribute
2103                                attr_filter(animate_tag_name, &*animate_name, &*animate_from)
2104                            }
2105                            (Some(animate_name), None, None, Some(animate_to), Some(animate_tag_name)) => {
2106                                // https://svgwg.org/specs/animations/#FromAttribute
2107                                attr_filter(animate_tag_name, &*animate_name, &*animate_to)
2108                            }
2109                            _ => false,
2110                        }
2111                    } else {
2112                        true
2113                    }
2114                } else {
2115                    false
2116                }
2117            }
2118        }
2119    }
2120
2121    // Check for unexpected namespace changes.
2122    //
2123    // The issue happens if developers added to the list of allowed tags any
2124    // tag which is parsed in RCDATA state, PLAINTEXT state or RAWTEXT state,
2125    // that is:
2126    //
2127    // * title
2128    // * textarea
2129    // * xmp
2130    // * iframe
2131    // * noembed
2132    // * noframes
2133    // * plaintext
2134    // * noscript
2135    // * style
2136    // * script
2137    //
2138    // An example in the wild is Plume, that allows iframe [1].  So in next
2139    // examples I'll assume the following policy:
2140    //
2141    //     Builder::new()
2142    //        .add_tags(&["iframe"])
2143    //
2144    // In HTML namespace `<iframe>` is parsed specially; that is, its content is
2145    // treated as text. For instance, the following html:
2146    //
2147    //     <iframe><a>test
2148    //
2149    // Is parsed into the following DOM tree:
2150    //
2151    //     iframe
2152    //     └─ #text: <a>test
2153    //
2154    // So iframe cannot have any children other than a text node.
2155    //
2156    // The same is not true, though, in "foreign content"; that is, within
2157    // <svg> or <math> tags. The following html:
2158    //
2159    //     <svg><iframe><a>test
2160    //
2161    // is parsed differently:
2162    //
2163    //    svg
2164    //    └─ iframe
2165    //       └─ a
2166    //          └─ #text: test
2167    //
2168    // So in SVG namespace iframe can have children.
2169    //
2170    // Ammonia disallows <svg> but it keeps its content after deleting it. And
2171    // the parser internally keeps track of the namespace of the element. So
2172    // assume we have the following snippet:
2173    //
2174    //     <svg><iframe><a title="</iframe><img src onerror=alert(1)>">test
2175    //
2176    // It is parsed into:
2177    //
2178    //     svg
2179    //     └─ iframe
2180    //        └─ a title="</iframe><img src onerror=alert(1)>"
2181    //           └─ #text: test
2182    //
2183    // This DOM tree is harmless from ammonia point of view because the piece
2184    // of code that looks like XSS is in a title attribute. Hence, the
2185    // resulting "safe" HTML from ammonia would be:
2186    //
2187    //     <iframe><a title="</iframe><img src onerror=alert(1)>" rel="noopener
2188    // noreferrer">test</a></iframe>
2189    //
2190    // However, at this point, the information about namespace is lost, which
2191    // means that the browser will parse this snippet into:
2192    //
2193    //     ├─ iframe
2194    //     │  └─ #text: <a title="
2195    //     ├─ img src="" onerror="alert(1)"
2196    //     └─ #text: " rel="noopener noreferrer">test
2197    //
2198    // Leading to XSS.
2199    //
2200    // To solve this issue, check for unexpected namespace switches after cleanup.
2201    // Elements which change namespace at an unexpected point are removed.
2202    // This function returns `true` if `child` should be kept, and `false` if it
2203    // should be removed.
2204    //
2205    // [1]: https://github.com/Plume-org/Plume/blob/main/plume-models/src/safe_string.rs#L21
2206    fn check_expected_namespace(&self, parent: &Handle, child: &Handle) -> bool {
2207        let (parent, parent_attr, child) = match (&parent.data, &child.data) {
2208            (NodeData::Element { name: pn, attrs, .. }, NodeData::Element { name: cn, .. }) => (pn, attrs, cn),
2209            _ => return true,
2210        };
2211        // The only way to switch from html to svg is with the <svg> tag
2212        if parent.ns == ns!(html) && child.ns == ns!(svg) {
2213            child.local == local_name!("svg")
2214        // The only way to switch from html to mathml is with the <math> tag
2215        } else if parent.ns == ns!(html) && child.ns == ns!(mathml) {
2216            child.local == local_name!("math")
2217        // The only way to switch from mathml to svg/html is with a text integration point
2218        } else if parent.ns == ns!(mathml) && child.ns != ns!(mathml) {
2219            // https://html.spec.whatwg.org/#mathml
2220            if &*parent.local == "annotation-xml" {
2221                let parent_attr = parent_attr.borrow();
2222                // https://html.spec.whatwg.org/#tree-construction
2223                if child.ns == ns!(html)
2224                    && parent_attr
2225                        .iter()
2226                        .filter(|attr| attr.name.local == local_name!("encoding"))
2227                        .all(|attr| {
2228                            &*attr.value == "text/html" || &*attr.value == "application/xhtml+xml"
2229                        })
2230                {
2231                    is_html_tag(&child.local)
2232                    && parent_attr
2233                        .iter()
2234                        .filter(|attr| attr.name.local == local_name!("encoding"))
2235                        .count()
2236                        == 1
2237                } else {
2238                    child.local == local_name!("svg") && child.ns == ns!(svg)
2239                }
2240            } else {
2241                matches!(&*parent.local, "mi" | "mo" | "mn" | "ms" | "mtext")
2242                    && if child.ns == ns!(html) {
2243                        is_html_tag(&child.local)
2244                    } else {
2245                        true
2246                    }
2247            }
2248
2249        // The only way to switch from svg to mathml/html is with an html integration point
2250        } else if parent.ns == ns!(svg) && child.ns != ns!(svg) {
2251            // https://html.spec.whatwg.org/#svg-0
2252            matches!(&*parent.local, "foreignObject")
2253                && if child.ns == ns!(html) { is_html_tag(&child.local) } else { true }
2254        } else if child.ns == ns!(svg) {
2255            is_svg_tag(&child.local)
2256        } else if child.ns == ns!(mathml) {
2257            is_mathml_tag(&child.local)
2258        } else if child.ns == ns!(html) {
2259            is_html_tag(&child.local)
2260        } else {
2261            // There are no other supported ways to switch namespace
2262            parent.ns == child.ns
2263        }
2264    }
2265
2266    /// Add and transform special-cased attributes and elements.
2267    ///
2268    /// This function handles:
2269    ///
2270    /// * relative URL rewriting
2271    /// * adding `<a rel>` attributes
2272    /// * filtering out banned style properties
2273    /// * filtering out banned classes
2274    fn adjust_node_attributes(
2275        &self,
2276        child: &mut Handle,
2277        link_rel: &Option<StrTendril>,
2278        id_prefix: Option<&'a str>,
2279        parent: &Handle,
2280        id_to_tag_name_map: &HashMap<String, Option<String>>,
2281    ) {
2282        if let NodeData::Element {
2283            ref name,
2284            ref attrs,
2285            ..
2286        } = child.data
2287        {
2288            if let Some(set_attrs) = self.set_tag_attribute_values.get(&*name.local) {
2289                let mut attrs = attrs.borrow_mut();
2290                for (&set_name, &set_value) in set_attrs {
2291                    // set the value of the attribute if the attribute is already present
2292                    if let Some(attr) = attrs.iter_mut().find(|attr| &*attr.name.local == set_name)
2293                    {
2294                        if &*attr.value != set_value {
2295                            attr.value = set_value.into();
2296                        }
2297                    } else {
2298                        // otherwise, add the attribute
2299                        let attr = Attribute {
2300                            name: QualName::new(None, ns!(), set_name.into()),
2301                            value: set_value.into(),
2302                        };
2303                        attrs.push(attr);
2304                    }
2305                }
2306            }
2307            if let Some(ref link_rel) = *link_rel {
2308                if &*name.local == "a" {
2309                    attrs.borrow_mut().push(Attribute {
2310                        name: QualName::new(None, ns!(), local_name!("rel")),
2311                        value: link_rel.clone(),
2312                    })
2313                }
2314            }
2315            if let Some(ref id_prefix) = id_prefix {
2316                for attr in &mut *attrs.borrow_mut() {
2317                    if &attr.name.local == "id" && !attr.value.starts_with(id_prefix) {
2318                        attr.value = format_tendril!("{}{}", id_prefix, attr.value);
2319                    }
2320                }
2321            }
2322            if let Some(ref attr_filter) = self.attribute_filter {
2323                let mut drop_attrs = Vec::new();
2324                let mut attrs = attrs.borrow_mut();
2325                for (i, attr) in &mut attrs.iter_mut().enumerate() {
2326                    let replace_with = if let Some(new) =
2327                        attr_filter.filter(&name.local, &attr.name.local, &attr.value)
2328                    {
2329                        if *new != *attr.value {
2330                            Some(format_tendril!("{}", new))
2331                        } else {
2332                            None // no need to replace the attr if filter returned the same value
2333                        }
2334                    } else {
2335                        drop_attrs.push(i);
2336                        None
2337                    };
2338                    if let Some(replace_with) = replace_with {
2339                        attr.value = replace_with;
2340                    }
2341                }
2342                for i in drop_attrs.into_iter().rev() {
2343                    attrs.swap_remove(i);
2344                }
2345            }
2346            {
2347                let mut drop_attrs = Vec::new();
2348                let mut attrs = attrs.borrow_mut();
2349                for (i, attr) in attrs.iter_mut().enumerate() {
2350                    if is_url_attr(&name.local, &attr.name.local) && is_url_relative(&attr.value) {
2351                        let new_value = self.url_relative.evaluate(&attr.value);
2352                        if let Some(new_value) = new_value {
2353                            attr.value = new_value;
2354                        } else {
2355                            drop_attrs.push(i);
2356                        }
2357                    }
2358                }
2359                // Swap remove scrambles the vector after the current point.
2360                // We will not do anything except with items before the current point.
2361                // The `rev()` is, as such, necessary for correctness.
2362                // We could use regular `remove(usize)` and a forward iterator,
2363                // but that's slower.
2364                for i in drop_attrs.into_iter().rev() {
2365                    attrs.swap_remove(i);
2366                }
2367            }
2368            if let Some(allowed_values) = &self.style_properties {
2369                for attr in &mut *attrs.borrow_mut() {
2370                    if &attr.name.local == "style" {
2371                        attr.value = style::filter_style_attribute(&attr.value, allowed_values).into();
2372                    }
2373                }
2374            }
2375            if let Some(allowed_values) = self.allowed_classes.get(&*name.local) {
2376                for attr in &mut *attrs.borrow_mut() {
2377                    if &attr.name.local == "class" {
2378                        let mut classes = vec![];
2379                        // https://html.spec.whatwg.org/#global-attributes:classes-2
2380                        for class in attr.value.split_ascii_whitespace() {
2381                            if allowed_values.contains(class) {
2382                                classes.push(class.to_owned());
2383                            }
2384                        }
2385                        attr.value = format_tendril!("{}", classes.join(" "));
2386                    }
2387                }
2388            }
2389            if
2390                // https://svgwg.org/specs/animations/#AnimateElement
2391                name.ns == ns!(svg) &&
2392                (&*name.local == "animate" || &*name.local == "set")
2393            {
2394                let mut attrs = attrs.borrow_mut();
2395                let animate_name = attrs
2396                    .iter()
2397                    .find(|attr| &*attr.name.local == "attributeName")
2398                    .map(|attr| attr.value.clone());
2399                let animate_href = attrs
2400                    .iter()
2401                    .find(|attr| &*attr.name.local == "href")
2402                    .map(|attr| attr.value.clone());
2403                let animate_tag_name = animate_href
2404                    .map(|href| {
2405                        if href.starts_with("#") {
2406                            id_to_tag_name_map.get(&href[1..]).and_then(|inner| Some(&inner.as_ref()?[..]))
2407                        } else {
2408                            None
2409                        }
2410                    })
2411                    .unwrap_or_else(|| {
2412                        if let &NodeData::Element { name: ref parent_name, .. } = &parent.data {
2413                            Some(&*parent_name.local)
2414                        } else {
2415                            None
2416                        }
2417                    });
2418                if let (Some(animate_name), Some(animate_tag_name)) = (animate_name, animate_tag_name) {
2419                    if let Some(ref attr_filter) = self.attribute_filter {
2420                        if let Some((i, animate_values)) = attrs
2421                            .iter_mut()
2422                            .enumerate()
2423                            .find(|(_, attr)| &*attr.name.local == "values")
2424                            .map(|(i, attr)| (i, &mut attr.value))
2425                        {
2426                            let mut drop = false;
2427                            let new_value = animate_values.split(';')
2428                                .map(|value| {
2429                                    if let Some(new_value) = attr_filter.filter(animate_tag_name, &animate_name, &value) {
2430                                        String::from(new_value)
2431                                    } else {
2432                                        drop = true;
2433                                        String::new()
2434                                    }
2435                                })
2436                                .collect::<Vec<String>>()
2437                                .join(";");
2438                            if drop {
2439                                attrs.swap_remove(i);
2440                            } else {
2441                                *animate_values = new_value.into();
2442                            }
2443                        }
2444                        let mut drop_attrs = Vec::new();
2445                        for (i, animate_value) in attrs
2446                            .iter_mut()
2447                            .enumerate()
2448                            .filter(|(_, attr)| &*attr.name.local == "from" || &*attr.name.local == "to")
2449                            .map(|(i, attr)| (i, &mut attr.value))
2450                        {
2451                            if let Some(new_value) = attr_filter.filter(animate_tag_name, &animate_name, &animate_value) {
2452                                *animate_value = new_value[..].into();
2453                            } else {
2454                                drop_attrs.push(i);
2455                            };
2456                        }
2457                        for i in drop_attrs.into_iter().rev() {
2458                            attrs.swap_remove(i);
2459                        }
2460                    }
2461                    if is_url_attr(animate_tag_name, &*animate_name) {
2462                        if let Some((i, animate_values)) = attrs
2463                            .iter_mut()
2464                            .enumerate()
2465                            .find(|(_, attr)| &*attr.name.local == "values")
2466                            .map(|(i, attr)| (i, &mut attr.value))
2467                        {
2468                            let mut drop = false;
2469                            let new_value = animate_values.split(';')
2470                                .map(|value| {
2471                                    if !is_url_relative(value) {
2472                                        String::from(value)
2473                                    } else if let Some(new_value) = self.url_relative.evaluate(value) {
2474                                        String::from(new_value)
2475                                    } else {
2476                                        drop = true;
2477                                        String::new()
2478                                    }
2479                                })
2480                                .collect::<Vec<String>>()
2481                                .join(";");
2482                            if drop {
2483                                attrs.swap_remove(i);
2484                            } else {
2485                                *animate_values = new_value.into();
2486                            }
2487                        }
2488                        let mut drop_attrs = Vec::new();
2489                        for (i, animate_value) in attrs
2490                            .iter_mut()
2491                            .enumerate()
2492                            .filter(|(_, attr)| &*attr.name.local == "from" || &*attr.name.local == "to")
2493                            .map(|(i, attr)| (i, &mut attr.value))
2494                        {
2495                            if !is_url_relative(animate_value) {
2496                                // do nothing
2497                            } else if let Some(new_value) = self.url_relative.evaluate(animate_value) {
2498                                *animate_value = new_value;
2499                            } else {
2500                                drop_attrs.push(i);
2501                            };
2502                        }
2503                        for i in drop_attrs.into_iter().rev() {
2504                            attrs.swap_remove(i);
2505                        }
2506                    }
2507                    if &*animate_name == "style" {
2508                        if let Some(allowed_values) = &self.style_properties {
2509                            if let Some(animate_values) = attrs
2510                                .iter_mut()
2511                                .find(|attr| &*attr.name.local == "values")
2512                                .map(|attr| &mut attr.value)
2513                            {
2514                                let new_value = animate_values.split(';')
2515                                    .map(|value| {
2516                                        style::filter_style_attribute(&value, allowed_values)
2517                                    })
2518                                    .collect::<Vec<String>>()
2519                                    .join(";");
2520                                *animate_values = new_value.into();
2521                            }
2522                            for animate_value in attrs
2523                                .iter_mut()
2524                                .filter(|attr| &*attr.name.local == "from" || &*attr.name.local == "to")
2525                                .map(|attr| &mut attr.value)
2526                            {
2527                                *animate_value = style::filter_style_attribute(&animate_value, allowed_values).into();
2528                            }
2529                        }
2530                    }
2531                    if &*animate_name == "class" {
2532                        if let Some(allowed_values) = self.allowed_classes.get(animate_tag_name) {
2533                            if let Some(animate_values) = attrs
2534                                .iter_mut()
2535                                .find(|attr| &*attr.name.local == "values")
2536                                .map(|attr| &mut attr.value)
2537                            {
2538                                let new_value = animate_values.split(';')
2539                                    .map(|value| {
2540                                        let mut classes = vec![];
2541                                        // https://html.spec.whatwg.org/#global-attributes:classes-2
2542                                        for class in value.split_ascii_whitespace() {
2543                                            if allowed_values.contains(class) {
2544                                                classes.push(class.to_owned());
2545                                            }
2546                                        }
2547                                        classes.join(" ")
2548                                    })
2549                                    .collect::<Vec<String>>()
2550                                    .join(";");
2551                                *animate_values = new_value.into();
2552                            }
2553                            for animate_value in attrs
2554                                .iter_mut()
2555                                .filter(|attr| &*attr.name.local == "from" || &*attr.name.local == "to")
2556                                .map(|attr| &mut attr.value)
2557                            {
2558                                let mut classes = vec![];
2559                                // https://html.spec.whatwg.org/#global-attributes:classes-2
2560                                for class in animate_value.split_ascii_whitespace() {
2561                                    if allowed_values.contains(class) {
2562                                        classes.push(class.to_owned());
2563                                    }
2564                                }
2565                                *animate_value = classes.join(" ").into();
2566                            }
2567                        }
2568                    }
2569                }
2570            }
2571        }
2572    }
2573
2574    /// Initializes an HTML fragment parser.
2575    ///
2576    /// Ammonia conforms to the HTML5 fragment parsing rules,
2577    /// by parsing the given fragment as if it were included in a <div> tag.
2578    fn make_parser() -> html::Parser<RcDom> {
2579        html::parse_fragment(
2580            RcDom::default(),
2581            html::ParseOpts::default(),
2582            QualName::new(None, ns!(html), local_name!("div")),
2583            vec![],
2584            false,
2585        )
2586    }
2587}
2588
2589/// Given an element name and attribute name, determine if the given attribute contains a URL.
2590fn is_url_attr(element: &str, attr: &str) -> bool {
2591    (element != "animate" && element != "set" && attr == "href")
2592        // Don't have to worry about alternate xmlns prefixes, because HTML doesn't
2593        // parse them, anyway:
2594        // https://html.spec.whatwg.org/#adjust-foreign-attributes
2595        || (element != "animate" && element != "set" && attr == "xlink:href")
2596        || attr == "src"
2597        || (element == "form" && attr == "action")
2598        || (element == "object" && attr == "data")
2599        || ((element == "button" || element == "input") && attr == "formaction")
2600        || (element == "a" && attr == "ping")
2601        || (element == "video" && attr == "poster")
2602}
2603
2604fn is_html_tag(element: &str) -> bool {
2605    (!is_svg_tag(element) && !is_mathml_tag(element))
2606        || matches!(
2607            element,
2608            "title" | "style" | "font" | "a" | "script" | "span"
2609        )
2610}
2611
2612/// Given an element name, check if it's SVG
2613fn is_svg_tag(element: &str) -> bool {
2614    // https://svgwg.org/svg2-draft/eltindex.html
2615    matches!(
2616        element,
2617        "a" | "animate"
2618            | "animateMotion"
2619            | "animateTransform"
2620            | "circle"
2621            | "clipPath"
2622            | "defs"
2623            | "desc"
2624            | "discard"
2625            | "ellipse"
2626            | "feBlend"
2627            | "feColorMatrix"
2628            | "feComponentTransfer"
2629            | "feComposite"
2630            | "feConvolveMatrix"
2631            | "feDiffuseLighting"
2632            | "feDisplacementMap"
2633            | "feDistantLight"
2634            | "feDropShadow"
2635            | "feFlood"
2636            | "feFuncA"
2637            | "feFuncB"
2638            | "feFuncG"
2639            | "feFuncR"
2640            | "feGaussianBlur"
2641            | "feImage"
2642            | "feMerge"
2643            | "feMergeNode"
2644            | "feMorphology"
2645            | "feOffset"
2646            | "fePointLight"
2647            | "feSpecularLighting"
2648            | "feSpotLight"
2649            | "feTile"
2650            | "feTurbulence"
2651            | "filter"
2652            | "foreignObject"
2653            | "g"
2654            | "image"
2655            | "line"
2656            | "linearGradient"
2657            | "marker"
2658            | "mask"
2659            | "metadata"
2660            | "mpath"
2661            | "path"
2662            | "pattern"
2663            | "polygon"
2664            | "polyline"
2665            | "radialGradient"
2666            | "rect"
2667            | "script"
2668            | "set"
2669            | "stop"
2670            | "style"
2671            | "svg"
2672            | "switch"
2673            | "symbol"
2674            | "text"
2675            | "textPath"
2676            | "title"
2677            | "tspan"
2678            | "use"
2679            | "view"
2680    )
2681}
2682
2683/// Given an element name, check if it's Math
2684fn is_mathml_tag(element: &str) -> bool {
2685    // https://svgwg.org/svg2-draft/eltindex.html
2686    matches!(
2687        element,
2688        "abs"
2689            | "and"
2690            | "annotation"
2691            | "annotation-xml"
2692            | "apply"
2693            | "approx"
2694            | "arccos"
2695            | "arccosh"
2696            | "arccot"
2697            | "arccoth"
2698            | "arccsc"
2699            | "arccsch"
2700            | "arcsec"
2701            | "arcsech"
2702            | "arcsin"
2703            | "arcsinh"
2704            | "arctan"
2705            | "arctanh"
2706            | "arg"
2707            | "bind"
2708            | "bvar"
2709            | "card"
2710            | "cartesianproduct"
2711            | "cbytes"
2712            | "ceiling"
2713            | "cerror"
2714            | "ci"
2715            | "cn"
2716            | "codomain"
2717            | "complexes"
2718            | "compose"
2719            | "condition"
2720            | "conjugate"
2721            | "cos"
2722            | "cosh"
2723            | "cot"
2724            | "coth"
2725            | "cs"
2726            | "csc"
2727            | "csch"
2728            | "csymbol"
2729            | "curl"
2730            | "declare"
2731            | "degree"
2732            | "determinant"
2733            | "diff"
2734            | "divergence"
2735            | "divide"
2736            | "domain"
2737            | "domainofapplication"
2738            | "emptyset"
2739            | "eq"
2740            | "equivalent"
2741            | "eulergamma"
2742            | "exists"
2743            | "exp"
2744            | "exponentiale"
2745            | "factorial"
2746            | "factorof"
2747            | "false"
2748            | "floor"
2749            | "fn"
2750            | "forall"
2751            | "gcd"
2752            | "geq"
2753            | "grad"
2754            | "gt"
2755            | "ident"
2756            | "image"
2757            | "imaginary"
2758            | "imaginaryi"
2759            | "implies"
2760            | "in"
2761            | "infinity"
2762            | "int"
2763            | "integers"
2764            | "intersect"
2765            | "interval"
2766            | "inverse"
2767            | "lambda"
2768            | "laplacian"
2769            | "lcm"
2770            | "leq"
2771            | "limit"
2772            | "list"
2773            | "ln"
2774            | "log"
2775            | "logbase"
2776            | "lowlimit"
2777            | "lt"
2778            | "maction"
2779            | "maligngroup"
2780            | "malignmark"
2781            | "math"
2782            | "matrix"
2783            | "matrixrow"
2784            | "max"
2785            | "mean"
2786            | "median"
2787            | "menclose"
2788            | "merror"
2789            | "mfenced"
2790            | "mfrac"
2791            | "mglyph"
2792            | "mi"
2793            | "min"
2794            | "minus"
2795            | "mlabeledtr"
2796            | "mlongdiv"
2797            | "mmultiscripts"
2798            | "mn"
2799            | "mo"
2800            | "mode"
2801            | "moment"
2802            | "momentabout"
2803            | "mover"
2804            | "mpadded"
2805            | "mphantom"
2806            | "mprescripts"
2807            | "mroot"
2808            | "mrow"
2809            | "ms"
2810            | "mscarries"
2811            | "mscarry"
2812            | "msgroup"
2813            | "msline"
2814            | "mspace"
2815            | "msqrt"
2816            | "msrow"
2817            | "mstack"
2818            | "mstyle"
2819            | "msub"
2820            | "msubsup"
2821            | "msup"
2822            | "mtable"
2823            | "mtd"
2824            | "mtext"
2825            | "mtr"
2826            | "munder"
2827            | "munderover"
2828            | "naturalnumbers"
2829            | "neq"
2830            | "none"
2831            | "not"
2832            | "notanumber"
2833            | "notin"
2834            | "notprsubset"
2835            | "notsubset"
2836            | "or"
2837            | "otherwise"
2838            | "outerproduct"
2839            | "partialdiff"
2840            | "pi"
2841            | "piece"
2842            | "piecewise"
2843            | "plus"
2844            | "power"
2845            | "primes"
2846            | "product"
2847            | "prsubset"
2848            | "quotient"
2849            | "rationals"
2850            | "real"
2851            | "reals"
2852            | "reln"
2853            | "rem"
2854            | "root"
2855            | "scalarproduct"
2856            | "sdev"
2857            | "sec"
2858            | "sech"
2859            | "selector"
2860            | "semantics"
2861            | "sep"
2862            | "set"
2863            | "setdiff"
2864            | "share"
2865            | "sin"
2866            | "sinh"
2867            | "span"
2868            | "subset"
2869            | "sum"
2870            | "tan"
2871            | "tanh"
2872            | "tendsto"
2873            | "times"
2874            | "transpose"
2875            | "true"
2876            | "union"
2877            | "uplimit"
2878            | "variance"
2879            | "vector"
2880            | "vectorproduct"
2881            | "xor"
2882    )
2883}
2884
2885fn is_url_relative(url: &str) -> bool {
2886    matches!(
2887        Url::parse(url),
2888        Err(url::ParseError::RelativeUrlWithoutBase)
2889    )
2890}
2891
2892/// Policy for [relative URLs], that is, URLs that do not specify the scheme in full.
2893///
2894/// This policy kicks in, if set, for any attribute named `src` or `href`,
2895/// as well as the `data` attribute of an `object` tag.
2896///
2897/// [relative URLs]: struct.Builder.html#method.url_relative
2898///
2899/// # Examples
2900///
2901/// ## `Deny`
2902///
2903/// * `<a href="test">` is a file-relative URL, and will be removed
2904/// * `<a href="/test">` is a domain-relative URL, and will be removed
2905/// * `<a href="//example.com/test">` is a scheme-relative URL, and will be removed
2906/// * `<a href="http://example.com/test">` is an absolute URL, and will be kept
2907///
2908/// ## `PassThrough`
2909///
2910/// No changes will be made to any URLs, except if a disallowed scheme is used.
2911///
2912/// ## `RewriteWithBase`
2913///
2914/// If the base is set to `http://notriddle.com/some-directory/some-file`
2915///
2916/// * `<a href="test">` will be rewritten to `<a href="http://notriddle.com/some-directory/test">`
2917/// * `<a href="/test">` will be rewritten to `<a href="http://notriddle.com/test">`
2918/// * `<a href="//example.com/test">` will be rewritten to `<a href="http://example.com/test">`
2919/// * `<a href="http://example.com/test">` is an absolute URL, so it will be kept as-is
2920///
2921/// ## `Custom`
2922///
2923/// Pass the relative URL to a function.
2924/// If it returns `Some(string)`, then that one gets used.
2925/// Otherwise, it will remove the attribute (like `Deny` does).
2926///
2927///     use std::borrow::Cow;
2928///     fn is_absolute_path(url: &str) -> bool {
2929///         let u = url.as_bytes();
2930///         // `//a/b/c` is "protocol-relative", meaning "a" is a hostname
2931///         // `/a/b/c` is an absolute path, and what we want to do stuff to.
2932///         u.get(0) == Some(&b'/') && u.get(1) != Some(&b'/')
2933///     }
2934///     fn evaluate(url: &str) -> Option<Cow<str>> {
2935///         if is_absolute_path(url) {
2936///             Some(Cow::Owned(String::from("/root") + url))
2937///         } else {
2938///             Some(Cow::Borrowed(url))
2939///         }
2940///     }
2941///     fn main() {
2942///         let a = ammonia::Builder::new()
2943///             .url_relative(ammonia::UrlRelative::Custom(Box::new(evaluate)))
2944///             .clean("<a href=/test/path>fixed</a><a href=path>passed</a><a href=http://google.com/>skipped</a>")
2945///             .to_string();
2946///         assert_eq!(a, "<a href=\"/root/test/path\" rel=\"noopener noreferrer\">fixed</a><a href=\"path\" rel=\"noopener noreferrer\">passed</a><a href=\"http://google.com/\" rel=\"noopener noreferrer\">skipped</a>");
2947///     }
2948///
2949/// This function is only applied to relative URLs.
2950/// To filter all of the URLs,
2951/// use the not-yet-implemented Content Security Policy.
2952#[non_exhaustive]
2953pub enum UrlRelative<'a> {
2954    /// Relative URLs will be completely stripped from the document.
2955    Deny,
2956    /// Relative URLs will be passed through unchanged.
2957    PassThrough,
2958    /// Relative URLs will be changed into absolute URLs, based on this base URL.
2959    RewriteWithBase(Url),
2960    /// Force absolute and relative paths into a particular directory.
2961    ///
2962    /// Since the resolver does not affect fully-qualified URLs, it doesn't
2963    /// prevent users from linking wherever they want. This feature only
2964    /// serves to make content more portable.
2965    ///
2966    /// # Examples
2967    ///
2968    /// <table>
2969    /// <thead>
2970    /// <tr>
2971    ///     <th>root</th>
2972    ///     <th>path</th>
2973    ///     <th>url</th>
2974    ///     <th>result</th>
2975    /// </tr>
2976    /// </thead>
2977    /// <tbody>
2978    /// <tr>
2979    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2980    ///     <td>README.md</td>
2981    ///     <td></td>
2982    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/README.md</td>
2983    /// </tr><tr>
2984    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2985    ///     <td>README.md</td>
2986    ///     <td>/</td>
2987    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2988    /// </tr><tr>
2989    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2990    ///     <td>README.md</td>
2991    ///     <td>/CONTRIBUTING.md</td>
2992    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md</td>
2993    /// </tr><tr>
2994    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master</td>
2995    ///     <td>README.md</td>
2996    ///     <td></td>
2997    ///     <td>https://github.com/rust-ammonia/ammonia/blob/README.md</td>
2998    /// </tr><tr>
2999    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master</td>
3000    ///     <td>README.md</td>
3001    ///     <td>/</td>
3002    ///     <td>https://github.com/rust-ammonia/ammonia/blob/</td>
3003    /// </tr><tr>
3004    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master</td>
3005    ///     <td>README.md</td>
3006    ///     <td>/CONTRIBUTING.md</td>
3007    ///     <td>https://github.com/rust-ammonia/ammonia/blob/CONTRIBUTING.md</td>
3008    /// </tr><tr>
3009    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
3010    ///     <td></td>
3011    ///     <td></td>
3012    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
3013    /// </tr><tr>
3014    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
3015    ///     <td></td>
3016    ///     <td>/</td>
3017    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
3018    /// </tr><tr>
3019    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
3020    ///     <td></td>
3021    ///     <td>/CONTRIBUTING.md</td>
3022    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md</td>
3023    /// </tr><tr>
3024    ///     <td>https://github.com/</td>
3025    ///     <td>rust-ammonia/ammonia/blob/master/README.md</td>
3026    ///     <td></td>
3027    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/README.md</td>
3028    /// </tr><tr>
3029    ///     <td>https://github.com/</td>
3030    ///     <td>rust-ammonia/ammonia/blob/master/README.md</td>
3031    ///     <td>/</td>
3032    ///     <td>https://github.com/</td>
3033    /// </tr><tr>
3034    ///     <td>https://github.com/</td>
3035    ///     <td>rust-ammonia/ammonia/blob/master/README.md</td>
3036    ///     <td>CONTRIBUTING.md</td>
3037    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md</td>
3038    /// </tr><tr>
3039    ///     <td>https://github.com/</td>
3040    ///     <td>rust-ammonia/ammonia/blob/master/README.md</td>
3041    ///     <td>/CONTRIBUTING.md</td>
3042    ///     <td>https://github.com/CONTRIBUTING.md</td>
3043    /// </tr>
3044    /// </tbody>
3045    /// </table>
3046    RewriteWithRoot {
3047        /// The URL that is treated as the root by the resolver.
3048        root: Url,
3049        /// The "current path" used to resolve relative paths.
3050        path: String,
3051    },
3052    /// Rewrite URLs with a custom function.
3053    Custom(Box<dyn UrlRelativeEvaluate<'a>>),
3054}
3055
3056impl<'a> UrlRelative<'a> {
3057    fn evaluate(&self, url: &str) -> Option<html5ever::tendril::StrTendril> {
3058        match self {
3059            UrlRelative::RewriteWithBase(ref url_base) => url_base
3060                .join(url)
3061                .ok()
3062                .and_then(|x| StrTendril::from_str(x.as_str()).ok()),
3063            UrlRelative::RewriteWithRoot { ref root, ref path } => {
3064                (match url.as_bytes() {
3065                    // Scheme-relative URL
3066                    [b'/', b'/', ..] => root.join(url),
3067                    // Path-absolute URL
3068                    b"/" => root.join("."),
3069                    [b'/', ..] => root.join(&url[1..]),
3070                    // Path-relative URL
3071                    _ => root.join(path).and_then(|r| r.join(url)),
3072                })
3073                .ok()
3074                .and_then(|x| StrTendril::from_str(x.as_str()).ok())
3075            }
3076            UrlRelative::Custom(ref evaluate) => evaluate
3077                .evaluate(url)
3078                .as_ref()
3079                .map(Cow::as_ref)
3080                .map(StrTendril::from_str)
3081                .and_then(Result::ok),
3082            UrlRelative::PassThrough => StrTendril::from_str(url).ok(),
3083            UrlRelative::Deny => None,
3084        }
3085    }
3086}
3087
3088impl<'a> fmt::Debug for UrlRelative<'a> {
3089    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3090        match *self {
3091            UrlRelative::Deny => write!(f, "UrlRelative::Deny"),
3092            UrlRelative::PassThrough => write!(f, "UrlRelative::PassThrough"),
3093            UrlRelative::RewriteWithBase(ref base) => {
3094                write!(f, "UrlRelative::RewriteWithBase({})", base)
3095            }
3096            UrlRelative::RewriteWithRoot { ref root, ref path } => {
3097                write!(
3098                    f,
3099                    "UrlRelative::RewriteWithRoot {{ root: {root}, path: {path} }}"
3100                )
3101            }
3102            UrlRelative::Custom(_) => write!(f, "UrlRelative::Custom"),
3103        }
3104    }
3105}
3106
3107/// Types that implement this trait can be used to convert a relative URL into an absolute URL.
3108///
3109/// This evaluator is only called when the URL is relative; absolute URLs are not evaluated.
3110///
3111/// See [`url_relative`][url_relative] for more details.
3112///
3113/// [url_relative]: struct.Builder.html#method.url_relative
3114pub trait UrlRelativeEvaluate<'a>: Send + Sync + 'a {
3115    /// Return `None` to remove the attribute. Return `Some(str)` to replace it with a new string.
3116    fn evaluate<'url>(&self, _: &'url str) -> Option<Cow<'url, str>>;
3117}
3118impl<'a, T> UrlRelativeEvaluate<'a> for T
3119where
3120    T: Fn(&str) -> Option<Cow<'_, str>> + Send + Sync + 'a,
3121{
3122    fn evaluate<'url>(&self, url: &'url str) -> Option<Cow<'url, str>> {
3123        self(url)
3124    }
3125}
3126
3127impl fmt::Debug for dyn AttributeFilter {
3128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3129        f.write_str("AttributeFilter")
3130    }
3131}
3132
3133/// Types that implement this trait can be used to remove or rewrite arbitrary attributes.
3134///
3135/// See [`attribute_filter`][attribute_filter] for more details.
3136///
3137/// [attribute_filter]: struct.Builder.html#method.attribute_filter
3138pub trait AttributeFilter: Send + Sync {
3139    /// Return `None` to remove the attribute. Return `Some(str)` to replace it with a new string.
3140    fn filter<'a>(&self, _: &str, _: &str, _: &'a str) -> Option<Cow<'a, str>>;
3141}
3142
3143impl<T> AttributeFilter for T
3144where
3145    T: for<'a> Fn(&str, &str, &'a str) -> Option<Cow<'a, str>> + Send + Sync + 'static,
3146{
3147    fn filter<'a>(&self, element: &str, attribute: &str, value: &'a str) -> Option<Cow<'a, str>> {
3148        self(element, attribute, value)
3149    }
3150}
3151
3152/// A sanitized HTML document.
3153///
3154/// The `Document` type is an opaque struct representing an HTML fragment that was sanitized by
3155/// `ammonia`. It can be converted to a [`String`] or written to a [`Write`] instance. This allows
3156/// users to avoid buffering the serialized representation to a [`String`] when desired.
3157///
3158/// This type is opaque to insulate the caller from breaking changes in the `html5ever` interface.
3159///
3160/// Note that this type wraps an `html5ever` DOM tree. `ammonia` does not support streaming, so
3161/// the complete fragment needs to be stored in memory during processing.
3162///
3163/// [`String`]: https://doc.rust-lang.org/nightly/std/string/struct.String.html
3164/// [`Write`]: https://doc.rust-lang.org/nightly/std/io/trait.Write.html
3165///
3166/// # Examples
3167///
3168///     use ammonia::Builder;
3169///
3170///     let input = "<!-- comments will be stripped -->This is an Ammonia example.";
3171///     let output = "This is an Ammonia example.";
3172///
3173///     let document = Builder::new()
3174///         .clean(input);
3175///     assert_eq!(document.to_string(), output);
3176pub struct Document(RcDom);
3177
3178impl Document {
3179    /// Serializes a `Document` instance to a writer.
3180    ///
3181    /// This method writes the sanitized HTML to a [`Write`] instance, avoiding a buffering step.
3182    ///
3183    /// To avoid consuming the writer, a mutable reference can be passed, like in the example below.
3184    ///
3185    /// Note that the in-memory representation of `Document` is larger than the serialized
3186    /// `String`.
3187    ///
3188    /// [`Write`]: https://doc.rust-lang.org/nightly/std/io/trait.Write.html
3189    ///
3190    /// # Examples
3191    ///
3192    ///     use ammonia::Builder;
3193    ///
3194    ///     let input = "Some <style></style>HTML here";
3195    ///     let expected = b"Some HTML here";
3196    ///
3197    ///     let document = Builder::new()
3198    ///         .clean(input);
3199    ///
3200    ///     let mut sanitized = Vec::new();
3201    ///     document.write_to(&mut sanitized)
3202    ///         .expect("Writing to a string should not fail (except on OOM)");
3203    ///     assert_eq!(sanitized, expected);
3204    pub fn write_to<W>(&self, writer: W) -> io::Result<()>
3205    where
3206        W: io::Write,
3207    {
3208        let opts = Self::serialize_opts();
3209        let inner: SerializableHandle = self.0.document.children.borrow()[0].clone().into();
3210        serialize(writer, &inner, opts)
3211    }
3212
3213    /// Exposes the `Document` instance as an [`rcdom::Handle`].
3214    ///
3215    /// This method returns the inner object backing the `Document` instance. This allows
3216    /// making further changes to the DOM without introducing redundant serialization and
3217    /// parsing.
3218    ///
3219    /// Note that this method should be considered unstable and sits outside of the semver
3220    /// stability guarantees. It may change, break, or go away at any time, either because
3221    /// of `html5ever` changes or `ammonia` implementation changes.
3222    ///
3223    /// For this method to be accessible, a `cfg` flag is required. The easiest way is to
3224    /// use the `RUSTFLAGS` environment variable:
3225    ///
3226    /// ```text
3227    /// RUSTFLAGS='--cfg ammonia_unstable' cargo build
3228    /// ```
3229    ///
3230    /// on Unix-like platforms, or
3231    ///
3232    /// ```text
3233    /// set RUSTFLAGS=--cfg ammonia_unstable
3234    /// cargo build
3235    /// ```
3236    ///
3237    /// on Windows.
3238    ///
3239    /// This requirement also applies to crates that transitively depend on crates that use
3240    /// this flag.
3241    ///
3242    /// # Examples
3243    ///
3244    ///     use ammonia::Builder;
3245    ///     use ammonia::rcdom::SerializableHandle;
3246    ///     use maplit::hashset;
3247    ///     use html5ever::serialize::{serialize, SerializeOpts};
3248    ///
3249    ///     # use std::error::Error;
3250    ///     # fn do_main() -> Result<(), Box<dyn Error>> {
3251    ///     let input = "<a>one link</a> and <a>one more</a>";
3252    ///     let expected = "<a>one more</a> and <a>one link</a>";
3253    ///
3254    ///     let document = Builder::new()
3255    ///         .link_rel(None)
3256    ///         .clean(input);
3257    ///
3258    ///     let node = document.to_dom_node();
3259    ///     node.children.borrow_mut().reverse();
3260    ///
3261    ///     let mut buf = Vec::new();
3262    ///     let handle: SerializableHandle = node.into();
3263    ///     serialize(&mut buf, &handle, SerializeOpts::default())?;
3264    ///     let output = String::from_utf8(buf)?;
3265    ///
3266    ///     assert_eq!(output, expected);
3267    ///     # Ok(())
3268    ///     # }
3269    ///     # fn main() { do_main().unwrap() }
3270    #[cfg(ammonia_unstable)]
3271    pub fn to_dom_node(&self) -> Handle {
3272        self.0.document.children.borrow()[0].clone()
3273    }
3274
3275    fn serialize_opts() -> SerializeOpts {
3276        SerializeOpts::default()
3277    }
3278}
3279
3280impl Clone for Document {
3281    fn clone(&self) -> Self {
3282        let parser = Builder::make_parser();
3283        let dom = parser.one(&self.to_string()[..]);
3284        Document(dom)
3285    }
3286}
3287
3288/// Convert a `Document` to stringified HTML.
3289///
3290/// Since [`Document`] implements [`Display`], it can be converted to a [`String`] using the
3291/// standard [`ToString::to_string`] method. This is the simplest way to use `ammonia`.
3292///
3293/// [`Document`]: ammonia::Document
3294/// [`Display`]: std::fmt::Display
3295/// [`ToString::to_string`]: std::string::ToString
3296///
3297/// # Examples
3298///
3299///     use ammonia::Builder;
3300///
3301///     let input = "Some <style></style>HTML here";
3302///     let output = "Some HTML here";
3303///
3304///     let document = Builder::new()
3305///         .clean(input);
3306///     assert_eq!(document.to_string(), output);
3307impl Display for Document {
3308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3309        let opts = Self::serialize_opts();
3310        let mut ret_val = Vec::new();
3311        let inner: SerializableHandle = self.0.document.children.borrow()[0].clone().into();
3312        serialize(&mut ret_val, &inner, opts)
3313            .expect("Writing to a string shouldn't fail (expect on OOM)");
3314        String::from_utf8(ret_val)
3315            .expect("html5ever only supports UTF8")
3316            .fmt(f)
3317    }
3318}
3319
3320impl fmt::Debug for Document {
3321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3322        write!(f, "Document({})", self)
3323    }
3324}
3325
3326impl From<Document> for String {
3327    fn from(document: Document) -> Self {
3328        document.to_string()
3329    }
3330}
3331
3332#[cfg(test)]
3333mod test {
3334    use super::*;
3335    #[test]
3336    fn deeply_nested_whitelisted_does_not_cause_stack_overflow() {
3337        clean(&"<b>".repeat(60_000));
3338    }
3339    #[test]
3340    fn deeply_nested_blacklisted_does_not_cause_stack_overflow() {
3341        clean(&"<b-b>".repeat(60_000));
3342    }
3343    #[test]
3344    fn deeply_nested_alternating_does_not_cause_stack_overflow() {
3345        clean(&"<b-b>".repeat(35_000));
3346    }
3347    #[test]
3348    fn document_level_tags_cannot_be_whitelisted() {
3349        // Adding `html`, `head`, or `body` to the allowed tags has no effect
3350        // because the parser runs in fragment mode and strips them before
3351        // the sanitizer sees the tree. This test pins that documented
3352        // behavior; if it ever changes, the docs on `Builder::tags` need to
3353        // change too.
3354        let fragment =
3355            "<html><head>head content</head><body><div>test</div></body></html>";
3356        let result = Builder::default()
3357            .add_tags(["html", "head", "body"])
3358            .clean(fragment)
3359            .to_string();
3360        assert_eq!(result, "head content<div>test</div>");
3361    }
3362    #[test]
3363    fn included_angles() {
3364        let fragment = "1 < 2";
3365        let result = clean(fragment);
3366        assert_eq!(result, "1 &lt; 2");
3367    }
3368    #[test]
3369    fn remove_script() {
3370        let fragment = "an <script>evil()</script> example";
3371        let result = clean(fragment);
3372        assert_eq!(result, "an  example");
3373    }
3374    #[test]
3375    fn ignore_link() {
3376        let fragment = "a <a href=\"http://www.google.com\">good</a> example";
3377        let expected = "a <a href=\"http://www.google.com\" rel=\"noopener noreferrer\">\
3378                        good</a> example";
3379        let result = clean(fragment);
3380        assert_eq!(result, expected);
3381    }
3382    #[test]
3383    fn remove_unsafe_link() {
3384        let fragment = "an <a onclick=\"evil()\" href=\"http://www.google.com\">evil</a> example";
3385        let result = clean(fragment);
3386        assert_eq!(
3387            result,
3388            "an <a href=\"http://www.google.com\" rel=\"noopener noreferrer\">evil</a> example"
3389        );
3390    }
3391    #[test]
3392    fn remove_js_link() {
3393        let fragment = "an <a href=\"javascript:evil()\">evil</a> example";
3394        let result = clean(fragment);
3395        assert_eq!(result, "an <a rel=\"noopener noreferrer\">evil</a> example");
3396    }
3397    #[test]
3398    fn tag_rebalance() {
3399        let fragment = "<b>AWESOME!";
3400        let result = clean(fragment);
3401        assert_eq!(result, "<b>AWESOME!</b>");
3402    }
3403    #[test]
3404    fn allow_url_relative() {
3405        let fragment = "<a href=test>Test</a>";
3406        let result = Builder::new()
3407            .url_relative(UrlRelative::PassThrough)
3408            .clean(fragment)
3409            .to_string();
3410        assert_eq!(
3411            result,
3412            "<a href=\"test\" rel=\"noopener noreferrer\">Test</a>"
3413        );
3414    }
3415    #[test]
3416    fn rewrite_url_relative() {
3417        let fragment = "<a href=test>Test</a>";
3418        let result = Builder::new()
3419            .url_relative(UrlRelative::RewriteWithBase(
3420                Url::parse("http://example.com/").unwrap(),
3421            ))
3422            .clean(fragment)
3423            .to_string();
3424        assert_eq!(
3425            result,
3426            "<a href=\"http://example.com/test\" rel=\"noopener noreferrer\">Test</a>"
3427        );
3428    }
3429    #[test]
3430    fn rewrite_url_relative_with_invalid_url() {
3431        // Reduced from https://github.com/Bauke/ammonia-crash-test
3432        let fragment = r##"<a href="\\"https://example.com\\"">test</a>"##;
3433        let result = Builder::new()
3434            .url_relative(UrlRelative::RewriteWithBase(
3435                Url::parse("http://example.com/").unwrap(),
3436            ))
3437            .clean(fragment)
3438            .to_string();
3439        assert_eq!(result, r##"<a rel="noopener noreferrer">test</a>"##);
3440    }
3441    #[test]
3442    fn attribute_filter_nop() {
3443        let fragment = "<a href=test>Test</a>";
3444        let result = Builder::new()
3445            .attribute_filter(|elem, attr, value| {
3446                assert_eq!("a", elem);
3447                assert!(
3448                    matches!(
3449                        (attr, value),
3450                        ("href", "test") | ("rel", "noopener noreferrer")
3451                    ),
3452                    "{}",
3453                    value.to_string()
3454                );
3455                Some(value.into())
3456            })
3457            .clean(fragment)
3458            .to_string();
3459        assert_eq!(
3460            result,
3461            "<a href=\"test\" rel=\"noopener noreferrer\">Test</a>"
3462        );
3463    }
3464
3465    #[test]
3466    fn attribute_filter_drop() {
3467        let fragment = "Test<img alt=test src=imgtest>";
3468        let result = Builder::new()
3469            .attribute_filter(|elem, attr, value| {
3470                assert_eq!("img", elem);
3471                match (attr, value) {
3472                    ("src", "imgtest") => None,
3473                    ("alt", "test") => Some(value.into()),
3474                    _ => panic!("unexpected"),
3475                }
3476            })
3477            .clean(fragment)
3478            .to_string();
3479        assert_eq!(result, r#"Test<img alt="test">"#);
3480    }
3481
3482    #[test]
3483    fn url_filter_absolute() {
3484        let fragment = "Test<img alt=test src=imgtest>";
3485        let result = Builder::new()
3486            .attribute_filter(|elem, attr, value| {
3487                assert_eq!("img", elem);
3488                match (attr, value) {
3489                    ("src", "imgtest") => {
3490                        Some(format!("https://example.com/images/{}", value).into())
3491                    }
3492                    ("alt", "test") => None,
3493                    _ => panic!("unexpected"),
3494                }
3495            })
3496            .url_relative(UrlRelative::RewriteWithBase(
3497                Url::parse("http://wrong.invalid/").unwrap(),
3498            ))
3499            .clean(fragment)
3500            .to_string();
3501        assert_eq!(
3502            result,
3503            r#"Test<img src="https://example.com/images/imgtest">"#
3504        );
3505    }
3506
3507    #[test]
3508    fn url_filter_relative() {
3509        let fragment = "Test<img alt=test src=imgtest>";
3510        let result = Builder::new()
3511            .attribute_filter(|elem, attr, value| {
3512                assert_eq!("img", elem);
3513                match (attr, value) {
3514                    ("src", "imgtest") => Some("rewrite".into()),
3515                    ("alt", "test") => Some("altalt".into()),
3516                    _ => panic!("unexpected"),
3517                }
3518            })
3519            .url_relative(UrlRelative::RewriteWithBase(
3520                Url::parse("https://example.com/base/#").unwrap(),
3521            ))
3522            .clean(fragment)
3523            .to_string();
3524        assert_eq!(
3525            result,
3526            r#"Test<img alt="altalt" src="https://example.com/base/rewrite">"#
3527        );
3528    }
3529
3530    #[test]
3531    fn rewrite_url_relative_no_rel() {
3532        let fragment = "<a href=test>Test</a>";
3533        let result = Builder::new()
3534            .url_relative(UrlRelative::RewriteWithBase(
3535                Url::parse("http://example.com/").unwrap(),
3536            ))
3537            .link_rel(None)
3538            .clean(fragment)
3539            .to_string();
3540        assert_eq!(result, "<a href=\"http://example.com/test\">Test</a>");
3541    }
3542    #[test]
3543    fn deny_url_relative() {
3544        let fragment = "<a href=test>Test</a>";
3545        let result = Builder::new()
3546            .url_relative(UrlRelative::Deny)
3547            .clean(fragment)
3548            .to_string();
3549        assert_eq!(result, "<a rel=\"noopener noreferrer\">Test</a>");
3550    }
3551    #[test]
3552    fn replace_rel() {
3553        let fragment = "<a href=test rel=\"garbage\">Test</a>";
3554        let result = Builder::new()
3555            .url_relative(UrlRelative::PassThrough)
3556            .clean(fragment)
3557            .to_string();
3558        assert_eq!(
3559            result,
3560            "<a href=\"test\" rel=\"noopener noreferrer\">Test</a>"
3561        );
3562    }
3563    #[test]
3564    fn consider_rel_still_banned() {
3565        let fragment = "<a href=test rel=\"garbage\">Test</a>";
3566        let result = Builder::new()
3567            .url_relative(UrlRelative::PassThrough)
3568            .link_rel(None)
3569            .clean(fragment)
3570            .to_string();
3571        assert_eq!(result, "<a href=\"test\">Test</a>");
3572    }
3573    #[test]
3574    fn object_data() {
3575        let fragment = "<span data=\"javascript:evil()\">Test</span>\
3576                        <object data=\"javascript:evil()\"></object>M";
3577        let expected = r#"<span data="javascript:evil()">Test</span><object></object>M"#;
3578        let result = Builder::new()
3579            .tags(hashset!["span", "object"])
3580            .generic_attributes(hashset!["data"])
3581            .clean(fragment)
3582            .to_string();
3583        assert_eq!(result, expected);
3584    }
3585    #[test]
3586    fn remove_attributes() {
3587        let fragment = "<table border=\"1\"><tr></tr></table>";
3588        let result = Builder::new().clean(fragment);
3589        assert_eq!(
3590            result.to_string(),
3591            "<table><tbody><tr></tr></tbody></table>"
3592        );
3593    }
3594    #[test]
3595    fn quotes_in_attrs() {
3596        let fragment = "<b title='\"'>contents</b>";
3597        let result = clean(fragment);
3598        assert_eq!(result, "<b title=\"&quot;\">contents</b>");
3599    }
3600    #[test]
3601    #[should_panic]
3602    fn panic_if_rel_is_allowed_and_replaced_generic() {
3603        Builder::new()
3604            .link_rel(Some("noopener noreferrer"))
3605            .generic_attributes(hashset!["rel"])
3606            .clean("something");
3607    }
3608    #[test]
3609    #[should_panic]
3610    fn panic_if_rel_is_allowed_and_replaced_a() {
3611        Builder::new()
3612            .link_rel(Some("noopener noreferrer"))
3613            .tag_attributes(hashmap![
3614                "a" => hashset!["rel"],
3615            ])
3616            .clean("something");
3617    }
3618    #[test]
3619    fn no_panic_if_rel_is_allowed_and_replaced_span() {
3620        Builder::new()
3621            .link_rel(Some("noopener noreferrer"))
3622            .tag_attributes(hashmap![
3623                "span" => hashset!["rel"],
3624            ])
3625            .clean("<span rel=\"what\">s</span>");
3626    }
3627    #[test]
3628    fn no_panic_if_rel_is_allowed_and_not_replaced_generic() {
3629        Builder::new()
3630            .link_rel(None)
3631            .generic_attributes(hashset!["rel"])
3632            .clean("<a rel=\"what\">s</a>");
3633    }
3634    #[test]
3635    fn no_panic_if_rel_is_allowed_and_not_replaced_a() {
3636        Builder::new()
3637            .link_rel(None)
3638            .tag_attributes(hashmap![
3639                "a" => hashset!["rel"],
3640            ])
3641            .clean("<a rel=\"what\">s</a>");
3642    }
3643    #[test]
3644    fn dont_close_void_elements() {
3645        let fragment = "<br>";
3646        let result = clean(fragment);
3647        assert_eq!(result.to_string(), "<br>");
3648    }
3649    #[should_panic]
3650    #[test]
3651    fn panic_on_allowed_classes_tag_attributes() {
3652        let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3653        Builder::new()
3654            .link_rel(None)
3655            .tag_attributes(hashmap![
3656                "p" => hashset!["class"],
3657                "a" => hashset!["class"],
3658            ])
3659            .allowed_classes(hashmap![
3660                "p" => hashset!["foo", "bar"],
3661                "a" => hashset!["baz"],
3662            ])
3663            .clean(fragment);
3664    }
3665    #[should_panic]
3666    #[test]
3667    fn panic_on_allowed_classes_generic_attributes() {
3668        let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3669        Builder::new()
3670            .link_rel(None)
3671            .generic_attributes(hashset!["class", "href", "some-foo"])
3672            .allowed_classes(hashmap![
3673                "p" => hashset!["foo", "bar"],
3674                "a" => hashset!["baz"],
3675            ])
3676            .clean(fragment);
3677    }
3678    #[test]
3679    fn remove_non_allowed_classes() {
3680        let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3681        let result = Builder::new()
3682            .link_rel(None)
3683            .allowed_classes(hashmap![
3684                "p" => hashset!["foo", "bar"],
3685                "a" => hashset!["baz"],
3686            ])
3687            .clean(fragment);
3688        assert_eq!(
3689            result.to_string(),
3690            "<p class=\"foo bar\"><a class=\"baz\">Hey</a></p>"
3691        );
3692    }
3693    #[test]
3694    fn remove_non_allowed_classes_with_tag_class() {
3695        let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3696        let result = Builder::new()
3697            .link_rel(None)
3698            .tag_attributes(hashmap![
3699                "div" => hashset!["class"],
3700            ])
3701            .allowed_classes(hashmap![
3702                "p" => hashset!["foo", "bar"],
3703                "a" => hashset!["baz"],
3704            ])
3705            .clean(fragment);
3706        assert_eq!(
3707            result.to_string(),
3708            "<p class=\"foo bar\"><a class=\"baz\">Hey</a></p>"
3709        );
3710    }
3711    #[test]
3712    fn allowed_classes_ascii_whitespace() {
3713        // According to https://infra.spec.whatwg.org/#ascii-whitespace,
3714        // TAB (\t), LF (\n), FF (\x0C), CR (\x0D) and SPACE (\x20) are
3715        // considered to be ASCII whitespace. Unicode whitespace characters
3716        // and VT (\x0B) aren't ASCII whitespace.
3717        let fragment = "<p class=\"a\tb\nc\x0Cd\re f\x0B g\u{2000}\">";
3718        let result = Builder::new()
3719            .allowed_classes(hashmap![
3720                "p" => hashset!["a", "b", "c", "d", "e", "f", "g"],
3721            ])
3722            .clean(fragment);
3723        assert_eq!(result.to_string(), r#"<p class="a b c d e"></p>"#);
3724    }
3725    #[test]
3726    fn remove_non_allowed_attributes_with_tag_attribute_values() {
3727        let fragment = "<p data-label=\"baz\" name=\"foo\"></p>";
3728        let result = Builder::new()
3729            .tag_attribute_values(hashmap![
3730                "p" => hashmap![
3731                    "data-label" => hashset!["bar"],
3732                ],
3733            ])
3734            .tag_attributes(hashmap![
3735                "p" => hashset!["name"],
3736            ])
3737            .clean(fragment);
3738        assert_eq!(result.to_string(), "<p name=\"foo\"></p>",);
3739    }
3740    #[test]
3741    fn keep_allowed_attributes_with_tag_attribute_values() {
3742        let fragment = "<p data-label=\"bar\" name=\"foo\"></p>";
3743        let result = Builder::new()
3744            .tag_attribute_values(hashmap![
3745                "p" => hashmap![
3746                    "data-label" => hashset!["bar"],
3747                ],
3748            ])
3749            .tag_attributes(hashmap![
3750                "p" => hashset!["name"],
3751            ])
3752            .clean(fragment);
3753        assert_eq!(
3754            result.to_string(),
3755            "<p data-label=\"bar\" name=\"foo\"></p>",
3756        );
3757    }
3758    #[test]
3759    fn tag_attribute_values_case_insensitive() {
3760        let fragment = "<input type=\"CHECKBOX\" name=\"foo\">";
3761        let result = Builder::new()
3762            .tags(hashset!["input"])
3763            .tag_attribute_values(hashmap![
3764                "input" => hashmap![
3765                    "type" => hashset!["checkbox"],
3766                ],
3767            ])
3768            .tag_attributes(hashmap![
3769                "input" => hashset!["name"],
3770            ])
3771            .clean(fragment);
3772        assert_eq!(result.to_string(), "<input type=\"CHECKBOX\" name=\"foo\">",);
3773    }
3774    #[test]
3775    fn set_tag_attribute_values() {
3776        let fragment = "<a href=\"https://example.com/\">Link</a>";
3777        let result = Builder::new()
3778            .link_rel(None)
3779            .add_tag_attributes("a", &["target"])
3780            .set_tag_attribute_value("a", "target", "_blank")
3781            .clean(fragment);
3782        assert_eq!(
3783            result.to_string(),
3784            "<a href=\"https://example.com/\" target=\"_blank\">Link</a>",
3785        );
3786    }
3787    #[test]
3788    fn update_existing_set_tag_attribute_values() {
3789        let fragment = "<a target=\"bad\" href=\"https://example.com/\">Link</a>";
3790        let result = Builder::new()
3791            .link_rel(None)
3792            .add_tag_attributes("a", &["target"])
3793            .set_tag_attribute_value("a", "target", "_blank")
3794            .clean(fragment);
3795        assert_eq!(
3796            result.to_string(),
3797            "<a target=\"_blank\" href=\"https://example.com/\">Link</a>",
3798        );
3799    }
3800    #[test]
3801    fn unwhitelisted_set_tag_attribute_values() {
3802        let fragment = "<span>hi</span><my-elem>";
3803        let result = Builder::new()
3804            .set_tag_attribute_value("my-elem", "my-attr", "val")
3805            .clean(fragment);
3806        assert_eq!(result.to_string(), "<span>hi</span>",);
3807    }
3808    #[test]
3809    fn remove_entity_link() {
3810        let fragment = "<a href=\"&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61\
3811                        &#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29\">Click me!</a>";
3812        let result = clean(fragment);
3813        assert_eq!(
3814            result.to_string(),
3815            "<a rel=\"noopener noreferrer\">Click me!</a>"
3816        );
3817    }
3818    #[test]
3819    fn remove_relative_url_evaluate() {
3820        fn is_absolute_path(url: &str) -> bool {
3821            let u = url.as_bytes();
3822            // `//a/b/c` is "protocol-relative", meaning "a" is a hostname
3823            // `/a/b/c` is an absolute path, and what we want to do stuff to.
3824            u.first() == Some(&b'/') && u.get(1) != Some(&b'/')
3825        }
3826        fn is_banned(url: &str) -> bool {
3827            let u = url.as_bytes();
3828            u.first() == Some(&b'b') && u.get(1) == Some(&b'a')
3829        }
3830        fn evaluate(url: &str) -> Option<Cow<'_, str>> {
3831            if is_absolute_path(url) {
3832                Some(Cow::Owned(String::from("/root") + url))
3833            } else if is_banned(url) {
3834                None
3835            } else {
3836                Some(Cow::Borrowed(url))
3837            }
3838        }
3839        let a = Builder::new()
3840            .url_relative(UrlRelative::Custom(Box::new(evaluate)))
3841            .clean("<a href=banned>banned</a><a href=/test/path>fixed</a><a href=path>passed</a><a href=http://google.com/>skipped</a>")
3842            .to_string();
3843        assert_eq!(a, "<a rel=\"noopener noreferrer\">banned</a><a href=\"/root/test/path\" rel=\"noopener noreferrer\">fixed</a><a href=\"path\" rel=\"noopener noreferrer\">passed</a><a href=\"http://google.com/\" rel=\"noopener noreferrer\">skipped</a>");
3844    }
3845    #[test]
3846    fn remove_relative_url_evaluate_b() {
3847        fn is_absolute_path(url: &str) -> bool {
3848            let u = url.as_bytes();
3849            // `//a/b/c` is "protocol-relative", meaning "a" is a hostname
3850            // `/a/b/c` is an absolute path, and what we want to do stuff to.
3851            u.first() == Some(&b'/') && u.get(1) != Some(&b'/')
3852        }
3853        fn is_banned(url: &str) -> bool {
3854            let u = url.as_bytes();
3855            u.first() == Some(&b'b') && u.get(1) == Some(&b'a')
3856        }
3857        fn evaluate(url: &str) -> Option<Cow<'_, str>> {
3858            if is_absolute_path(url) {
3859                Some(Cow::Owned(String::from("/root") + url))
3860            } else if is_banned(url) {
3861                None
3862            } else {
3863                Some(Cow::Borrowed(url))
3864            }
3865        }
3866        let a = Builder::new()
3867            .url_relative(UrlRelative::Custom(Box::new(evaluate)))
3868            .clean("<a href=banned>banned</a><a href=banned title=test>banned</a><a title=test href=banned>banned</a>")
3869            .to_string();
3870        assert_eq!(a, "<a rel=\"noopener noreferrer\">banned</a><a rel=\"noopener noreferrer\" title=\"test\">banned</a><a title=\"test\" rel=\"noopener noreferrer\">banned</a>");
3871    }
3872    #[test]
3873    fn remove_relative_url_evaluate_c() {
3874        // Don't run on absolute URLs.
3875        fn evaluate(_: &str) -> Option<Cow<'_, str>> {
3876            return Some(Cow::Owned(String::from("invalid")));
3877        }
3878        let a = Builder::new()
3879            .url_relative(UrlRelative::Custom(Box::new(evaluate)))
3880            .clean("<a href=\"https://www.google.com/\">google</a>")
3881            .to_string();
3882        assert_eq!(
3883            a,
3884            "<a href=\"https://www.google.com/\" rel=\"noopener noreferrer\">google</a>"
3885        );
3886    }
3887    #[test]
3888    fn clean_children_of_bad_element() {
3889        let fragment = "<bad><evil>a</evil>b</bad>";
3890        let result = Builder::new().clean(fragment);
3891        assert_eq!(result.to_string(), "ab");
3892    }
3893    #[test]
3894    fn reader_input() {
3895        let fragment = b"an <script>evil()</script> example";
3896        let result = Builder::new().clean_from_reader(&fragment[..]);
3897        assert!(result.is_ok());
3898        assert_eq!(result.unwrap().to_string(), "an  example");
3899    }
3900    #[test]
3901    fn reader_non_utf8() {
3902        let fragment = b"non-utf8 \xF0\x90\x80string";
3903        let result = Builder::new().clean_from_reader(&fragment[..]);
3904        assert!(result.is_ok());
3905        assert_eq!(result.unwrap().to_string(), "non-utf8 \u{fffd}string");
3906    }
3907    #[test]
3908    fn display_impl() {
3909        let fragment = r#"a <a>link</a>"#;
3910        let result = Builder::new().link_rel(None).clean(fragment);
3911        assert_eq!(format!("{}", result), "a <a>link</a>");
3912    }
3913    #[test]
3914    fn debug_impl() {
3915        let fragment = r#"a <a>link</a>"#;
3916        let result = Builder::new().link_rel(None).clean(fragment);
3917        assert_eq!(format!("{:?}", result), "Document(a <a>link</a>)");
3918    }
3919    #[cfg(ammonia_unstable)]
3920    #[test]
3921    fn to_dom_node() {
3922        let fragment = r#"a <a>link</a>"#;
3923        let result = Builder::new().link_rel(None).clean(fragment);
3924        let _node = result.to_dom_node();
3925    }
3926    #[test]
3927    fn string_from_document() {
3928        let fragment = r#"a <a>link"#;
3929        let result = String::from(Builder::new().link_rel(None).clean(fragment));
3930        assert_eq!(format!("{}", result), "a <a>link</a>");
3931    }
3932    fn require_sync<T: Sync>(_: T) {}
3933    fn require_send<T: Send>(_: T) {}
3934    #[test]
3935    fn require_sync_and_send() {
3936        require_sync(Builder::new());
3937        require_send(Builder::new());
3938    }
3939    #[test]
3940    fn id_prefixed() {
3941        let fragment = "<a id=\"hello\"></a><b id=\"hello\"></a>";
3942        let result = String::from(
3943            Builder::new()
3944                .tag_attributes(hashmap![
3945                    "a" => hashset!["id"],
3946                ])
3947                .id_prefix(Some("prefix-"))
3948                .clean(fragment),
3949        );
3950        assert_eq!(
3951            result.to_string(),
3952            "<a id=\"prefix-hello\" rel=\"noopener noreferrer\"></a><b></b>"
3953        );
3954    }
3955    #[test]
3956    fn id_already_prefixed() {
3957        let fragment = "<a id=\"prefix-hello\"></a>";
3958        let result = String::from(
3959            Builder::new()
3960                .tag_attributes(hashmap![
3961                    "a" => hashset!["id"],
3962                ])
3963                .id_prefix(Some("prefix-"))
3964                .clean(fragment),
3965        );
3966        assert_eq!(
3967            result.to_string(),
3968            "<a id=\"prefix-hello\" rel=\"noopener noreferrer\"></a>"
3969        );
3970    }
3971    #[test]
3972    fn clean_content_tags() {
3973        let fragment = "<script type=\"text/javascript\"><a>Hello!</a></script>";
3974        let result = String::from(
3975            Builder::new()
3976                .clean_content_tags(hashset!["script"])
3977                .clean(fragment),
3978        );
3979        assert_eq!(result.to_string(), "");
3980    }
3981    #[test]
3982    fn only_clean_content_tags() {
3983        let fragment = "<em>This is</em><script><a>Hello!</a></script><p>still here!</p>";
3984        let result = String::from(
3985            Builder::new()
3986                .clean_content_tags(hashset!["script"])
3987                .clean(fragment),
3988        );
3989        assert_eq!(result.to_string(), "<em>This is</em><p>still here!</p>");
3990    }
3991    #[test]
3992    fn clean_removed_default_tag() {
3993        let fragment = "<em>This is</em><script><a>Hello!</a></script><p>still here!</p>";
3994        let result = String::from(
3995            Builder::new()
3996                .rm_tags(hashset!["a"])
3997                .rm_tag_attributes("a", hashset!["href", "hreflang"])
3998                .clean_content_tags(hashset!["script"])
3999                .clean(fragment),
4000        );
4001        assert_eq!(result.to_string(), "<em>This is</em><p>still here!</p>");
4002    }
4003    #[test]
4004    #[should_panic]
4005    fn panic_on_clean_content_tag_attribute() {
4006        Builder::new()
4007            .rm_tags(std::iter::once("a"))
4008            .clean_content_tags(hashset!["a"])
4009            .clean("");
4010    }
4011    #[test]
4012    #[should_panic]
4013    fn panic_on_clean_content_tag() {
4014        Builder::new().clean_content_tags(hashset!["a"]).clean("");
4015    }
4016
4017    #[test]
4018    fn clean_text_test() {
4019        assert_eq!(
4020            clean_text("<this> is <a test function"),
4021            "&lt;this&gt;&#32;is&#32;&lt;a&#32;test&#32;function"
4022        );
4023    }
4024
4025    #[test]
4026    fn clean_text_spaces_test() {
4027        assert_eq!(clean_text("\x09\x0a\x0c\x20"), "&#9;&#10;&#12;&#32;");
4028    }
4029
4030    #[test]
4031    fn ns_svg() {
4032        // https://github.com/cure53/DOMPurify/pull/495
4033        let fragment = r##"<svg><iframe><a title="</iframe><img src onerror=alert(1)>">test"##;
4034        let result = String::from(Builder::new().add_tags(&["iframe"]).clean(fragment));
4035        assert_eq!(result.to_string(), "");
4036
4037        let fragment = "<svg><iframe>remove me</iframe></svg><iframe>keep me</iframe>";
4038        let result = String::from(Builder::new().add_tags(&["iframe"]).clean(fragment));
4039        assert_eq!(result.to_string(), "<iframe>keep me</iframe>");
4040
4041        let fragment = "<svg><a>remove me</a></svg><iframe>keep me</iframe>";
4042        let result = String::from(Builder::new().add_tags(&["iframe"]).clean(fragment));
4043        assert_eq!(result.to_string(), "<iframe>keep me</iframe>");
4044
4045        let fragment = "<svg><a>keep me</a></svg><iframe>keep me</iframe>";
4046        let result = String::from(Builder::new().add_tags(&["iframe", "svg"]).clean(fragment));
4047        assert_eq!(
4048            result.to_string(),
4049            "<svg><a rel=\"noopener noreferrer\">keep me</a></svg><iframe>keep me</iframe>"
4050        );
4051    }
4052
4053    #[test]
4054    fn ns_svg_2() {
4055        let fragment = "<svg><foreignObject><table><path><xmp><!--</xmp><img title'--&gt;&lt;img src=1 onerror=alert(1)&gt;'>";
4056        let result =  Builder::default()
4057            .strip_comments(false)
4058            .add_tags(&["svg","foreignObject","table","path","xmp"])
4059            .clean(fragment);
4060        assert_eq!(
4061            result.to_string(),
4062            "<svg><foreignObject><table></table></foreignObject></svg>"
4063        );
4064    }
4065
4066    #[test]
4067    fn ns_mathml() {
4068        // https://github.com/cure53/DOMPurify/pull/495
4069        let fragment = "<mglyph></mglyph>";
4070        let result = String::from(
4071            Builder::new()
4072                .add_tags(&["math", "mtext", "mglyph"])
4073                .clean(fragment),
4074        );
4075        assert_eq!(result.to_string(), "");
4076        let fragment = "<math><mtext><div><mglyph>";
4077        let result = String::from(
4078            Builder::new()
4079                .add_tags(&["math", "mtext", "mglyph"])
4080                .clean(fragment),
4081        );
4082        assert_eq!(
4083            result.to_string(),
4084            "<math><mtext><div></div></mtext></math>"
4085        );
4086        let fragment = "<math><mtext><mglyph>";
4087        let result = String::from(
4088            Builder::new()
4089                .add_tags(&["math", "mtext", "mglyph"])
4090                .clean(fragment),
4091        );
4092        assert_eq!(
4093            result.to_string(),
4094            "<math><mtext><mglyph></mglyph></mtext></math>"
4095        );
4096    }
4097
4098    #[test]
4099    fn ns_mathml_2() {
4100        let fragment = "<math><mtext><table><mglyph><xmp><!--</xmp><img title='--&gt;&lt;img src=1 onerror=alert(1)&gt;'>";
4101        let result =  Builder::default()
4102            .strip_comments(false)
4103            .add_tags(&["math","mtext","table","mglyph","xmp"])
4104            .clean(fragment);
4105        assert_eq!(
4106            result.to_string(),
4107            "<math><mtext><table></table></mtext></math>"
4108        );
4109    }
4110
4111    #[test]
4112    fn ns_mathml_3() {
4113        // try without the attr
4114        let fragment = "<math><annotation-xml encoding='text/html'><xmp><!--</xmp><img title='--&gt;&lt;img src=1 onerror=alert(1)&gt;'>";
4115        let result =  Builder::default()
4116            .strip_comments(false)
4117            .add_tags(&["math","annotation-xml","table","mglyph","xmp"])
4118            .clean(fragment);
4119        assert_eq!(
4120            result.to_string(),
4121            "<math><annotation-xml></annotation-xml></math>"
4122        );
4123        // now with the attr
4124        let fragment = "<math><annotation-xml encoding='text/html'><xmp><!--</xmp><img title='--&gt;&lt;img src=1 onerror=alert(1)&gt;'>";
4125        let result =  Builder::default()
4126            .strip_comments(false)
4127            .add_tags(&["math","annotation-xml","table","mglyph","xmp"])
4128            .add_tag_attribute_values("annotation-xml", "encoding", ["text/html"])
4129            .clean(fragment);
4130        assert_eq!(
4131            result.to_string(),
4132            // yes, I tried it in Firefox, and the script didn't run
4133            r#"<math><annotation-xml encoding="text/html"><xmp><!--</xmp><img title="--&gt;&lt;img src=1 onerror=alert(1)&gt;"></annotation-xml></math>"#
4134        );
4135        // now with a tweaked attr
4136        let fragment = "<math><annotation-xml encoding='image/svg+xml'><xmp><!--</xmp><img title='--&gt;&lt;img src=1 onerror=alert(1)&gt;'>";
4137        let result =  Builder::default()
4138            .strip_comments(false)
4139            .add_tags(&["math","annotation-xml","table","mglyph","xmp"])
4140            .add_tag_attribute_values("annotation-xml", "encoding", ["image/svg+xml"])
4141            .clean(fragment);
4142        assert_eq!(
4143            result.to_string(),
4144            r#"<math><annotation-xml encoding="image/svg+xml"></annotation-xml></math>"#
4145        );
4146        // now with actual SVG
4147        let fragment = "<math><annotation-xml encoding='image/svg+xml'><svg>";
4148        let result =  Builder::default()
4149            .strip_comments(false)
4150            .add_tags(&["math","annotation-xml","svg"])
4151            .add_tag_attribute_values("annotation-xml", "encoding", ["image/svg+xml"])
4152            .clean(fragment);
4153        assert_eq!(
4154            result.to_string(),
4155            r#"<math><annotation-xml encoding="image/svg+xml"><svg></svg></annotation-xml></math>"#
4156        );
4157    }
4158
4159    #[test]
4160    fn ns_svg_animate_url_attr() {
4161        let fragment = r##"
4162            <svg>
4163                <a>
4164                    <animate attributeName="xss" values="http://example.com"></animate>
4165                    <animate attributeName="href" values="http://example.com"></animate>
4166                    <animate attributeName="href" values="http://example.com;/test"></animate>
4167                    <animate attributeName="href" values="http://example.com;/test;javascript:xss"></animate>
4168                    <animate attributeName="href" from="http://example.com" to="http://example.com"></animate>
4169                    <animate attributeName="href" from="javascript:xss" to="http://example.com"></animate>
4170                    <animate attributeName="href" from="http://example.com" to="javascript:xss"></animate>
4171                    <animate attributeName="href" from="http://example.com" to="./test2"></animate>
4172                    <animate attributeName="href" from="./test2" to="http://example.com"></animate>
4173                </a>
4174            </svg>
4175        "##;
4176        let filtered = r##"
4177            <svg>
4178                <a rel="noopener noreferrer">
4179                    
4180                    <animate attributeName="href" values="http://example.com"></animate>
4181                    <animate attributeName="href" values="http://example.com;http://notriddle.com/test"></animate>
4182                    
4183                    <animate attributeName="href" from="http://example.com" to="http://example.com"></animate>
4184                    
4185                    
4186                    <animate attributeName="href" from="http://example.com" to="http://notriddle.com/test2"></animate>
4187                    <animate attributeName="href" from="http://notriddle.com/test2" to="http://example.com"></animate>
4188                </a>
4189            </svg>
4190        "##;
4191        let result =  Builder::default()
4192            .add_tags(&["svg","a","animate"])
4193            .add_tag_attributes("animate", ["attributeName","values","from","to"])
4194            .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4195            .clean(fragment);
4196        assert_eq!(
4197            result.to_string(),
4198            filtered,
4199        );
4200    }
4201
4202    #[test]
4203    fn ns_svg_set_url_attr() {
4204        let fragment = r##"
4205            <svg>
4206                <a>
4207                    <set attributeName="href" to="./test2"></set>
4208                    <set attributeName="href" to="http://example.com"></set>
4209                    <set attributeName="href" to="javascript:xss"></set>
4210                </a>
4211            </svg>
4212        "##;
4213        let filtered = r##"
4214            <svg>
4215                <a rel="noopener noreferrer">
4216                    <set attributeName="href" to="http://notriddle.com/test2"></set>
4217                    <set attributeName="href" to="http://example.com"></set>
4218                    
4219                </a>
4220            </svg>
4221        "##;
4222        let result =  Builder::default()
4223            .add_tags(&["svg","a","set"])
4224            .add_tag_attributes("set", ["attributeName","values","from","to"])
4225            .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4226            .clean(fragment);
4227        assert_eq!(
4228            result.to_string(),
4229            filtered,
4230        );
4231    }
4232
4233    #[test]
4234    fn ns_svg_set_url_xlink_attr() {
4235        let fragment = r##"
4236            <svg>
4237                <a>
4238                    <set attributeName="xlink:href" to="./test2"></set>
4239                    <set attributeName="xlink:href" to="http://example.com"></set>
4240                    <set attributeName="xlink:href" to="javascript:xss"></set>
4241                </a>
4242            </svg>
4243        "##;
4244        let filtered = r##"
4245            <svg>
4246                <a rel="noopener noreferrer">
4247                    <set attributeName="xlink:href" to="http://notriddle.com/test2"></set>
4248                    <set attributeName="xlink:href" to="http://example.com"></set>
4249                    
4250                </a>
4251            </svg>
4252        "##;
4253        let result =  Builder::default()
4254            .add_tags(&["svg","a","set"])
4255            .add_tag_attributes("a", ["xlink:href"])
4256            .add_tag_attributes("set", ["attributeName","values","from","to"])
4257            .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4258            .clean(fragment);
4259        assert_eq!(
4260            result.to_string(),
4261            filtered,
4262        );
4263    }
4264
4265    #[test]
4266    fn ns_svg_set_url_attr_non_path() {
4267        let fragment = r##"
4268            <svg>
4269                <a>
4270                    <set attributeName="href" to="./test2"></set>
4271                    <set attributeName="href" to="http://example.com"></set>
4272                    <set attributeName="href" to="javascript:xss"></set>
4273                </a>
4274            </svg>
4275        "##;
4276        let filtered = r##"
4277            <svg>
4278                <a rel="noopener noreferrer">
4279                    <set attributeName="href"></set>
4280                    <set attributeName="href" to="http://example.com"></set>
4281                    
4282                </a>
4283            </svg>
4284        "##;
4285        let result =  Builder::default()
4286            .add_tags(&["svg","a","set"])
4287            .add_tag_attributes("set", ["attributeName","values","from","to"])
4288            .url_relative(UrlRelative::RewriteWithBase(Url::parse("magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709&xt=urn:btmh:1220e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855").unwrap()))
4289            .clean(fragment);
4290        assert_eq!(
4291            result.to_string(),
4292            filtered,
4293        );
4294    }
4295
4296    #[test]
4297    fn ns_svg_animate_set_attr() {
4298        let fragment = r##"
4299            <svg>
4300                <a>
4301                    <animate attributeName="x" values="1;2;3"></animate>
4302                    <animate attributeName="x" from="1" to="2"></animate>
4303                    <animate attributeName="x" from="1"></animate>
4304                    <animate attributeName="x" to="2"></animate>
4305                    <animate attributeName="y" values="1;2;3"></animate>
4306                    <animate attributeName="y" from="1" to="2"></animate>
4307                    <animate attributeName="y" from="1"></animate>
4308                    <animate attributeName="y" to="2"></animate>
4309                </a>
4310            </svg>
4311        "##;
4312        let filtered = r##"
4313            <svg>
4314                <a x="0" rel="noopener noreferrer">
4315                    
4316                    
4317                    
4318                    
4319                    <animate attributeName="y" values="1;2;3"></animate>
4320                    <animate attributeName="y" from="1" to="2"></animate>
4321                    <animate attributeName="y" from="1"></animate>
4322                    <animate attributeName="y" to="2"></animate>
4323                </a>
4324            </svg>
4325        "##;
4326        let result =  Builder::default()
4327            .add_tags(&["svg","a","animate"])
4328            .add_tag_attributes("animate", ["attributeName","values","from","to"])
4329            .add_tag_attributes("a", ["x", "y"])
4330            .set_tag_attribute_value("a", "x", "0")
4331            .clean(fragment);
4332        assert_eq!(
4333            result.to_string(),
4334            filtered,
4335        );
4336    }
4337
4338    #[test]
4339    fn ns_svg_animate_attr_filter() {
4340        let fragment = r##"
4341            <svg>
4342                <a>
4343                    <animate attributeName="x" values="1;2;3"></animate>
4344                    <animate attributeName="x" from="1" to="2"></animate>
4345                    <animate attributeName="x" from="1"></animate>
4346                    <animate attributeName="x" to="2"></animate>
4347                    <animate attributeName="y" values="1;2;3"></animate>
4348                    <animate attributeName="y" from="1" to="2"></animate>
4349                    <animate attributeName="y" from="1"></animate>
4350                    <animate attributeName="y" to="2"></animate>
4351                </a>
4352            </svg>
4353        "##;
4354        let filtered = r##"
4355            <svg>
4356                <a rel="noopener noreferrer">
4357                    <animate attributeName="x" values="0;0;0"></animate>
4358                    <animate attributeName="x" from="0" to="0"></animate>
4359                    <animate attributeName="x" from="0"></animate>
4360                    <animate attributeName="x" to="0"></animate>
4361                    <animate attributeName="y"></animate>
4362                    <animate attributeName="y" to="2"></animate>
4363                    <animate attributeName="y"></animate>
4364                    <animate attributeName="y" to="2"></animate>
4365                </a>
4366            </svg>
4367        "##;
4368        let result =  Builder::default()
4369            .add_tags(&["svg","a","animate"])
4370            .add_tag_attributes("animate", ["attributeName","values","from","to"])
4371            .add_tag_attributes("a", ["x", "y"])
4372            .attribute_filter(|_tag, key, value| Some(if key == "x" {
4373                "0".into()
4374            } else if key == "y" && value == "1" {
4375                return None;
4376            } else {
4377                value.into()
4378            }))
4379            .clean(fragment);
4380        assert_eq!(
4381            result.to_string(),
4382            filtered,
4383        );
4384    }
4385
4386    #[test]
4387    fn ns_svg_animate_allowed_classes() {
4388        let fragment = r##"
4389            <svg>
4390                <a>
4391                    <animate attributeName="class" values="a b c;a b c d;a b"></animate>
4392                    <animate attributeName="class" from="a b c" to="a b c d"></animate>
4393                    <animate attributeName="class" from="a b c d"></animate>
4394                    <animate attributeName="class" to="a d c"></animate>
4395                </a>
4396            </svg>
4397        "##;
4398        let filtered = r##"
4399            <svg>
4400                <a rel="noopener noreferrer">
4401                    <animate attributeName="class" values="a b c;a b c;a b"></animate>
4402                    <animate attributeName="class" from="a b c" to="a b c"></animate>
4403                    <animate attributeName="class" from="a b c"></animate>
4404                    <animate attributeName="class" to="a c"></animate>
4405                </a>
4406            </svg>
4407        "##;
4408        let result =  Builder::default()
4409            .add_tags(&["svg","a","animate"])
4410            .add_tag_attributes("animate", ["attributeName","values","from","to"])
4411            .add_allowed_classes("a", ["a", "b", "c"])
4412            .clean(fragment);
4413        assert_eq!(
4414            result.to_string(),
4415            filtered,
4416        );
4417    }
4418
4419    #[test]
4420    fn ns_svg_animate_allowed_styles() {
4421        let fragment = r##"
4422            <svg>
4423                <a>
4424                    <animate attributeName="style" values="background:red;color:blue"></animate>
4425                    <animate attributeName="style" from="background:red;text-decoration:none" to="color: blue;background:red"></animate>
4426                    <animate attributeName="style" from="background:red;text-decoration:none"></animate>
4427                    <animate attributeName="style" to="text-decoration:none"></animate>
4428                </a>
4429            </svg>
4430        "##;
4431        let filtered = r##"
4432            <svg>
4433                <a rel="noopener noreferrer">
4434                    <animate attributeName="style" values="background:red;"></animate>
4435                    <animate attributeName="style" from="background:red" to="background:red"></animate>
4436                    <animate attributeName="style" from="background:red"></animate>
4437                    <animate attributeName="style" to=""></animate>
4438                </a>
4439            </svg>
4440        "##;
4441        let result =  Builder::default()
4442            .add_tags(&["svg","a","animate"])
4443            .add_tag_attributes("animate", ["attributeName","values","from","to"])
4444            .add_tag_attributes("a", ["style"])
4445            .filter_style_properties(["background"].into())
4446            .clean(fragment);
4447        assert_eq!(
4448            result.to_string(),
4449            filtered,
4450        );
4451    }
4452
4453    #[test]
4454    fn ns_svg_animate_url_attr_href() {
4455        let fragment = r##"
4456            <svg>
4457                <a id="x1">
4458                </a>
4459                <animate href="#x1" attributeName="xss" values="http://example.com"></animate>
4460                <animate href="#x1" attributeName="href" values="http://example.com"></animate>
4461                <animate href="#x2" attributeName="href" values="http://example.com;/test"></animate>
4462                <animate href="#x2" attributeName="href" values="http://example.com;/test;javascript:xss"></animate>
4463                <animate href="#x2" attributeName="href" from="http://example.com" to="http://example.com"></animate>
4464                <animate href="#x2" attributeName="href" from="javascript:xss" to="http://example.com"></animate>
4465                <animate href="#x2" attributeName="href" from="http://example.com" to="javascript:xss"></animate>
4466                <animate href="#x2" attributeName="href" from="http://example.com" to="./test2"></animate>
4467                <animate href="#x2" attributeName="href" from="./test2" to="http://example.com"></animate>
4468            </svg>
4469        "##;
4470        let filtered = r##"
4471            <svg>
4472                <a rel="noopener noreferrer">
4473                </a>
4474                
4475                <animate href="#x1" attributeName="href" values="http://example.com"></animate>
4476                
4477                
4478                
4479                
4480                
4481                
4482                
4483            </svg>
4484        "##;
4485        let result =  Builder::default()
4486            .add_tags(&["svg","a","animate"])
4487            .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4488            .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4489            .clean(fragment);
4490        assert_eq!(
4491            result.to_string(),
4492            filtered,
4493        );
4494    }
4495
4496    #[test]
4497    fn ns_svg_animate_url_attr_href_depends_on_tag_name() {
4498        let fragment = r##"
4499            <object id="x2"></object>
4500            <svg>
4501                <g id="x1">
4502                </g>
4503                <animate href="#x1" attributeName="data" values="javascript:xss"></animate>
4504                <animate href="#x2" attributeName="data" values="javascript:xss"></animate>
4505            </svg>
4506        "##;
4507        let filtered = r##"
4508            <object id="x2"></object>
4509            <svg>
4510                <g id="x1">
4511                </g>
4512                <animate href="#x1" attributeName="data" values="javascript:xss"></animate>
4513                
4514            </svg>
4515        "##;
4516        let result =  Builder::default()
4517            .add_tags(&["svg","g","animate","object"])
4518            .add_tag_attributes("animate", ["attributeName","values","href"])
4519            .add_tag_attributes("g", ["data","id"])
4520            .add_tag_attributes("object", ["data","id"])
4521            .clean(fragment);
4522        assert_eq!(
4523            result.to_string(),
4524            filtered,
4525        );
4526    }
4527
4528    #[test]
4529    fn ns_svg_animate_set_attr_href() {
4530        let fragment = r##"
4531            <svg>
4532                <a id="x1">
4533                </a>
4534                <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4535                <animate href="#x1" attributeName="x" from="1" to="2"></animate>
4536                <animate href="#x1" attributeName="x" from="1"></animate>
4537                <animate href="#x1" attributeName="x" to="2"></animate>
4538                <animate href="#x1" attributeName="y" values="1;2;3"></animate>
4539                <animate href="#x1" attributeName="y" from="1" to="2"></animate>
4540                <animate href="#x1" attributeName="y" from="1"></animate>
4541                <animate href="#x2" attributeName="y" to="2"></animate>
4542            </svg>
4543        "##;
4544        let filtered = r##"
4545            <svg>
4546                <a id="x1" x="0" rel="noopener noreferrer">
4547                </a>
4548                
4549                
4550                
4551                
4552                <animate href="#x1" attributeName="y" values="1;2;3"></animate>
4553                <animate href="#x1" attributeName="y" from="1" to="2"></animate>
4554                <animate href="#x1" attributeName="y" from="1"></animate>
4555                
4556            </svg>
4557        "##;
4558        let result =  Builder::default()
4559            .add_tags(&["svg","a","animate"])
4560            .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4561            .add_tag_attributes("a", ["id", "x", "y"])
4562            .set_tag_attribute_value("a", "x", "0")
4563            .clean(fragment);
4564        assert_eq!(
4565            result.to_string(),
4566            filtered,
4567        );
4568    }
4569
4570    #[test]
4571    fn ns_svg_animate_attr_filter_href() {
4572        let fragment = r##"
4573            <svg>
4574                <a id="x1">
4575                </a>
4576                <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4577                <animate href="#x1" attributeName="x" from="1" to="2"></animate>
4578                <animate href="#x1" attributeName="x" from="1"></animate>
4579                <animate href="#x1" attributeName="x" to="2"></animate>
4580                <animate href="#x1" attributeName="y" values="1;2;3"></animate>
4581                <animate href="#x1" attributeName="y" from="1" to="2"></animate>
4582                <animate href="x1" attributeName="y" from="1"></animate>
4583                <animate href="#x2" attributeName="y" to="2"></animate>
4584            </svg>
4585        "##;
4586        let filtered = r##"
4587            <svg>
4588                <a id="x1" rel="noopener noreferrer">
4589                </a>
4590                <animate href="#x1" attributeName="x" values="0;0;0"></animate>
4591                <animate href="#x1" attributeName="x" from="0" to="0"></animate>
4592                <animate href="#x1" attributeName="x" from="0"></animate>
4593                <animate href="#x1" attributeName="x" to="0"></animate>
4594                <animate href="#x1" attributeName="y"></animate>
4595                <animate href="#x1" attributeName="y" to="2"></animate>
4596                
4597                
4598            </svg>
4599        "##;
4600        let result =  Builder::default()
4601            .add_tags(&["svg","a","animate"])
4602            .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4603            .add_tag_attributes("a", ["id", "x", "y"])
4604            .attribute_filter(|_tag, key, value| Some(if key == "x" {
4605                "0".into()
4606            } else if key == "y" && value == "1" {
4607                return None;
4608            } else {
4609                value.into()
4610            }))
4611            .clean(fragment);
4612        assert_eq!(
4613            result.to_string(),
4614            filtered,
4615        );
4616    }
4617
4618    #[test]
4619    fn ns_svg_animate_id_conflict() {
4620        let fragment = r##"
4621            <svg>
4622                <a id="x1">
4623                </a>
4624                <a id="x2">
4625                </a>
4626                <a id="x2">
4627                </a>
4628                <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4629                <animate href="#x2" attributeName="x" values="1;2;3"></animate>
4630                <animate href="#x3" attributeName="x" values="1;2;3"></animate>
4631            </svg>
4632        "##;
4633        let filtered = r##"
4634            <svg>
4635                <a id="x1" rel="noopener noreferrer">
4636                </a>
4637                <a id="x2" rel="noopener noreferrer">
4638                </a>
4639                <a id="x2" rel="noopener noreferrer">
4640                </a>
4641                <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4642                
4643                
4644            </svg>
4645        "##;
4646        let result =  Builder::default()
4647            .add_tags(&["svg","a","animate"])
4648            .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4649            .add_tag_attributes("a", ["id", "x", "y"])
4650            .clean(fragment);
4651        assert_eq!(
4652            result.to_string(),
4653            filtered,
4654        );
4655    }
4656
4657    #[test]
4658    fn xml_processing_instruction() {
4659        // https://blog.slonser.info/posts/dompurify-node-type-confusion/
4660        let fragment = r##"<svg><?xml-stylesheet src='slonser' ?></svg>"##;
4661        let result = String::from(Builder::new().clean(fragment));
4662        assert_eq!(result.to_string(), "");
4663
4664        let fragment = r##"<svg><?xml-stylesheet src='slonser' ?></svg>"##;
4665        let result = String::from(Builder::new().add_tags(&["svg"]).clean(fragment));
4666        assert_eq!(result.to_string(), "<svg></svg>");
4667
4668        let fragment = r##"<svg><?xml-stylesheet ><img src=x onerror="alert('Ammonia bypassed!!!')"> ?></svg>"##;
4669        let result = String::from(Builder::new().add_tags(&["svg"]).clean(fragment));
4670        assert_eq!(result.to_string(), "<svg></svg><img src=\"x\"> ?&gt;");
4671    }
4672
4673    #[test]
4674    fn generic_attribute_prefixes() {
4675        let prefix_data = ["data-"];
4676        let prefix_code = ["code-"];
4677        let mut b = Builder::new();
4678        let mut hs: HashSet<&'_ str> = HashSet::new();
4679        hs.insert("data-");
4680        assert!(b.generic_attribute_prefixes.is_none());
4681        b.generic_attribute_prefixes(hs);
4682        assert!(b.generic_attribute_prefixes.is_some());
4683        assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4684        b.add_generic_attribute_prefixes(&prefix_data);
4685        assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4686        b.add_generic_attribute_prefixes(&prefix_code);
4687        assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 2);
4688        b.rm_generic_attribute_prefixes(&prefix_code);
4689        assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4690        b.rm_generic_attribute_prefixes(&prefix_code);
4691        assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4692        b.rm_generic_attribute_prefixes(&prefix_data);
4693        assert!(b.generic_attribute_prefixes.is_none());
4694    }
4695
4696    #[test]
4697    fn selectedcontent() {
4698        // https://github.com/servo/html5ever/issues/712
4699        let fragment1 = r#"<select><selectedcontent></selectedcontent><option>X"#;
4700        let fragment2 = r#"<select><selectedcontent></selectedcontent><option>X</option></select>"#;
4701        let expected = r#"<select><selectedcontent></selectedcontent><option>X</option></select>"#;
4702        assert_eq!(String::from(Builder::new().add_tags(&["select", "selectedcontent", "option"]).clean(fragment1)), expected);
4703        assert_eq!(String::from(Builder::new().add_tags(&["select", "selectedcontent", "option"]).clean(fragment2)), expected);
4704    }
4705    
4706    #[test]
4707    fn new_select_parse() {
4708        // https://github.com/whatwg/html/issues/10310#issuecomment-2304377029
4709        let fragment = r#"
4710<select><style></select><img src onerror=xss()></style></select>
4711        "#;
4712        let expected = r#"
4713<select></select>
4714        "#;
4715        assert_eq!(String::from(Builder::new().add_tags(&["select", "new-select"]).clean_content_tags(hashset!["style"]).clean(fragment)), expected);
4716    }
4717
4718    #[test]
4719    fn selectedcontent_not_in_select() {
4720        // https://github.com/whatwg/html/issues/10310#issuecomment-2304377029
4721        let fragment = r#"
4722<selectedcontent>first</selectedcontent>
4723<div><selectedcontent>second</selectedcontent></div>
4724<select><selectedcontent>third</selectedcontent></select>
4725        "#;
4726        let expected = r#"
4727<selectedcontent>first</selectedcontent>
4728<div><selectedcontent>second</selectedcontent></div>
4729<select><selectedcontent></selectedcontent></select>
4730        "#;
4731        assert_eq!(String::from(Builder::new().add_tags(&["select", "selectedcontent"]).clean(fragment)), expected);
4732    }
4733
4734    #[test]
4735    fn generic_attribute_prefixes_clean() {
4736        let fragment = r#"<a data-1 data-2 code-1 code-2><a>Hello!</a></a>"#;
4737        let result_cleaned = String::from(
4738            Builder::new()
4739                .add_tag_attributes("a", &["data-1"])
4740                .clean(fragment),
4741        );
4742        assert_eq!(
4743            result_cleaned,
4744            r#"<a data-1="" rel="noopener noreferrer"></a><a rel="noopener noreferrer">Hello!</a>"#
4745        );
4746        let result_allowed = String::from(
4747            Builder::new()
4748                .add_tag_attributes("a", &["data-1"])
4749                .add_generic_attribute_prefixes(&["data-"])
4750                .clean(fragment),
4751        );
4752        assert_eq!(
4753            result_allowed,
4754            r#"<a data-1="" data-2="" rel="noopener noreferrer"></a><a rel="noopener noreferrer">Hello!</a>"#
4755        );
4756        let result_allowed = String::from(
4757            Builder::new()
4758                .add_tag_attributes("a", &["data-1", "code-1"])
4759                .add_generic_attribute_prefixes(&["data-", "code-"])
4760                .clean(fragment),
4761        );
4762        assert_eq!(
4763            result_allowed,
4764            r#"<a data-1="" data-2="" code-1="" code-2="" rel="noopener noreferrer"></a><a rel="noopener noreferrer">Hello!</a>"#
4765        );
4766    }
4767    #[test]
4768    fn lesser_than_isnt_html() {
4769        let fragment = "1 < 2";
4770        assert!(!is_html(fragment));
4771    }
4772    #[test]
4773    fn dense_lesser_than_isnt_html() {
4774        let fragment = "1<2";
4775        assert!(!is_html(fragment));
4776    }
4777    #[test]
4778    fn what_about_number_elements() {
4779        let fragment = "foo<2>bar";
4780        assert!(!is_html(fragment));
4781    }
4782    #[test]
4783    fn turbofish_is_html_sadly() {
4784        let fragment = "Vec::<u8>::new()";
4785        assert!(is_html(fragment));
4786    }
4787    #[test]
4788    fn stop_grinning() {
4789        let fragment = "did you really believe me? <g>";
4790        assert!(is_html(fragment));
4791    }
4792    #[test]
4793    fn dont_be_bold() {
4794        let fragment = "<b>";
4795        assert!(is_html(fragment));
4796    }
4797
4798    #[test]
4799    fn rewrite_with_root() {
4800        let tests = [
4801            (
4802                "https://github.com/rust-ammonia/ammonia/blob/master/",
4803                "README.md",
4804                "",
4805                "https://github.com/rust-ammonia/ammonia/blob/master/README.md",
4806            ),
4807            (
4808                "https://github.com/rust-ammonia/ammonia/blob/master/",
4809                "README.md",
4810                "/",
4811                "https://github.com/rust-ammonia/ammonia/blob/master/",
4812            ),
4813            (
4814                "https://github.com/rust-ammonia/ammonia/blob/master/",
4815                "README.md",
4816                "/CONTRIBUTING.md",
4817                "https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md",
4818            ),
4819            (
4820                "https://github.com/rust-ammonia/ammonia/blob/master",
4821                "README.md",
4822                "",
4823                "https://github.com/rust-ammonia/ammonia/blob/README.md",
4824            ),
4825            (
4826                "https://github.com/rust-ammonia/ammonia/blob/master",
4827                "README.md",
4828                "/",
4829                "https://github.com/rust-ammonia/ammonia/blob/",
4830            ),
4831            (
4832                "https://github.com/rust-ammonia/ammonia/blob/master",
4833                "README.md",
4834                "/CONTRIBUTING.md",
4835                "https://github.com/rust-ammonia/ammonia/blob/CONTRIBUTING.md",
4836            ),
4837            (
4838                "https://github.com/rust-ammonia/ammonia/blob/master/",
4839                "",
4840                "",
4841                "https://github.com/rust-ammonia/ammonia/blob/master/",
4842            ),
4843            (
4844                "https://github.com/rust-ammonia/ammonia/blob/master/",
4845                "",
4846                "/",
4847                "https://github.com/rust-ammonia/ammonia/blob/master/",
4848            ),
4849            (
4850                "https://github.com/rust-ammonia/ammonia/blob/master/",
4851                "",
4852                "/CONTRIBUTING.md",
4853                "https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md",
4854            ),
4855            (
4856                "https://github.com/",
4857                "rust-ammonia/ammonia/blob/master/README.md",
4858                "",
4859                "https://github.com/rust-ammonia/ammonia/blob/master/README.md",
4860            ),
4861            (
4862                "https://github.com/",
4863                "rust-ammonia/ammonia/blob/master/README.md",
4864                "/",
4865                "https://github.com/",
4866            ),
4867            (
4868                "https://github.com/",
4869                "rust-ammonia/ammonia/blob/master/README.md",
4870                "CONTRIBUTING.md",
4871                "https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md",
4872            ),
4873            (
4874                "https://github.com/",
4875                "rust-ammonia/ammonia/blob/master/README.md",
4876                "/CONTRIBUTING.md",
4877                "https://github.com/CONTRIBUTING.md",
4878            ),
4879        ];
4880        for (root, path, url, result) in tests {
4881            let h = format!(r#"<a href="{url}">test</a>"#);
4882            let r = format!(r#"<a href="{result}" rel="noopener noreferrer">test</a>"#);
4883            let a = Builder::new()
4884                .url_relative(UrlRelative::RewriteWithRoot {
4885                    root: Url::parse(root).unwrap(),
4886                    path: path.to_string(),
4887                })
4888                .clean(&h)
4889                .to_string();
4890            if r != a {
4891                println!(
4892                    "failed to check ({root}, {path}, {url}, {result})\n{r} != {a}",
4893                    r = r
4894                );
4895                assert_eq!(r, a);
4896            }
4897        }
4898    }
4899}