use crate::{
Parser,
blocks::{Block, FindBlocks, SimpleBlock},
content::{Content, SubstitutionStep},
parser::ModificationContext,
warnings::WarningType,
};
fn render_paragraph(src: &str) -> String {
let mut parser = Parser::default();
let doc = parser.parse(src);
fn walk<'a>(mut blocks: impl Iterator<Item = &'a Block<'a>>) -> Option<&'a SimpleBlock<'a>> {
blocks.find_map(|block| {
if let Block::Simple(simple) = block {
Some(simple)
} else {
walk(block.child_blocks())
}
})
}
walk(doc.child_blocks())
.expect("expected at least one simple block")
.content()
.rendered()
.to_string()
}
fn render_icon(src: &str, icons: &str) -> String {
let mut content = Content::from(crate::Span::new(src));
let parser =
Parser::default().with_intrinsic_attribute("icons", icons, ModificationContext::ApiOnly);
SubstitutionStep::SpecialCharacters.apply(&mut content, &parser, None);
SubstitutionStep::Macros.apply(&mut content, &parser, None);
content.rendered().to_string()
}
fn render_macros(src: &str) -> String {
let mut content = Content::from(crate::Span::new(src));
let parser = Parser::default();
SubstitutionStep::SpecialCharacters.apply(&mut content, &parser, None);
SubstitutionStep::Macros.apply(&mut content, &parser, None);
content.rendered().to_string()
}
#[test]
fn link_target_escapes_quote_delimiter() {
assert_eq!(
render_paragraph(r#"link:x"onmouseover=alert(1)//[click]"#),
r#"<a href="x"onmouseover=alert(1)//">click</a>"#
);
}
#[test]
fn link_target_does_not_double_escape_special_characters() {
assert_eq!(
render_paragraph("link:/a<b>&c[txt]"),
r#"<a href="/a<b>&c">txt</a>"#
);
}
#[test]
fn link_target_preserves_single_escaped_ampersand_in_query() {
assert_eq!(
render_paragraph("link:/search?a=1&b=2[txt]"),
r#"<a href="/search?a=1&b=2">txt</a>"#
);
}
#[test]
fn xref_unresolved_target_escapes_quote_delimiter() {
assert_eq!(
render_paragraph(r#"xref:foo"onmouseover=alert(1)//[txt]"#),
r##"<a href="#foo"onmouseover=alert(1)//">txt</a>"##
);
}
#[test]
fn xref_unresolved_target_does_not_double_escape_special_characters() {
assert_eq!(
render_paragraph("xref:foo<bar>[txt]"),
r##"<a href="#foo<bar>">txt</a>"##
);
}
#[test]
fn icon_font_class_and_title_escape_quote_delimiter() {
assert_eq!(
render_icon(r#"icon:home[size="2x\"y",title="a\"b"]"#, "font"),
r#"<span class="icon"><i class="fa fa-home fa-2x"y" title="a"b"></i></span>"#
);
}
#[test]
fn icon_image_dimensions_and_title_escape_quote_delimiter() {
assert_eq!(
render_icon(r#"icon:home[width="1\"x",height="2\"y",title="a\"b"]"#, ""),
r#"<span class="icon"><img src="./images/icons/home.png" alt="home" width="1"x" height="2"y" title="a"b"></span>"#
);
}
#[test]
fn icon_image_src_escapes_quote_delimiter() {
assert_eq!(
render_icon(r#"icon:a"x[]"#, ""),
r#"<span class="icon"><img src="./images/icons/a"x.png" alt="a"x"></span>"#
);
}
#[test]
fn image_src_escapes_quote_delimiter() {
assert_eq!(
render_macros(r#"image:a"x.png[alt]"#),
r#"<span class="image"><img src="a"x.png" alt="alt"></span>"#
);
}
#[test]
fn image_width_escapes_quote_delimiter() {
assert_eq!(
render_macros(r#"image:x.png[alt,width="1\"y"]"#),
r#"<span class="image"><img src="x.png" alt="alt" width="1"y"></span>"#
);
}
#[test]
fn image_link_href_escapes_quote_delimiter() {
assert_eq!(
render_macros(r#"image:x.png[alt,link="a\"b"]"#),
r#"<span class="image"><a class="image" href="a"b"><img src="x.png" alt="alt"></a></span>"#
);
}
#[test]
fn image_link_self_escapes_src_and_href_without_double_escaping() {
assert_eq!(
render_macros(r#"image:a"x.png[alt,link=self]"#),
r#"<span class="image"><a class="image" href="a"x.png"><img src="a"x.png" alt="alt"></a></span>"#
);
}
#[test]
fn image_link_with_dangerous_scheme_drops_the_wrapping_link() {
assert_eq!(
render_macros("image:safe.png[alt,link=javascript:alert(1)]"),
r#"<span class="image"><img src="safe.png" alt="alt"></span>"#
);
}
#[test]
fn image_link_dangerous_scheme_records_a_warning() {
let mut parser = Parser::default();
let doc = parser.parse("image:safe.png[alt,link=javascript:alert(1)]");
let warnings: Vec<_> = doc.warnings().collect();
assert_eq!(warnings.len(), 1);
assert_eq!(
warnings[0].warning,
WarningType::UnsafeLinkSchemeRejected("javascript:alert(1)".to_string())
);
}
#[test]
fn image_link_dangerous_scheme_rejection_is_case_insensitive() {
assert_eq!(
render_macros("image:safe.png[alt,link=JavaScript:alert(1)]"),
r#"<span class="image"><img src="safe.png" alt="alt"></span>"#
);
}
#[test]
fn icon_link_with_dangerous_scheme_drops_the_wrapping_link() {
assert_eq!(
render_icon("icon:home[link=javascript:alert(1)]", ""),
r#"<span class="icon"><img src="./images/icons/home.png" alt="home"></span>"#
);
}
#[test]
fn icon_link_self_with_dangerous_target_drops_the_wrapping_link() {
assert_eq!(
render_icon("icon:javascript:alert(1)[link=self]", ""),
r#"<span class="icon"><img src="javascript:alert(1).png" alt="javascript:alert(1)"></span>"#
);
}
#[test]
fn font_icon_link_self_with_dangerous_target_keeps_literal_self_without_warning() {
assert_eq!(
render_icon("icon:javascript:alert(1)[link=self]", "font"),
r#"<span class="icon"><a class="image" href="self"><i class="fa fa-javascript:alert(1)"></i></a></span>"#
);
let mut parser =
Parser::default().with_intrinsic_attribute("icons", "font", ModificationContext::ApiOnly);
let doc = parser.parse("icon:javascript:alert(1)[link=self]");
assert_eq!(doc.warnings().count(), 0);
}
#[test]
fn image_link_self_is_not_rejected() {
assert_eq!(
render_macros("image:safe.png[alt,link=self]"),
r#"<span class="image"><a class="image" href="safe.png"><img src="safe.png" alt="alt"></a></span>"#
);
}
#[test]
fn image_link_self_with_dangerous_target_drops_the_wrapping_link() {
assert_eq!(
render_macros("image:javascript:alert(1)[alt,link=self]"),
r#"<span class="image"><img src="javascript:alert(1)" alt="alt"></span>"#
);
}
#[test]
fn image_link_self_with_dangerous_target_records_a_warning() {
let mut parser = Parser::default();
let doc = parser.parse("image:javascript:alert(1)[alt,link=self]");
let warnings: Vec<_> = doc.warnings().collect();
assert_eq!(warnings.len(), 1);
assert_eq!(
warnings[0].warning,
WarningType::UnsafeLinkSchemeRejected("javascript:alert(1)".to_string())
);
}
#[test]
fn image_link_self_with_data_text_html_target_drops_the_wrapping_link() {
assert_eq!(
render_macros("image:data:text/html,<script>alert(1)</script>[alt,link=self]"),
r#"<span class="image"><img src="data:text/html,<script>alert(1)</script>" alt="alt"></span>"#
);
}
#[test]
fn image_link_self_dangerous_target_rejection_is_case_insensitive() {
assert_eq!(
render_macros("image:JavaScript:alert(1)[alt,link=self]"),
r#"<span class="image"><img src="JavaScript:alert(1)" alt="alt"></span>"#
);
}
#[test]
fn image_link_self_with_inline_raster_data_image_target_stays_a_live_link() {
assert_eq!(
render_macros("image:data:image/png;base64,iVBORw0KGgo=[alt,link=self]"),
r#"<span class="image"><a class="image" href="data:image/png;base64,iVBORw0KGgo="><img src="data:image/png;base64,iVBORw0KGgo=" alt="alt"></a></span>"#
);
}
#[test]
fn image_link_self_with_author_supplied_svg_data_uri_drops_the_wrapping_link() {
assert_eq!(
render_macros("image:data:image/svg+xml,<svg onload='alert(1)'></svg>[alt,link=self]"),
r#"<span class="image"><img src="data:image/svg+xml,<svg%20onload='alert(1)'></svg>" alt="alt"></span>"#
);
}
#[test]
fn image_link_self_with_author_supplied_svg_data_uri_records_a_warning() {
let mut parser = Parser::default();
let doc = parser.parse("image:data:image/svg+xml,<svg onload='alert(1)'></svg>[alt,link=self]");
let warnings: Vec<_> = doc.warnings().collect();
assert_eq!(warnings.len(), 1);
assert_eq!(
warnings[0].warning,
WarningType::UnsafeLinkSchemeRejected(
"data:image/svg+xml,<svg onload='alert(1)'></svg>".to_string()
)
);
}
#[test]
fn link_macro_neutralizes_javascript_scheme() {
assert_eq!(
render_paragraph("link:javascript:alert(1)[click]"),
"link:javascript:alert(1)[click]"
);
}
#[test]
fn link_macro_scheme_rejection_is_case_insensitive() {
assert_eq!(
render_paragraph("link:JavaScript:alert(1)[click]"),
"link:JavaScript:alert(1)[click]"
);
}
#[test]
fn link_macro_neutralizes_data_scheme() {
assert_eq!(
render_paragraph("link:data:text/html,<script>alert(1)</script>[click]"),
"link:data:text/html,<script>alert(1)</script>[click]"
);
}
#[test]
fn link_macro_neutralizes_vbscript_scheme() {
assert_eq!(
render_paragraph("link:vbscript:msgbox(1)[click]"),
"link:vbscript:msgbox(1)[click]"
);
}
#[test]
fn link_macro_neutralizes_scheme_with_authority_separator() {
assert_eq!(
render_paragraph("link:javascript://alert(1)[click]"),
"link:javascript://alert(1)[click]"
);
}
#[test]
fn link_macro_allows_relative_path() {
assert_eq!(
render_paragraph("link:/home.html[Home]"),
r#"<a href="/home.html">Home</a>"#
);
}
#[test]
fn link_macro_allows_https_scheme() {
assert_eq!(
render_paragraph("link:https://example.com[x]"),
r#"<a href="https://example.com">x</a>"#
);
}
#[test]
fn mailto_macro_is_not_rejected() {
assert_eq!(
render_paragraph("mailto:a@b.com[mail]"),
r#"<a href="mailto:a@b.com">mail</a>"#
);
}
#[test]
fn link_macro_allows_target_that_merely_starts_with_scheme_letters() {
assert_eq!(
render_paragraph("link:javascriptfoo.html[x]"),
r#"<a href="javascriptfoo.html">x</a>"#
);
}
#[test]
fn link_macro_allows_nonstandard_scheme_that_is_not_dangerous() {
assert_eq!(
render_paragraph("link:mydata:thing[x]"),
r#"<a href="mydata:thing">x</a>"#
);
}
#[test]
fn rejected_scheme_records_a_warning() {
let mut parser = Parser::default();
let doc = parser.parse("link:javascript:alert(1)[click]");
let warnings: Vec<_> = doc.warnings().collect();
assert_eq!(warnings.len(), 1);
assert_eq!(
warnings[0].warning,
WarningType::UnsafeLinkSchemeRejected("javascript:alert(1)".to_string())
);
assert_eq!(warnings[0].source.line(), 1);
}
#[test]
fn allowed_link_records_no_warning() {
let mut parser = Parser::default();
let doc = parser.parse("link:https://example.com[x]");
assert_eq!(doc.warnings().count(), 0);
}