use std::borrow::Cow;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ElementAction {
Keep,
Unwrap,
Remove,
}
pub trait HtmlPolicy {
fn element(&self, name: &str) -> ElementAction;
fn attribute<'v>(&self, element: &str, name: &str, value: &'v str) -> Option<Cow<'v, str>>;
fn destination<'u>(&self, url: &'u str, image: bool) -> Option<Cow<'u, str>>;
}
#[derive(Clone, Copy, Default, Debug)]
pub struct SafeHtml;
#[derive(Clone, Copy, Default, Debug)]
pub struct TrustedHtml;
const ELEMENTS: &[&str] = &[
"a",
"abbr",
"acronym",
"area",
"article",
"aside",
"b",
"bdi",
"bdo",
"blockquote",
"br",
"caption",
"center",
"cite",
"code",
"col",
"colgroup",
"data",
"dd",
"del",
"details",
"dfn",
"div",
"dl",
"dt",
"em",
"figcaption",
"figure",
"footer",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"header",
"hgroup",
"hr",
"i",
"img",
"ins",
"kbd",
"li",
"map",
"mark",
"nav",
"ol",
"p",
"pre",
"q",
"rp",
"rt",
"rtc",
"ruby",
"s",
"samp",
"small",
"span",
"strike",
"strong",
"sub",
"summary",
"sup",
"table",
"tbody",
"td",
"th",
"thead",
"time",
"tr",
"tt",
"u",
"ul",
"var",
"wbr",
];
const REMOVED_WITH_CONTENT: &[&str] = &["script", "style"];
const GENERIC_ATTRIBUTES: &[&str] = &["lang", "title"];
const ELEMENT_ATTRIBUTES: &[(&str, &[&str])] = &[
("a", &["href", "hreflang"]),
("bdo", &["dir"]),
("blockquote", &["cite"]),
("col", &["align", "char", "charoff", "span"]),
("colgroup", &["align", "char", "charoff", "span"]),
("del", &["cite", "datetime"]),
("hr", &["align", "size", "width"]),
("img", &["align", "alt", "height", "src", "width"]),
("ins", &["cite", "datetime"]),
("ol", &["start"]),
("q", &["cite"]),
("table", &["align", "char", "charoff", "summary"]),
("tbody", &["align", "char", "charoff"]),
(
"td",
&["align", "char", "charoff", "colspan", "headers", "rowspan"],
),
("tfoot", &["align", "char", "charoff"]),
(
"th",
&[
"align", "char", "charoff", "colspan", "headers", "rowspan", "scope",
],
),
("thead", &["align", "char", "charoff"]),
("tr", &["align", "char", "charoff"]),
];
const URL_ATTRIBUTES: &[&str] = &[
"action",
"background",
"cite",
"data",
"formaction",
"href",
"longdesc",
"ping",
"poster",
"src",
"xlink:href",
];
const SAFE_SCHEMES: &[&str] = &[
"bitcoin",
"ftp",
"ftps",
"geo",
"http",
"https",
"im",
"irc",
"ircs",
"magnet",
"mailto",
"mms",
"mx",
"news",
"nntp",
"openpgp4fpr",
"sip",
"sms",
"smsto",
"ssh",
"tel",
"url",
"webcal",
"wtai",
"xmpp",
];
impl HtmlPolicy for SafeHtml {
fn element(&self, name: &str) -> ElementAction {
if REMOVED_WITH_CONTENT.contains(&name) {
ElementAction::Remove
} else if ELEMENTS.contains(&name) {
ElementAction::Keep
} else {
ElementAction::Unwrap
}
}
fn attribute<'v>(&self, element: &str, name: &str, value: &'v str) -> Option<Cow<'v, str>> {
let listed = GENERIC_ATTRIBUTES.contains(&name)
|| ELEMENT_ATTRIBUTES
.iter()
.any(|(tag, attributes)| *tag == element && attributes.contains(&name));
let safe = !URL_ATTRIBUTES.contains(&name) || is_safe_url(value);
(listed && safe).then_some(Cow::Borrowed(value))
}
fn destination<'u>(&self, url: &'u str, _image: bool) -> Option<Cow<'u, str>> {
is_safe_url(url).then_some(Cow::Borrowed(url))
}
}
impl HtmlPolicy for TrustedHtml {
fn element(&self, _name: &str) -> ElementAction {
ElementAction::Keep
}
fn attribute<'v>(&self, _element: &str, _name: &str, value: &'v str) -> Option<Cow<'v, str>> {
Some(Cow::Borrowed(value))
}
fn destination<'u>(&self, url: &'u str, _image: bool) -> Option<Cow<'u, str>> {
Some(Cow::Borrowed(url))
}
}
pub fn is_safe_url(url: &str) -> bool {
scheme(url).is_none_or(|scheme| SAFE_SCHEMES.contains(&scheme.as_str()))
}
fn scheme(url: &str) -> Option<String> {
let cleaned: String = url
.trim_matches(|character: char| character <= ' ')
.chars()
.filter(|character| !matches!(character, '\t' | '\n' | '\r'))
.collect();
let (candidate, _) = cleaned.split_once(':')?;
let mut characters = candidate.chars();
let first = characters.next()?;
let is_scheme = first.is_ascii_alphabetic()
&& characters.all(|character| {
character.is_ascii_alphanumeric() || matches!(character, '+' | '-' | '.')
});
is_scheme.then(|| candidate.to_ascii_lowercase())
}