diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 90eec6bb5e61a..c10b81f833692 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -430,6 +430,8 @@ If you need to render the unescaped value: <1> `title` that resolves to `Expressions & Escapes` will be rendered as `Expressions &amp; Escapes` <2> `paragraph` that resolves to `

My text!

` will be rendered as `

My text!

` +TIP: By default, a template with one of the following content types is escaped: `text/html`, `text/xml`, `application/xml` and `application/xhtml+xml`. However, it's possible to extend this list via the `quarkus.qute.escape-content-types` configuration property. + [[virtual_methods]] ==== Virtual Methods diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/EscapingTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/EscapingTest.java index 49d8331a4c4c8..c3770401554bb 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/EscapingTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/EscapingTest.java @@ -26,11 +26,13 @@ public class EscapingTest { .addAsResource(new StringAsset("{text} {other} {text.raw} {text.safe} {item.foo}"), "templates/foo.html") .addAsResource(new StringAsset("{item} {item.raw}"), - "templates/item.html") + "templates/item.xhtml") .addAsResource(new StringAsset("{text} {other} {text.raw} {text.safe} {item.foo}"), "templates/bar.txt") .addAsResource(new StringAsset("{@java.lang.String text}{text} {text.raw} {text.safe}"), - "templates/validation.html")); + "templates/validation.html")) + .overrideConfigKey("quarkus.qute.content-types.xhtml", "application/xhtml+xml") + .overrideConfigKey("quarkus.qute.suffixes", "qute.html,qute.txt,html,txt,xhtml"); @Inject Template foo; @@ -67,6 +69,13 @@ public void testEngineParse() { assertEquals("<div>
", engine.parse("{text} {text.raw}", new Variant(Locale.ENGLISH, "text/html", "UTF-8")).data("text", "
").render()); + assertEquals("<div>
", + engine.parse("{text} {text.raw}", + new Variant(Locale.ENGLISH, "application/xml", "UTF-8")).data("text", "
").render()); + assertEquals("<div>
", + engine.parse("{text} {text.raw}", + new Variant(Locale.ENGLISH, "application/xhtml+xml;charset=UTF-8", "UTF-8")).data("text", "
") + .render()); } @TemplateData diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java index 26bc5754ad824..f01e316cdd49a 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java @@ -113,8 +113,8 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig } } - // Escape some characters for HTML templates - builder.addResultMapper(new HtmlEscaper()); + // Escape some characters for HTML/XML templates + builder.addResultMapper(new HtmlEscaper(List.copyOf(config.escapeContentTypes))); // Fallback reflection resolver builder.addValueResolver(new ReflectionValueResolver()); diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java index 416b5624646fc..28bf7ed2d4a81 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteConfig.java @@ -74,4 +74,11 @@ public class QuteConfig { @ConfigItem(defaultValue = "") public String iterationMetadataPrefix; + /** + * The list of content types for which the {@code '}, {@code "}, {@code <}, {@code >} and {@code &} characters are escaped + * if a template variant is set. + */ + @ConfigItem(defaultValue = "text/html,text/xml,application/xml,application/xhtml+xml") + public List escapeContentTypes; + } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java index 14ceea7639254..14bd0496f5627 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java @@ -516,7 +516,7 @@ private Engine buildEngine(List devTemplatePaths, EngineBuilder builder = Engine.builder().addDefaults(); // Escape some characters for HTML templates - builder.addResultMapper(new HtmlEscaper()); + builder.addResultMapper(new HtmlEscaper(List.of(Variant.TEXT_HTML))); builder.strictRendering(true) .addValueResolver(new ReflectionValueResolver()) diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/HtmlEscaper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/HtmlEscaper.java index 5788c7e2656f6..f7405dc45d038 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/HtmlEscaper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/HtmlEscaper.java @@ -1,11 +1,18 @@ package io.quarkus.qute; import io.quarkus.qute.TemplateNode.Origin; +import java.util.List; import java.util.Objects; import java.util.Optional; public class HtmlEscaper implements ResultMapper { + private final List escapedContentTypes; + + public HtmlEscaper(List escapedContentTypes) { + this.escapedContentTypes = escapedContentTypes; + } + @Override public boolean appliesTo(Origin origin, Object result) { if (result instanceof RawString) { @@ -37,10 +44,17 @@ String escape(CharSequence value) { return value.toString(); } - static boolean requiresDefaultEscaping(Variant variant) { - return variant.getContentType() != null - ? (Variant.TEXT_HTML.equals(variant.getContentType()) || Variant.TEXT_XML.equals(variant.getContentType())) - : false; + private boolean requiresDefaultEscaping(Variant variant) { + String contentType = variant.getContentType(); + if (contentType == null) { + return false; + } + for (String escaped : escapedContentTypes) { + if (contentType.startsWith(escaped)) { + return true; + } + } + return false; } private String doEscape(CharSequence value, int index, StringBuilder builder) { diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/HtmlEscaperTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/HtmlEscaperTest.java index 9f24a043e1fff..87ff040002539 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/HtmlEscaperTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/HtmlEscaperTest.java @@ -6,6 +6,7 @@ import io.quarkus.qute.TemplateNode.Origin; import java.io.IOException; +import java.util.List; import java.util.Locale; import java.util.Optional; import org.junit.jupiter.api.Test; @@ -14,7 +15,7 @@ public class HtmlEscaperTest { @Test public void testAppliesTo() { - HtmlEscaper html = new HtmlEscaper(); + HtmlEscaper html = new HtmlEscaper(List.of(Variant.TEXT_HTML)); Origin htmlOrigin = new Origin() { @Override @@ -53,7 +54,7 @@ public int getLine() { @Test public void testEscaping() throws IOException { - HtmlEscaper html = new HtmlEscaper(); + HtmlEscaper html = new HtmlEscaper(List.of(Variant.TEXT_HTML)); assertEquals("Čolek", html.escape("Čolek")); assertEquals("<strong>Čolek</strong>", html.escape("Čolek")); assertEquals("<a>&link"'</a>", html.escape("&link\"'"));