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    /// Configures a `rel` attribute that will be added on links.
1444    ///
1445    /// If `rel` is in the generic or tag attributes, this must be set to `None`.
1446    /// Common `rel` values to include:
1447    ///
1448    /// * `noopener`: This prevents [a particular type of XSS attack],
1449    ///   and should usually be turned on for untrusted HTML.
1450    /// * `noreferrer`: This prevents the browser from [sending the source URL]
1451    ///   to the website that is linked to.
1452    /// * `nofollow`: This prevents search engines from [using this link for
1453    ///   ranking], which disincentivizes spammers.
1454    ///
1455    /// To turn on rel-insertion, call this function with a space-separated list.
1456    /// Ammonia does not parse rel-attributes;
1457    /// it just puts the given string into the attribute directly.
1458    ///
1459    /// [a particular type of XSS attack]: https://mathiasbynens.github.io/rel-noopener/
1460    /// [sending the source URL]: https://en.wikipedia.org/wiki/HTTP_referer
1461    /// [using this link for ranking]: https://en.wikipedia.org/wiki/Nofollow
1462    ///
1463    /// # Examples
1464    ///
1465    ///     use ammonia::Builder;
1466    ///
1467    ///     let a = Builder::new().link_rel(None)
1468    ///         .clean("<a href=https://rust-lang.org/>Rust</a>")
1469    ///         .to_string();
1470    ///     assert_eq!(
1471    ///       a,
1472    ///       "<a href=\"https://rust-lang.org/\">Rust</a>");
1473    ///
1474    /// # Defaults
1475    ///
1476    /// ```notest
1477    /// Some("noopener noreferrer")
1478    /// ```
1479    pub fn link_rel(&mut self, value: Option<&'a str>) -> &mut Self {
1480        self.link_rel = value;
1481        self
1482    }
1483
1484    /// Returns the settings for links' `rel` attribute, if one is set.
1485    ///
1486    /// # Examples
1487    ///
1488    ///     use ammonia::{Builder, UrlRelative};
1489    ///     let mut a = Builder::default();
1490    ///     a.link_rel(Some("a b"));
1491    ///     assert_eq!(a.get_link_rel(), Some("a b"));
1492    pub fn get_link_rel(&self) -> Option<&str> {
1493        self.link_rel
1494    }
1495
1496    /// Sets the CSS classes that are allowed on specific tags.
1497    ///
1498    /// The values is structured as a map from tag names to a set of class names.
1499    ///
1500    /// If the `class` attribute is itself whitelisted for a tag, then adding entries to
1501    /// this map will cause a panic.
1502    ///
1503    /// # Examples
1504    ///
1505    ///     use ammonia::Builder;
1506    ///     use maplit::{hashmap, hashset};
1507    ///
1508    ///     # fn main() {
1509    ///     let allowed_classes = hashmap![
1510    ///         "code" => hashset!["rs", "ex", "c", "cxx", "js"]
1511    ///     ];
1512    ///     let a = Builder::new()
1513    ///         .allowed_classes(allowed_classes)
1514    ///         .clean("<code class=rs>fn main() {}</code>")
1515    ///         .to_string();
1516    ///     assert_eq!(
1517    ///       a,
1518    ///       "<code class=\"rs\">fn main() {}</code>");
1519    ///     # }
1520    ///
1521    /// # Defaults
1522    ///
1523    /// The set of allowed classes is empty by default.
1524    pub fn allowed_classes(&mut self, value: HashMap<&'a str, HashSet<&'a str>>) -> &mut Self {
1525        self.allowed_classes = value;
1526        self
1527    }
1528
1529    /// Add additonal whitelisted classes without overwriting old ones.
1530    ///
1531    /// # Examples
1532    ///
1533    ///     let a = ammonia::Builder::default()
1534    ///         .add_allowed_classes("a", &["onebox"])
1535    ///         .clean("<a href=/ class=onebox>mess</span>").to_string();
1536    ///     assert_eq!("<a href=\"/\" class=\"onebox\" rel=\"noopener noreferrer\">mess</a>", a);
1537    pub fn add_allowed_classes<
1538        T: 'a + ?Sized + Borrow<str>,
1539        U: 'a + ?Sized + Borrow<str>,
1540        I: IntoIter<Item = &'a T>,
1541    >(
1542        &mut self,
1543        tag: &'a U,
1544        it: I,
1545    ) -> &mut Self {
1546        self.allowed_classes
1547            .entry(tag.borrow())
1548            .or_default()
1549            .extend(it.into_iter().map(Borrow::borrow));
1550        self
1551    }
1552
1553    /// Remove already-whitelisted attributes.
1554    ///
1555    /// Does nothing if the attribute is already gone.
1556    ///
1557    /// # Examples
1558    ///
1559    ///     let a = ammonia::Builder::default()
1560    ///         .add_allowed_classes("span", &["active"])
1561    ///         .rm_allowed_classes("span", &["active"])
1562    ///         .clean("<span class=active>").to_string();
1563    ///     assert_eq!("<span class=\"\"></span>", a);
1564    pub fn rm_allowed_classes<
1565        'b,
1566        'c,
1567        T: 'b + ?Sized + Borrow<str>,
1568        U: 'c + ?Sized + Borrow<str>,
1569        I: IntoIter<Item = &'b T>,
1570    >(
1571        &mut self,
1572        tag: &'c U,
1573        it: I,
1574    ) -> &mut Self {
1575        if let Some(tag) = self.allowed_classes.get_mut(tag.borrow()) {
1576            for i in it {
1577                tag.remove(i.borrow());
1578            }
1579        }
1580        self
1581    }
1582
1583    /// Returns a copy of the set of whitelisted class attributes.
1584    ///
1585    /// # Examples
1586    ///
1587    ///     use maplit::{hashmap, hashset};
1588    ///
1589    ///     let allowed_classes = hashmap![
1590    ///         "my-tag" => hashset!["my-class-1", "my-class-2"]
1591    ///     ];
1592    ///
1593    ///     let mut b = ammonia::Builder::default();
1594    ///     b.allowed_classes(Clone::clone(&allowed_classes));
1595    ///     assert_eq!(allowed_classes, b.clone_allowed_classes());
1596    pub fn clone_allowed_classes(&self) -> HashMap<&'a str, HashSet<&'a str>> {
1597        self.allowed_classes.clone()
1598    }
1599
1600    /// Configures the handling of HTML comments.
1601    ///
1602    /// If this option is false, comments will be preserved.
1603    ///
1604    /// # Examples
1605    ///
1606    ///     use ammonia::Builder;
1607    ///
1608    ///     let a = Builder::new().strip_comments(false)
1609    ///         .clean("<!-- yes -->")
1610    ///         .to_string();
1611    ///     assert_eq!(
1612    ///       a,
1613    ///       "<!-- yes -->");
1614    ///
1615    /// # Defaults
1616    ///
1617    /// `true`
1618    pub fn strip_comments(&mut self, value: bool) -> &mut Self {
1619        self.strip_comments = value;
1620        self
1621    }
1622
1623    /// Returns `true` if comment stripping is turned on.
1624    ///
1625    /// # Examples
1626    ///
1627    ///     let mut a = ammonia::Builder::new();
1628    ///     a.strip_comments(true);
1629    ///     assert!(a.will_strip_comments());
1630    ///     a.strip_comments(false);
1631    ///     assert!(!a.will_strip_comments());
1632    pub fn will_strip_comments(&self) -> bool {
1633        self.strip_comments
1634    }
1635
1636    /// Prefixes all "id" attribute values with a given string.  Note that the tag and
1637    /// attribute themselves must still be whitelisted.
1638    ///
1639    /// # Examples
1640    ///
1641    ///     use ammonia::Builder;
1642    ///     use maplit::hashset;
1643    ///
1644    ///     # fn main() {
1645    ///     let attributes = hashset!["id"];
1646    ///     let a = Builder::new()
1647    ///         .generic_attributes(attributes)
1648    ///         .id_prefix(Some("safe-"))
1649    ///         .clean("<b id=42>")
1650    ///         .to_string();
1651    ///     assert_eq!(a, "<b id=\"safe-42\"></b>");
1652    ///     # }
1653
1654    ///
1655    /// # Defaults
1656    ///
1657    /// `None`
1658    pub fn id_prefix(&mut self, value: Option<&'a str>) -> &mut Self {
1659        self.id_prefix = value;
1660        self
1661    }
1662
1663    /// Only allows the specified properties in `style` attributes.
1664    ///
1665    /// Irrelevant if `style` is not an allowed attribute.
1666    ///
1667    /// Note that if style filtering is enabled style properties will be normalised e.g.
1668    /// invalid declarations and @rules will be removed, with only syntactically valid
1669    /// declarations kept.
1670    ///
1671    /// # Examples
1672    ///
1673    ///     use ammonia::Builder;
1674    ///     use maplit::hashset;
1675    ///
1676    ///     # fn main() {
1677    ///     let attributes = hashset!["style"];
1678    ///     let properties = hashset!["color"];
1679    ///     let a = Builder::new()
1680    ///         .generic_attributes(attributes)
1681    ///         .filter_style_properties(properties)
1682    ///         .clean("<p style=\"font-weight: heavy; color: red\">my html</p>")
1683    ///         .to_string();
1684    ///     assert_eq!(a, "<p style=\"color:red\">my html</p>");
1685    ///     # }
1686    pub fn filter_style_properties(&mut self, value: HashSet<&'a str>) -> &mut Self {
1687        self.style_properties = Some(value);
1688        self
1689    }
1690
1691    /// Constructs a [`Builder`] instance configured with the [default options].
1692    ///
1693    /// # Examples
1694    ///
1695    ///     use ammonia::{Builder, Url, UrlRelative};
1696    ///     # use std::error::Error;
1697    ///
1698    ///     # fn do_main() -> Result<(), Box<dyn Error>> {
1699    ///     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>.";
1700    ///     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>.";
1701    ///
1702    ///     let result = Builder::new() // <--
1703    ///         .url_relative(UrlRelative::RewriteWithBase(Url::parse("https://docs.rs/ammonia/1.0/ammonia/")?))
1704    ///         .clean(input)
1705    ///         .to_string();
1706    ///     assert_eq!(result, output);
1707    ///     # Ok(())
1708    ///     # }
1709    ///     # fn main() { do_main().unwrap() }
1710    ///
1711    /// [default options]: fn.clean.html
1712    /// [`Builder`]: struct.Builder.html
1713    pub fn new() -> Self {
1714        Self::default()
1715    }
1716
1717    /// Constructs a [`Builder`] instance configured with no allowed tags.
1718    ///
1719    /// # Examples
1720    ///
1721    ///     use ammonia::{Builder, Url, UrlRelative};
1722    ///     # use std::error::Error;
1723    ///
1724    ///     # fn do_main() -> Result<(), Box<dyn Error>> {
1725    ///     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>.";
1726    ///     let output = "This is an Ammonia example using the empty() function.";
1727    ///
1728    ///     let result = Builder::empty() // <--
1729    ///         .url_relative(UrlRelative::RewriteWithBase(Url::parse("https://docs.rs/ammonia/1.0/ammonia/")?))
1730    ///         .clean(input)
1731    ///         .to_string();
1732    ///     assert_eq!(result, output);
1733    ///     # Ok(())
1734    ///     # }
1735    ///     # fn main() { do_main().unwrap() }
1736    ///
1737    /// [default options]: fn.clean.html
1738    /// [`Builder`]: struct.Builder.html
1739    pub fn empty() -> Self {
1740        Self {
1741            tags: hashset![],
1742            ..Self::default()
1743        }
1744    }
1745
1746    /// Sanitizes an HTML fragment in a string according to the configured options.
1747    ///
1748    /// # Examples
1749    ///
1750    ///     use ammonia::{Builder, Url, UrlRelative};
1751    ///     # use std::error::Error;
1752    ///
1753    ///     # fn do_main() -> Result<(), Box<dyn Error>> {
1754    ///     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>.";
1755    ///     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>.";
1756    ///
1757    ///     let result = Builder::new()
1758    ///         .url_relative(UrlRelative::RewriteWithBase(Url::parse("https://docs.rs/ammonia/1.0/ammonia/")?))
1759    ///         .clean(input)
1760    ///         .to_string(); // <--
1761    ///     assert_eq!(result, output);
1762    ///     # Ok(())
1763    ///     # }
1764    ///     # fn main() { do_main().unwrap() }
1765    pub fn clean(&self, src: &str) -> Document {
1766        let parser = Self::make_parser();
1767        let dom = parser.one(src);
1768        self.clean_dom(dom)
1769    }
1770
1771    /// Sanitizes an HTML fragment from a reader according to the configured options.
1772    ///
1773    /// The input should be in UTF-8 encoding, otherwise the decoding is lossy, just
1774    /// like when using [`String::from_utf8_lossy`].
1775    ///
1776    /// To avoid consuming the reader, a mutable reference can be passed to this method.
1777    ///
1778    /// # Examples
1779    ///
1780    ///     use ammonia::Builder;
1781    ///     # use std::error::Error;
1782    ///
1783    ///     # fn do_main() -> Result<(), Box<dyn Error>> {
1784    ///     let a = Builder::new()
1785    ///         .clean_from_reader(&b"<!-- no -->"[..])? // notice the `b`
1786    ///         .to_string();
1787    ///     assert_eq!(a, "");
1788    ///     # Ok(()) }
1789    ///     # fn main() { do_main().unwrap() }
1790    ///
1791    /// [`String::from_utf8_lossy`]: https://doc.rust-lang.org/std/string/struct.String.html#method.from_utf8_lossy
1792    pub fn clean_from_reader<R>(&self, mut src: R) -> io::Result<Document>
1793    where
1794        R: io::Read,
1795    {
1796        let parser = Self::make_parser().from_utf8();
1797        let dom = parser.read_from(&mut src)?;
1798        Ok(self.clean_dom(dom))
1799    }
1800
1801    /// Clean a post-parsing DOM.
1802    ///
1803    /// This is not a public API because RcDom isn't really stable.
1804    /// We want to be able to take breaking changes to html5ever itself
1805    /// without having to break Ammonia's API.
1806    fn clean_dom(&self, dom: RcDom) -> Document {
1807        let mut id_to_tag_name_map = HashMap::new();
1808        let mut id_to_tag_name_stack = vec![{
1809            let children = dom.document.children.borrow();
1810            children[0].clone()
1811        }];
1812        while let Some(tag) = id_to_tag_name_stack.pop() {
1813            if let NodeData::Element { name, attrs, .. } = &tag.data {
1814                let attrs = attrs.borrow();
1815                for attr in &attrs[..] {
1816                    if &*attr.name.local == "id" {
1817                        id_to_tag_name_map.entry(attr.value.to_string()).and_modify(|ent| *ent = None).or_insert_with(|| Some(name.local.to_string()));
1818                    }
1819                }
1820            }
1821            id_to_tag_name_stack.extend(tag.children.borrow().iter().map(|x| x.clone()));
1822        }
1823
1824        let mut stack = Vec::new();
1825        let mut removed = Vec::new();
1826        let link_rel = self
1827            .link_rel
1828            .map(|link_rel| format_tendril!("{}", link_rel));
1829        if link_rel.is_some() {
1830            assert!(self.generic_attributes.get("rel").is_none());
1831            assert!(self
1832                .tag_attributes
1833                .get("a")
1834                .and_then(|a| a.get("rel"))
1835                .is_none());
1836        }
1837        assert!(self.allowed_classes.is_empty() || !self.generic_attributes.contains("class"));
1838        for tag_name in self.allowed_classes.keys() {
1839            assert!(self
1840                .tag_attributes
1841                .get(tag_name)
1842                .and_then(|a| a.get("class"))
1843                .is_none());
1844        }
1845        for tag_name in &self.clean_content_tags {
1846            assert!(!self.tags.contains(tag_name), "`{tag_name}` appears in `clean_content_tags` and in `tags` at the same time");
1847            assert!(!self.tag_attributes.contains_key(tag_name), "`{tag_name}` appears in `clean_content_tags` and in `tag_attributes` at the same time");
1848        }
1849        let body = {
1850            let children = dom.document.children.borrow();
1851            children[0].clone()
1852        };
1853        stack.extend(
1854            mem::take(&mut *body.children.borrow_mut())
1855                .into_iter()
1856                .rev(),
1857        );
1858        // This design approach is used to prevent pathological content from producing
1859        // a stack overflow. The `stack` contains to-be-cleaned nodes, while `remove`,
1860        // of course, contains nodes that need to be dropped (we can't just drop them,
1861        // because they could have a very deep child tree).
1862        while let Some(mut node) = stack.pop() {
1863            if matches!(node.data, NodeData::Element { ref name, .. } if &*name.local == "selectedcontent" && name.ns == ns!(html)) &&
1864                self.is_within(node.clone(), ns!(html), "select")
1865            {
1866                for sub in node.children.borrow_mut().iter_mut() {
1867                    sub.parent.replace(None);
1868                }
1869                *node.children.borrow_mut() = Vec::new();
1870            }
1871            let parent = node.parent
1872                .replace(None).expect("a node in the DOM will have a parent, except the root, which is not processed")
1873                .upgrade().expect("a node's parent will be pointed to by its parent (or the root pointer), and will not be dropped");
1874            let pass = self.clean_child(&mut node, &parent, &id_to_tag_name_map);
1875            self.adjust_node_attributes(&mut node, &link_rel, self.id_prefix, &parent, &id_to_tag_name_map);
1876            if self.clean_node_content(&node) || !self.check_expected_namespace(&parent, &node) {
1877                removed.push(node);
1878                continue;
1879            }
1880            if pass {
1881                dom.append(&parent.clone(), NodeOrText::AppendNode(node.clone()));
1882            } else {
1883                for sub in node.children.borrow_mut().iter_mut() {
1884                    sub.parent.replace(Some(Rc::downgrade(&parent)));
1885                }
1886            }
1887            stack.extend(
1888                mem::take(&mut *node.children.borrow_mut())
1889                    .into_iter()
1890                    .rev(),
1891            );
1892            if !pass {
1893                removed.push(node);
1894            }
1895        }
1896        // Now, imperatively clean up all of the child nodes.
1897        // Otherwise, we could wind up with a DoS, either caused by a memory leak,
1898        // or caused by a stack overflow.
1899        while let Some(node) = removed.pop() {
1900            removed.extend_from_slice(&mem::take(&mut *node.children.borrow_mut())[..]);
1901        }
1902        Document(dom)
1903    }
1904
1905    fn is_within(&self, mut child: Handle, ns: Namespace, tag: &str) -> bool {
1906        while let Some(parent) = child.parent.take() {
1907            child.parent.set(Some(parent.clone()));
1908            match child.data {
1909                NodeData::Element { ref name, .. } if name.ns == ns && &*name.local == tag => return true,
1910                _ => {
1911                    if let Some(parent) = parent.upgrade() {
1912                        child = parent;
1913                    } else {
1914                        return false;
1915                    }
1916                }
1917            }
1918        }
1919        false
1920    }
1921
1922    /// Returns `true` if a node and all its content should be removed.
1923    fn clean_node_content(&self, node: &Handle) -> bool {
1924        match node.data {
1925            NodeData::Text { .. }
1926            | NodeData::Comment { .. }
1927            | NodeData::Doctype { .. }
1928            | NodeData::Document
1929            | NodeData::ProcessingInstruction { .. } => false,
1930            NodeData::Element { ref name, .. } => self.clean_content_tags.contains(&*name.local),
1931        }
1932    }
1933
1934    /// Remove unwanted attributes, and check if the node should be kept or not.
1935    ///
1936    /// The root node doesn't need cleaning because we create the root node ourselves,
1937    /// and it doesn't get serialized, and ... it just exists to give the parser
1938    /// a context (in this case, a div-like block context).
1939    fn clean_child(&self, child: &mut Handle, parent: &Handle, id_to_tag_name_map: &HashMap<String, Option<String>>) -> bool {
1940        match child.data {
1941            NodeData::Text { .. } => true,
1942            NodeData::Comment { .. } => !self.strip_comments,
1943            NodeData::Doctype { .. }
1944            | NodeData::Document
1945            | NodeData::ProcessingInstruction { .. } => false,
1946            NodeData::Element {
1947                ref name,
1948                ref attrs,
1949                ..
1950            } => {
1951                if self.tags.contains(&*name.local) {
1952                    let whitelisted = |tag_name: &str, attr_name: &str, attr_val: &str|
1953                        self.generic_attributes.contains(attr_name)
1954                            || self.generic_attribute_prefixes.as_ref().map(|prefixes| {
1955                                prefixes.iter().any(|&p| attr_name.starts_with(p))
1956                            }) == Some(true)
1957                            || self
1958                                .tag_attributes
1959                                .get(tag_name)
1960                                .map(|ta| ta.contains(attr_name))
1961                                == Some(true)
1962                            || self
1963                                .tag_attribute_values
1964                                .get(tag_name)
1965                                .and_then(|tav| tav.get(attr_name))
1966                                .map(|vs| {
1967                                    vs.iter().any(|v| v.to_lowercase() == attr_val.to_lowercase())
1968                                })
1969                                == Some(true);
1970                    let attr_filter = |tag_name: &str, attr_name: &str, attr_val: &str| {
1971                        if !whitelisted(tag_name, attr_name, attr_val) {
1972                            // If the class attribute is not whitelisted,
1973                            // but there is a whitelisted set of allowed_classes,
1974                            // do not strip out the class attribute.
1975                            // Banned classes will be filtered later.
1976                            attr_name == "class" && self.allowed_classes.contains_key(tag_name)
1977                        } else if is_url_attr(tag_name, attr_name) {
1978                            let url = Url::parse(attr_val);
1979                            if let Ok(url) = url {
1980                                self.url_schemes.contains(url.scheme())
1981                            } else if url == Err(url::ParseError::RelativeUrlWithoutBase) {
1982                                !matches!(self.url_relative, UrlRelative::Deny)
1983                            } else {
1984                                false
1985                            }
1986                        } else {
1987                            true
1988                        }
1989                    };
1990                    attrs.borrow_mut().retain(|attr| attr_filter(&*name.local, &*attr.name.local, &*attr.value));
1991                    if
1992                        // https://svgwg.org/specs/animations/#AnimateElement
1993                        name.ns == ns!(svg) &&
1994                        (&*name.local == "animate" || &*name.local == "set")
1995                    {
1996                        let animate_name = attrs.borrow()
1997                            .iter()
1998                            .find(|attr| &*attr.name.local == "attributeName")
1999                            .map(|attr| attr.value.clone());
2000                        let animate_values = attrs.borrow()
2001                            .iter()
2002                            .find(|attr| &*attr.name.local == "values")
2003                            .map(|attr| attr.value.clone());
2004                        let animate_from = attrs.borrow()
2005                            .iter()
2006                            .find(|attr| &*attr.name.local == "from")
2007                            .map(|attr| attr.value.clone());
2008                        let animate_to = attrs.borrow()
2009                            .iter()
2010                            .find(|attr| &*attr.name.local == "to")
2011                            .map(|attr| attr.value.clone());
2012                        let animate_href = attrs.borrow()
2013                            .iter()
2014                            .find(|attr| &*attr.name.local == "href")
2015                            .map(|attr| attr.value.clone());
2016                        let animate_tag_name = animate_href
2017                            .map(|href| {
2018                                if href.starts_with("#") {
2019                                    id_to_tag_name_map.get(&href[1..]).and_then(|inner| Some(&inner.as_ref()?[..]))
2020                                } else {
2021                                    None
2022                                }
2023                            })
2024                            .unwrap_or_else(|| {
2025                                if let &NodeData::Element { name: ref parent_name, .. } = &parent.data {
2026                                    Some(&*parent_name.local)
2027                                } else {
2028                                    None
2029                                }
2030                            });
2031                        match (animate_name, animate_values, animate_from, animate_to, animate_tag_name) {
2032                            (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,
2033                            (Some(animate_name), Some(animate_values), None, None, Some(animate_tag_name)) => {
2034                                // https://svgwg.org/specs/animations/#ValuesAttribute
2035                                animate_values.split(';').all(|attr_val| attr_filter(animate_tag_name, &*animate_name, attr_val))
2036                            }
2037                            (Some(animate_name), None, Some(animate_from), Some(animate_to), Some(animate_tag_name)) => {
2038                                // https://svgwg.org/specs/animations/#FromAttribute
2039                                attr_filter(animate_tag_name, &*animate_name, &*animate_from) &&
2040                                    attr_filter(animate_tag_name, &*animate_name, &*animate_to)
2041                            }
2042                            (Some(animate_name), None, Some(animate_from), None, Some(animate_tag_name)) => {
2043                                // https://svgwg.org/specs/animations/#FromAttribute
2044                                attr_filter(animate_tag_name, &*animate_name, &*animate_from)
2045                            }
2046                            (Some(animate_name), None, None, Some(animate_to), Some(animate_tag_name)) => {
2047                                // https://svgwg.org/specs/animations/#FromAttribute
2048                                attr_filter(animate_tag_name, &*animate_name, &*animate_to)
2049                            }
2050                            _ => false,
2051                        }
2052                    } else {
2053                        true
2054                    }
2055                } else {
2056                    false
2057                }
2058            }
2059        }
2060    }
2061
2062    // Check for unexpected namespace changes.
2063    //
2064    // The issue happens if developers added to the list of allowed tags any
2065    // tag which is parsed in RCDATA state, PLAINTEXT state or RAWTEXT state,
2066    // that is:
2067    //
2068    // * title
2069    // * textarea
2070    // * xmp
2071    // * iframe
2072    // * noembed
2073    // * noframes
2074    // * plaintext
2075    // * noscript
2076    // * style
2077    // * script
2078    //
2079    // An example in the wild is Plume, that allows iframe [1].  So in next
2080    // examples I'll assume the following policy:
2081    //
2082    //     Builder::new()
2083    //        .add_tags(&["iframe"])
2084    //
2085    // In HTML namespace `<iframe>` is parsed specially; that is, its content is
2086    // treated as text. For instance, the following html:
2087    //
2088    //     <iframe><a>test
2089    //
2090    // Is parsed into the following DOM tree:
2091    //
2092    //     iframe
2093    //     └─ #text: <a>test
2094    //
2095    // So iframe cannot have any children other than a text node.
2096    //
2097    // The same is not true, though, in "foreign content"; that is, within
2098    // <svg> or <math> tags. The following html:
2099    //
2100    //     <svg><iframe><a>test
2101    //
2102    // is parsed differently:
2103    //
2104    //    svg
2105    //    └─ iframe
2106    //       └─ a
2107    //          └─ #text: test
2108    //
2109    // So in SVG namespace iframe can have children.
2110    //
2111    // Ammonia disallows <svg> but it keeps its content after deleting it. And
2112    // the parser internally keeps track of the namespace of the element. So
2113    // assume we have the following snippet:
2114    //
2115    //     <svg><iframe><a title="</iframe><img src onerror=alert(1)>">test
2116    //
2117    // It is parsed into:
2118    //
2119    //     svg
2120    //     └─ iframe
2121    //        └─ a title="</iframe><img src onerror=alert(1)>"
2122    //           └─ #text: test
2123    //
2124    // This DOM tree is harmless from ammonia point of view because the piece
2125    // of code that looks like XSS is in a title attribute. Hence, the
2126    // resulting "safe" HTML from ammonia would be:
2127    //
2128    //     <iframe><a title="</iframe><img src onerror=alert(1)>" rel="noopener
2129    // noreferrer">test</a></iframe>
2130    //
2131    // However, at this point, the information about namespace is lost, which
2132    // means that the browser will parse this snippet into:
2133    //
2134    //     ├─ iframe
2135    //     │  └─ #text: <a title="
2136    //     ├─ img src="" onerror="alert(1)"
2137    //     └─ #text: " rel="noopener noreferrer">test
2138    //
2139    // Leading to XSS.
2140    //
2141    // To solve this issue, check for unexpected namespace switches after cleanup.
2142    // Elements which change namespace at an unexpected point are removed.
2143    // This function returns `true` if `child` should be kept, and `false` if it
2144    // should be removed.
2145    //
2146    // [1]: https://github.com/Plume-org/Plume/blob/main/plume-models/src/safe_string.rs#L21
2147    fn check_expected_namespace(&self, parent: &Handle, child: &Handle) -> bool {
2148        let (parent, parent_attr, child) = match (&parent.data, &child.data) {
2149            (NodeData::Element { name: pn, attrs, .. }, NodeData::Element { name: cn, .. }) => (pn, attrs, cn),
2150            _ => return true,
2151        };
2152        // The only way to switch from html to svg is with the <svg> tag
2153        if parent.ns == ns!(html) && child.ns == ns!(svg) {
2154            child.local == local_name!("svg")
2155        // The only way to switch from html to mathml is with the <math> tag
2156        } else if parent.ns == ns!(html) && child.ns == ns!(mathml) {
2157            child.local == local_name!("math")
2158        // The only way to switch from mathml to svg/html is with a text integration point
2159        } else if parent.ns == ns!(mathml) && child.ns != ns!(mathml) {
2160            // https://html.spec.whatwg.org/#mathml
2161            if &*parent.local == "annotation-xml" {
2162                let parent_attr = parent_attr.borrow();
2163                // https://html.spec.whatwg.org/#tree-construction
2164                if child.ns == ns!(html)
2165                    && parent_attr
2166                        .iter()
2167                        .filter(|attr| attr.name.local == local_name!("encoding"))
2168                        .all(|attr| {
2169                            &*attr.value == "text/html" || &*attr.value == "application/xhtml+xml"
2170                        })
2171                {
2172                    is_html_tag(&child.local)
2173                    && parent_attr
2174                        .iter()
2175                        .filter(|attr| attr.name.local == local_name!("encoding"))
2176                        .count()
2177                        == 1
2178                } else {
2179                    child.local == local_name!("svg") && child.ns == ns!(svg)
2180                }
2181            } else {
2182                matches!(&*parent.local, "mi" | "mo" | "mn" | "ms" | "mtext")
2183                    && if child.ns == ns!(html) {
2184                        is_html_tag(&child.local)
2185                    } else {
2186                        true
2187                    }
2188            }
2189
2190        // The only way to switch from svg to mathml/html is with an html integration point
2191        } else if parent.ns == ns!(svg) && child.ns != ns!(svg) {
2192            // https://html.spec.whatwg.org/#svg-0
2193            matches!(&*parent.local, "foreignObject")
2194                && if child.ns == ns!(html) { is_html_tag(&child.local) } else { true }
2195        } else if child.ns == ns!(svg) {
2196            is_svg_tag(&child.local)
2197        } else if child.ns == ns!(mathml) {
2198            is_mathml_tag(&child.local)
2199        } else if child.ns == ns!(html) {
2200            is_html_tag(&child.local)
2201        } else {
2202            // There are no other supported ways to switch namespace
2203            parent.ns == child.ns
2204        }
2205    }
2206
2207    /// Add and transform special-cased attributes and elements.
2208    ///
2209    /// This function handles:
2210    ///
2211    /// * relative URL rewriting
2212    /// * adding `<a rel>` attributes
2213    /// * filtering out banned style properties
2214    /// * filtering out banned classes
2215    fn adjust_node_attributes(
2216        &self,
2217        child: &mut Handle,
2218        link_rel: &Option<StrTendril>,
2219        id_prefix: Option<&'a str>,
2220        parent: &Handle,
2221        id_to_tag_name_map: &HashMap<String, Option<String>>,
2222    ) {
2223        if let NodeData::Element {
2224            ref name,
2225            ref attrs,
2226            ..
2227        } = child.data
2228        {
2229            if let Some(set_attrs) = self.set_tag_attribute_values.get(&*name.local) {
2230                let mut attrs = attrs.borrow_mut();
2231                for (&set_name, &set_value) in set_attrs {
2232                    // set the value of the attribute if the attribute is already present
2233                    if let Some(attr) = attrs.iter_mut().find(|attr| &*attr.name.local == set_name)
2234                    {
2235                        if &*attr.value != set_value {
2236                            attr.value = set_value.into();
2237                        }
2238                    } else {
2239                        // otherwise, add the attribute
2240                        let attr = Attribute {
2241                            name: QualName::new(None, ns!(), set_name.into()),
2242                            value: set_value.into(),
2243                        };
2244                        attrs.push(attr);
2245                    }
2246                }
2247            }
2248            if let Some(ref link_rel) = *link_rel {
2249                if &*name.local == "a" {
2250                    attrs.borrow_mut().push(Attribute {
2251                        name: QualName::new(None, ns!(), local_name!("rel")),
2252                        value: link_rel.clone(),
2253                    })
2254                }
2255            }
2256            if let Some(ref id_prefix) = id_prefix {
2257                for attr in &mut *attrs.borrow_mut() {
2258                    if &attr.name.local == "id" && !attr.value.starts_with(id_prefix) {
2259                        attr.value = format_tendril!("{}{}", id_prefix, attr.value);
2260                    }
2261                }
2262            }
2263            if let Some(ref attr_filter) = self.attribute_filter {
2264                let mut drop_attrs = Vec::new();
2265                let mut attrs = attrs.borrow_mut();
2266                for (i, attr) in &mut attrs.iter_mut().enumerate() {
2267                    let replace_with = if let Some(new) =
2268                        attr_filter.filter(&name.local, &attr.name.local, &attr.value)
2269                    {
2270                        if *new != *attr.value {
2271                            Some(format_tendril!("{}", new))
2272                        } else {
2273                            None // no need to replace the attr if filter returned the same value
2274                        }
2275                    } else {
2276                        drop_attrs.push(i);
2277                        None
2278                    };
2279                    if let Some(replace_with) = replace_with {
2280                        attr.value = replace_with;
2281                    }
2282                }
2283                for i in drop_attrs.into_iter().rev() {
2284                    attrs.swap_remove(i);
2285                }
2286            }
2287            {
2288                let mut drop_attrs = Vec::new();
2289                let mut attrs = attrs.borrow_mut();
2290                for (i, attr) in attrs.iter_mut().enumerate() {
2291                    if is_url_attr(&name.local, &attr.name.local) && is_url_relative(&attr.value) {
2292                        let new_value = self.url_relative.evaluate(&attr.value);
2293                        if let Some(new_value) = new_value {
2294                            attr.value = new_value;
2295                        } else {
2296                            drop_attrs.push(i);
2297                        }
2298                    }
2299                }
2300                // Swap remove scrambles the vector after the current point.
2301                // We will not do anything except with items before the current point.
2302                // The `rev()` is, as such, necessary for correctness.
2303                // We could use regular `remove(usize)` and a forward iterator,
2304                // but that's slower.
2305                for i in drop_attrs.into_iter().rev() {
2306                    attrs.swap_remove(i);
2307                }
2308            }
2309            if let Some(allowed_values) = &self.style_properties {
2310                for attr in &mut *attrs.borrow_mut() {
2311                    if &attr.name.local == "style" {
2312                        attr.value = style::filter_style_attribute(&attr.value, allowed_values).into();
2313                    }
2314                }
2315            }
2316            if let Some(allowed_values) = self.allowed_classes.get(&*name.local) {
2317                for attr in &mut *attrs.borrow_mut() {
2318                    if &attr.name.local == "class" {
2319                        let mut classes = vec![];
2320                        // https://html.spec.whatwg.org/#global-attributes:classes-2
2321                        for class in attr.value.split_ascii_whitespace() {
2322                            if allowed_values.contains(class) {
2323                                classes.push(class.to_owned());
2324                            }
2325                        }
2326                        attr.value = format_tendril!("{}", classes.join(" "));
2327                    }
2328                }
2329            }
2330            if
2331                // https://svgwg.org/specs/animations/#AnimateElement
2332                name.ns == ns!(svg) &&
2333                (&*name.local == "animate" || &*name.local == "set")
2334            {
2335                let mut attrs = attrs.borrow_mut();
2336                let animate_name = attrs
2337                    .iter()
2338                    .find(|attr| &*attr.name.local == "attributeName")
2339                    .map(|attr| attr.value.clone());
2340                let animate_href = attrs
2341                    .iter()
2342                    .find(|attr| &*attr.name.local == "href")
2343                    .map(|attr| attr.value.clone());
2344                let animate_tag_name = animate_href
2345                    .map(|href| {
2346                        if href.starts_with("#") {
2347                            id_to_tag_name_map.get(&href[1..]).and_then(|inner| Some(&inner.as_ref()?[..]))
2348                        } else {
2349                            None
2350                        }
2351                    })
2352                    .unwrap_or_else(|| {
2353                        if let &NodeData::Element { name: ref parent_name, .. } = &parent.data {
2354                            Some(&*parent_name.local)
2355                        } else {
2356                            None
2357                        }
2358                    });
2359                if let (Some(animate_name), Some(animate_tag_name)) = (animate_name, animate_tag_name) {
2360                    if let Some(ref attr_filter) = self.attribute_filter {
2361                        if let Some((i, animate_values)) = attrs
2362                            .iter_mut()
2363                            .enumerate()
2364                            .find(|(_, attr)| &*attr.name.local == "values")
2365                            .map(|(i, attr)| (i, &mut attr.value))
2366                        {
2367                            let mut drop = false;
2368                            let new_value = animate_values.split(';')
2369                                .map(|value| {
2370                                    if let Some(new_value) = attr_filter.filter(animate_tag_name, &animate_name, &value) {
2371                                        String::from(new_value)
2372                                    } else {
2373                                        drop = true;
2374                                        String::new()
2375                                    }
2376                                })
2377                                .collect::<Vec<String>>()
2378                                .join(";");
2379                            if drop {
2380                                attrs.swap_remove(i);
2381                            } else {
2382                                *animate_values = new_value.into();
2383                            }
2384                        }
2385                        let mut drop_attrs = Vec::new();
2386                        for (i, animate_value) in attrs
2387                            .iter_mut()
2388                            .enumerate()
2389                            .filter(|(_, attr)| &*attr.name.local == "from" || &*attr.name.local == "to")
2390                            .map(|(i, attr)| (i, &mut attr.value))
2391                        {
2392                            if let Some(new_value) = attr_filter.filter(animate_tag_name, &animate_name, &animate_value) {
2393                                *animate_value = new_value[..].into();
2394                            } else {
2395                                drop_attrs.push(i);
2396                            };
2397                        }
2398                        for i in drop_attrs.into_iter().rev() {
2399                            attrs.swap_remove(i);
2400                        }
2401                    }
2402                    if is_url_attr(animate_tag_name, &*animate_name) {
2403                        if let Some((i, animate_values)) = attrs
2404                            .iter_mut()
2405                            .enumerate()
2406                            .find(|(_, attr)| &*attr.name.local == "values")
2407                            .map(|(i, attr)| (i, &mut attr.value))
2408                        {
2409                            let mut drop = false;
2410                            let new_value = animate_values.split(';')
2411                                .map(|value| {
2412                                    if !is_url_relative(value) {
2413                                        String::from(value)
2414                                    } else if let Some(new_value) = self.url_relative.evaluate(value) {
2415                                        String::from(new_value)
2416                                    } else {
2417                                        drop = true;
2418                                        String::new()
2419                                    }
2420                                })
2421                                .collect::<Vec<String>>()
2422                                .join(";");
2423                            if drop {
2424                                attrs.swap_remove(i);
2425                            } else {
2426                                *animate_values = new_value.into();
2427                            }
2428                        }
2429                        let mut drop_attrs = Vec::new();
2430                        for (i, animate_value) in attrs
2431                            .iter_mut()
2432                            .enumerate()
2433                            .filter(|(_, attr)| &*attr.name.local == "from" || &*attr.name.local == "to")
2434                            .map(|(i, attr)| (i, &mut attr.value))
2435                        {
2436                            if !is_url_relative(animate_value) {
2437                                // do nothing
2438                            } else if let Some(new_value) = self.url_relative.evaluate(animate_value) {
2439                                *animate_value = new_value;
2440                            } else {
2441                                drop_attrs.push(i);
2442                            };
2443                        }
2444                        for i in drop_attrs.into_iter().rev() {
2445                            attrs.swap_remove(i);
2446                        }
2447                    }
2448                    if &*animate_name == "style" {
2449                        if let Some(allowed_values) = &self.style_properties {
2450                            if let Some(animate_values) = attrs
2451                                .iter_mut()
2452                                .find(|attr| &*attr.name.local == "values")
2453                                .map(|attr| &mut attr.value)
2454                            {
2455                                let new_value = animate_values.split(';')
2456                                    .map(|value| {
2457                                        style::filter_style_attribute(&value, allowed_values)
2458                                    })
2459                                    .collect::<Vec<String>>()
2460                                    .join(";");
2461                                *animate_values = new_value.into();
2462                            }
2463                            for animate_value in attrs
2464                                .iter_mut()
2465                                .filter(|attr| &*attr.name.local == "from" || &*attr.name.local == "to")
2466                                .map(|attr| &mut attr.value)
2467                            {
2468                                *animate_value = style::filter_style_attribute(&animate_value, allowed_values).into();
2469                            }
2470                        }
2471                    }
2472                    if &*animate_name == "class" {
2473                        if let Some(allowed_values) = self.allowed_classes.get(animate_tag_name) {
2474                            if let Some(animate_values) = attrs
2475                                .iter_mut()
2476                                .find(|attr| &*attr.name.local == "values")
2477                                .map(|attr| &mut attr.value)
2478                            {
2479                                let new_value = animate_values.split(';')
2480                                    .map(|value| {
2481                                        let mut classes = vec![];
2482                                        // https://html.spec.whatwg.org/#global-attributes:classes-2
2483                                        for class in value.split_ascii_whitespace() {
2484                                            if allowed_values.contains(class) {
2485                                                classes.push(class.to_owned());
2486                                            }
2487                                        }
2488                                        classes.join(" ")
2489                                    })
2490                                    .collect::<Vec<String>>()
2491                                    .join(";");
2492                                *animate_values = new_value.into();
2493                            }
2494                            for animate_value in attrs
2495                                .iter_mut()
2496                                .filter(|attr| &*attr.name.local == "from" || &*attr.name.local == "to")
2497                                .map(|attr| &mut attr.value)
2498                            {
2499                                let mut classes = vec![];
2500                                // https://html.spec.whatwg.org/#global-attributes:classes-2
2501                                for class in animate_value.split_ascii_whitespace() {
2502                                    if allowed_values.contains(class) {
2503                                        classes.push(class.to_owned());
2504                                    }
2505                                }
2506                                *animate_value = classes.join(" ").into();
2507                            }
2508                        }
2509                    }
2510                }
2511            }
2512        }
2513    }
2514
2515    /// Initializes an HTML fragment parser.
2516    ///
2517    /// Ammonia conforms to the HTML5 fragment parsing rules,
2518    /// by parsing the given fragment as if it were included in a <div> tag.
2519    fn make_parser() -> html::Parser<RcDom> {
2520        html::parse_fragment(
2521            RcDom::default(),
2522            html::ParseOpts::default(),
2523            QualName::new(None, ns!(html), local_name!("div")),
2524            vec![],
2525            false,
2526        )
2527    }
2528}
2529
2530/// Given an element name and attribute name, determine if the given attribute contains a URL.
2531fn is_url_attr(element: &str, attr: &str) -> bool {
2532    (element != "animate" && element != "set" && attr == "href")
2533        // Don't have to worry about alternate xmlns prefixes, because HTML doesn't
2534        // parse them, anyway:
2535        // https://html.spec.whatwg.org/#adjust-foreign-attributes
2536        || (element != "animate" && element != "set" && attr == "xlink:href")
2537        || attr == "src"
2538        || (element == "form" && attr == "action")
2539        || (element == "object" && attr == "data")
2540        || ((element == "button" || element == "input") && attr == "formaction")
2541        || (element == "a" && attr == "ping")
2542        || (element == "video" && attr == "poster")
2543}
2544
2545fn is_html_tag(element: &str) -> bool {
2546    (!is_svg_tag(element) && !is_mathml_tag(element))
2547        || matches!(
2548            element,
2549            "title" | "style" | "font" | "a" | "script" | "span"
2550        )
2551}
2552
2553/// Given an element name, check if it's SVG
2554fn is_svg_tag(element: &str) -> bool {
2555    // https://svgwg.org/svg2-draft/eltindex.html
2556    matches!(
2557        element,
2558        "a" | "animate"
2559            | "animateMotion"
2560            | "animateTransform"
2561            | "circle"
2562            | "clipPath"
2563            | "defs"
2564            | "desc"
2565            | "discard"
2566            | "ellipse"
2567            | "feBlend"
2568            | "feColorMatrix"
2569            | "feComponentTransfer"
2570            | "feComposite"
2571            | "feConvolveMatrix"
2572            | "feDiffuseLighting"
2573            | "feDisplacementMap"
2574            | "feDistantLight"
2575            | "feDropShadow"
2576            | "feFlood"
2577            | "feFuncA"
2578            | "feFuncB"
2579            | "feFuncG"
2580            | "feFuncR"
2581            | "feGaussianBlur"
2582            | "feImage"
2583            | "feMerge"
2584            | "feMergeNode"
2585            | "feMorphology"
2586            | "feOffset"
2587            | "fePointLight"
2588            | "feSpecularLighting"
2589            | "feSpotLight"
2590            | "feTile"
2591            | "feTurbulence"
2592            | "filter"
2593            | "foreignObject"
2594            | "g"
2595            | "image"
2596            | "line"
2597            | "linearGradient"
2598            | "marker"
2599            | "mask"
2600            | "metadata"
2601            | "mpath"
2602            | "path"
2603            | "pattern"
2604            | "polygon"
2605            | "polyline"
2606            | "radialGradient"
2607            | "rect"
2608            | "script"
2609            | "set"
2610            | "stop"
2611            | "style"
2612            | "svg"
2613            | "switch"
2614            | "symbol"
2615            | "text"
2616            | "textPath"
2617            | "title"
2618            | "tspan"
2619            | "use"
2620            | "view"
2621    )
2622}
2623
2624/// Given an element name, check if it's Math
2625fn is_mathml_tag(element: &str) -> bool {
2626    // https://svgwg.org/svg2-draft/eltindex.html
2627    matches!(
2628        element,
2629        "abs"
2630            | "and"
2631            | "annotation"
2632            | "annotation-xml"
2633            | "apply"
2634            | "approx"
2635            | "arccos"
2636            | "arccosh"
2637            | "arccot"
2638            | "arccoth"
2639            | "arccsc"
2640            | "arccsch"
2641            | "arcsec"
2642            | "arcsech"
2643            | "arcsin"
2644            | "arcsinh"
2645            | "arctan"
2646            | "arctanh"
2647            | "arg"
2648            | "bind"
2649            | "bvar"
2650            | "card"
2651            | "cartesianproduct"
2652            | "cbytes"
2653            | "ceiling"
2654            | "cerror"
2655            | "ci"
2656            | "cn"
2657            | "codomain"
2658            | "complexes"
2659            | "compose"
2660            | "condition"
2661            | "conjugate"
2662            | "cos"
2663            | "cosh"
2664            | "cot"
2665            | "coth"
2666            | "cs"
2667            | "csc"
2668            | "csch"
2669            | "csymbol"
2670            | "curl"
2671            | "declare"
2672            | "degree"
2673            | "determinant"
2674            | "diff"
2675            | "divergence"
2676            | "divide"
2677            | "domain"
2678            | "domainofapplication"
2679            | "emptyset"
2680            | "eq"
2681            | "equivalent"
2682            | "eulergamma"
2683            | "exists"
2684            | "exp"
2685            | "exponentiale"
2686            | "factorial"
2687            | "factorof"
2688            | "false"
2689            | "floor"
2690            | "fn"
2691            | "forall"
2692            | "gcd"
2693            | "geq"
2694            | "grad"
2695            | "gt"
2696            | "ident"
2697            | "image"
2698            | "imaginary"
2699            | "imaginaryi"
2700            | "implies"
2701            | "in"
2702            | "infinity"
2703            | "int"
2704            | "integers"
2705            | "intersect"
2706            | "interval"
2707            | "inverse"
2708            | "lambda"
2709            | "laplacian"
2710            | "lcm"
2711            | "leq"
2712            | "limit"
2713            | "list"
2714            | "ln"
2715            | "log"
2716            | "logbase"
2717            | "lowlimit"
2718            | "lt"
2719            | "maction"
2720            | "maligngroup"
2721            | "malignmark"
2722            | "math"
2723            | "matrix"
2724            | "matrixrow"
2725            | "max"
2726            | "mean"
2727            | "median"
2728            | "menclose"
2729            | "merror"
2730            | "mfenced"
2731            | "mfrac"
2732            | "mglyph"
2733            | "mi"
2734            | "min"
2735            | "minus"
2736            | "mlabeledtr"
2737            | "mlongdiv"
2738            | "mmultiscripts"
2739            | "mn"
2740            | "mo"
2741            | "mode"
2742            | "moment"
2743            | "momentabout"
2744            | "mover"
2745            | "mpadded"
2746            | "mphantom"
2747            | "mprescripts"
2748            | "mroot"
2749            | "mrow"
2750            | "ms"
2751            | "mscarries"
2752            | "mscarry"
2753            | "msgroup"
2754            | "msline"
2755            | "mspace"
2756            | "msqrt"
2757            | "msrow"
2758            | "mstack"
2759            | "mstyle"
2760            | "msub"
2761            | "msubsup"
2762            | "msup"
2763            | "mtable"
2764            | "mtd"
2765            | "mtext"
2766            | "mtr"
2767            | "munder"
2768            | "munderover"
2769            | "naturalnumbers"
2770            | "neq"
2771            | "none"
2772            | "not"
2773            | "notanumber"
2774            | "notin"
2775            | "notprsubset"
2776            | "notsubset"
2777            | "or"
2778            | "otherwise"
2779            | "outerproduct"
2780            | "partialdiff"
2781            | "pi"
2782            | "piece"
2783            | "piecewise"
2784            | "plus"
2785            | "power"
2786            | "primes"
2787            | "product"
2788            | "prsubset"
2789            | "quotient"
2790            | "rationals"
2791            | "real"
2792            | "reals"
2793            | "reln"
2794            | "rem"
2795            | "root"
2796            | "scalarproduct"
2797            | "sdev"
2798            | "sec"
2799            | "sech"
2800            | "selector"
2801            | "semantics"
2802            | "sep"
2803            | "set"
2804            | "setdiff"
2805            | "share"
2806            | "sin"
2807            | "sinh"
2808            | "span"
2809            | "subset"
2810            | "sum"
2811            | "tan"
2812            | "tanh"
2813            | "tendsto"
2814            | "times"
2815            | "transpose"
2816            | "true"
2817            | "union"
2818            | "uplimit"
2819            | "variance"
2820            | "vector"
2821            | "vectorproduct"
2822            | "xor"
2823    )
2824}
2825
2826fn is_url_relative(url: &str) -> bool {
2827    matches!(
2828        Url::parse(url),
2829        Err(url::ParseError::RelativeUrlWithoutBase)
2830    )
2831}
2832
2833/// Policy for [relative URLs], that is, URLs that do not specify the scheme in full.
2834///
2835/// This policy kicks in, if set, for any attribute named `src` or `href`,
2836/// as well as the `data` attribute of an `object` tag.
2837///
2838/// [relative URLs]: struct.Builder.html#method.url_relative
2839///
2840/// # Examples
2841///
2842/// ## `Deny`
2843///
2844/// * `<a href="test">` is a file-relative URL, and will be removed
2845/// * `<a href="/test">` is a domain-relative URL, and will be removed
2846/// * `<a href="//example.com/test">` is a scheme-relative URL, and will be removed
2847/// * `<a href="http://example.com/test">` is an absolute URL, and will be kept
2848///
2849/// ## `PassThrough`
2850///
2851/// No changes will be made to any URLs, except if a disallowed scheme is used.
2852///
2853/// ## `RewriteWithBase`
2854///
2855/// If the base is set to `http://notriddle.com/some-directory/some-file`
2856///
2857/// * `<a href="test">` will be rewritten to `<a href="http://notriddle.com/some-directory/test">`
2858/// * `<a href="/test">` will be rewritten to `<a href="http://notriddle.com/test">`
2859/// * `<a href="//example.com/test">` will be rewritten to `<a href="http://example.com/test">`
2860/// * `<a href="http://example.com/test">` is an absolute URL, so it will be kept as-is
2861///
2862/// ## `Custom`
2863///
2864/// Pass the relative URL to a function.
2865/// If it returns `Some(string)`, then that one gets used.
2866/// Otherwise, it will remove the attribute (like `Deny` does).
2867///
2868///     use std::borrow::Cow;
2869///     fn is_absolute_path(url: &str) -> bool {
2870///         let u = url.as_bytes();
2871///         // `//a/b/c` is "protocol-relative", meaning "a" is a hostname
2872///         // `/a/b/c` is an absolute path, and what we want to do stuff to.
2873///         u.get(0) == Some(&b'/') && u.get(1) != Some(&b'/')
2874///     }
2875///     fn evaluate(url: &str) -> Option<Cow<str>> {
2876///         if is_absolute_path(url) {
2877///             Some(Cow::Owned(String::from("/root") + url))
2878///         } else {
2879///             Some(Cow::Borrowed(url))
2880///         }
2881///     }
2882///     fn main() {
2883///         let a = ammonia::Builder::new()
2884///             .url_relative(ammonia::UrlRelative::Custom(Box::new(evaluate)))
2885///             .clean("<a href=/test/path>fixed</a><a href=path>passed</a><a href=http://google.com/>skipped</a>")
2886///             .to_string();
2887///         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>");
2888///     }
2889///
2890/// This function is only applied to relative URLs.
2891/// To filter all of the URLs,
2892/// use the not-yet-implemented Content Security Policy.
2893#[non_exhaustive]
2894pub enum UrlRelative<'a> {
2895    /// Relative URLs will be completely stripped from the document.
2896    Deny,
2897    /// Relative URLs will be passed through unchanged.
2898    PassThrough,
2899    /// Relative URLs will be changed into absolute URLs, based on this base URL.
2900    RewriteWithBase(Url),
2901    /// Force absolute and relative paths into a particular directory.
2902    ///
2903    /// Since the resolver does not affect fully-qualified URLs, it doesn't
2904    /// prevent users from linking wherever they want. This feature only
2905    /// serves to make content more portable.
2906    ///
2907    /// # Examples
2908    ///
2909    /// <table>
2910    /// <thead>
2911    /// <tr>
2912    ///     <th>root</th>
2913    ///     <th>path</th>
2914    ///     <th>url</th>
2915    ///     <th>result</th>
2916    /// </tr>
2917    /// </thead>
2918    /// <tbody>
2919    /// <tr>
2920    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2921    ///     <td>README.md</td>
2922    ///     <td></td>
2923    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/README.md</td>
2924    /// </tr><tr>
2925    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2926    ///     <td>README.md</td>
2927    ///     <td>/</td>
2928    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2929    /// </tr><tr>
2930    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2931    ///     <td>README.md</td>
2932    ///     <td>/CONTRIBUTING.md</td>
2933    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md</td>
2934    /// </tr><tr>
2935    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master</td>
2936    ///     <td>README.md</td>
2937    ///     <td></td>
2938    ///     <td>https://github.com/rust-ammonia/ammonia/blob/README.md</td>
2939    /// </tr><tr>
2940    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master</td>
2941    ///     <td>README.md</td>
2942    ///     <td>/</td>
2943    ///     <td>https://github.com/rust-ammonia/ammonia/blob/</td>
2944    /// </tr><tr>
2945    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master</td>
2946    ///     <td>README.md</td>
2947    ///     <td>/CONTRIBUTING.md</td>
2948    ///     <td>https://github.com/rust-ammonia/ammonia/blob/CONTRIBUTING.md</td>
2949    /// </tr><tr>
2950    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2951    ///     <td></td>
2952    ///     <td></td>
2953    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2954    /// </tr><tr>
2955    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2956    ///     <td></td>
2957    ///     <td>/</td>
2958    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2959    /// </tr><tr>
2960    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/</td>
2961    ///     <td></td>
2962    ///     <td>/CONTRIBUTING.md</td>
2963    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md</td>
2964    /// </tr><tr>
2965    ///     <td>https://github.com/</td>
2966    ///     <td>rust-ammonia/ammonia/blob/master/README.md</td>
2967    ///     <td></td>
2968    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/README.md</td>
2969    /// </tr><tr>
2970    ///     <td>https://github.com/</td>
2971    ///     <td>rust-ammonia/ammonia/blob/master/README.md</td>
2972    ///     <td>/</td>
2973    ///     <td>https://github.com/</td>
2974    /// </tr><tr>
2975    ///     <td>https://github.com/</td>
2976    ///     <td>rust-ammonia/ammonia/blob/master/README.md</td>
2977    ///     <td>CONTRIBUTING.md</td>
2978    ///     <td>https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md</td>
2979    /// </tr><tr>
2980    ///     <td>https://github.com/</td>
2981    ///     <td>rust-ammonia/ammonia/blob/master/README.md</td>
2982    ///     <td>/CONTRIBUTING.md</td>
2983    ///     <td>https://github.com/CONTRIBUTING.md</td>
2984    /// </tr>
2985    /// </tbody>
2986    /// </table>
2987    RewriteWithRoot {
2988        /// The URL that is treated as the root by the resolver.
2989        root: Url,
2990        /// The "current path" used to resolve relative paths.
2991        path: String,
2992    },
2993    /// Rewrite URLs with a custom function.
2994    Custom(Box<dyn UrlRelativeEvaluate<'a>>),
2995}
2996
2997impl<'a> UrlRelative<'a> {
2998    fn evaluate(&self, url: &str) -> Option<html5ever::tendril::StrTendril> {
2999        match self {
3000            UrlRelative::RewriteWithBase(ref url_base) => url_base
3001                .join(url)
3002                .ok()
3003                .and_then(|x| StrTendril::from_str(x.as_str()).ok()),
3004            UrlRelative::RewriteWithRoot { ref root, ref path } => {
3005                (match url.as_bytes() {
3006                    // Scheme-relative URL
3007                    [b'/', b'/', ..] => root.join(url),
3008                    // Path-absolute URL
3009                    b"/" => root.join("."),
3010                    [b'/', ..] => root.join(&url[1..]),
3011                    // Path-relative URL
3012                    _ => root.join(path).and_then(|r| r.join(url)),
3013                })
3014                .ok()
3015                .and_then(|x| StrTendril::from_str(x.as_str()).ok())
3016            }
3017            UrlRelative::Custom(ref evaluate) => evaluate
3018                .evaluate(url)
3019                .as_ref()
3020                .map(Cow::as_ref)
3021                .map(StrTendril::from_str)
3022                .and_then(Result::ok),
3023            UrlRelative::PassThrough => StrTendril::from_str(url).ok(),
3024            UrlRelative::Deny => None,
3025        }
3026    }
3027}
3028
3029impl<'a> fmt::Debug for UrlRelative<'a> {
3030    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3031        match *self {
3032            UrlRelative::Deny => write!(f, "UrlRelative::Deny"),
3033            UrlRelative::PassThrough => write!(f, "UrlRelative::PassThrough"),
3034            UrlRelative::RewriteWithBase(ref base) => {
3035                write!(f, "UrlRelative::RewriteWithBase({})", base)
3036            }
3037            UrlRelative::RewriteWithRoot { ref root, ref path } => {
3038                write!(
3039                    f,
3040                    "UrlRelative::RewriteWithRoot {{ root: {root}, path: {path} }}"
3041                )
3042            }
3043            UrlRelative::Custom(_) => write!(f, "UrlRelative::Custom"),
3044        }
3045    }
3046}
3047
3048/// Types that implement this trait can be used to convert a relative URL into an absolute URL.
3049///
3050/// This evaluator is only called when the URL is relative; absolute URLs are not evaluated.
3051///
3052/// See [`url_relative`][url_relative] for more details.
3053///
3054/// [url_relative]: struct.Builder.html#method.url_relative
3055pub trait UrlRelativeEvaluate<'a>: Send + Sync + 'a {
3056    /// Return `None` to remove the attribute. Return `Some(str)` to replace it with a new string.
3057    fn evaluate<'url>(&self, _: &'url str) -> Option<Cow<'url, str>>;
3058}
3059impl<'a, T> UrlRelativeEvaluate<'a> for T
3060where
3061    T: Fn(&str) -> Option<Cow<'_, str>> + Send + Sync + 'a,
3062{
3063    fn evaluate<'url>(&self, url: &'url str) -> Option<Cow<'url, str>> {
3064        self(url)
3065    }
3066}
3067
3068impl fmt::Debug for dyn AttributeFilter {
3069    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3070        f.write_str("AttributeFilter")
3071    }
3072}
3073
3074/// Types that implement this trait can be used to remove or rewrite arbitrary attributes.
3075///
3076/// See [`attribute_filter`][attribute_filter] for more details.
3077///
3078/// [attribute_filter]: struct.Builder.html#method.attribute_filter
3079pub trait AttributeFilter: Send + Sync {
3080    /// Return `None` to remove the attribute. Return `Some(str)` to replace it with a new string.
3081    fn filter<'a>(&self, _: &str, _: &str, _: &'a str) -> Option<Cow<'a, str>>;
3082}
3083
3084impl<T> AttributeFilter for T
3085where
3086    T: for<'a> Fn(&str, &str, &'a str) -> Option<Cow<'a, str>> + Send + Sync + 'static,
3087{
3088    fn filter<'a>(&self, element: &str, attribute: &str, value: &'a str) -> Option<Cow<'a, str>> {
3089        self(element, attribute, value)
3090    }
3091}
3092
3093/// A sanitized HTML document.
3094///
3095/// The `Document` type is an opaque struct representing an HTML fragment that was sanitized by
3096/// `ammonia`. It can be converted to a [`String`] or written to a [`Write`] instance. This allows
3097/// users to avoid buffering the serialized representation to a [`String`] when desired.
3098///
3099/// This type is opaque to insulate the caller from breaking changes in the `html5ever` interface.
3100///
3101/// Note that this type wraps an `html5ever` DOM tree. `ammonia` does not support streaming, so
3102/// the complete fragment needs to be stored in memory during processing.
3103///
3104/// [`String`]: https://doc.rust-lang.org/nightly/std/string/struct.String.html
3105/// [`Write`]: https://doc.rust-lang.org/nightly/std/io/trait.Write.html
3106///
3107/// # Examples
3108///
3109///     use ammonia::Builder;
3110///
3111///     let input = "<!-- comments will be stripped -->This is an Ammonia example.";
3112///     let output = "This is an Ammonia example.";
3113///
3114///     let document = Builder::new()
3115///         .clean(input);
3116///     assert_eq!(document.to_string(), output);
3117pub struct Document(RcDom);
3118
3119impl Document {
3120    /// Serializes a `Document` instance to a writer.
3121    ///
3122    /// This method writes the sanitized HTML to a [`Write`] instance, avoiding a buffering step.
3123    ///
3124    /// To avoid consuming the writer, a mutable reference can be passed, like in the example below.
3125    ///
3126    /// Note that the in-memory representation of `Document` is larger than the serialized
3127    /// `String`.
3128    ///
3129    /// [`Write`]: https://doc.rust-lang.org/nightly/std/io/trait.Write.html
3130    ///
3131    /// # Examples
3132    ///
3133    ///     use ammonia::Builder;
3134    ///
3135    ///     let input = "Some <style></style>HTML here";
3136    ///     let expected = b"Some HTML here";
3137    ///
3138    ///     let document = Builder::new()
3139    ///         .clean(input);
3140    ///
3141    ///     let mut sanitized = Vec::new();
3142    ///     document.write_to(&mut sanitized)
3143    ///         .expect("Writing to a string should not fail (except on OOM)");
3144    ///     assert_eq!(sanitized, expected);
3145    pub fn write_to<W>(&self, writer: W) -> io::Result<()>
3146    where
3147        W: io::Write,
3148    {
3149        let opts = Self::serialize_opts();
3150        let inner: SerializableHandle = self.0.document.children.borrow()[0].clone().into();
3151        serialize(writer, &inner, opts)
3152    }
3153
3154    /// Exposes the `Document` instance as an [`rcdom::Handle`].
3155    ///
3156    /// This method returns the inner object backing the `Document` instance. This allows
3157    /// making further changes to the DOM without introducing redundant serialization and
3158    /// parsing.
3159    ///
3160    /// Note that this method should be considered unstable and sits outside of the semver
3161    /// stability guarantees. It may change, break, or go away at any time, either because
3162    /// of `html5ever` changes or `ammonia` implementation changes.
3163    ///
3164    /// For this method to be accessible, a `cfg` flag is required. The easiest way is to
3165    /// use the `RUSTFLAGS` environment variable:
3166    ///
3167    /// ```text
3168    /// RUSTFLAGS='--cfg ammonia_unstable' cargo build
3169    /// ```
3170    ///
3171    /// on Unix-like platforms, or
3172    ///
3173    /// ```text
3174    /// set RUSTFLAGS=--cfg ammonia_unstable
3175    /// cargo build
3176    /// ```
3177    ///
3178    /// on Windows.
3179    ///
3180    /// This requirement also applies to crates that transitively depend on crates that use
3181    /// this flag.
3182    ///
3183    /// # Examples
3184    ///
3185    ///     use ammonia::Builder;
3186    ///     use ammonia::rcdom::SerializableHandle;
3187    ///     use maplit::hashset;
3188    ///     use html5ever::serialize::{serialize, SerializeOpts};
3189    ///
3190    ///     # use std::error::Error;
3191    ///     # fn do_main() -> Result<(), Box<dyn Error>> {
3192    ///     let input = "<a>one link</a> and <a>one more</a>";
3193    ///     let expected = "<a>one more</a> and <a>one link</a>";
3194    ///
3195    ///     let document = Builder::new()
3196    ///         .link_rel(None)
3197    ///         .clean(input);
3198    ///
3199    ///     let node = document.to_dom_node();
3200    ///     node.children.borrow_mut().reverse();
3201    ///
3202    ///     let mut buf = Vec::new();
3203    ///     let handle: SerializableHandle = node.into();
3204    ///     serialize(&mut buf, &handle, SerializeOpts::default())?;
3205    ///     let output = String::from_utf8(buf)?;
3206    ///
3207    ///     assert_eq!(output, expected);
3208    ///     # Ok(())
3209    ///     # }
3210    ///     # fn main() { do_main().unwrap() }
3211    #[cfg(ammonia_unstable)]
3212    pub fn to_dom_node(&self) -> Handle {
3213        self.0.document.children.borrow()[0].clone()
3214    }
3215
3216    fn serialize_opts() -> SerializeOpts {
3217        SerializeOpts::default()
3218    }
3219}
3220
3221impl Clone for Document {
3222    fn clone(&self) -> Self {
3223        let parser = Builder::make_parser();
3224        let dom = parser.one(&self.to_string()[..]);
3225        Document(dom)
3226    }
3227}
3228
3229/// Convert a `Document` to stringified HTML.
3230///
3231/// Since [`Document`] implements [`Display`], it can be converted to a [`String`] using the
3232/// standard [`ToString::to_string`] method. This is the simplest way to use `ammonia`.
3233///
3234/// [`Document`]: ammonia::Document
3235/// [`Display`]: std::fmt::Display
3236/// [`ToString::to_string`]: std::string::ToString
3237///
3238/// # Examples
3239///
3240///     use ammonia::Builder;
3241///
3242///     let input = "Some <style></style>HTML here";
3243///     let output = "Some HTML here";
3244///
3245///     let document = Builder::new()
3246///         .clean(input);
3247///     assert_eq!(document.to_string(), output);
3248impl Display for Document {
3249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3250        let opts = Self::serialize_opts();
3251        let mut ret_val = Vec::new();
3252        let inner: SerializableHandle = self.0.document.children.borrow()[0].clone().into();
3253        serialize(&mut ret_val, &inner, opts)
3254            .expect("Writing to a string shouldn't fail (expect on OOM)");
3255        String::from_utf8(ret_val)
3256            .expect("html5ever only supports UTF8")
3257            .fmt(f)
3258    }
3259}
3260
3261impl fmt::Debug for Document {
3262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3263        write!(f, "Document({})", self)
3264    }
3265}
3266
3267impl From<Document> for String {
3268    fn from(document: Document) -> Self {
3269        document.to_string()
3270    }
3271}
3272
3273#[cfg(test)]
3274mod test {
3275    use super::*;
3276    #[test]
3277    fn deeply_nested_whitelisted_does_not_cause_stack_overflow() {
3278        clean(&"<b>".repeat(60_000));
3279    }
3280    #[test]
3281    fn deeply_nested_blacklisted_does_not_cause_stack_overflow() {
3282        clean(&"<b-b>".repeat(60_000));
3283    }
3284    #[test]
3285    fn deeply_nested_alternating_does_not_cause_stack_overflow() {
3286        clean(&"<b-b>".repeat(35_000));
3287    }
3288    #[test]
3289    fn document_level_tags_cannot_be_whitelisted() {
3290        // Adding `html`, `head`, or `body` to the allowed tags has no effect
3291        // because the parser runs in fragment mode and strips them before
3292        // the sanitizer sees the tree. This test pins that documented
3293        // behavior; if it ever changes, the docs on `Builder::tags` need to
3294        // change too.
3295        let fragment =
3296            "<html><head>head content</head><body><div>test</div></body></html>";
3297        let result = Builder::default()
3298            .add_tags(["html", "head", "body"])
3299            .clean(fragment)
3300            .to_string();
3301        assert_eq!(result, "head content<div>test</div>");
3302    }
3303    #[test]
3304    fn included_angles() {
3305        let fragment = "1 < 2";
3306        let result = clean(fragment);
3307        assert_eq!(result, "1 &lt; 2");
3308    }
3309    #[test]
3310    fn remove_script() {
3311        let fragment = "an <script>evil()</script> example";
3312        let result = clean(fragment);
3313        assert_eq!(result, "an  example");
3314    }
3315    #[test]
3316    fn ignore_link() {
3317        let fragment = "a <a href=\"http://www.google.com\">good</a> example";
3318        let expected = "a <a href=\"http://www.google.com\" rel=\"noopener noreferrer\">\
3319                        good</a> example";
3320        let result = clean(fragment);
3321        assert_eq!(result, expected);
3322    }
3323    #[test]
3324    fn remove_unsafe_link() {
3325        let fragment = "an <a onclick=\"evil()\" href=\"http://www.google.com\">evil</a> example";
3326        let result = clean(fragment);
3327        assert_eq!(
3328            result,
3329            "an <a href=\"http://www.google.com\" rel=\"noopener noreferrer\">evil</a> example"
3330        );
3331    }
3332    #[test]
3333    fn remove_js_link() {
3334        let fragment = "an <a href=\"javascript:evil()\">evil</a> example";
3335        let result = clean(fragment);
3336        assert_eq!(result, "an <a rel=\"noopener noreferrer\">evil</a> example");
3337    }
3338    #[test]
3339    fn tag_rebalance() {
3340        let fragment = "<b>AWESOME!";
3341        let result = clean(fragment);
3342        assert_eq!(result, "<b>AWESOME!</b>");
3343    }
3344    #[test]
3345    fn allow_url_relative() {
3346        let fragment = "<a href=test>Test</a>";
3347        let result = Builder::new()
3348            .url_relative(UrlRelative::PassThrough)
3349            .clean(fragment)
3350            .to_string();
3351        assert_eq!(
3352            result,
3353            "<a href=\"test\" rel=\"noopener noreferrer\">Test</a>"
3354        );
3355    }
3356    #[test]
3357    fn rewrite_url_relative() {
3358        let fragment = "<a href=test>Test</a>";
3359        let result = Builder::new()
3360            .url_relative(UrlRelative::RewriteWithBase(
3361                Url::parse("http://example.com/").unwrap(),
3362            ))
3363            .clean(fragment)
3364            .to_string();
3365        assert_eq!(
3366            result,
3367            "<a href=\"http://example.com/test\" rel=\"noopener noreferrer\">Test</a>"
3368        );
3369    }
3370    #[test]
3371    fn rewrite_url_relative_with_invalid_url() {
3372        // Reduced from https://github.com/Bauke/ammonia-crash-test
3373        let fragment = r##"<a href="\\"https://example.com\\"">test</a>"##;
3374        let result = Builder::new()
3375            .url_relative(UrlRelative::RewriteWithBase(
3376                Url::parse("http://example.com/").unwrap(),
3377            ))
3378            .clean(fragment)
3379            .to_string();
3380        assert_eq!(result, r##"<a rel="noopener noreferrer">test</a>"##);
3381    }
3382    #[test]
3383    fn attribute_filter_nop() {
3384        let fragment = "<a href=test>Test</a>";
3385        let result = Builder::new()
3386            .attribute_filter(|elem, attr, value| {
3387                assert_eq!("a", elem);
3388                assert!(
3389                    matches!(
3390                        (attr, value),
3391                        ("href", "test") | ("rel", "noopener noreferrer")
3392                    ),
3393                    "{}",
3394                    value.to_string()
3395                );
3396                Some(value.into())
3397            })
3398            .clean(fragment)
3399            .to_string();
3400        assert_eq!(
3401            result,
3402            "<a href=\"test\" rel=\"noopener noreferrer\">Test</a>"
3403        );
3404    }
3405
3406    #[test]
3407    fn attribute_filter_drop() {
3408        let fragment = "Test<img alt=test src=imgtest>";
3409        let result = Builder::new()
3410            .attribute_filter(|elem, attr, value| {
3411                assert_eq!("img", elem);
3412                match (attr, value) {
3413                    ("src", "imgtest") => None,
3414                    ("alt", "test") => Some(value.into()),
3415                    _ => panic!("unexpected"),
3416                }
3417            })
3418            .clean(fragment)
3419            .to_string();
3420        assert_eq!(result, r#"Test<img alt="test">"#);
3421    }
3422
3423    #[test]
3424    fn url_filter_absolute() {
3425        let fragment = "Test<img alt=test src=imgtest>";
3426        let result = Builder::new()
3427            .attribute_filter(|elem, attr, value| {
3428                assert_eq!("img", elem);
3429                match (attr, value) {
3430                    ("src", "imgtest") => {
3431                        Some(format!("https://example.com/images/{}", value).into())
3432                    }
3433                    ("alt", "test") => None,
3434                    _ => panic!("unexpected"),
3435                }
3436            })
3437            .url_relative(UrlRelative::RewriteWithBase(
3438                Url::parse("http://wrong.invalid/").unwrap(),
3439            ))
3440            .clean(fragment)
3441            .to_string();
3442        assert_eq!(
3443            result,
3444            r#"Test<img src="https://example.com/images/imgtest">"#
3445        );
3446    }
3447
3448    #[test]
3449    fn url_filter_relative() {
3450        let fragment = "Test<img alt=test src=imgtest>";
3451        let result = Builder::new()
3452            .attribute_filter(|elem, attr, value| {
3453                assert_eq!("img", elem);
3454                match (attr, value) {
3455                    ("src", "imgtest") => Some("rewrite".into()),
3456                    ("alt", "test") => Some("altalt".into()),
3457                    _ => panic!("unexpected"),
3458                }
3459            })
3460            .url_relative(UrlRelative::RewriteWithBase(
3461                Url::parse("https://example.com/base/#").unwrap(),
3462            ))
3463            .clean(fragment)
3464            .to_string();
3465        assert_eq!(
3466            result,
3467            r#"Test<img alt="altalt" src="https://example.com/base/rewrite">"#
3468        );
3469    }
3470
3471    #[test]
3472    fn rewrite_url_relative_no_rel() {
3473        let fragment = "<a href=test>Test</a>";
3474        let result = Builder::new()
3475            .url_relative(UrlRelative::RewriteWithBase(
3476                Url::parse("http://example.com/").unwrap(),
3477            ))
3478            .link_rel(None)
3479            .clean(fragment)
3480            .to_string();
3481        assert_eq!(result, "<a href=\"http://example.com/test\">Test</a>");
3482    }
3483    #[test]
3484    fn deny_url_relative() {
3485        let fragment = "<a href=test>Test</a>";
3486        let result = Builder::new()
3487            .url_relative(UrlRelative::Deny)
3488            .clean(fragment)
3489            .to_string();
3490        assert_eq!(result, "<a rel=\"noopener noreferrer\">Test</a>");
3491    }
3492    #[test]
3493    fn replace_rel() {
3494        let fragment = "<a href=test rel=\"garbage\">Test</a>";
3495        let result = Builder::new()
3496            .url_relative(UrlRelative::PassThrough)
3497            .clean(fragment)
3498            .to_string();
3499        assert_eq!(
3500            result,
3501            "<a href=\"test\" rel=\"noopener noreferrer\">Test</a>"
3502        );
3503    }
3504    #[test]
3505    fn consider_rel_still_banned() {
3506        let fragment = "<a href=test rel=\"garbage\">Test</a>";
3507        let result = Builder::new()
3508            .url_relative(UrlRelative::PassThrough)
3509            .link_rel(None)
3510            .clean(fragment)
3511            .to_string();
3512        assert_eq!(result, "<a href=\"test\">Test</a>");
3513    }
3514    #[test]
3515    fn object_data() {
3516        let fragment = "<span data=\"javascript:evil()\">Test</span>\
3517                        <object data=\"javascript:evil()\"></object>M";
3518        let expected = r#"<span data="javascript:evil()">Test</span><object></object>M"#;
3519        let result = Builder::new()
3520            .tags(hashset!["span", "object"])
3521            .generic_attributes(hashset!["data"])
3522            .clean(fragment)
3523            .to_string();
3524        assert_eq!(result, expected);
3525    }
3526    #[test]
3527    fn remove_attributes() {
3528        let fragment = "<table border=\"1\"><tr></tr></table>";
3529        let result = Builder::new().clean(fragment);
3530        assert_eq!(
3531            result.to_string(),
3532            "<table><tbody><tr></tr></tbody></table>"
3533        );
3534    }
3535    #[test]
3536    fn quotes_in_attrs() {
3537        let fragment = "<b title='\"'>contents</b>";
3538        let result = clean(fragment);
3539        assert_eq!(result, "<b title=\"&quot;\">contents</b>");
3540    }
3541    #[test]
3542    #[should_panic]
3543    fn panic_if_rel_is_allowed_and_replaced_generic() {
3544        Builder::new()
3545            .link_rel(Some("noopener noreferrer"))
3546            .generic_attributes(hashset!["rel"])
3547            .clean("something");
3548    }
3549    #[test]
3550    #[should_panic]
3551    fn panic_if_rel_is_allowed_and_replaced_a() {
3552        Builder::new()
3553            .link_rel(Some("noopener noreferrer"))
3554            .tag_attributes(hashmap![
3555                "a" => hashset!["rel"],
3556            ])
3557            .clean("something");
3558    }
3559    #[test]
3560    fn no_panic_if_rel_is_allowed_and_replaced_span() {
3561        Builder::new()
3562            .link_rel(Some("noopener noreferrer"))
3563            .tag_attributes(hashmap![
3564                "span" => hashset!["rel"],
3565            ])
3566            .clean("<span rel=\"what\">s</span>");
3567    }
3568    #[test]
3569    fn no_panic_if_rel_is_allowed_and_not_replaced_generic() {
3570        Builder::new()
3571            .link_rel(None)
3572            .generic_attributes(hashset!["rel"])
3573            .clean("<a rel=\"what\">s</a>");
3574    }
3575    #[test]
3576    fn no_panic_if_rel_is_allowed_and_not_replaced_a() {
3577        Builder::new()
3578            .link_rel(None)
3579            .tag_attributes(hashmap![
3580                "a" => hashset!["rel"],
3581            ])
3582            .clean("<a rel=\"what\">s</a>");
3583    }
3584    #[test]
3585    fn dont_close_void_elements() {
3586        let fragment = "<br>";
3587        let result = clean(fragment);
3588        assert_eq!(result.to_string(), "<br>");
3589    }
3590    #[should_panic]
3591    #[test]
3592    fn panic_on_allowed_classes_tag_attributes() {
3593        let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3594        Builder::new()
3595            .link_rel(None)
3596            .tag_attributes(hashmap![
3597                "p" => hashset!["class"],
3598                "a" => hashset!["class"],
3599            ])
3600            .allowed_classes(hashmap![
3601                "p" => hashset!["foo", "bar"],
3602                "a" => hashset!["baz"],
3603            ])
3604            .clean(fragment);
3605    }
3606    #[should_panic]
3607    #[test]
3608    fn panic_on_allowed_classes_generic_attributes() {
3609        let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3610        Builder::new()
3611            .link_rel(None)
3612            .generic_attributes(hashset!["class", "href", "some-foo"])
3613            .allowed_classes(hashmap![
3614                "p" => hashset!["foo", "bar"],
3615                "a" => hashset!["baz"],
3616            ])
3617            .clean(fragment);
3618    }
3619    #[test]
3620    fn remove_non_allowed_classes() {
3621        let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3622        let result = Builder::new()
3623            .link_rel(None)
3624            .allowed_classes(hashmap![
3625                "p" => hashset!["foo", "bar"],
3626                "a" => hashset!["baz"],
3627            ])
3628            .clean(fragment);
3629        assert_eq!(
3630            result.to_string(),
3631            "<p class=\"foo bar\"><a class=\"baz\">Hey</a></p>"
3632        );
3633    }
3634    #[test]
3635    fn remove_non_allowed_classes_with_tag_class() {
3636        let fragment = "<p class=\"foo bar\"><a class=\"baz bleh\">Hey</a></p>";
3637        let result = Builder::new()
3638            .link_rel(None)
3639            .tag_attributes(hashmap![
3640                "div" => hashset!["class"],
3641            ])
3642            .allowed_classes(hashmap![
3643                "p" => hashset!["foo", "bar"],
3644                "a" => hashset!["baz"],
3645            ])
3646            .clean(fragment);
3647        assert_eq!(
3648            result.to_string(),
3649            "<p class=\"foo bar\"><a class=\"baz\">Hey</a></p>"
3650        );
3651    }
3652    #[test]
3653    fn allowed_classes_ascii_whitespace() {
3654        // According to https://infra.spec.whatwg.org/#ascii-whitespace,
3655        // TAB (\t), LF (\n), FF (\x0C), CR (\x0D) and SPACE (\x20) are
3656        // considered to be ASCII whitespace. Unicode whitespace characters
3657        // and VT (\x0B) aren't ASCII whitespace.
3658        let fragment = "<p class=\"a\tb\nc\x0Cd\re f\x0B g\u{2000}\">";
3659        let result = Builder::new()
3660            .allowed_classes(hashmap![
3661                "p" => hashset!["a", "b", "c", "d", "e", "f", "g"],
3662            ])
3663            .clean(fragment);
3664        assert_eq!(result.to_string(), r#"<p class="a b c d e"></p>"#);
3665    }
3666    #[test]
3667    fn remove_non_allowed_attributes_with_tag_attribute_values() {
3668        let fragment = "<p data-label=\"baz\" name=\"foo\"></p>";
3669        let result = Builder::new()
3670            .tag_attribute_values(hashmap![
3671                "p" => hashmap![
3672                    "data-label" => hashset!["bar"],
3673                ],
3674            ])
3675            .tag_attributes(hashmap![
3676                "p" => hashset!["name"],
3677            ])
3678            .clean(fragment);
3679        assert_eq!(result.to_string(), "<p name=\"foo\"></p>",);
3680    }
3681    #[test]
3682    fn keep_allowed_attributes_with_tag_attribute_values() {
3683        let fragment = "<p data-label=\"bar\" name=\"foo\"></p>";
3684        let result = Builder::new()
3685            .tag_attribute_values(hashmap![
3686                "p" => hashmap![
3687                    "data-label" => hashset!["bar"],
3688                ],
3689            ])
3690            .tag_attributes(hashmap![
3691                "p" => hashset!["name"],
3692            ])
3693            .clean(fragment);
3694        assert_eq!(
3695            result.to_string(),
3696            "<p data-label=\"bar\" name=\"foo\"></p>",
3697        );
3698    }
3699    #[test]
3700    fn tag_attribute_values_case_insensitive() {
3701        let fragment = "<input type=\"CHECKBOX\" name=\"foo\">";
3702        let result = Builder::new()
3703            .tags(hashset!["input"])
3704            .tag_attribute_values(hashmap![
3705                "input" => hashmap![
3706                    "type" => hashset!["checkbox"],
3707                ],
3708            ])
3709            .tag_attributes(hashmap![
3710                "input" => hashset!["name"],
3711            ])
3712            .clean(fragment);
3713        assert_eq!(result.to_string(), "<input type=\"CHECKBOX\" name=\"foo\">",);
3714    }
3715    #[test]
3716    fn set_tag_attribute_values() {
3717        let fragment = "<a href=\"https://example.com/\">Link</a>";
3718        let result = Builder::new()
3719            .link_rel(None)
3720            .add_tag_attributes("a", &["target"])
3721            .set_tag_attribute_value("a", "target", "_blank")
3722            .clean(fragment);
3723        assert_eq!(
3724            result.to_string(),
3725            "<a href=\"https://example.com/\" target=\"_blank\">Link</a>",
3726        );
3727    }
3728    #[test]
3729    fn update_existing_set_tag_attribute_values() {
3730        let fragment = "<a target=\"bad\" href=\"https://example.com/\">Link</a>";
3731        let result = Builder::new()
3732            .link_rel(None)
3733            .add_tag_attributes("a", &["target"])
3734            .set_tag_attribute_value("a", "target", "_blank")
3735            .clean(fragment);
3736        assert_eq!(
3737            result.to_string(),
3738            "<a target=\"_blank\" href=\"https://example.com/\">Link</a>",
3739        );
3740    }
3741    #[test]
3742    fn unwhitelisted_set_tag_attribute_values() {
3743        let fragment = "<span>hi</span><my-elem>";
3744        let result = Builder::new()
3745            .set_tag_attribute_value("my-elem", "my-attr", "val")
3746            .clean(fragment);
3747        assert_eq!(result.to_string(), "<span>hi</span>",);
3748    }
3749    #[test]
3750    fn remove_entity_link() {
3751        let fragment = "<a href=\"&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61\
3752                        &#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29\">Click me!</a>";
3753        let result = clean(fragment);
3754        assert_eq!(
3755            result.to_string(),
3756            "<a rel=\"noopener noreferrer\">Click me!</a>"
3757        );
3758    }
3759    #[test]
3760    fn remove_relative_url_evaluate() {
3761        fn is_absolute_path(url: &str) -> bool {
3762            let u = url.as_bytes();
3763            // `//a/b/c` is "protocol-relative", meaning "a" is a hostname
3764            // `/a/b/c` is an absolute path, and what we want to do stuff to.
3765            u.first() == Some(&b'/') && u.get(1) != Some(&b'/')
3766        }
3767        fn is_banned(url: &str) -> bool {
3768            let u = url.as_bytes();
3769            u.first() == Some(&b'b') && u.get(1) == Some(&b'a')
3770        }
3771        fn evaluate(url: &str) -> Option<Cow<'_, str>> {
3772            if is_absolute_path(url) {
3773                Some(Cow::Owned(String::from("/root") + url))
3774            } else if is_banned(url) {
3775                None
3776            } else {
3777                Some(Cow::Borrowed(url))
3778            }
3779        }
3780        let a = Builder::new()
3781            .url_relative(UrlRelative::Custom(Box::new(evaluate)))
3782            .clean("<a href=banned>banned</a><a href=/test/path>fixed</a><a href=path>passed</a><a href=http://google.com/>skipped</a>")
3783            .to_string();
3784        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>");
3785    }
3786    #[test]
3787    fn remove_relative_url_evaluate_b() {
3788        fn is_absolute_path(url: &str) -> bool {
3789            let u = url.as_bytes();
3790            // `//a/b/c` is "protocol-relative", meaning "a" is a hostname
3791            // `/a/b/c` is an absolute path, and what we want to do stuff to.
3792            u.first() == Some(&b'/') && u.get(1) != Some(&b'/')
3793        }
3794        fn is_banned(url: &str) -> bool {
3795            let u = url.as_bytes();
3796            u.first() == Some(&b'b') && u.get(1) == Some(&b'a')
3797        }
3798        fn evaluate(url: &str) -> Option<Cow<'_, str>> {
3799            if is_absolute_path(url) {
3800                Some(Cow::Owned(String::from("/root") + url))
3801            } else if is_banned(url) {
3802                None
3803            } else {
3804                Some(Cow::Borrowed(url))
3805            }
3806        }
3807        let a = Builder::new()
3808            .url_relative(UrlRelative::Custom(Box::new(evaluate)))
3809            .clean("<a href=banned>banned</a><a href=banned title=test>banned</a><a title=test href=banned>banned</a>")
3810            .to_string();
3811        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>");
3812    }
3813    #[test]
3814    fn remove_relative_url_evaluate_c() {
3815        // Don't run on absolute URLs.
3816        fn evaluate(_: &str) -> Option<Cow<'_, str>> {
3817            return Some(Cow::Owned(String::from("invalid")));
3818        }
3819        let a = Builder::new()
3820            .url_relative(UrlRelative::Custom(Box::new(evaluate)))
3821            .clean("<a href=\"https://www.google.com/\">google</a>")
3822            .to_string();
3823        assert_eq!(
3824            a,
3825            "<a href=\"https://www.google.com/\" rel=\"noopener noreferrer\">google</a>"
3826        );
3827    }
3828    #[test]
3829    fn clean_children_of_bad_element() {
3830        let fragment = "<bad><evil>a</evil>b</bad>";
3831        let result = Builder::new().clean(fragment);
3832        assert_eq!(result.to_string(), "ab");
3833    }
3834    #[test]
3835    fn reader_input() {
3836        let fragment = b"an <script>evil()</script> example";
3837        let result = Builder::new().clean_from_reader(&fragment[..]);
3838        assert!(result.is_ok());
3839        assert_eq!(result.unwrap().to_string(), "an  example");
3840    }
3841    #[test]
3842    fn reader_non_utf8() {
3843        let fragment = b"non-utf8 \xF0\x90\x80string";
3844        let result = Builder::new().clean_from_reader(&fragment[..]);
3845        assert!(result.is_ok());
3846        assert_eq!(result.unwrap().to_string(), "non-utf8 \u{fffd}string");
3847    }
3848    #[test]
3849    fn display_impl() {
3850        let fragment = r#"a <a>link</a>"#;
3851        let result = Builder::new().link_rel(None).clean(fragment);
3852        assert_eq!(format!("{}", result), "a <a>link</a>");
3853    }
3854    #[test]
3855    fn debug_impl() {
3856        let fragment = r#"a <a>link</a>"#;
3857        let result = Builder::new().link_rel(None).clean(fragment);
3858        assert_eq!(format!("{:?}", result), "Document(a <a>link</a>)");
3859    }
3860    #[cfg(ammonia_unstable)]
3861    #[test]
3862    fn to_dom_node() {
3863        let fragment = r#"a <a>link</a>"#;
3864        let result = Builder::new().link_rel(None).clean(fragment);
3865        let _node = result.to_dom_node();
3866    }
3867    #[test]
3868    fn string_from_document() {
3869        let fragment = r#"a <a>link"#;
3870        let result = String::from(Builder::new().link_rel(None).clean(fragment));
3871        assert_eq!(format!("{}", result), "a <a>link</a>");
3872    }
3873    fn require_sync<T: Sync>(_: T) {}
3874    fn require_send<T: Send>(_: T) {}
3875    #[test]
3876    fn require_sync_and_send() {
3877        require_sync(Builder::new());
3878        require_send(Builder::new());
3879    }
3880    #[test]
3881    fn id_prefixed() {
3882        let fragment = "<a id=\"hello\"></a><b id=\"hello\"></a>";
3883        let result = String::from(
3884            Builder::new()
3885                .tag_attributes(hashmap![
3886                    "a" => hashset!["id"],
3887                ])
3888                .id_prefix(Some("prefix-"))
3889                .clean(fragment),
3890        );
3891        assert_eq!(
3892            result.to_string(),
3893            "<a id=\"prefix-hello\" rel=\"noopener noreferrer\"></a><b></b>"
3894        );
3895    }
3896    #[test]
3897    fn id_already_prefixed() {
3898        let fragment = "<a id=\"prefix-hello\"></a>";
3899        let result = String::from(
3900            Builder::new()
3901                .tag_attributes(hashmap![
3902                    "a" => hashset!["id"],
3903                ])
3904                .id_prefix(Some("prefix-"))
3905                .clean(fragment),
3906        );
3907        assert_eq!(
3908            result.to_string(),
3909            "<a id=\"prefix-hello\" rel=\"noopener noreferrer\"></a>"
3910        );
3911    }
3912    #[test]
3913    fn clean_content_tags() {
3914        let fragment = "<script type=\"text/javascript\"><a>Hello!</a></script>";
3915        let result = String::from(
3916            Builder::new()
3917                .clean_content_tags(hashset!["script"])
3918                .clean(fragment),
3919        );
3920        assert_eq!(result.to_string(), "");
3921    }
3922    #[test]
3923    fn only_clean_content_tags() {
3924        let fragment = "<em>This is</em><script><a>Hello!</a></script><p>still here!</p>";
3925        let result = String::from(
3926            Builder::new()
3927                .clean_content_tags(hashset!["script"])
3928                .clean(fragment),
3929        );
3930        assert_eq!(result.to_string(), "<em>This is</em><p>still here!</p>");
3931    }
3932    #[test]
3933    fn clean_removed_default_tag() {
3934        let fragment = "<em>This is</em><script><a>Hello!</a></script><p>still here!</p>";
3935        let result = String::from(
3936            Builder::new()
3937                .rm_tags(hashset!["a"])
3938                .rm_tag_attributes("a", hashset!["href", "hreflang"])
3939                .clean_content_tags(hashset!["script"])
3940                .clean(fragment),
3941        );
3942        assert_eq!(result.to_string(), "<em>This is</em><p>still here!</p>");
3943    }
3944    #[test]
3945    #[should_panic]
3946    fn panic_on_clean_content_tag_attribute() {
3947        Builder::new()
3948            .rm_tags(std::iter::once("a"))
3949            .clean_content_tags(hashset!["a"])
3950            .clean("");
3951    }
3952    #[test]
3953    #[should_panic]
3954    fn panic_on_clean_content_tag() {
3955        Builder::new().clean_content_tags(hashset!["a"]).clean("");
3956    }
3957
3958    #[test]
3959    fn clean_text_test() {
3960        assert_eq!(
3961            clean_text("<this> is <a test function"),
3962            "&lt;this&gt;&#32;is&#32;&lt;a&#32;test&#32;function"
3963        );
3964    }
3965
3966    #[test]
3967    fn clean_text_spaces_test() {
3968        assert_eq!(clean_text("\x09\x0a\x0c\x20"), "&#9;&#10;&#12;&#32;");
3969    }
3970
3971    #[test]
3972    fn ns_svg() {
3973        // https://github.com/cure53/DOMPurify/pull/495
3974        let fragment = r##"<svg><iframe><a title="</iframe><img src onerror=alert(1)>">test"##;
3975        let result = String::from(Builder::new().add_tags(&["iframe"]).clean(fragment));
3976        assert_eq!(result.to_string(), "");
3977
3978        let fragment = "<svg><iframe>remove me</iframe></svg><iframe>keep me</iframe>";
3979        let result = String::from(Builder::new().add_tags(&["iframe"]).clean(fragment));
3980        assert_eq!(result.to_string(), "<iframe>keep me</iframe>");
3981
3982        let fragment = "<svg><a>remove me</a></svg><iframe>keep me</iframe>";
3983        let result = String::from(Builder::new().add_tags(&["iframe"]).clean(fragment));
3984        assert_eq!(result.to_string(), "<iframe>keep me</iframe>");
3985
3986        let fragment = "<svg><a>keep me</a></svg><iframe>keep me</iframe>";
3987        let result = String::from(Builder::new().add_tags(&["iframe", "svg"]).clean(fragment));
3988        assert_eq!(
3989            result.to_string(),
3990            "<svg><a rel=\"noopener noreferrer\">keep me</a></svg><iframe>keep me</iframe>"
3991        );
3992    }
3993
3994    #[test]
3995    fn ns_svg_2() {
3996        let fragment = "<svg><foreignObject><table><path><xmp><!--</xmp><img title'--&gt;&lt;img src=1 onerror=alert(1)&gt;'>";
3997        let result =  Builder::default()
3998            .strip_comments(false)
3999            .add_tags(&["svg","foreignObject","table","path","xmp"])
4000            .clean(fragment);
4001        assert_eq!(
4002            result.to_string(),
4003            "<svg><foreignObject><table></table></foreignObject></svg>"
4004        );
4005    }
4006
4007    #[test]
4008    fn ns_mathml() {
4009        // https://github.com/cure53/DOMPurify/pull/495
4010        let fragment = "<mglyph></mglyph>";
4011        let result = String::from(
4012            Builder::new()
4013                .add_tags(&["math", "mtext", "mglyph"])
4014                .clean(fragment),
4015        );
4016        assert_eq!(result.to_string(), "");
4017        let fragment = "<math><mtext><div><mglyph>";
4018        let result = String::from(
4019            Builder::new()
4020                .add_tags(&["math", "mtext", "mglyph"])
4021                .clean(fragment),
4022        );
4023        assert_eq!(
4024            result.to_string(),
4025            "<math><mtext><div></div></mtext></math>"
4026        );
4027        let fragment = "<math><mtext><mglyph>";
4028        let result = String::from(
4029            Builder::new()
4030                .add_tags(&["math", "mtext", "mglyph"])
4031                .clean(fragment),
4032        );
4033        assert_eq!(
4034            result.to_string(),
4035            "<math><mtext><mglyph></mglyph></mtext></math>"
4036        );
4037    }
4038
4039    #[test]
4040    fn ns_mathml_2() {
4041        let fragment = "<math><mtext><table><mglyph><xmp><!--</xmp><img title='--&gt;&lt;img src=1 onerror=alert(1)&gt;'>";
4042        let result =  Builder::default()
4043            .strip_comments(false)
4044            .add_tags(&["math","mtext","table","mglyph","xmp"])
4045            .clean(fragment);
4046        assert_eq!(
4047            result.to_string(),
4048            "<math><mtext><table></table></mtext></math>"
4049        );
4050    }
4051
4052    #[test]
4053    fn ns_mathml_3() {
4054        // try without the attr
4055        let fragment = "<math><annotation-xml encoding='text/html'><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(&["math","annotation-xml","table","mglyph","xmp"])
4059            .clean(fragment);
4060        assert_eq!(
4061            result.to_string(),
4062            "<math><annotation-xml></annotation-xml></math>"
4063        );
4064        // now with the attr
4065        let fragment = "<math><annotation-xml encoding='text/html'><xmp><!--</xmp><img title='--&gt;&lt;img src=1 onerror=alert(1)&gt;'>";
4066        let result =  Builder::default()
4067            .strip_comments(false)
4068            .add_tags(&["math","annotation-xml","table","mglyph","xmp"])
4069            .add_tag_attribute_values("annotation-xml", "encoding", ["text/html"])
4070            .clean(fragment);
4071        assert_eq!(
4072            result.to_string(),
4073            // yes, I tried it in Firefox, and the script didn't run
4074            r#"<math><annotation-xml encoding="text/html"><xmp><!--</xmp><img title="--&gt;&lt;img src=1 onerror=alert(1)&gt;"></annotation-xml></math>"#
4075        );
4076        // now with a tweaked attr
4077        let fragment = "<math><annotation-xml encoding='image/svg+xml'><xmp><!--</xmp><img title='--&gt;&lt;img src=1 onerror=alert(1)&gt;'>";
4078        let result =  Builder::default()
4079            .strip_comments(false)
4080            .add_tags(&["math","annotation-xml","table","mglyph","xmp"])
4081            .add_tag_attribute_values("annotation-xml", "encoding", ["image/svg+xml"])
4082            .clean(fragment);
4083        assert_eq!(
4084            result.to_string(),
4085            r#"<math><annotation-xml encoding="image/svg+xml"></annotation-xml></math>"#
4086        );
4087        // now with actual SVG
4088        let fragment = "<math><annotation-xml encoding='image/svg+xml'><svg>";
4089        let result =  Builder::default()
4090            .strip_comments(false)
4091            .add_tags(&["math","annotation-xml","svg"])
4092            .add_tag_attribute_values("annotation-xml", "encoding", ["image/svg+xml"])
4093            .clean(fragment);
4094        assert_eq!(
4095            result.to_string(),
4096            r#"<math><annotation-xml encoding="image/svg+xml"><svg></svg></annotation-xml></math>"#
4097        );
4098    }
4099
4100    #[test]
4101    fn ns_svg_animate_url_attr() {
4102        let fragment = r##"
4103            <svg>
4104                <a>
4105                    <animate attributeName="xss" values="http://example.com"></animate>
4106                    <animate attributeName="href" values="http://example.com"></animate>
4107                    <animate attributeName="href" values="http://example.com;/test"></animate>
4108                    <animate attributeName="href" values="http://example.com;/test;javascript:xss"></animate>
4109                    <animate attributeName="href" from="http://example.com" to="http://example.com"></animate>
4110                    <animate attributeName="href" from="javascript:xss" to="http://example.com"></animate>
4111                    <animate attributeName="href" from="http://example.com" to="javascript:xss"></animate>
4112                    <animate attributeName="href" from="http://example.com" to="./test2"></animate>
4113                    <animate attributeName="href" from="./test2" to="http://example.com"></animate>
4114                </a>
4115            </svg>
4116        "##;
4117        let filtered = r##"
4118            <svg>
4119                <a rel="noopener noreferrer">
4120                    
4121                    <animate attributeName="href" values="http://example.com"></animate>
4122                    <animate attributeName="href" values="http://example.com;http://notriddle.com/test"></animate>
4123                    
4124                    <animate attributeName="href" from="http://example.com" to="http://example.com"></animate>
4125                    
4126                    
4127                    <animate attributeName="href" from="http://example.com" to="http://notriddle.com/test2"></animate>
4128                    <animate attributeName="href" from="http://notriddle.com/test2" to="http://example.com"></animate>
4129                </a>
4130            </svg>
4131        "##;
4132        let result =  Builder::default()
4133            .add_tags(&["svg","a","animate"])
4134            .add_tag_attributes("animate", ["attributeName","values","from","to"])
4135            .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4136            .clean(fragment);
4137        assert_eq!(
4138            result.to_string(),
4139            filtered,
4140        );
4141    }
4142
4143    #[test]
4144    fn ns_svg_set_url_attr() {
4145        let fragment = r##"
4146            <svg>
4147                <a>
4148                    <set attributeName="href" to="./test2"></set>
4149                    <set attributeName="href" to="http://example.com"></set>
4150                    <set attributeName="href" to="javascript:xss"></set>
4151                </a>
4152            </svg>
4153        "##;
4154        let filtered = r##"
4155            <svg>
4156                <a rel="noopener noreferrer">
4157                    <set attributeName="href" to="http://notriddle.com/test2"></set>
4158                    <set attributeName="href" to="http://example.com"></set>
4159                    
4160                </a>
4161            </svg>
4162        "##;
4163        let result =  Builder::default()
4164            .add_tags(&["svg","a","set"])
4165            .add_tag_attributes("set", ["attributeName","values","from","to"])
4166            .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4167            .clean(fragment);
4168        assert_eq!(
4169            result.to_string(),
4170            filtered,
4171        );
4172    }
4173
4174    #[test]
4175    fn ns_svg_set_url_xlink_attr() {
4176        let fragment = r##"
4177            <svg>
4178                <a>
4179                    <set attributeName="xlink:href" to="./test2"></set>
4180                    <set attributeName="xlink:href" to="http://example.com"></set>
4181                    <set attributeName="xlink:href" to="javascript:xss"></set>
4182                </a>
4183            </svg>
4184        "##;
4185        let filtered = r##"
4186            <svg>
4187                <a rel="noopener noreferrer">
4188                    <set attributeName="xlink:href" to="http://notriddle.com/test2"></set>
4189                    <set attributeName="xlink:href" to="http://example.com"></set>
4190                    
4191                </a>
4192            </svg>
4193        "##;
4194        let result =  Builder::default()
4195            .add_tags(&["svg","a","set"])
4196            .add_tag_attributes("a", ["xlink:href"])
4197            .add_tag_attributes("set", ["attributeName","values","from","to"])
4198            .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4199            .clean(fragment);
4200        assert_eq!(
4201            result.to_string(),
4202            filtered,
4203        );
4204    }
4205
4206    #[test]
4207    fn ns_svg_set_url_attr_non_path() {
4208        let fragment = r##"
4209            <svg>
4210                <a>
4211                    <set attributeName="href" to="./test2"></set>
4212                    <set attributeName="href" to="http://example.com"></set>
4213                    <set attributeName="href" to="javascript:xss"></set>
4214                </a>
4215            </svg>
4216        "##;
4217        let filtered = r##"
4218            <svg>
4219                <a rel="noopener noreferrer">
4220                    <set attributeName="href"></set>
4221                    <set attributeName="href" to="http://example.com"></set>
4222                    
4223                </a>
4224            </svg>
4225        "##;
4226        let result =  Builder::default()
4227            .add_tags(&["svg","a","set"])
4228            .add_tag_attributes("set", ["attributeName","values","from","to"])
4229            .url_relative(UrlRelative::RewriteWithBase(Url::parse("magnet:?xt=urn:btih:da39a3ee5e6b4b0d3255bfef95601890afd80709&xt=urn:btmh:1220e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855").unwrap()))
4230            .clean(fragment);
4231        assert_eq!(
4232            result.to_string(),
4233            filtered,
4234        );
4235    }
4236
4237    #[test]
4238    fn ns_svg_animate_set_attr() {
4239        let fragment = r##"
4240            <svg>
4241                <a>
4242                    <animate attributeName="x" values="1;2;3"></animate>
4243                    <animate attributeName="x" from="1" to="2"></animate>
4244                    <animate attributeName="x" from="1"></animate>
4245                    <animate attributeName="x" to="2"></animate>
4246                    <animate attributeName="y" values="1;2;3"></animate>
4247                    <animate attributeName="y" from="1" to="2"></animate>
4248                    <animate attributeName="y" from="1"></animate>
4249                    <animate attributeName="y" to="2"></animate>
4250                </a>
4251            </svg>
4252        "##;
4253        let filtered = r##"
4254            <svg>
4255                <a x="0" rel="noopener noreferrer">
4256                    
4257                    
4258                    
4259                    
4260                    <animate attributeName="y" values="1;2;3"></animate>
4261                    <animate attributeName="y" from="1" to="2"></animate>
4262                    <animate attributeName="y" from="1"></animate>
4263                    <animate attributeName="y" to="2"></animate>
4264                </a>
4265            </svg>
4266        "##;
4267        let result =  Builder::default()
4268            .add_tags(&["svg","a","animate"])
4269            .add_tag_attributes("animate", ["attributeName","values","from","to"])
4270            .add_tag_attributes("a", ["x", "y"])
4271            .set_tag_attribute_value("a", "x", "0")
4272            .clean(fragment);
4273        assert_eq!(
4274            result.to_string(),
4275            filtered,
4276        );
4277    }
4278
4279    #[test]
4280    fn ns_svg_animate_attr_filter() {
4281        let fragment = r##"
4282            <svg>
4283                <a>
4284                    <animate attributeName="x" values="1;2;3"></animate>
4285                    <animate attributeName="x" from="1" to="2"></animate>
4286                    <animate attributeName="x" from="1"></animate>
4287                    <animate attributeName="x" to="2"></animate>
4288                    <animate attributeName="y" values="1;2;3"></animate>
4289                    <animate attributeName="y" from="1" to="2"></animate>
4290                    <animate attributeName="y" from="1"></animate>
4291                    <animate attributeName="y" to="2"></animate>
4292                </a>
4293            </svg>
4294        "##;
4295        let filtered = r##"
4296            <svg>
4297                <a rel="noopener noreferrer">
4298                    <animate attributeName="x" values="0;0;0"></animate>
4299                    <animate attributeName="x" from="0" to="0"></animate>
4300                    <animate attributeName="x" from="0"></animate>
4301                    <animate attributeName="x" to="0"></animate>
4302                    <animate attributeName="y"></animate>
4303                    <animate attributeName="y" to="2"></animate>
4304                    <animate attributeName="y"></animate>
4305                    <animate attributeName="y" to="2"></animate>
4306                </a>
4307            </svg>
4308        "##;
4309        let result =  Builder::default()
4310            .add_tags(&["svg","a","animate"])
4311            .add_tag_attributes("animate", ["attributeName","values","from","to"])
4312            .add_tag_attributes("a", ["x", "y"])
4313            .attribute_filter(|_tag, key, value| Some(if key == "x" {
4314                "0".into()
4315            } else if key == "y" && value == "1" {
4316                return None;
4317            } else {
4318                value.into()
4319            }))
4320            .clean(fragment);
4321        assert_eq!(
4322            result.to_string(),
4323            filtered,
4324        );
4325    }
4326
4327    #[test]
4328    fn ns_svg_animate_allowed_classes() {
4329        let fragment = r##"
4330            <svg>
4331                <a>
4332                    <animate attributeName="class" values="a b c;a b c d;a b"></animate>
4333                    <animate attributeName="class" from="a b c" to="a b c d"></animate>
4334                    <animate attributeName="class" from="a b c d"></animate>
4335                    <animate attributeName="class" to="a d c"></animate>
4336                </a>
4337            </svg>
4338        "##;
4339        let filtered = r##"
4340            <svg>
4341                <a rel="noopener noreferrer">
4342                    <animate attributeName="class" values="a b c;a b c;a b"></animate>
4343                    <animate attributeName="class" from="a b c" to="a b c"></animate>
4344                    <animate attributeName="class" from="a b c"></animate>
4345                    <animate attributeName="class" to="a c"></animate>
4346                </a>
4347            </svg>
4348        "##;
4349        let result =  Builder::default()
4350            .add_tags(&["svg","a","animate"])
4351            .add_tag_attributes("animate", ["attributeName","values","from","to"])
4352            .add_allowed_classes("a", ["a", "b", "c"])
4353            .clean(fragment);
4354        assert_eq!(
4355            result.to_string(),
4356            filtered,
4357        );
4358    }
4359
4360    #[test]
4361    fn ns_svg_animate_allowed_styles() {
4362        let fragment = r##"
4363            <svg>
4364                <a>
4365                    <animate attributeName="style" values="background:red;color:blue"></animate>
4366                    <animate attributeName="style" from="background:red;text-decoration:none" to="color: blue;background:red"></animate>
4367                    <animate attributeName="style" from="background:red;text-decoration:none"></animate>
4368                    <animate attributeName="style" to="text-decoration:none"></animate>
4369                </a>
4370            </svg>
4371        "##;
4372        let filtered = r##"
4373            <svg>
4374                <a rel="noopener noreferrer">
4375                    <animate attributeName="style" values="background:red;"></animate>
4376                    <animate attributeName="style" from="background:red" to="background:red"></animate>
4377                    <animate attributeName="style" from="background:red"></animate>
4378                    <animate attributeName="style" to=""></animate>
4379                </a>
4380            </svg>
4381        "##;
4382        let result =  Builder::default()
4383            .add_tags(&["svg","a","animate"])
4384            .add_tag_attributes("animate", ["attributeName","values","from","to"])
4385            .add_tag_attributes("a", ["style"])
4386            .filter_style_properties(["background"].into())
4387            .clean(fragment);
4388        assert_eq!(
4389            result.to_string(),
4390            filtered,
4391        );
4392    }
4393
4394    #[test]
4395    fn ns_svg_animate_url_attr_href() {
4396        let fragment = r##"
4397            <svg>
4398                <a id="x1">
4399                </a>
4400                <animate href="#x1" attributeName="xss" values="http://example.com"></animate>
4401                <animate href="#x1" attributeName="href" values="http://example.com"></animate>
4402                <animate href="#x2" attributeName="href" values="http://example.com;/test"></animate>
4403                <animate href="#x2" attributeName="href" values="http://example.com;/test;javascript:xss"></animate>
4404                <animate href="#x2" attributeName="href" from="http://example.com" to="http://example.com"></animate>
4405                <animate href="#x2" attributeName="href" from="javascript:xss" to="http://example.com"></animate>
4406                <animate href="#x2" attributeName="href" from="http://example.com" to="javascript:xss"></animate>
4407                <animate href="#x2" attributeName="href" from="http://example.com" to="./test2"></animate>
4408                <animate href="#x2" attributeName="href" from="./test2" to="http://example.com"></animate>
4409            </svg>
4410        "##;
4411        let filtered = r##"
4412            <svg>
4413                <a rel="noopener noreferrer">
4414                </a>
4415                
4416                <animate href="#x1" attributeName="href" values="http://example.com"></animate>
4417                
4418                
4419                
4420                
4421                
4422                
4423                
4424            </svg>
4425        "##;
4426        let result =  Builder::default()
4427            .add_tags(&["svg","a","animate"])
4428            .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4429            .url_relative(UrlRelative::RewriteWithBase(Url::parse("http://notriddle.com").unwrap()))
4430            .clean(fragment);
4431        assert_eq!(
4432            result.to_string(),
4433            filtered,
4434        );
4435    }
4436
4437    #[test]
4438    fn ns_svg_animate_url_attr_href_depends_on_tag_name() {
4439        let fragment = r##"
4440            <object id="x2"></object>
4441            <svg>
4442                <g id="x1">
4443                </g>
4444                <animate href="#x1" attributeName="data" values="javascript:xss"></animate>
4445                <animate href="#x2" attributeName="data" values="javascript:xss"></animate>
4446            </svg>
4447        "##;
4448        let filtered = r##"
4449            <object id="x2"></object>
4450            <svg>
4451                <g id="x1">
4452                </g>
4453                <animate href="#x1" attributeName="data" values="javascript:xss"></animate>
4454                
4455            </svg>
4456        "##;
4457        let result =  Builder::default()
4458            .add_tags(&["svg","g","animate","object"])
4459            .add_tag_attributes("animate", ["attributeName","values","href"])
4460            .add_tag_attributes("g", ["data","id"])
4461            .add_tag_attributes("object", ["data","id"])
4462            .clean(fragment);
4463        assert_eq!(
4464            result.to_string(),
4465            filtered,
4466        );
4467    }
4468
4469    #[test]
4470    fn ns_svg_animate_set_attr_href() {
4471        let fragment = r##"
4472            <svg>
4473                <a id="x1">
4474                </a>
4475                <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4476                <animate href="#x1" attributeName="x" from="1" to="2"></animate>
4477                <animate href="#x1" attributeName="x" from="1"></animate>
4478                <animate href="#x1" attributeName="x" to="2"></animate>
4479                <animate href="#x1" attributeName="y" values="1;2;3"></animate>
4480                <animate href="#x1" attributeName="y" from="1" to="2"></animate>
4481                <animate href="#x1" attributeName="y" from="1"></animate>
4482                <animate href="#x2" attributeName="y" to="2"></animate>
4483            </svg>
4484        "##;
4485        let filtered = r##"
4486            <svg>
4487                <a id="x1" x="0" rel="noopener noreferrer">
4488                </a>
4489                
4490                
4491                
4492                
4493                <animate href="#x1" attributeName="y" values="1;2;3"></animate>
4494                <animate href="#x1" attributeName="y" from="1" to="2"></animate>
4495                <animate href="#x1" attributeName="y" from="1"></animate>
4496                
4497            </svg>
4498        "##;
4499        let result =  Builder::default()
4500            .add_tags(&["svg","a","animate"])
4501            .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4502            .add_tag_attributes("a", ["id", "x", "y"])
4503            .set_tag_attribute_value("a", "x", "0")
4504            .clean(fragment);
4505        assert_eq!(
4506            result.to_string(),
4507            filtered,
4508        );
4509    }
4510
4511    #[test]
4512    fn ns_svg_animate_attr_filter_href() {
4513        let fragment = r##"
4514            <svg>
4515                <a id="x1">
4516                </a>
4517                <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4518                <animate href="#x1" attributeName="x" from="1" to="2"></animate>
4519                <animate href="#x1" attributeName="x" from="1"></animate>
4520                <animate href="#x1" attributeName="x" to="2"></animate>
4521                <animate href="#x1" attributeName="y" values="1;2;3"></animate>
4522                <animate href="#x1" attributeName="y" from="1" to="2"></animate>
4523                <animate href="x1" attributeName="y" from="1"></animate>
4524                <animate href="#x2" attributeName="y" to="2"></animate>
4525            </svg>
4526        "##;
4527        let filtered = r##"
4528            <svg>
4529                <a id="x1" rel="noopener noreferrer">
4530                </a>
4531                <animate href="#x1" attributeName="x" values="0;0;0"></animate>
4532                <animate href="#x1" attributeName="x" from="0" to="0"></animate>
4533                <animate href="#x1" attributeName="x" from="0"></animate>
4534                <animate href="#x1" attributeName="x" to="0"></animate>
4535                <animate href="#x1" attributeName="y"></animate>
4536                <animate href="#x1" attributeName="y" to="2"></animate>
4537                
4538                
4539            </svg>
4540        "##;
4541        let result =  Builder::default()
4542            .add_tags(&["svg","a","animate"])
4543            .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4544            .add_tag_attributes("a", ["id", "x", "y"])
4545            .attribute_filter(|_tag, key, value| Some(if key == "x" {
4546                "0".into()
4547            } else if key == "y" && value == "1" {
4548                return None;
4549            } else {
4550                value.into()
4551            }))
4552            .clean(fragment);
4553        assert_eq!(
4554            result.to_string(),
4555            filtered,
4556        );
4557    }
4558
4559    #[test]
4560    fn ns_svg_animate_id_conflict() {
4561        let fragment = r##"
4562            <svg>
4563                <a id="x1">
4564                </a>
4565                <a id="x2">
4566                </a>
4567                <a id="x2">
4568                </a>
4569                <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4570                <animate href="#x2" attributeName="x" values="1;2;3"></animate>
4571                <animate href="#x3" attributeName="x" values="1;2;3"></animate>
4572            </svg>
4573        "##;
4574        let filtered = r##"
4575            <svg>
4576                <a id="x1" rel="noopener noreferrer">
4577                </a>
4578                <a id="x2" rel="noopener noreferrer">
4579                </a>
4580                <a id="x2" rel="noopener noreferrer">
4581                </a>
4582                <animate href="#x1" attributeName="x" values="1;2;3"></animate>
4583                
4584                
4585            </svg>
4586        "##;
4587        let result =  Builder::default()
4588            .add_tags(&["svg","a","animate"])
4589            .add_tag_attributes("animate", ["attributeName","values","from","to","href"])
4590            .add_tag_attributes("a", ["id", "x", "y"])
4591            .clean(fragment);
4592        assert_eq!(
4593            result.to_string(),
4594            filtered,
4595        );
4596    }
4597
4598    #[test]
4599    fn xml_processing_instruction() {
4600        // https://blog.slonser.info/posts/dompurify-node-type-confusion/
4601        let fragment = r##"<svg><?xml-stylesheet src='slonser' ?></svg>"##;
4602        let result = String::from(Builder::new().clean(fragment));
4603        assert_eq!(result.to_string(), "");
4604
4605        let fragment = r##"<svg><?xml-stylesheet src='slonser' ?></svg>"##;
4606        let result = String::from(Builder::new().add_tags(&["svg"]).clean(fragment));
4607        assert_eq!(result.to_string(), "<svg></svg>");
4608
4609        let fragment = r##"<svg><?xml-stylesheet ><img src=x onerror="alert('Ammonia bypassed!!!')"> ?></svg>"##;
4610        let result = String::from(Builder::new().add_tags(&["svg"]).clean(fragment));
4611        assert_eq!(result.to_string(), "<svg></svg><img src=\"x\"> ?&gt;");
4612    }
4613
4614    #[test]
4615    fn generic_attribute_prefixes() {
4616        let prefix_data = ["data-"];
4617        let prefix_code = ["code-"];
4618        let mut b = Builder::new();
4619        let mut hs: HashSet<&'_ str> = HashSet::new();
4620        hs.insert("data-");
4621        assert!(b.generic_attribute_prefixes.is_none());
4622        b.generic_attribute_prefixes(hs);
4623        assert!(b.generic_attribute_prefixes.is_some());
4624        assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4625        b.add_generic_attribute_prefixes(&prefix_data);
4626        assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4627        b.add_generic_attribute_prefixes(&prefix_code);
4628        assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 2);
4629        b.rm_generic_attribute_prefixes(&prefix_code);
4630        assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4631        b.rm_generic_attribute_prefixes(&prefix_code);
4632        assert_eq!(b.generic_attribute_prefixes.as_ref().unwrap().len(), 1);
4633        b.rm_generic_attribute_prefixes(&prefix_data);
4634        assert!(b.generic_attribute_prefixes.is_none());
4635    }
4636
4637    #[test]
4638    fn selectedcontent() {
4639        // https://github.com/servo/html5ever/issues/712
4640        let fragment1 = r#"<select><selectedcontent></selectedcontent><option>X"#;
4641        let fragment2 = r#"<select><selectedcontent></selectedcontent><option>X</option></select>"#;
4642        let expected = r#"<select><selectedcontent></selectedcontent><option>X</option></select>"#;
4643        assert_eq!(String::from(Builder::new().add_tags(&["select", "selectedcontent", "option"]).clean(fragment1)), expected);
4644        assert_eq!(String::from(Builder::new().add_tags(&["select", "selectedcontent", "option"]).clean(fragment2)), expected);
4645    }
4646    
4647    #[test]
4648    fn new_select_parse() {
4649        // https://github.com/whatwg/html/issues/10310#issuecomment-2304377029
4650        let fragment = r#"
4651<select><style></select><img src onerror=xss()></style></select>
4652        "#;
4653        let expected = r#"
4654<select></select>
4655        "#;
4656        assert_eq!(String::from(Builder::new().add_tags(&["select", "new-select"]).clean_content_tags(hashset!["style"]).clean(fragment)), expected);
4657    }
4658
4659    #[test]
4660    fn selectedcontent_not_in_select() {
4661        // https://github.com/whatwg/html/issues/10310#issuecomment-2304377029
4662        let fragment = r#"
4663<selectedcontent>first</selectedcontent>
4664<div><selectedcontent>second</selectedcontent></div>
4665<select><selectedcontent>third</selectedcontent></select>
4666        "#;
4667        let expected = r#"
4668<selectedcontent>first</selectedcontent>
4669<div><selectedcontent>second</selectedcontent></div>
4670<select><selectedcontent></selectedcontent></select>
4671        "#;
4672        assert_eq!(String::from(Builder::new().add_tags(&["select", "selectedcontent"]).clean(fragment)), expected);
4673    }
4674
4675    #[test]
4676    fn generic_attribute_prefixes_clean() {
4677        let fragment = r#"<a data-1 data-2 code-1 code-2><a>Hello!</a></a>"#;
4678        let result_cleaned = String::from(
4679            Builder::new()
4680                .add_tag_attributes("a", &["data-1"])
4681                .clean(fragment),
4682        );
4683        assert_eq!(
4684            result_cleaned,
4685            r#"<a data-1="" rel="noopener noreferrer"></a><a rel="noopener noreferrer">Hello!</a>"#
4686        );
4687        let result_allowed = String::from(
4688            Builder::new()
4689                .add_tag_attributes("a", &["data-1"])
4690                .add_generic_attribute_prefixes(&["data-"])
4691                .clean(fragment),
4692        );
4693        assert_eq!(
4694            result_allowed,
4695            r#"<a data-1="" data-2="" rel="noopener noreferrer"></a><a rel="noopener noreferrer">Hello!</a>"#
4696        );
4697        let result_allowed = String::from(
4698            Builder::new()
4699                .add_tag_attributes("a", &["data-1", "code-1"])
4700                .add_generic_attribute_prefixes(&["data-", "code-"])
4701                .clean(fragment),
4702        );
4703        assert_eq!(
4704            result_allowed,
4705            r#"<a data-1="" data-2="" code-1="" code-2="" rel="noopener noreferrer"></a><a rel="noopener noreferrer">Hello!</a>"#
4706        );
4707    }
4708    #[test]
4709    fn lesser_than_isnt_html() {
4710        let fragment = "1 < 2";
4711        assert!(!is_html(fragment));
4712    }
4713    #[test]
4714    fn dense_lesser_than_isnt_html() {
4715        let fragment = "1<2";
4716        assert!(!is_html(fragment));
4717    }
4718    #[test]
4719    fn what_about_number_elements() {
4720        let fragment = "foo<2>bar";
4721        assert!(!is_html(fragment));
4722    }
4723    #[test]
4724    fn turbofish_is_html_sadly() {
4725        let fragment = "Vec::<u8>::new()";
4726        assert!(is_html(fragment));
4727    }
4728    #[test]
4729    fn stop_grinning() {
4730        let fragment = "did you really believe me? <g>";
4731        assert!(is_html(fragment));
4732    }
4733    #[test]
4734    fn dont_be_bold() {
4735        let fragment = "<b>";
4736        assert!(is_html(fragment));
4737    }
4738
4739    #[test]
4740    fn rewrite_with_root() {
4741        let tests = [
4742            (
4743                "https://github.com/rust-ammonia/ammonia/blob/master/",
4744                "README.md",
4745                "",
4746                "https://github.com/rust-ammonia/ammonia/blob/master/README.md",
4747            ),
4748            (
4749                "https://github.com/rust-ammonia/ammonia/blob/master/",
4750                "README.md",
4751                "/",
4752                "https://github.com/rust-ammonia/ammonia/blob/master/",
4753            ),
4754            (
4755                "https://github.com/rust-ammonia/ammonia/blob/master/",
4756                "README.md",
4757                "/CONTRIBUTING.md",
4758                "https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md",
4759            ),
4760            (
4761                "https://github.com/rust-ammonia/ammonia/blob/master",
4762                "README.md",
4763                "",
4764                "https://github.com/rust-ammonia/ammonia/blob/README.md",
4765            ),
4766            (
4767                "https://github.com/rust-ammonia/ammonia/blob/master",
4768                "README.md",
4769                "/",
4770                "https://github.com/rust-ammonia/ammonia/blob/",
4771            ),
4772            (
4773                "https://github.com/rust-ammonia/ammonia/blob/master",
4774                "README.md",
4775                "/CONTRIBUTING.md",
4776                "https://github.com/rust-ammonia/ammonia/blob/CONTRIBUTING.md",
4777            ),
4778            (
4779                "https://github.com/rust-ammonia/ammonia/blob/master/",
4780                "",
4781                "",
4782                "https://github.com/rust-ammonia/ammonia/blob/master/",
4783            ),
4784            (
4785                "https://github.com/rust-ammonia/ammonia/blob/master/",
4786                "",
4787                "/",
4788                "https://github.com/rust-ammonia/ammonia/blob/master/",
4789            ),
4790            (
4791                "https://github.com/rust-ammonia/ammonia/blob/master/",
4792                "",
4793                "/CONTRIBUTING.md",
4794                "https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md",
4795            ),
4796            (
4797                "https://github.com/",
4798                "rust-ammonia/ammonia/blob/master/README.md",
4799                "",
4800                "https://github.com/rust-ammonia/ammonia/blob/master/README.md",
4801            ),
4802            (
4803                "https://github.com/",
4804                "rust-ammonia/ammonia/blob/master/README.md",
4805                "/",
4806                "https://github.com/",
4807            ),
4808            (
4809                "https://github.com/",
4810                "rust-ammonia/ammonia/blob/master/README.md",
4811                "CONTRIBUTING.md",
4812                "https://github.com/rust-ammonia/ammonia/blob/master/CONTRIBUTING.md",
4813            ),
4814            (
4815                "https://github.com/",
4816                "rust-ammonia/ammonia/blob/master/README.md",
4817                "/CONTRIBUTING.md",
4818                "https://github.com/CONTRIBUTING.md",
4819            ),
4820        ];
4821        for (root, path, url, result) in tests {
4822            let h = format!(r#"<a href="{url}">test</a>"#);
4823            let r = format!(r#"<a href="{result}" rel="noopener noreferrer">test</a>"#);
4824            let a = Builder::new()
4825                .url_relative(UrlRelative::RewriteWithRoot {
4826                    root: Url::parse(root).unwrap(),
4827                    path: path.to_string(),
4828                })
4829                .clean(&h)
4830                .to_string();
4831            if r != a {
4832                println!(
4833                    "failed to check ({root}, {path}, {url}, {result})\n{r} != {a}",
4834                    r = r
4835                );
4836                assert_eq!(r, a);
4837            }
4838        }
4839    }
4840}