Repository: PebbleTemplates/pebble
Pull request #574 (2021-02-03 15:30:19)

New base64encode and sha256 filters
Adding new filters.
Fixes issues  #573 and #572.

objectProgrammer: Perhaps base64 is common enough to be in the core, but if SHA256 gets added then suddenly we could end up with MD5, SHA-1, SHA-2, SHA-3 and more. I feel like there are too many hashing functions for any of them to be included in the core.

Just curious, what's the use for having a hashing filter?

And base64encode sounds too long, maybe just `base64`?
silviuvergoti: Sure, I'll shorten it. I thought that a simple `base64` would not be clear enough if it concerns encoding or decoding.  
Regarding the hashing filters, there are a lot of web services requiring hashing of PII (emails, ids, phones). If someone needs it, the filter could be changed to something like `{{ originalstring | hash('SHA256') }}` and it could support many more hash algorithms.

Patch:
diff --git a/docs/src/orchid/resources/wiki/filter/base64decode.md b/docs/src/orchid/resources/wiki/filter/base64decode.md
new file mode 100644
index 00000000..84a51793
--- /dev/null
+++ b/docs/src/orchid/resources/wiki/filter/base64decode.md
@@ -0,0 +1,14 @@
+---
+---
+
+# `base64decode`
+The `base64decode` filter takes the given input, Base64-decodes it, if possible, and returns the byte array converted to UTF-8 String.
+Applying the filter on an incorrect base64-encoded string will throw an exception. 
+
+```twig
+{% verbatim %}{{ "dGVzdA==" | base64decode }}{% endverbatim %}
+```
+The above example will output the following:
+```
+test
+```
\ No newline at end of file
diff --git a/docs/src/orchid/resources/wiki/filter/base64encode.md b/docs/src/orchid/resources/wiki/filter/base64encode.md
new file mode 100644
index 00000000..99211d5e
--- /dev/null
+++ b/docs/src/orchid/resources/wiki/filter/base64encode.md
@@ -0,0 +1,13 @@
+---
+---
+
+# `base64encode`
+The `base64encode` filter takes the given input, converts it to an UTF-8 String (`.toString()`) and Base64-encodes it.
+
+```twig
+{% verbatim %}{{ "test" | base64encode }}{% endverbatim %}
+```
+The above example will output the following:
+```
+dGVzdA==
+```
diff --git a/docs/src/orchid/resources/wiki/filter/sha256.md b/docs/src/orchid/resources/wiki/filter/sha256.md
new file mode 100644
index 00000000..26e25a2c
--- /dev/null
+++ b/docs/src/orchid/resources/wiki/filter/sha256.md
@@ -0,0 +1,13 @@
+---
+---
+
+# `sha256`
+The `sha256` filter returns the SHA-256 hash of the given UTF-8 String.
+
+```twig
+{% verbatim %}{{ "test" | sha256 }}{% endverbatim %}
+```
+The above example will output the following:
+```
+9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
+```
\ No newline at end of file
diff --git a/docs/src/orchid/resources/wiki/summary.md b/docs/src/orchid/resources/wiki/summary.md
index 9512175c..53864cba 100644
--- a/docs/src/orchid/resources/wiki/summary.md
+++ b/docs/src/orchid/resources/wiki/summary.md
@@ -30,6 +30,8 @@
 ## Filters
 - [abbreviate](filter/abbreviate.md)
 - [abs](filter/abs.md)
+- [base64decode](filter/base64decode.md)
+- [base64encode](filter/base64encode.md)
 - [capitalize](filter/capitalize.md)
 - [date](filter/date.md)
 - [default](filter/default.md)
@@ -44,6 +46,7 @@
 - [replace](filter/replace.md)
 - [reverse](filter/reverse.md)
 - [rsort](filter/rsort.md)
+- [sha256](filter/sha256.md)
 - [slice](filter/slice.md)
 - [sort](filter/sort.md)
 - [split](filter/split.md)
diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/extension/core/Base64DecoderFilter.java b/pebble/src/main/java/com/mitchellbosecke/pebble/extension/core/Base64DecoderFilter.java
new file mode 100644
index 00000000..542fd429
--- /dev/null
+++ b/pebble/src/main/java/com/mitchellbosecke/pebble/extension/core/Base64DecoderFilter.java
@@ -0,0 +1,49 @@
+package com.mitchellbosecke.pebble.extension.core;
+
+import com.mitchellbosecke.pebble.error.PebbleException;
+import com.mitchellbosecke.pebble.extension.Filter;
+import com.mitchellbosecke.pebble.template.EvaluationContext;
+import com.mitchellbosecke.pebble.template.PebbleTemplate;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class implements the 'base64encode' filter.
+ *
+ * @author Silviu Vergoti
+ */
+public class Base64DecoderFilter implements Filter {
+
+  public static final String FILTER_NAME = "base64decode";
+
+  @Override
+  public List<String> getArgumentNames() {
+    return null;
+  }
+
+  @Override
+  public Object apply(Object input, Map<String, Object> args, PebbleTemplate self,
+      EvaluationContext context, int lineNumber) throws PebbleException {
+    if (input == null) {
+      return null;
+    }
+
+    String decoded = null;
+    if (input instanceof String) {
+      try {
+        byte [] bytes = Base64.getDecoder().decode(((String) input).getBytes(StandardCharsets.UTF_8));
+        decoded = new String(bytes, StandardCharsets.UTF_8);
+      } catch (Exception e) {
+        throw new PebbleException(e, "Please provide a correctly Base64 encoded string containing an UTF-8 string\n", lineNumber, self.getName());
+      }
+    } else {
+      throw new PebbleException(null, "This filter applies to String\n", lineNumber, self.getName());
+    }
+    return decoded;
+  }
+
+}
diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/extension/core/Base64EncoderFilter.java b/pebble/src/main/java/com/mitchellbosecke/pebble/extension/core/Base64EncoderFilter.java
new file mode 100644
index 00000000..ccb72742
--- /dev/null
+++ b/pebble/src/main/java/com/mitchellbosecke/pebble/extension/core/Base64EncoderFilter.java
@@ -0,0 +1,37 @@
+package com.mitchellbosecke.pebble.extension.core;
+
+import com.mitchellbosecke.pebble.error.PebbleException;
+import com.mitchellbosecke.pebble.extension.Filter;
+import com.mitchellbosecke.pebble.template.EvaluationContext;
+import com.mitchellbosecke.pebble.template.PebbleTemplate;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class implements the 'base64encode' filter.
+ *
+ * @author Silviu Vergoti
+ */
+public class Base64EncoderFilter implements Filter {
+
+  public static final String FILTER_NAME = "base64encode";
+
+  @Override
+  public List<String> getArgumentNames() {
+    return null;
+  }
+
+  @Override
+  public Object apply(Object input, Map<String, Object> args, PebbleTemplate self,
+      EvaluationContext context, int lineNumber) throws PebbleException {
+    if (input == null) {
+      return null;
+    }
+
+    return Base64.getEncoder().encodeToString(input.toString().getBytes(StandardCharsets.UTF_8));
+  }
+
+}
diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/extension/core/CoreExtension.java b/pebble/src/main/java/com/mitchellbosecke/pebble/extension/core/CoreExtension.java
index d69f5600..73b95b87 100644
--- a/pebble/src/main/java/com/mitchellbosecke/pebble/extension/core/CoreExtension.java
+++ b/pebble/src/main/java/com/mitchellbosecke/pebble/extension/core/CoreExtension.java
@@ -149,6 +149,9 @@ public class CoreExtension extends AbstractExtension {
     filters.put(ReplaceFilter.FILTER_NAME, new ReplaceFilter());
     filters.put(MergeFilter.FILTER_NAME, new MergeFilter());
     filters.put(SplitFilter.FILTER_NAME, new SplitFilter());
+    filters.put(Base64EncoderFilter.FILTER_NAME, new Base64EncoderFilter());
+    filters.put(Base64DecoderFilter.FILTER_NAME, new Base64DecoderFilter());
+    filters.put(Sha256Filter.FILTER_NAME, new Sha256Filter());
     return filters;
   }
 
diff --git a/pebble/src/main/java/com/mitchellbosecke/pebble/extension/core/Sha256Filter.java b/pebble/src/main/java/com/mitchellbosecke/pebble/extension/core/Sha256Filter.java
new file mode 100644
index 00000000..9dd4016c
--- /dev/null
+++ b/pebble/src/main/java/com/mitchellbosecke/pebble/extension/core/Sha256Filter.java
@@ -0,0 +1,61 @@
+package com.mitchellbosecke.pebble.extension.core;
+
+import com.mitchellbosecke.pebble.error.PebbleException;
+import com.mitchellbosecke.pebble.extension.Filter;
+import com.mitchellbosecke.pebble.template.EvaluationContext;
+import com.mitchellbosecke.pebble.template.PebbleTemplate;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class implements the 'sha256' filter.
+ *
+ * @author Silviu Vergoti
+ */
+public class Sha256Filter implements Filter {
+
+  public static final String FILTER_NAME = "sha256";
+
+  @Override
+  public List<String> getArgumentNames() {
+    return null;
+  }
+
+  @Override
+  public Object apply(Object input, Map<String, Object> args, PebbleTemplate self,
+      EvaluationContext context, int lineNumber) throws PebbleException {
+    if (input == null) {
+      return null;
+    }
+
+    if (input instanceof String) {
+      MessageDigest digest = null;
+      byte[] encodedHash = null;
+      try {
+        digest = MessageDigest.getInstance("SHA-256");
+        encodedHash = digest.digest(((String) input).getBytes(StandardCharsets.UTF_8));
+      } catch (Exception e) {
+        throw new PebbleException(e, "Hashing exception encountered\n", lineNumber, self.getName());
+      }
+      return bytesToHex(encodedHash);
+    } else {
+      throw new PebbleException(null, "Need a string to hash\n", lineNumber, self.getName());
+    }
+  }
+
+  private static String bytesToHex(byte[] bytes) {
+    StringBuilder hexString = new StringBuilder(2 * bytes.length);
+    for (int i = 0; i < bytes.length; i++) {
+      String hex = Integer.toHexString(0xff & bytes[i]);
+      if (hex.length() == 1) {
+        hexString.append('0');
+      }
+      hexString.append(hex);
+    }
+    return hexString.toString();
+  }
+
+}


