diff --git a/docs/src/orchid/resources/wiki/guide/customize-defaults.md b/docs/src/orchid/resources/wiki/guide/customize-defaults.md new file mode 100644 index 000000000..50be2cced --- /dev/null +++ b/docs/src/orchid/resources/wiki/guide/customize-defaults.md @@ -0,0 +1,37 @@ +--- +--- + +Pebble comes with a rich set of built-in tags and filters that will help you render your templates into websites and other documents with ease. However, imagine a more specific use-case where the templates are not entirely under your control. + +In these cases it might be advised to consider stripping-down Pebbles' built-in functionality that may otherwise introduce security-concers regarding the integrity and stability of your application. + +### Opt-Out using ExtensionCustomizer + +The `ExtensionCustomizer` base class can be used to gain access to the default functionality before it is loaded into Pebbles template engine. Overwrite methods to get hold on provided default-functionality and modify whatever should be available for the template engine. + +The following example removes the `ForTokenParser`, i.e. the ability to parse `{% for %}{{ ... }}{% endfor %}` constructs: + +```java +class ExampleOptOuts extends ExtensionCustomizer { + + public ExampleOptOuts(Extension ext) { + super(ext); + } + + @Override + public List getTokenParsers() { + List tokenParsers = Optional.ofNullable(super.getTokenParsers()) + .map(ArrayList::new).orElseGet(ArrayList::new); + + tokenParsers.removeIf(x -> x instanceof ForTokenParser); + return tokenParsers; + } + +} +``` + +The `ExtensionCustomizer` will be used to wrap any Pebble-extension which is provided by default. It can be registered in your setup code to create `PebbleEngine`: + +```java +PebbleEngine engine = new PebbleEngine.Builder().registerCustomizer(ExampleOptOuts::new).build(); +``` diff --git a/docs/src/orchid/resources/wiki/summary.md b/docs/src/orchid/resources/wiki/summary.md index 5209ffed2..9512175c3 100644 --- a/docs/src/orchid/resources/wiki/summary.md +++ b/docs/src/orchid/resources/wiki/summary.md @@ -4,6 +4,7 @@ - [Spring petclinic](https://github.com/PebbleTemplates/spring-petclinic) - [Pebble Spring Example](https://github.com/PebbleTemplates/pebble-example-spring) - [Basic Usage](guide/basic-usage.md) +- [Customize Defaults](guide/customize-defaults.md) - [Escaping](guide/escaping.md) - [Extending Pebble](guide/extending-pebble.md) - [High Performance Techniques](guide/high-performance.md) diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/PebbleEngine.java b/pebble/src/main/java/com/mitchellbosecke/pebble/PebbleEngine.java index 498d5b358..7e5d0109a 100644 --- a/pebble/src/main/java/com/mitchellbosecke/pebble/PebbleEngine.java +++ b/pebble/src/main/java/com/mitchellbosecke/pebble/PebbleEngine.java @@ -18,14 +18,8 @@ import com.mitchellbosecke.pebble.cache.template.ConcurrentMapTemplateCache; import com.mitchellbosecke.pebble.cache.template.NoOpTemplateCache; import com.mitchellbosecke.pebble.error.LoaderException; -import com.mitchellbosecke.pebble.extension.Extension; -import com.mitchellbosecke.pebble.extension.ExtensionRegistry; -import com.mitchellbosecke.pebble.extension.NodeVisitorFactory; -import com.mitchellbosecke.pebble.extension.core.AttributeResolverExtension; -import com.mitchellbosecke.pebble.extension.core.CoreExtension; -import com.mitchellbosecke.pebble.extension.escaper.EscaperExtension; +import com.mitchellbosecke.pebble.extension.*; import com.mitchellbosecke.pebble.extension.escaper.EscapingStrategy; -import com.mitchellbosecke.pebble.extension.i18n.I18nExtension; import com.mitchellbosecke.pebble.lexer.LexerImpl; import com.mitchellbosecke.pebble.lexer.Syntax; import com.mitchellbosecke.pebble.lexer.TokenStream; @@ -44,10 +38,11 @@ import java.io.IOException; import java.io.Reader; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutorService; +import java.util.function.Function; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -277,8 +272,6 @@ public static class Builder { private Loader loader; - private final List userProvidedExtensions = new ArrayList<>(); - private Syntax syntax; private boolean strictVariables = false; @@ -297,18 +290,16 @@ public static class Builder { private PebbleCache tagCache; - private final EscaperExtension escaperExtension = new EscaperExtension(); - private boolean literalDecimalTreatedAsInteger = false; private boolean greedyMatchMethod = false; - private boolean allowOverrideCoreOperators = false; - private boolean literalNumbersAsBigDecimals = false; private MethodAccessValidator methodAccessValidator = new BlacklistMethodAccessValidator(); + private final ExtensionRegistryFactory factory = new ExtensionRegistryFactory(); + /** * Creates the builder. */ @@ -334,7 +325,7 @@ public Builder loader(Loader loader) { * @return This builder object */ public Builder extension(Extension... extensions) { - Collections.addAll(this.userProvidedExtensions, extensions); + this.factory.extension(extensions); return this; } @@ -455,7 +446,7 @@ public Builder tagCache(PebbleCache tagCache) { * @return This builder object */ public Builder autoEscaping(boolean autoEscaping) { - this.escaperExtension.setAutoEscaping(autoEscaping); + this.factory.autoEscaping(autoEscaping); return this; } @@ -466,7 +457,7 @@ public Builder autoEscaping(boolean autoEscaping) { * @return This builder object */ public Builder allowOverrideCoreOperators(boolean allowOverrideCoreOperators) { - this.allowOverrideCoreOperators = allowOverrideCoreOperators; + this.factory.allowOverrideCoreOperators(allowOverrideCoreOperators); return this; } @@ -477,7 +468,7 @@ public Builder allowOverrideCoreOperators(boolean allowOverrideCoreOperators) { * @return This builder object */ public Builder defaultEscapingStrategy(String strategy) { - this.escaperExtension.setDefaultStrategy(strategy); + this.factory.defaultEscapingStrategy(strategy); return this; } @@ -489,7 +480,7 @@ public Builder defaultEscapingStrategy(String strategy) { * @return This builder object */ public Builder addEscapingStrategy(String name, EscapingStrategy strategy) { - this.escaperExtension.addEscapingStrategy(name, strategy); + this.factory.addEscapingStrategy(name, strategy); return this; } @@ -566,6 +557,18 @@ public Builder greedyMatchMethod(boolean greedyMatchMethod) { return this; } + /** + * Registers an implementation of {@link ExtensionCustomizer} to change runtime-behaviour of standard + * functionality. + * + * @param customizer The customizer which wraps any non-user-provided extension + * @return This build object + */ + public Builder registerExtensionCustomizer(Function customizer) { + this.factory.registerExtensionCustomizer(customizer); + return this; + } + /** * Creates the PebbleEngine instance. * @@ -573,7 +576,7 @@ public Builder greedyMatchMethod(boolean greedyMatchMethod) { */ public PebbleEngine build() { - ExtensionRegistry extensionRegistry = this.buildExtensionRegistry(); + ExtensionRegistry extensionRegistry = this.factory.buildExtensionRegistry(); // default loader if (this.loader == null) { @@ -617,26 +620,6 @@ public PebbleEngine build() { this.tagCache, this.templateCache, this.executorService, extensionRegistry, parserOptions, evaluationOptions); } - - private ExtensionRegistry buildExtensionRegistry() { - ExtensionRegistry extensionRegistry = new ExtensionRegistry(); - - extensionRegistry.addExtension(new CoreExtension()); - extensionRegistry.addExtension(this.escaperExtension); - extensionRegistry.addExtension(new I18nExtension()); - - for (Extension userProvidedExtension : this.userProvidedExtensions) { - if (this.allowOverrideCoreOperators) { - extensionRegistry.addOperatorOverridingExtension(userProvidedExtension); - } else { - extensionRegistry.addExtension(userProvidedExtension); - } - } - - extensionRegistry.addExtension(new AttributeResolverExtension()); - - return extensionRegistry; - } } public EvaluationOptions getEvaluationOptions() { diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/extension/ExtensionCustomizer.java b/pebble/src/main/java/com/mitchellbosecke/pebble/extension/ExtensionCustomizer.java new file mode 100644 index 000000000..ca9a2f98f --- /dev/null +++ b/pebble/src/main/java/com/mitchellbosecke/pebble/extension/ExtensionCustomizer.java @@ -0,0 +1,77 @@ +package com.mitchellbosecke.pebble.extension; + +import com.mitchellbosecke.pebble.attributes.AttributeResolver; +import com.mitchellbosecke.pebble.operator.BinaryOperator; +import com.mitchellbosecke.pebble.operator.UnaryOperator; +import com.mitchellbosecke.pebble.tokenParser.TokenParser; + +import java.util.List; +import java.util.Map; + +/** + * Base class that allows implementing a customizer to modify Pebbles build-in extensions. + * It is meant to provide a way to remove or replace functions, filters, tags, etc. to change + * the standard behaviour. Use-cases can be down-stripping available functionality for security + * reasons. + * + * Implementations of this class are meant to overwrite methods and access registered functionality + * before it is loaded into the PebbleEngine by calling super. + * + * The ExentsionCustomizer can be registred via {@link com.mitchellbosecke.pebble.PebbleEngine.Builder#registerExtensionCustomizer} + * and is applied for every non-user-provided extension. + * + */ +public abstract class ExtensionCustomizer implements Extension { + + private final Extension delegate; + + public ExtensionCustomizer(Extension delegate) { + this.delegate = delegate; + } + + @Override + public Map getFilters() { + return delegate.getFilters(); + } + + @Override + public Map getTests() { + return delegate.getTests(); + } + + @Override + public Map getFunctions() { + return delegate.getFunctions(); + } + + @Override + public List getTokenParsers() { + return delegate.getTokenParsers(); + } + + @Override + public List getBinaryOperators() { + return delegate.getBinaryOperators(); + } + + @Override + public List getUnaryOperators() { + return delegate.getUnaryOperators(); + } + + @Override + public Map getGlobalVariables() { + return delegate.getGlobalVariables(); + } + + @Override + public List getNodeVisitors() { + return delegate.getNodeVisitors(); + } + + @Override + public List getAttributeResolver() { + return delegate.getAttributeResolver(); + } + +} diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/extension/ExtensionRegistryFactory.java b/pebble/src/main/java/com/mitchellbosecke/pebble/extension/ExtensionRegistryFactory.java new file mode 100644 index 000000000..c2c67426b --- /dev/null +++ b/pebble/src/main/java/com/mitchellbosecke/pebble/extension/ExtensionRegistryFactory.java @@ -0,0 +1,72 @@ +package com.mitchellbosecke.pebble.extension; + +import com.mitchellbosecke.pebble.extension.core.AttributeResolverExtension; +import com.mitchellbosecke.pebble.extension.core.CoreExtension; +import com.mitchellbosecke.pebble.extension.escaper.EscaperExtension; +import com.mitchellbosecke.pebble.extension.escaper.EscapingStrategy; +import com.mitchellbosecke.pebble.extension.i18n.I18nExtension; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Provides configuration methods and builds the {@link ExtensionRegistry}. Used only internally by + * the {@link com.mitchellbosecke.pebble.PebbleEngine.Builder}. + * + */ +public class ExtensionRegistryFactory { + + private final List userProvidedExtensions = new ArrayList<>(); + + private final EscaperExtension escaperExtension = new EscaperExtension(); + + private boolean allowOverrideCoreOperators = false; + + private Function customizer = Function.identity(); + + public ExtensionRegistry buildExtensionRegistry() { + ExtensionRegistry extensionRegistry = new ExtensionRegistry(); + + Stream.of(new CoreExtension(), this.escaperExtension, new I18nExtension()) + .map(customizer::apply) + .forEach(extensionRegistry::addExtension); + + for (Extension userProvidedExtension : this.userProvidedExtensions) { + if (this.allowOverrideCoreOperators) { + extensionRegistry.addOperatorOverridingExtension(userProvidedExtension); + } else { + extensionRegistry.addExtension(userProvidedExtension); + } + } + + extensionRegistry.addExtension(customizer.apply(new AttributeResolverExtension())); + + return extensionRegistry; + } + + public void autoEscaping(boolean autoEscaping) { + this.escaperExtension.setAutoEscaping(autoEscaping); + } + + public void addEscapingStrategy(String name, EscapingStrategy strategy) { + this.escaperExtension.addEscapingStrategy(name, strategy); + } + + public void extension(Extension... extensions) { + Collections.addAll(this.userProvidedExtensions, extensions); + } + + public void allowOverrideCoreOperators(boolean allowOverrideCoreOperators) { + this.allowOverrideCoreOperators = allowOverrideCoreOperators; + } + + public void defaultEscapingStrategy(String strategy) { + this.escaperExtension.setDefaultStrategy(strategy); + } + + public void registerExtensionCustomizer(Function customizer) { + this.customizer = customizer::apply; + } + +} \ No newline at end of file diff --git a/pebble/src/test/java/com/mitchellbosecke/pebble/extension/ExtensionCustomizerTest.java b/pebble/src/test/java/com/mitchellbosecke/pebble/extension/ExtensionCustomizerTest.java new file mode 100644 index 000000000..27e339738 --- /dev/null +++ b/pebble/src/test/java/com/mitchellbosecke/pebble/extension/ExtensionCustomizerTest.java @@ -0,0 +1,56 @@ +package com.mitchellbosecke.pebble.extension; + +import com.mitchellbosecke.pebble.PebbleEngine; +import com.mitchellbosecke.pebble.error.PebbleException; +import com.mitchellbosecke.pebble.template.PebbleTemplate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ExtensionCustomizerTest { + + PebbleEngine pebble; + + @BeforeEach + void setUp() { + pebble = new PebbleEngine.Builder() + .registerExtensionCustomizer(RemoveUpperCustomizer::new) + .build(); + } + + @Test + void upperFilterCannotBeUsed() throws IOException { + Map obj = new HashMap<>(); + obj.put("test", "abc"); + PebbleTemplate template = pebble.getLiteralTemplate("{{ test | upper }}"); + + PebbleException exception = assertThrows(PebbleException.class, () -> template.evaluate(new StringWriter(), obj)); + assertTrue(exception.getMessage().contains("upper"), + () -> "Expect upper-Filter to not exist, actual Problem: " + exception.getMessage()); + } + + private static class RemoveUpperCustomizer extends ExtensionCustomizer { + + public RemoveUpperCustomizer(Extension core) { + super(core); + } + + @Override + public Map getFilters() { + Map filters = Optional.ofNullable(super.getFilters()).map(HashMap::new) + .orElseGet(HashMap::new); + filters.remove("upper"); + return filters; + } + + } + +}