diff --git a/src/main/java/ch/digitalfondue/mjml4j/GlobalContext.java b/src/main/java/ch/digitalfondue/mjml4j/GlobalContext.java index 6e314ae..df07300 100644 --- a/src/main/java/ch/digitalfondue/mjml4j/GlobalContext.java +++ b/src/main/java/ch/digitalfondue/mjml4j/GlobalContext.java @@ -19,7 +19,6 @@ class GlobalContext { final String dir; // final Mjml4j.IncludeResolver includeResolver; - final String basePath; final ArrayDeque currentResourcePaths = new ArrayDeque<>(); // @@ -48,7 +47,6 @@ class GlobalContext { // this.includeResolver = configuration.includeResolver(); - this.basePath = configuration.basePath(); // } diff --git a/src/main/java/ch/digitalfondue/mjml4j/Mjml4j.java b/src/main/java/ch/digitalfondue/mjml4j/Mjml4j.java index e026aec..41c4f91 100644 --- a/src/main/java/ch/digitalfondue/mjml4j/Mjml4j.java +++ b/src/main/java/ch/digitalfondue/mjml4j/Mjml4j.java @@ -47,13 +47,46 @@ public String value() { } interface IncludeResolver { + /** + * Read the content of the file (with UTF-8) at a given resolved path. + * + * @param resolvedResourcePath + * @return + * @throws IOException + */ String resolveAsString(String resolvedResourcePath) throws IOException; + + /** + * Read the content of the file (with UTF-8 content) and parse it's content. + * + * @param resolvedResourcePath + * @return + */ org.w3c.dom.Document resolveAsDocument(String resolvedResourcePath); - String resolvePath(String name, String base, Deque parents); + + /** + * Resolve the given path. + * + * @param name + * @param parents + * @return + */ + String resolvePath(String name, Deque parents); } + /** + * Filesystem based resolver. The content _must_ be within the provided basePath. + * + * The check can be customized, see {@link #checkAccess(Path, Path)}. + */ public static class FileSystemResolver implements IncludeResolver { + private final Path basePath; + + public FileSystemResolver(Path basePath) { + this.basePath = Objects.requireNonNull(basePath).toAbsolutePath(); + } + @Override public String resolveAsString(String resolvedResourcePath) throws IOException { return Files.readString(Path.of(resolvedResourcePath), StandardCharsets.UTF_8); @@ -64,28 +97,26 @@ public org.w3c.dom.Document resolveAsDocument(String resolvedResourcePath) { throw new IllegalStateException("to implement"); } - @Override - public String resolvePath(String name, String base, Deque parents) { - if (parents.isEmpty()) { - return Path.of(base, name).toAbsolutePath().toString(); - } else { - return Path.of(parents.peek(), name).toAbsolutePath().toString(); + public void checkAccess(Path basePath, Path resolvedPath) { + if (!resolvedPath.startsWith(basePath)) { + throw new IllegalStateException("Cannot access path outside of basePath"); } } + + @Override + public String resolvePath(String name, Deque parents) { + var resolvedPath = (parents.isEmpty() ? basePath.resolve(name) : Path.of(parents.peek(), name)).toAbsolutePath(); + checkAccess(basePath, resolvedPath); + return resolvedPath.toString(); + } } public record Configuration( String language, TextDirection dir, - IncludeResolver includeResolver, String basePath + IncludeResolver includeResolver ) { - public Configuration { - if (includeResolver != null && basePath == null) { - throw new IllegalStateException("basepath must be defined if includeResolver is defined"); - } - } - public Configuration(String language, TextDirection dir) { - this(language, dir, null, null); + this(language, dir, null); } public Configuration(String language) { @@ -438,12 +469,12 @@ private static BaseComponent createMjmlComponent(Element element, BaseComponent private static BaseComponent handleInclude(Element element, BaseComponent parent, GlobalContext context) { var path = element.getAttribute("path"); - if (context.includeResolver == null || path == null) { + if (context.includeResolver == null || path == null || path.isEmpty()) { return new HtmlComponent.HtmlRawComponent(element, parent, context); } var includeResolver = context.includeResolver; - var resolvedPath = includeResolver.resolvePath(path, context.basePath, context.currentResourcePaths); + var resolvedPath = includeResolver.resolvePath(path, context.currentResourcePaths); var attributeType = element.getAttribute("type"); if ("html".equals(attributeType) || "css".equals(attributeType)) { diff --git a/src/test/java/ch/digitalfondue/mjml4j/Helpers.java b/src/test/java/ch/digitalfondue/mjml4j/Helpers.java index 0dae5b1..3d542b5 100644 --- a/src/test/java/ch/digitalfondue/mjml4j/Helpers.java +++ b/src/test/java/ch/digitalfondue/mjml4j/Helpers.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.regex.Pattern; @@ -70,7 +71,7 @@ static String alignIdFor(String input) { static void testTemplate(String name) { try { var template = Files.readString(new File("data/" + name + ".mjml").toPath(), StandardCharsets.UTF_8); - var conf = new Mjml4j.Configuration("und", Mjml4j.TextDirection.AUTO, new Mjml4j.FileSystemResolver(), "data"); + var conf = new Mjml4j.Configuration("und", Mjml4j.TextDirection.AUTO, new Mjml4j.FileSystemResolver(Path.of("data"))); var res = Mjml4j.render(template, conf); var comparison = Files.readString(new File("data/" + name + ".html").toPath(), StandardCharsets.UTF_8); Assertions.assertEquals(simplifyBrTags(alignIdFor(beautifyHtml(comparison))), alignIdFor(beautifyHtml(res)));