diff --git a/docs/src/orchid/resources/wiki/guide/customize-defaults.md b/docs/src/orchid/resources/wiki/guide/customize-defaults.md index e76d3725f..f14a88802 100644 --- a/docs/src/orchid/resources/wiki/guide/customize-defaults.md +++ b/docs/src/orchid/resources/wiki/guide/customize-defaults.md @@ -35,3 +35,22 @@ The `ExtensionCustomizer` will be used to wrap any Pebble-extension which is pro ```java PebbleEngine engine = new PebbleEngine.Builder().registerExtensionCustomizer(ExampleOptOuts::new).build(); ``` + +### Default implementation of ExtensionCustomizer + +The `DisallowExtensionCustomizerBuilder` class can be used to disallow some default functionality, make pebble more controllable. + +For example of use, see below: + +```java +PebbleEngine engine = new PebbleEngine.Builder() + .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() + .disallowedTokenParserTags(singletonList("flush")) + .disallowedFunctionKeys(singletonList("max")) + .disallowedFilterKeys(singletonList("upper")) + .disallowedTestKeys(singletonList("null")) + .disallowedBinaryOperatorSymbols(singletonList(">")) + .disallowedUnaryOperatorSymbols(singletonList("-")) + .build()) + .build(); +``` \ No newline at end of file diff --git a/pebble/src/main/java/io/pebbletemplates/pebble/extension/core/DisallowExtensionCustomizerBuilder.java b/pebble/src/main/java/io/pebbletemplates/pebble/extension/core/DisallowExtensionCustomizerBuilder.java new file mode 100644 index 000000000..b134a87e8 --- /dev/null +++ b/pebble/src/main/java/io/pebbletemplates/pebble/extension/core/DisallowExtensionCustomizerBuilder.java @@ -0,0 +1,136 @@ +package io.pebbletemplates.pebble.extension.core; + +import io.pebbletemplates.pebble.extension.Extension; +import io.pebbletemplates.pebble.extension.ExtensionCustomizer; +import io.pebbletemplates.pebble.extension.Filter; +import io.pebbletemplates.pebble.extension.Test; +import io.pebbletemplates.pebble.operator.BinaryOperator; +import io.pebbletemplates.pebble.operator.UnaryOperator; +import io.pebbletemplates.pebble.tokenParser.TokenParser; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * @author freshchen + * @since 2024/3/2 + */ +public class DisallowExtensionCustomizerBuilder { + + private Collection disallowedFilterKeys; + + private Collection disallowedTokenParserTags; + + private Collection disallowedFunctionKeys; + + private Collection disallowedBinaryOperatorSymbols; + + private Collection disallowedUnaryOperatorSymbols; + + private Collection disallowedTestKeys; + + public DisallowExtensionCustomizerBuilder disallowedFunctionKeys(Collection disallowedFunctionKeys) { + this.disallowedFunctionKeys = disallowedFunctionKeys; + return this; + } + + public DisallowExtensionCustomizerBuilder disallowedTokenParserTags(Collection disallowedTokenParserTags) { + this.disallowedTokenParserTags = disallowedTokenParserTags; + return this; + } + + public DisallowExtensionCustomizerBuilder disallowedFilterKeys(Collection disallowedFilterKeys) { + this.disallowedFilterKeys = disallowedFilterKeys; + return this; + } + + public DisallowExtensionCustomizerBuilder disallowedUnaryOperatorSymbols(Collection disallowedUnaryOperatorSymbols) { + this.disallowedUnaryOperatorSymbols = disallowedUnaryOperatorSymbols; + return this; + } + + public DisallowExtensionCustomizerBuilder disallowedBinaryOperatorSymbols(Collection disallowedBinaryOperatorSymbols) { + this.disallowedBinaryOperatorSymbols = disallowedBinaryOperatorSymbols; + return this; + } + + public DisallowExtensionCustomizerBuilder disallowedTestKeys(Collection disallowedTestKeys) { + this.disallowedTestKeys = disallowedTestKeys; + return this; + } + + public Function build() { + + return extension -> new ExtensionCustomizer(extension) { + + @Override + public Map getTests() { + return this.disallow(super::getTests, DisallowExtensionCustomizerBuilder.this.disallowedTestKeys); + } + + @Override + public List getUnaryOperators() { + return this.disallow(super::getUnaryOperators, DisallowExtensionCustomizerBuilder.this.disallowedUnaryOperatorSymbols, UnaryOperator::getSymbol); + } + + @Override + public List getBinaryOperators() { + return this.disallow(super::getBinaryOperators, DisallowExtensionCustomizerBuilder.this.disallowedBinaryOperatorSymbols, BinaryOperator::getSymbol); + } + + @Override + public Map getFunctions() { + return this.disallow(super::getFunctions, DisallowExtensionCustomizerBuilder.this.disallowedFunctionKeys); + } + + @Override + public Map getFilters() { + return this.disallow(super::getFilters, DisallowExtensionCustomizerBuilder.this.disallowedFilterKeys); + } + + @Override + public List getTokenParsers() { + return this.disallow(super::getTokenParsers, DisallowExtensionCustomizerBuilder.this.disallowedTokenParserTags, TokenParser::getTag); + } + + private List disallow(Supplier> superGetter, + Collection disallowedList, + Function keyGetter) { + List superList = superGetter.get(); + if (disallowedList == null || disallowedList.isEmpty()) { + return superList; + } + + List result = Optional.ofNullable(superList).map(ArrayList::new) + .orElseGet(ArrayList::new); + + disallowedList.stream() + .filter(Objects::nonNull) + .forEach(v -> result.removeIf(t -> v.equals(keyGetter.apply(t)))); + + return result; + } + + private Map disallow(Supplier> superGetter, + Collection disallowedList) { + Map superMap = superGetter.get(); + if (disallowedList == null || disallowedList.isEmpty()) { + return superMap; + } + + Map result = Optional.ofNullable(superMap).map(HashMap::new) + .orElseGet(HashMap::new); + + disallowedList.stream() + .filter(Objects::nonNull) + .forEach(result::remove); + + return result; + } + + }; + } + + +} diff --git a/pebble/src/test/java/io/pebbletemplates/pebble/extension/ExtensionCustomizerTest.java b/pebble/src/test/java/io/pebbletemplates/pebble/extension/ExtensionCustomizerTest.java index 98b2caaf1..0ba5e0a7a 100644 --- a/pebble/src/test/java/io/pebbletemplates/pebble/extension/ExtensionCustomizerTest.java +++ b/pebble/src/test/java/io/pebbletemplates/pebble/extension/ExtensionCustomizerTest.java @@ -2,12 +2,13 @@ import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.error.PebbleException; +import io.pebbletemplates.pebble.extension.core.DisallowExtensionCustomizerBuilder; import io.pebbletemplates.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.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -17,26 +18,111 @@ class ExtensionCustomizerTest { - PebbleEngine pebble; - - @BeforeEach - void setUp() { - pebble = new PebbleEngine.Builder() + @Test + void upperFilterCannotBeUsed() throws IOException { + PebbleEngine pebble = new PebbleEngine.Builder() .registerExtensionCustomizer(RemoveUpperCustomizer::new) .build(); + + 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()); + } + + @Test + void setDisallowedTokenParserTags() { + PebbleEngine pebbleEngine = new PebbleEngine.Builder() + .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() + .disallowedTokenParserTags(Collections.singletonList("flush")) + .build()) + .build(); + + PebbleException exception = assertThrows(PebbleException.class, + () -> pebbleEngine.getLiteralTemplate("{{ k1 }}\n" + + "{% flush %}\n" + + "{{ k2 }}")); + assertTrue(exception.getMessage().contains("Unexpected tag name \"flush\"")); } @Test - void upperFilterCannotBeUsed() throws IOException { + void setDisallowedFilters() { + PebbleEngine pebbleEngine = new PebbleEngine.Builder() + .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() + .disallowedFilterKeys(Collections.singletonList("upper")) + .build()) + .build(); + Map obj = new HashMap<>(); obj.put("test", "abc"); - PebbleTemplate template = pebble.getLiteralTemplate("{{ test | upper }}"); + PebbleTemplate template = pebbleEngine.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()); } + @Test + void setDisallowedFunctions() { + PebbleEngine pebbleEngine = new PebbleEngine.Builder() + .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() + .disallowedFunctionKeys(Collections.singletonList("max")) + .build()) + .build(); + + Map obj = new HashMap<>(); + obj.put("age", 30); + PebbleTemplate template = pebbleEngine.getLiteralTemplate("{{ max(age, 80) }}"); + + PebbleException exception = assertThrows(PebbleException.class, () -> template.evaluate(new StringWriter(), obj)); + assertTrue(exception.getMessage().contains("Function or Macro [max] does not exist")); + } + + @Test + void setDisallowedBinaryOperatorSymbols() { + PebbleEngine pebbleEngine = new PebbleEngine.Builder() + .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() + .disallowedBinaryOperatorSymbols(Collections.singletonList(">")) + .build()) + .build(); + + PebbleException exception = assertThrows(PebbleException.class, () -> pebbleEngine.getLiteralTemplate("{% if 10 > 9 %}\n" + + "{{ name }}" + + "{% endif %}")); + assertTrue(exception.getMessage().contains("Unexpected character [>]")); + } + + @Test + void setDisallowedUnaryOperatorSymbols() throws IOException { + PebbleEngine pebbleEngine = new PebbleEngine.Builder() + .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() + .disallowedUnaryOperatorSymbols(Collections.singletonList("-")) + .build()) + .build(); + + PebbleException exception = assertThrows(PebbleException.class, () -> pebbleEngine.getLiteralTemplate("{{ -num }}")); + assertTrue(exception.getMessage().contains("Unexpected token \"OPERATOR\" of value \"-\"")); + } + + @Test + void setDisallowedTestKeys() throws IOException { + PebbleEngine pebbleEngine = new PebbleEngine.Builder() + .registerExtensionCustomizer(new DisallowExtensionCustomizerBuilder() + .disallowedTestKeys(Collections.singletonList("null")) + .build()) + .build(); + + PebbleTemplate template = pebbleEngine.getLiteralTemplate("{% if 123 is null %}\n" + + "{{ name }}" + + "{% endif %}"); + + PebbleException exception = assertThrows(PebbleException.class, () -> template.evaluate(new StringWriter())); + assertTrue(exception.getMessage().contains("Wrong operand(s) type in conditional expression")); + } + private static class RemoveUpperCustomizer extends ExtensionCustomizer { public RemoveUpperCustomizer(Extension core) {