Skip to content

Commit

Permalink
Qute - escape expressions in html by default
Browse files Browse the repository at this point in the history
- resolves #6155
- API changes:
 - introduce TemplateLocator
 - add Template#getVariant()
  • Loading branch information
mkouba committed Dec 16, 2019
1 parent 535e033 commit 6321fc8
Show file tree
Hide file tree
Showing 24 changed files with 484 additions and 56 deletions.
18 changes: 18 additions & 0 deletions docs/src/main/asciidoc/qute-reference.adoc
Expand Up @@ -182,6 +182,24 @@ This could be useful to access data for which the key is overriden:
====

===== Character Escapes

For HTML and XML templates the `'`, `"`, `<`, `>`, `&` characters are escaped by default.
If you need to render the unescaped value:

1. Use the `raw` or `safe` properties implemented as extension methods of the `java.lang.Object`,
2. Wrap the `String` value in a `io.quarkus.qute.RawString`.

[source,html]
----
<html>
<h1>{title}</h1> <1>
{paragraph.raw} <2>
</html>
----
<1> `title` that resolves to `Expressions & Escapes` will be rendered as `Expressions &amp;amp; Escapes`
<2> `paragraph` that resolves to `<p>My text!</p>` will be rendered as `<p>My text!</p>`

==== Sections

A section:
Expand Down
@@ -0,0 +1,66 @@
package io.quarkus.qute.deployment;

import static org.junit.jupiter.api.Assertions.assertEquals;

import javax.inject.Inject;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.RawString;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateData;
import io.quarkus.test.QuarkusUnitTest;

public class EscapingTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClass(Item.class)
.addAsResource(new StringAsset("{text} {other} {text.raw} {text.safe} {item.foo}"),
"templates/foo.html")
.addAsResource(new StringAsset("{item} {item.raw}"),
"templates/item.html")
.addAsResource(new StringAsset("{text} {other} {text.raw} {text.safe} {item.foo}"),
"templates/bar.txt"));

@Inject
Template foo;

@Inject
Template bar;

@Inject
Template item;

@Test
public void testEscaper() {
assertEquals("&lt;div&gt; &amp;&quot;&#39; <div> <div> <span>",
foo.data("text", "<div>").data("other", "&\"'").data("item", new Item()).render());
// No escaping for txt templates
assertEquals("<div> &\"' <div> <div> <span>",
bar.data("text", "<div>").data("other", "&\"'").data("item", new Item()).render());
// Item.toString() is escaped too
assertEquals("&lt;h1&gt;Item&lt;/h1&gt; <h1>Item</h1>",
item.data("item", new Item()).render());
}

@TemplateData
public static class Item {

public RawString getFoo() {
return new RawString("<span>");
}

@Override
public String toString() {
return "<h1>Item</h1>";
}

}

}
Expand Up @@ -12,7 +12,7 @@
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.api.Variant;
import io.quarkus.qute.Variant;
import io.quarkus.qute.api.VariantTemplate;
import io.quarkus.test.QuarkusUnitTest;

Expand Down
@@ -1,6 +1,7 @@
package io.quarkus.qute.api;

import io.quarkus.qute.Template;
import io.quarkus.qute.Variant;

