Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ includes integrated support for internationalization.

For more information please visit the [website](https://pebbletemplates.io).

# Breaking changes in version 4.1.x

- If you do not provide a custom Loader, Pebble will now use only a `ClasspathLoader` by default, same as the spring autoconfiguration.
Before that, it would have used an instance of the `DelegatingLoader` which consists of a `ClasspathLoader` and a `FileLoader` behind the scenes to find your templates.
- Modify the `FileLoader` to use a mandatory sandboxed base directory parameter.

# Breaking changes in version 4.0.x

- Use one of the following artifactId according to the spring boot version that you are using
Expand Down
2 changes: 1 addition & 1 deletion docs/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>io.pebbletemplates</groupId>
<artifactId>pebble-project</artifactId>
<version>4.0.1-SNAPSHOT</version>
<version>4.1.0-SNAPSHOT</version>
</parent>

<artifactId>docs</artifactId>
Expand Down
8 changes: 0 additions & 8 deletions docs/src/orchid/resources/changelog/v4_0_1.md

This file was deleted.

17 changes: 17 additions & 0 deletions docs/src/orchid/resources/changelog/v4_1_0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
version: '4.1.0'
---

# BREAKING CHANGES
- Modify the `FileLoader` to use a mandatory sandboxed base directory parameter. (#715)
- If you do not provide a custom Loader, Pebble will now use only a `ClasspathLoader` by default, same as the spring autoconfiguration. (#715)
Before that, it would have used an instance of the `DelegatingLoader` which consists of a `ClasspathLoader` and a `FileLoader` behind the scenes to find your templates.

# New Features
- Use a default existing format of `yyyy-MM-dd'T'HH:mm:ssZ` when using date filter with a string (#677)
- Look for exact method / field match when doing reflection. Look for method get/is/has if none match
- Update some dependencies (#709)

# Bug Fixes
- NaN must return false instead of throwing an exception (#695)
- [CVE-2025-1686](https://nvd.nist.gov/vuln/detail/CVE-2025-1686).
11 changes: 5 additions & 6 deletions docs/src/orchid/resources/wiki/guide/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,17 @@ finding your templates.

Pebble ships with the following loader implementations:

- `DelegatingLoader`: Delegates responsibility to a collection of children loaders.
- `ClasspathLoader`: Uses a classloader to search the current classpath.
- `FileLoader`: Finds templates using a filesystem path.
- `FileLoader`: Finds templates using a filesystem path. Must provide a mandatory absolute base path.
- `ServletLoader`: Uses a servlet context to find the template. This is the recommended loader for use within an
application server but is not enabled by default.
- `Servlet5Loader`: Same as `ServletLoader`, but for Jakarta Servlet 5.0 or newer.
- `StringLoader`: Considers the name of the template to be the contents of the template.
- `DelegatingLoader`: Delegates responsibility to a collection of children loaders.
- `MemoryLoader`: Loader that supports inheritance and doesn't require a filesystem. This is useful for applications
- `StringLoader`: Considers the name of the template to be the contents of the template. Should not be used in a production environment. It is primarily for testing and debugging. Many tags may not work when using this loader, such as "extends", "imports", etc.
that retrieve templates from a database for example.

If you do not provide a custom Loader, Pebble will use an instance of the `DelegatingLoader` by default.
This delegating loader will use a `ClasspathLoader` and a `FileLoader` behind the scenes to find your templates.
If you do not provide a custom Loader, Pebble will use an instance of the `ClasspathLoader` by default.

## Pebble Engine Settings

Expand All @@ -85,7 +84,7 @@ All the settings are set during the construction of the `PebbleEngine` object.
| `tagCache` | An implementation of a ConcurrentMap cache that the Pebble engine will use for {{ anchor('cache tag', 'cache') }}. | Default implementation is `ConcurrentMapTagCache` and another implementation based on Caffeine is available (`CaffeineTagCache`) |
| `defaultLocale` | The default locale which will be passed to each compiled template. The templates then use this locale for functions such as i18n, etc. A template can also be given a unique locale during evaluation. | `Locale.getDefault()` |
| `executorService` | An `ExecutorService` that allows the usage of some advanced multithreading features, such as the `parallel` tag. | `null` |
| `loader` | An implementation of the `Loader` interface which is used to find templates. | An implementation of the `DelegatingLoader` which uses a `ClasspathLoader` and a `FileLoader` behind the scenes. |
| `loader` | An implementation of the `Loader` interface which is used to find templates. | An implementation of the `ClasspathLoader` |
| `strictVariables` | If set to true, Pebble will throw an exception if you try to access a variable or attribute that does not exist (or an attribute of a null variable). If set to false, your template will treat non-existing variables/attributes as null without ever skipping a beat. | `false` |
| `methodAccessValidator` | Pebble provides two implementations. NoOpMethodAccessValidator which do nothing and BlacklistMethodAccessValidator which checks that the method being called is not blacklisted. | `BlacklistMethodAccessValidator`
| `literalDecimalTreatedAsInteger` | option for treating literal decimals as `int`. Otherwise it is `long`. | `false` |
Expand Down
2 changes: 1 addition & 1 deletion pebble-spring/pebble-legacy-spring-boot-starter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<artifactId>pebble-spring</artifactId>
<groupId>io.pebbletemplates</groupId>
<version>4.0.1-SNAPSHOT</version>
<version>4.1.0-SNAPSHOT</version>
</parent>

<artifactId>pebble-legacy-spring-boot-starter</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion pebble-spring/pebble-spring-boot-starter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<artifactId>pebble-spring</artifactId>
<groupId>io.pebbletemplates</groupId>
<version>4.0.1-SNAPSHOT</version>
<version>4.1.0-SNAPSHOT</version>
</parent>

<artifactId>pebble-spring-boot-starter</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion pebble-spring/pebble-spring6/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<artifactId>pebble-spring</artifactId>
<groupId>io.pebbletemplates</groupId>
<version>4.0.1-SNAPSHOT</version>
<version>4.1.0-SNAPSHOT</version>
</parent>

<artifactId>pebble-spring6</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion pebble-spring/pebble-spring7/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<artifactId>pebble-spring</artifactId>
<groupId>io.pebbletemplates</groupId>
<version>4.0.1-SNAPSHOT</version>
<version>4.1.0-SNAPSHOT</version>
</parent>

<artifactId>pebble-spring7</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion pebble-spring/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>io.pebbletemplates</groupId>
<artifactId>pebble-project</artifactId>
<version>4.0.1-SNAPSHOT</version>
<version>4.1.0-SNAPSHOT</version>
</parent>

<artifactId>pebble-spring</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion pebble/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<parent>
<groupId>io.pebbletemplates</groupId>
<artifactId>pebble-project</artifactId>
<version>4.0.1-SNAPSHOT</version>
<version>4.1.0-SNAPSHOT</version>
</parent>

<artifactId>pebble</artifactId>
Expand Down
30 changes: 11 additions & 19 deletions pebble/src/main/java/io/pebbletemplates/pebble/PebbleEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,40 @@
package io.pebbletemplates.pebble;


import io.pebbletemplates.pebble.attributes.methodaccess.BlacklistMethodAccessValidator;
import io.pebbletemplates.pebble.attributes.methodaccess.MethodAccessValidator;
import io.pebbletemplates.pebble.cache.CacheKey;
import io.pebbletemplates.pebble.cache.PebbleCache;
import io.pebbletemplates.pebble.cache.tag.ConcurrentMapTagCache;
import io.pebbletemplates.pebble.cache.tag.NoOpTagCache;
import io.pebbletemplates.pebble.cache.template.ConcurrentMapTemplateCache;
import io.pebbletemplates.pebble.cache.template.NoOpTemplateCache;
import io.pebbletemplates.pebble.error.LoaderException;
import io.pebbletemplates.pebble.extension.*;
import io.pebbletemplates.pebble.extension.escaper.EscapingStrategy;
import io.pebbletemplates.pebble.lexer.LexerImpl;
import io.pebbletemplates.pebble.lexer.Syntax;
import io.pebbletemplates.pebble.lexer.TokenStream;
import io.pebbletemplates.pebble.loader.ClasspathLoader;
import io.pebbletemplates.pebble.loader.Loader;
import io.pebbletemplates.pebble.loader.StringLoader;
import io.pebbletemplates.pebble.node.RootNode;
import io.pebbletemplates.pebble.parser.Parser;
import io.pebbletemplates.pebble.parser.ParserImpl;
import io.pebbletemplates.pebble.parser.ParserOptions;
import io.pebbletemplates.pebble.attributes.methodaccess.BlacklistMethodAccessValidator;
import io.pebbletemplates.pebble.attributes.methodaccess.MethodAccessValidator;
import io.pebbletemplates.pebble.extension.escaper.EscapingStrategy;
import io.pebbletemplates.pebble.loader.ClasspathLoader;
import io.pebbletemplates.pebble.loader.DelegatingLoader;
import io.pebbletemplates.pebble.loader.FileLoader;
import io.pebbletemplates.pebble.loader.Loader;
import io.pebbletemplates.pebble.loader.StringLoader;
import io.pebbletemplates.pebble.extension.*;
import io.pebbletemplates.pebble.template.EvaluationOptions;
import io.pebbletemplates.pebble.template.PebbleTemplate;
import io.pebbletemplates.pebble.template.PebbleTemplateImpl;
import io.pebbletemplates.pebble.utils.TypeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.function.Function;

import io.pebbletemplates.pebble.utils.TypeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The main class used for compiling templates. The PebbleEngine is responsible for delegating
* responsibility to the lexer, parser, compiler, and template cache.
Expand Down Expand Up @@ -584,10 +579,7 @@ public PebbleEngine build() {

// default loader
if (this.loader == null) {
List<Loader<?>> defaultLoadingStrategies = new ArrayList<>();
defaultLoadingStrategies.add(new ClasspathLoader());
defaultLoadingStrategies.add(new FileLoader());
this.loader = new DelegatingLoader(defaultLoadingStrategies);
this.loader = new ClasspathLoader();
}

// default locale
Expand Down
103 changes: 46 additions & 57 deletions pebble/src/main/java/io/pebbletemplates/pebble/loader/FileLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,12 @@

import io.pebbletemplates.pebble.error.LoaderException;
import io.pebbletemplates.pebble.utils.PathUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.*;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
* This loader searches for a file located anywhere on the filesystem. It uses java.io.File to
Expand All @@ -34,69 +28,35 @@ public class FileLoader implements Loader<String> {
private static final Logger logger = LoggerFactory.getLogger(FileLoader.class);

private String prefix;

private String suffix;

private String charset = "UTF-8";

public FileLoader(String prefix) {
this.setPrefix(prefix);
}

@Override
public Reader getReader(String templateName) {
// try to load File
InputStream is = null;
File file = this.getFile(templateName);
if (file.exists() && file.isFile()) {
try {
is = new FileInputStream(file);
} catch (FileNotFoundException e) {
}
}

if (is == null) {
throw new LoaderException(null,
"Could not find template \"" + templateName + "\"");
}

try {
InputStream is = new FileInputStream(file);
return new BufferedReader(new InputStreamReader(is, this.charset));
} catch (FileNotFoundException e) {
throw new LoaderException(e, String.format("Could not find template [prefix='%s', templateName='%s']", this.prefix, templateName));
} catch (UnsupportedEncodingException e) {
throw new LoaderException(e, String.format("Invalid charset '%s'", this.charset));
}

return null;
}

private File getFile(String templateName) {
// add the prefix and ensure the prefix ends with a separator character
StringBuilder path = new StringBuilder();
if (this.getPrefix() != null) {

path.append(this.getPrefix());

if (!this.getPrefix().endsWith(String.valueOf(File.separatorChar))) {
path.append(File.separatorChar);
}
}

templateName = templateName + (this.getSuffix() == null ? "" : this.getSuffix());
templateName = PathUtils.sanitize(templateName, File.separatorChar);

logger.trace("Looking for template in {}{}.", path.toString(), templateName);
Path path = Paths.get(this.getPrefix(), templateName);
logger.trace("Looking for template in {}.", path);

/*
* if template name contains path segments, move those segments into the
* path variable. The below technique needs to know the difference
* between the path and file name.
*/
String[] pathSegments = PathUtils.PATH_SEPARATOR_REGEX.split(templateName);

if (pathSegments.length > 1) {
// file name is the last segment
templateName = pathSegments[pathSegments.length - 1];
}
for (int i = 0; i < (pathSegments.length - 1); i++) {
path.append(pathSegments[i]).append(File.separatorChar);
}

// try to load File
return new File(path.toString(), templateName);
this.checkIfDirectoryTraversal(templateName);
return path.toFile();
}

public String getSuffix() {
Expand All @@ -114,7 +74,17 @@ public String getPrefix() {

@Override
public void setPrefix(String prefix) {
this.prefix = prefix;
if (prefix == null) {
throw new LoaderException(null, "Prefix cannot be null");
}
String trimmedPrefix = prefix.trim();
if (trimmedPrefix.isEmpty()) {
throw new LoaderException(null, "Prefix cannot be empty");
}
if (!Paths.get(trimmedPrefix).isAbsolute()) {
throw new LoaderException(null, "Prefix must be an absolute path");
}
this.prefix = trimmedPrefix;
}

public String getCharset() {
Expand All @@ -140,4 +110,23 @@ public String createCacheKey(String templateName) {
public boolean resourceExists(String templateName) {
return this.getFile(templateName).exists();
}

private void checkIfDirectoryTraversal(String templateName) {
Path baseDirPath = Paths.get(prefix);
Path userPath = Paths.get(templateName);
if (userPath.isAbsolute()) {
throw new LoaderException(null, String.format("templateName '%s' must be relative", templateName));
}

// Join the two paths together, then normalize so that any ".." elements
// in the userPath can remove parts of baseDirPath.
// (e.g. "/foo/bar/baz" + "../attack" -> "/foo/bar/attack")
Path resolvedPath = baseDirPath.resolve(userPath).normalize();

// Make sure the resulting path is still within the required directory.
// (In the example above, "/foo/bar/attack" is not.)
if (!resolvedPath.startsWith(baseDirPath)) {
throw new LoaderException(null, String.format("template is not in the base directory path [baseDir='%s', templateName='%s']", this.prefix, templateName));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static String resolveRelativePath(String relativePath, String anchorPath,
return null;
}

private static String sanitize(String path, char expectedSeparator) {
public static String sanitize(String path, char expectedSeparator) {
return PATH_SEPARATOR_REGEX.matcher(path)
.replaceAll(Matcher.quoteReplacement(String.valueOf(expectedSeparator)));
}
Expand Down
Loading