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 bb272db
Show file tree
Hide file tree
Showing 23 changed files with 442 additions and 52 deletions.
@@ -0,0 +1,53 @@
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("{text} {other} {text.raw} {text.safe} {item.foo}"),
"templates/bar.txt"));

@Inject
Template foo;

@Inject
Template bar;

@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());
}

@TemplateData
public static class Item {

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

}

}
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,18 @@
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.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 +66,30 @@ 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.rawStringResolver());

// Escape strings 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 String && origin.getVariant().filter(Variant::isHtml).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 +161,73 @@ 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 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,8 +172,8 @@ public TemplateVariants(Map<Variant, String> variants, String defaultTemplate) {
}

static String parseMediaType(String suffix) {
// TODO support more media types...
if (suffix.equalsIgnoreCase(".html")) {
// TODO we need a proper way to parse the media type
if (suffix.equalsIgnoreCase(".html") || suffix.equalsIgnoreCase(".htm")) {
return "text/html";
} else if (suffix.equalsIgnoreCase(".xml")) {
return "text/xml";
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
@@ -1,5 +1,6 @@
package io.quarkus.qute;

import io.quarkus.qute.TemplateLocator.TemplateLocation;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
Expand Down Expand Up @@ -35,20 +36,20 @@ class EngineImpl implements Engine {
private final List<NamespaceResolver> namespaceResolvers;
private final Evaluator evaluator;
private final Map<String, Template> templates;
private final List<Function<String, Optional<Reader>>> locators;
private final List<TemplateLocator> locators;
private final List<ResultMapper> resultMappers;
private final PublisherFactory publisherFactory;
private final AtomicLong idGenerator = new AtomicLong(0);

EngineImpl(Map<String, SectionHelperFactory<?>> sectionHelperFactories, List<ValueResolver> valueResolvers,
List<NamespaceResolver> namespaceResolvers, List<Function<String, Optional<Reader>>> locators,
List<NamespaceResolver> namespaceResolvers, List<TemplateLocator> locators,
List<ResultMapper> resultMappers, Function<String, SectionHelperFactory<?>> sectionHelperFunc) {
this.sectionHelperFactories = new HashMap<>(sectionHelperFactories);
this.valueResolvers = sort(valueResolvers);
this.namespaceResolvers = ImmutableList.copyOf(namespaceResolvers);
this.evaluator = new EvaluatorImpl(this.valueResolvers);
this.templates = new ConcurrentHashMap<>();
this.locators = ImmutableList.copyOf(locators);
this.locators = sort(locators);
ServiceLoader<PublisherFactory> loader = ServiceLoader.load(PublisherFactory.class);
Iterator<PublisherFactory> iterator = loader.iterator();
if (iterator.hasNext()) {
Expand All @@ -65,8 +66,9 @@ class EngineImpl implements Engine {
this.sectionHelperFunc = sectionHelperFunc;
}

public Template parse(String content) {
return new Parser(this).parse(new StringReader(content));
@Override
public Template parse(String content, Variant variant) {
return new Parser(this).parse(new StringReader(content), Optional.ofNullable(variant));
}

@Override
Expand Down Expand Up @@ -125,11 +127,11 @@ String generateId() {
}

private Template load(String id) {
for (Function<String, Optional<Reader>> locator : locators) {
Optional<Reader> reader = locator.apply(id);
if (reader.isPresent()) {
try (Reader r = reader.get()) {
return new Parser(this).parse(ensureBufferedReader(reader.get()));
for (TemplateLocator locator : locators) {
Optional<TemplateLocation> location = locator.locate(id);
if (location.isPresent()) {
try (Reader r = location.get().read()) {
return new Parser(this).parse(ensureBufferedReader(r), location.get().getVariant());
} catch (IOException e) {
LOGGER.warn("Unable to close the reader for " + id, e);
}
Expand Down

0 comments on commit bb272db

Please sign in to comment.