/**
*
Expand Down
@@ -1,8 +1,9 @@
package io.quarkus.qute.runtime;

import java.io.InputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Optional;
Expand All @@ -19,12 +20,19 @@
import io.quarkus.arc.InstanceHandle;
import io.quarkus.qute.Engine;
import io.quarkus.qute.EngineBuilder;
import io.quarkus.qute.Escaper;
import io.quarkus.qute.Expression;
import io.quarkus.qute.NamespaceResolver;
import io.quarkus.qute.RawString;
import io.quarkus.qute.ReflectionValueResolver;
import io.quarkus.qute.ResultMapper;
import io.quarkus.qute.Results.Result;
import io.quarkus.qute.TemplateLocator.TemplateLocation;
import io.quarkus.qute.TemplateNode.Origin;
import io.quarkus.qute.UserTagSectionHelper;
import io.quarkus.qute.ValueResolver;
import io.quarkus.qute.ValueResolvers;
import io.quarkus.qute.Variant;

@Singleton
public class EngineProducer {
Expand Down Expand Up @@ -59,8 +67,31 @@ void init(QuteConfig config, List<String> resolverClasses, List<String> template

// We don't register the map resolver because of param declaration validation
// See DefaultTemplateExtensions
builder.addValueResolvers(ValueResolvers.thisResolver(), ValueResolvers.orResolver(), ValueResolvers.trueResolver(),
ValueResolvers.collectionResolver(), ValueResolvers.mapperResolver(), ValueResolvers.mapEntryResolver());
builder.addValueResolver(ValueResolvers.thisResolver());
builder.addValueResolver(ValueResolvers.orResolver());
builder.addValueResolver(ValueResolvers.trueResolver());
builder.addValueResolver(ValueResolvers.collectionResolver());
builder.addValueResolver(ValueResolvers.mapperResolver());
builder.addValueResolver(ValueResolvers.mapEntryResolver());
// foo.string.raw returns a RawString which is never escaped
builder.addValueResolver(ValueResolvers.rawResolver());

// Escape some characters for HTML templates
Escaper htmlEscaper = Escaper.builder().add('"', "&quot;").add('\'', "&#39;")
.add('&', "&amp;").add('<', "&lt;").add('>', "&gt;").build();
builder.addResultMapper(new ResultMapper() {

@Override
public boolean appliesTo(Origin origin, Object result) {
return !(result instanceof RawString)
&& origin.getVariant().filter(EngineProducer::requiresDefaultEscaping).isPresent();
}

@Override
public String map(Object result, Expression expression) {
return htmlEscaper.escape(result.toString());
}
});

// Fallback reflection resolver
builder.addValueResolver(new ReflectionValueResolver());
Expand Down Expand Up @@ -132,37 +163,79 @@ private ValueResolver createResolver(String resolverClassName) {
* @param path
* @return the optional reader
*/
private Optional<Reader> locate(String path) {
InputStream in = null;
private Optional<TemplateLocation> locate(String path) {
URL resource = null;
// First try to locate a tag template
if (tags.stream().anyMatch(tag -> tag.startsWith(path))) {
LOGGER.debugf("Locate tag for %s", path);
in = locatePath(tagPath + path);
resource = locatePath(tagPath + path);
// Try path with suffixes
for (String suffix : suffixes) {
in = locatePath(tagPath + path + "." + suffix);
if (in != null) {
resource = locatePath(tagPath + path + "." + suffix);
if (resource != null) {
break;
}
}
}
if (in == null) {
if (resource == null) {
String templatePath = basePath + path;
LOGGER.debugf("Locate template for %s", templatePath);
in = locatePath(templatePath);
resource = locatePath(templatePath);
}
if (in != null) {
return Optional.of(new InputStreamReader(in, Charset.forName("utf-8")));
if (resource != null) {
return Optional.of(new ResourceTemplateLocation(resource, guessVariant(path)));
}
return Optional.empty();
}

private InputStream locatePath(String path) {
private URL locatePath(String path) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
if (cl == null) {
cl = EngineProducer.class.getClassLoader();
}
return cl.getResourceAsStream(path);
return cl.getResource(path);
}

static Variant guessVariant(String path) {
// TODO we need a proper way to detect the variant
int suffixIdx = path.lastIndexOf('.');
if (suffixIdx != -1) {
String suffix = path.substring(suffixIdx);
return new Variant(null, VariantTemplateProducer.parseMediaType(suffix), null);
}
return null;
}

static boolean requiresDefaultEscaping(Variant variant) {
return variant.mediaType != null
? (Variant.TEXT_HTML.equals(variant.mediaType) || Variant.TEXT_XML.equals(variant.mediaType))
: false;
}

static class ResourceTemplateLocation implements TemplateLocation {

private final URL resource;
private final Optional<Variant> variant;

public ResourceTemplateLocation(URL resource, Variant variant) {
this.resource = resource;
this.variant = Optional.ofNullable(variant);
}

@Override
public Reader read() {
try {
return new InputStreamReader(resource.openStream(), Charset.forName("utf-8"));
} catch (IOException e) {
return null;
}
}

@Override
public Optional<Variant> getVariant() {
return variant;
}

}

}
Expand Up @@ -2,6 +2,7 @@

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;

Expand All @@ -16,6 +17,7 @@
import io.quarkus.qute.Expression;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Variant;
import io.quarkus.qute.api.ResourcePath;

@Singleton
Expand Down Expand Up @@ -104,6 +106,11 @@ public String getGeneratedId() {
return template.get().getGeneratedId();
}

@Override
public Optional<Variant> getVariant() {
return template.get().getVariant();
}

}

}
Expand Up @@ -7,6 +7,7 @@
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
Expand All @@ -27,8 +28,8 @@
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.TemplateInstanceBase;
import io.quarkus.qute.Variant;
import io.quarkus.qute.api.ResourcePath;
import io.quarkus.qute.api.Variant;
import io.quarkus.qute.api.VariantTemplate;

@Singleton
Expand Down Expand Up @@ -114,6 +115,11 @@ public String getGeneratedId() {
throw new UnsupportedOperationException();
}

@Override
public Optional<Variant> getVariant() {
throw new UnsupportedOperationException();
}

}

