#![cfg(feature = "dtd-validation")]
use std::sync::Arc;
use thymeleaf::context::Context;
use thymeleaf::dtd::ValidationPolicy;
use thymeleaf::templateresolver::StringTemplateResolver;
use thymeleaf::{TemplateEngine, TemplateMode};
const XHTML1_STRICT: &str = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd";
const XHTML1_TRANSITIONAL: &str = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";
const XHTML1_FRAMESET: &str = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd";
fn doctype(system_id: &str) -> String {
format!("<!DOCTYPE html SYSTEM \"{system_id}\">\n")
}
fn valid_strict() -> String {
format!(
"{}<html><head><title>t</title></head><body><p>x</p></body></html>",
doctype(XHTML1_STRICT)
)
}
fn invalid_undeclared_element() -> String {
format!(
"{}<html><head><title>t</title></head><body><p>x</p><bogus/></body></html>",
doctype(XHTML1_STRICT)
)
}
fn invalid_missing_head() -> String {
format!("{}<html><body/></html>", doctype(XHTML1_STRICT))
}
fn invalid_undeclared_attribute() -> String {
format!(
"{}<html><head><title>t</title></head><body><p bogusattr=\"1\">x</p></body></html>",
doctype(XHTML1_STRICT)
)
}
fn xml_engine(policy: ValidationPolicy) -> TemplateEngine {
let engine = TemplateEngine::new();
let mut resolver = StringTemplateResolver::new();
resolver.set_template_mode(TemplateMode::XML);
engine
.set_template_resolver(Arc::new(resolver))
.expect("engine not initialized");
engine
.set_dtd_validation_policy(policy)
.expect("engine not initialized");
engine
}
fn process(engine: &TemplateEngine, template: &str) -> Result<String, String> {
let context = Context::new();
engine
.process_template(template, &context)
.map(|output| output.to_string_lossy())
.map_err(|error| {
let mut message = format!("{error}");
let mut source = std::error::Error::source(&*error);
while let Some(current) = source {
message.push_str(" | ");
message.push_str(¤t.to_string());
source = current.source();
}
message
})
}
#[test]
fn disabled_policy_is_default_and_never_rejects() {
assert_eq!(ValidationPolicy::default(), ValidationPolicy::Disabled);
let engine = xml_engine(ValidationPolicy::Disabled);
for template in [
invalid_undeclared_element(),
invalid_missing_head(),
invalid_undeclared_attribute(),
] {
let result = process(&engine, &template);
assert!(result.is_ok(), "Disabled must not reject: {result:?}");
}
}
#[test]
fn strict_accepts_valid_document() {
let engine = xml_engine(ValidationPolicy::Strict);
let output = process(&engine, &valid_strict()).expect("valid document must pass");
assert!(output.contains("<p>x</p>"), "unexpected output: {output}");
}
#[test]
fn strict_accepts_mixed_content_nodes() {
let engine = xml_engine(ValidationPolicy::Strict);
let template = format!(
concat!(
"{}<html><head><title>t<!-- note --><?proc instr?></title></head>",
"<body><p>a&b<![CDATA[<c>]]></p></body></html>"
),
doctype(XHTML1_STRICT)
);
let result = process(&engine, &template);
assert!(result.is_ok(), "mixed content must pass Strict: {result:?}");
}
#[test]
fn strict_rejects_text_in_empty_element() {
let engine = xml_engine(ValidationPolicy::Strict);
let template = format!(
concat!(
"{}<html><head><title>t</title><base href=\"/\">x</base></head>",
"<body><p>x</p></body></html>"
),
doctype(XHTML1_STRICT)
);
let error = process(&engine, &template).expect_err("text in EMPTY element must fail");
assert!(
error.contains("DTD validation failed"),
"error must identify DTD validation: {error}"
);
}
#[test]
fn strict_rejects_undeclared_element() {
let engine = xml_engine(ValidationPolicy::Strict);
let error = process(&engine, &invalid_undeclared_element())
.expect_err("undeclared element must fail in Strict");
assert!(
error.contains("DTD validation failed"),
"error must identify DTD validation: {error}"
);
assert!(
error.to_lowercase().contains("bogus"),
"error must name the offending element: {error}"
);
}
#[test]
fn strict_rejects_missing_head_content_model() {
let engine = xml_engine(ValidationPolicy::Strict);
let error =
process(&engine, &invalid_missing_head()).expect_err("missing head must fail in Strict");
assert!(
error.contains("DTD validation failed"),
"error must identify DTD validation: {error}"
);
}
#[test]
fn strict_rejects_undeclared_attribute() {
let engine = xml_engine(ValidationPolicy::Strict);
let error = process(&engine, &invalid_undeclared_attribute())
.expect_err("undeclared attribute must fail in Strict");
assert!(
error.contains("DTD validation failed"),
"error must identify DTD validation: {error}"
);
}
#[test]
fn warn_validates_but_never_rejects() {
let engine = xml_engine(ValidationPolicy::Warn);
for template in [
invalid_undeclared_element(),
invalid_missing_head(),
invalid_undeclared_attribute(),
] {
let result = process(&engine, &template);
assert!(result.is_ok(), "Warn must not reject: {result:?}");
}
}
#[test]
fn strict_without_doctype_skips_validation() {
let engine = xml_engine(ValidationPolicy::Strict);
let result = process(&engine, "<html><body><bogus/></body></html>");
assert!(result.is_ok(), "no DOCTYPE means no validation: {result:?}");
}
#[test]
fn strict_with_unknown_system_id_fails_closed() {
let engine = xml_engine(ValidationPolicy::Strict);
let template = format!(
"{}<html><body><bogus/></body></html>",
doctype("http://example.com/unknown.dtd")
);
let error = process(&engine, &template).expect_err("unresolvable DTD must fail in Strict");
assert!(
error.contains("Cannot resolve DTD"),
"error must identify unresolvable DTD: {error}"
);
}
#[test]
fn warn_with_unknown_system_id_degrades_to_no_validation() {
let engine = xml_engine(ValidationPolicy::Warn);
let template = format!(
"{}<html><body><bogus/></body></html>",
doctype("http://example.com/unknown.dtd")
);
let result = process(&engine, &template);
assert!(
result.is_ok(),
"Warn must degrade to no validation: {result:?}"
);
}
#[test]
fn validator_builds_for_all_versioned_system_ids() {
use thymeleaf::dtd::DtdValidator;
for family in [
"xhtml1-strict",
"xhtml1-transitional",
"xhtml1-frameset",
"xhtml11",
] {
for version in ["1", "2", "3", "4"] {
let system_id =
format!("http://www.thymeleaf.org/dtd/{family}-thymeleaf-{version}.dtd");
let declaration = format!("html SYSTEM \"{system_id}\"");
assert!(
DtdValidator::new(&declaration).is_some(),
"versioned system id must resolve: {system_id}"
);
}
}
for system_id in [
XHTML1_STRICT,
XHTML1_TRANSITIONAL,
XHTML1_FRAMESET,
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd",
] {
let declaration = format!("html SYSTEM \"{system_id}\"");
assert!(
DtdValidator::new(&declaration).is_some(),
"W3C system id must resolve: {system_id}"
);
}
let unknown = "html SYSTEM \"http://example.com/x.dtd\"";
assert!(DtdValidator::new(unknown).is_none());
assert!(DtdValidator::new("html").is_some());
}
#[test]
fn validator_accepts_transitional_typo_aliases() {
use thymeleaf::dtd::DtdValidator;
for version in ["1", "2", "3", "4"] {
let system_id =
format!("http://www.thymeleaf.org/dtd/xhtml1-transitonal-thymeleaf-{version}.dtd");
let declaration = format!("html SYSTEM \"{system_id}\"");
assert!(
DtdValidator::new(&declaration).is_some(),
"typo alias must resolve: {system_id}"
);
}
}
#[test]
fn strict_validates_xhtml11_document() {
let engine = xml_engine(ValidationPolicy::Strict);
let template = format!(
"{}<html><head><title>t</title></head><body><p>x</p></body></html>",
doctype("http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd")
);
let output = process(&engine, &template).expect("xhtml11 document must pass Strict");
assert!(output.contains("<p>x</p>"), "unexpected output: {output}");
}
#[test]
fn strict_validates_transitional_doctype() {
let engine = xml_engine(ValidationPolicy::Strict);
let template = format!(
"{}<html><head><title>t</title></head><body><center>x</center></body></html>",
doctype(XHTML1_TRANSITIONAL)
);
let result = process(&engine, &template);
assert!(result.is_ok(), "transitional allows center: {result:?}");
let engine = xml_engine(ValidationPolicy::Strict);
let strict_template = template.replace(XHTML1_TRANSITIONAL, XHTML1_STRICT);
let error = process(&engine, &strict_template).expect_err("strict must reject center");
assert!(error.contains("DTD validation failed"), "{error}");
}