Skip to content

Commit

Permalink
issue #520: provide option to customize stock extensions (#552)
Browse files Browse the repository at this point in the history
* issue #520: provide option to customize stock extensions

* issue #520: only one customizer / hide implementation details

* issue #520: JavaDoc & cleanups

* issue #520: added documentation
  • Loading branch information
pepperbob authored Dec 18, 2020
1 parent 4a21f3b commit 4fc15f7
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 40 deletions.
37 changes: 37 additions & 0 deletions docs/src/orchid/resources/wiki/guide/customize-defaults.md
Original file line number Diff line number Diff line change
@@ -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<TokenParser> getTokenParsers() {
List<TokenParser> 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();
```
1 change: 1 addition & 0 deletions docs/src/orchid/resources/wiki/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
63 changes: 23 additions & 40 deletions pebble/src/main/java/com/mitchellbosecke/pebble/PebbleEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -277,8 +272,6 @@ public static class Builder {

private Loader<?> loader;

private final List<Extension> userProvidedExtensions = new ArrayList<>();

private Syntax syntax;

private boolean strictVariables = false;
Expand All @@ -297,18 +290,16 @@ public static class Builder {

private PebbleCache<CacheKey, Object> 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.
*/
Expand All @@ -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;
}

Expand Down Expand Up @@ -455,7 +446,7 @@ public Builder tagCache(PebbleCache<CacheKey, Object> tagCache) {
* @return This builder object
*/
public Builder autoEscaping(boolean autoEscaping) {
this.escaperExtension.setAutoEscaping(autoEscaping);
this.factory.autoEscaping(autoEscaping);
return this;
}

Expand All @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -566,14 +557,26 @@ 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<Extension, ExtensionCustomizer> customizer) {
this.factory.registerExtensionCustomizer(customizer);
return this;
}

/**
* Creates the PebbleEngine instance.
*
* @return A PebbleEngine object that can be used to create PebbleTemplate objects.
*/
public PebbleEngine build() {

ExtensionRegistry extensionRegistry = this.buildExtensionRegistry();
ExtensionRegistry extensionRegistry = this.factory.buildExtensionRegistry();

// default loader
if (this.loader == null) {
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Filter> getFilters() {
return delegate.getFilters();
}

@Override
public Map<String, Test> getTests() {
return delegate.getTests();
}

@Override
public Map<String, Function> getFunctions() {
return delegate.getFunctions();
}

@Override
public List<TokenParser> getTokenParsers() {
return delegate.getTokenParsers();
}

@Override
public List<BinaryOperator> getBinaryOperators() {
return delegate.getBinaryOperators();
}

@Override
public List<UnaryOperator> getUnaryOperators() {
return delegate.getUnaryOperators();
}

@Override
public Map<String, Object> getGlobalVariables() {
return delegate.getGlobalVariables();
}

@Override
public List<NodeVisitorFactory> getNodeVisitors() {
return delegate.getNodeVisitors();
}

@Override
public List<AttributeResolver> getAttributeResolver() {
return delegate.getAttributeResolver();
}

}
Original file line number Diff line number Diff line change
@@ -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<Extension> userProvidedExtensions = new ArrayList<>();

private final EscaperExtension escaperExtension = new EscaperExtension();

private boolean allowOverrideCoreOperators = false;

private Function<Extension, Extension> 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<Extension, ExtensionCustomizer> customizer) {
this.customizer = customizer::apply;
}

}
Loading

0 comments on commit 4fc15f7

Please sign in to comment.