class VariantTemplateInstanceImpl extends TemplateInstanceBase {
Expand Down Expand Up @@ -166,15 +172,15 @@ public TemplateVariants(Map<Variant, String> variants, String defaultTemplate) {
}

static String parseMediaType(String suffix) {
// TODO support more media types...
if (suffix.equalsIgnoreCase(".html")) {
return "text/html";
// TODO we need a proper way to parse the media type
if (suffix.equalsIgnoreCase(".html") || suffix.equalsIgnoreCase(".htm")) {
return Variant.TEXT_HTML;
} else if (suffix.equalsIgnoreCase(".xml")) {
return "text/xml";
return Variant.TEXT_XML;
} else if (suffix.equalsIgnoreCase(".txt")) {
return "text/plain";
return Variant.TEXT_PLAIN;
} else if (suffix.equalsIgnoreCase(".json")) {
return "application/json";
return Variant.APPLICATION_JSON;
}
LOGGER.warn("Unknown media type for suffix: " + suffix);
return "application/octet-stream";
Expand Down
Expand Up @@ -17,7 +17,7 @@
import org.jboss.resteasy.core.interception.jaxrs.SuspendableContainerResponseContext;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.api.Variant;
import io.quarkus.qute.Variant;
import io.quarkus.qute.api.VariantTemplate;

@Provider
Expand Down
Expand Up @@ -13,7 +13,11 @@ static EngineBuilder builder() {
return new EngineBuilder();
}

public Template parse(String content);
default Template parse(String content) {
return parse(content, null);
}

public Template parse(String content, Variant variant);

public SectionHelperFactory<?> getSectionHelperFactory(String name);

Expand Down
Expand Up @@ -13,7 +13,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;

Expand All @@ -25,7 +24,7 @@ public final class EngineBuilder {
private final Map<String, SectionHelperFactory<?>> sectionHelperFactories;
private final List<ValueResolver> valueResolvers;
private final List<NamespaceResolver> namespaceResolvers;
private final List<Function<String, Optional<Reader>>> locators;
private final List<TemplateLocator> locators;
private final List<ResultMapper> resultMappers;
private Function<String, SectionHelperFactory<?>> sectionHelperFunc;

Expand Down Expand Up @@ -105,7 +104,7 @@ public EngineBuilder addNamespaceResolver(NamespaceResolver resolver) {
* @return self
* @see Engine#getTemplate(String)
*/
public EngineBuilder addLocator(Function<String, Optional<Reader>> locator) {
public EngineBuilder addLocator(TemplateLocator locator) {
this.locators.add(locator);
return this;
}
Expand Down

0 comments on commit 6321fc8

Please sign in to comment.