Test patch:
diff --git a/pebble/src/test/java/com/mitchellbosecke/pebble/CoreFiltersTest.java b/pebble/src/test/java/com/mitchellbosecke/pebble/CoreFiltersTest.java
index 7eb8ad42..8f16ce64 100644
--- a/pebble/src/test/java/com/mitchellbosecke/pebble/CoreFiltersTest.java
+++ b/pebble/src/test/java/com/mitchellbosecke/pebble/CoreFiltersTest.java
@@ -1329,6 +1329,105 @@ class CoreFiltersTest {
     assertEquals("I like foo and bar.", writer.toString());
   }
 
+  /**
+   * Tests {@link com.mitchellbosecke.pebble.extension.core.Base64EncoderFilter} if the base64 encoding filter is working for a string value, a string constant, null.
+   */
+  @Test
+  void testBase64EncoderFilterInTemplate() throws PebbleException, IOException {
+    PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader())
+            .strictVariables(false).build();
+
+    PebbleTemplate template = pebble.getTemplate("var=\"{{ var | base64encode }}\" const=\"{{ \"test\" | base64encode }}\" null=\"{{ null | base64encode }}\"");
+
+    Map<String, Object> context = new HashMap<>();
+    context.put("var", "test");
+
+    Writer writer = new StringWriter();
+    template.evaluate(writer, context);
+    assertEquals("var=\"dGVzdA==\" const=\"dGVzdA==\" null=\"\"", writer.toString());
+  }
+
+  /**
+   * Tests {@link com.mitchellbosecke.pebble.extension.core.Base64DecoderFilter} if the base64 decoder filter is working for a string value, a string constant, null.
+   */
+  @Test
+  void testBase64DecoderFilterInTemplate() throws PebbleException, IOException {
+    PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader())
+            .strictVariables(false).build();
+
+    PebbleTemplate template = pebble.getTemplate("var=\"{{ var | base64decode }}\" const=\"{{ \"dGVzdA==\" | base64decode }}\" null=\"{{ null | base64decode }}\"");
+
+    Map<String, Object> context = new HashMap<>();
+    context.put("var", "dGVzdA==");
+
+    Writer writer = new StringWriter();
+    template.evaluate(writer, context);
+    assertEquals("var=\"test\" const=\"test\" null=\"\"", writer.toString());
+  }
+
+  @Test
+  void testBase64DecodeFilterBadEncodedStringFail() throws PebbleException, IOException {
+    assertThrows(PebbleException.class, () -> {
+      PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader())
+              .strictVariables(false).build();
+
+      PebbleTemplate template = pebble.getTemplate("{{ \"this is not a base64 encoded string\" | base64decode }}");
+
+      Map<String, Object> context = new HashMap<>();
+
+      Writer writer = new StringWriter();
+      template.evaluate(writer, context);
+    });
+  }
+
+  @Test
+  void testBase64DecodeFilterNoStringFail() throws PebbleException, IOException {
+    assertThrows(PebbleException.class, () -> {
+      PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader())
+              .strictVariables(false).build();
+
+      PebbleTemplate template = pebble.getTemplate("{{ {'foo':1} | base64decode }}");
+
+      Map<String, Object> context = new HashMap<>();
+
+      Writer writer = new StringWriter();
+      template.evaluate(writer, context);
+    });
+  }
+
+  @Test
+  void testSha256FilterNoStringFail() throws PebbleException, IOException {
+    assertThrows(PebbleException.class, () -> {
+      PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader())
+              .strictVariables(false).build();
+
+      PebbleTemplate template = pebble.getTemplate("{{ {'foo':1} | sha256 }}");
+
+      Map<String, Object> context = new HashMap<>();
+
+      Writer writer = new StringWriter();
+      template.evaluate(writer, context);
+    });
+  }
+
+  /**
+   * Tests {@link com.mitchellbosecke.pebble.extension.core.Sha256Filter} if the SHA256 hashing filter is working for a string value, a string constant, null.
+   */
+  @Test
+  void testSha256FilterInTemplate() throws PebbleException, IOException {
+    PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader())
+            .strictVariables(false).build();
+
+    PebbleTemplate template = pebble.getTemplate("var=\"{{ var | sha256 }}\" const=\"{{ \"test\" | sha256}}\" null=\"{{ null | sha256 }}\"");
+
+    Map<String, Object> context = new HashMap<>();
+    context.put("var", "test");
+
+    Writer writer = new StringWriter();
+    template.evaluate(writer, context);
+    assertEquals("var=\"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\" const=\"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\" null=\"\"", writer.toString());
+  }
+
   @Test
   void testMergeOk() throws PebbleException, IOException {
     PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader())