[core] Templating: limit compilation to supported extensions and constrain target paths (#6598)

This commit is contained in:
Jim Schubert 2020-09-02 15:52:35 -04:00 committed by GitHub
parent 91ea6a17d9
commit a6d30cac9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 225 additions and 64 deletions

View File

@ -45,6 +45,16 @@ public interface TemplatingEngineAdapter {
*/ */
String[] getFileExtensions(); String[] getFileExtensions();
/**
* Determine if the adapter handles compilation of the file
* @param filename The template filename
*
* @return True if the file should be compiled by this adapter, else false.
*/
default boolean handlesFile(String filename) {
return filename != null && filename.length() > 0 && Arrays.stream(getFileExtensions()).anyMatch(i -> filename.endsWith("." + i));
}
/** /**
* Compiles a template into a string * Compiles a template into a string
* *
@ -65,9 +75,10 @@ public interface TemplatingEngineAdapter {
* @param templateFile The original target filename * @param templateFile The original target filename
* @return True if the template is available in the template search path, false if it can not be found * @return True if the template is available in the template search path, false if it can not be found
*/ */
@SuppressWarnings({"java:S2093"}) // ignore java:S2093 because we have double-assignment to the closeable
default boolean templateExists(TemplatingExecutor generator, String templateFile) { default boolean templateExists(TemplatingExecutor generator, String templateFile) {
return Arrays.stream(getFileExtensions()).anyMatch(ext -> { return Arrays.stream(getFileExtensions()).anyMatch(ext -> {
int idx = templateFile.lastIndexOf("."); int idx = templateFile.lastIndexOf('.');
String baseName; String baseName;
if (idx > 0 && idx < templateFile.length() - 1) { if (idx > 0 && idx < templateFile.length() - 1) {
baseName = templateFile.substring(0, idx); baseName = templateFile.substring(0, idx);

View File

@ -913,6 +913,11 @@ public class DefaultGenerator implements Generator {
File target = new File(adjustedOutputFilename); File target = new File(adjustedOutputFilename);
if (ignoreProcessor.allowsFile(target)) { if (ignoreProcessor.allowsFile(target)) {
if (shouldGenerate) { if (shouldGenerate) {
Path outDir = java.nio.file.Paths.get(this.config.getOutputDir()).toAbsolutePath();
Path absoluteTarget = target.toPath().toAbsolutePath();
if (!absoluteTarget.startsWith(outDir)) {
throw new RuntimeException(String.format(Locale.ROOT, "Target files must be generated within the output directory; absoluteTarget=%s outDir=%s", absoluteTarget, outDir));
}
return this.templateProcessor.write(templateData,templateName, target); return this.templateProcessor.write(templateData,templateName, target);
} else { } else {
this.templateProcessor.skip(target.toPath(), String.format(Locale.ROOT, "Skipped by %s options supplied by user.", skippedByOption)); this.templateProcessor.skip(target.toPath(), String.format(Locale.ROOT, "Skipped by %s options supplied by user.", skippedByOption));

View File

@ -1,5 +1,6 @@
package org.openapitools.codegen; package org.openapitools.codegen;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.api.TemplatePathLocator; import org.openapitools.codegen.api.TemplatePathLocator;
import org.openapitools.codegen.api.TemplateProcessor; import org.openapitools.codegen.api.TemplateProcessor;
@ -12,14 +13,8 @@ import org.slf4j.LoggerFactory;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.*;
import java.nio.file.Path; import java.util.*;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Scanner;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
@ -59,6 +54,10 @@ public class TemplateManager implements TemplatingExecutor, TemplateProcessor {
throw new TemplateNotFoundException(name); throw new TemplateNotFoundException(name);
} }
if (name == null || name.contains("..")) {
throw new IllegalArgumentException("Template location must be constrained to template directory.");
}
return template; return template;
} }
@ -104,7 +103,12 @@ public class TemplateManager implements TemplatingExecutor, TemplateProcessor {
* @param name The location of the template * @param name The location of the template
* @return The raw template contents * @return The raw template contents
*/ */
@SuppressWarnings({"java:S112"})
// ignored rule java:S112 as RuntimeException is used to match previous exception type
public String readTemplate(String name) { public String readTemplate(String name) {
if (name == null || name.contains("..")) {
throw new IllegalArgumentException("Template location must be constrained to template directory.");
}
try { try {
Reader reader = getTemplateReader(name); Reader reader = getTemplateReader(name);
if (reader == null) { if (reader == null) {
@ -118,15 +122,12 @@ public class TemplateManager implements TemplatingExecutor, TemplateProcessor {
throw new RuntimeException("can't load template " + name); throw new RuntimeException("can't load template " + name);
} }
@SuppressWarnings("squid:S2095") @SuppressWarnings({"squid:S2095", "java:S112"})
// ignored rule as used in the CLI and it's required to return a reader // ignored rule squid:S2095 as used in the CLI and it's required to return a reader
// ignored rule java:S112 as RuntimeException is used to match previous exception type
public Reader getTemplateReader(String name) { public Reader getTemplateReader(String name) {
InputStream is = null;
try { try {
is = this.getClass().getClassLoader().getResourceAsStream(getCPResourcePath(name)); InputStream is = getInputStream(name);
if (is == null) {
is = new FileInputStream(new File(name)); // May throw but never return a null value
}
return new InputStreamReader(is, StandardCharsets.UTF_8); return new InputStreamReader(is, StandardCharsets.UTF_8);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
LOGGER.error(e.getMessage()); LOGGER.error(e.getMessage());
@ -134,6 +135,18 @@ public class TemplateManager implements TemplatingExecutor, TemplateProcessor {
} }
} }
private InputStream getInputStream(String name) throws FileNotFoundException {
InputStream is;
is = this.getClass().getClassLoader().getResourceAsStream(getCPResourcePath(name));
if (is == null) {
if (name == null || name.contains("..")) {
throw new IllegalArgumentException("Template location must be constrained to template directory.");
}
is = new FileInputStream(new File(name)); // May throw but never return a null value
}
return is;
}
/** /**
* Writes data to a compiled template * Writes data to a compiled template
* *
@ -145,18 +158,32 @@ public class TemplateManager implements TemplatingExecutor, TemplateProcessor {
*/ */
@Override @Override
public File write(Map<String, Object> data, String template, File target) throws IOException { public File write(Map<String, Object> data, String template, File target) throws IOException {
if (this.engineAdapter.handlesFile(template)) {
// Only pass files with valid endings through template engine
String templateContent = this.engineAdapter.compileTemplate(this, data, template); String templateContent = this.engineAdapter.compileTemplate(this, data, template);
return writeToFile(target.getPath(), templateContent); return writeToFile(target.getPath(), templateContent);
} else {
// Do a straight copy of the file if not listed as supported by the template engine.
InputStream is;
try {
// look up the file using the same template resolution logic the adapters would use.
String fullTemplatePath = getFullTemplateFile(template);
is = getInputStream(fullTemplatePath);
} catch (TemplateNotFoundException ex) {
is = new FileInputStream(Paths.get(template).toFile());
}
return writeToFile(target.getAbsolutePath(), IOUtils.toByteArray(is));
}
} }
@Override @Override
public void ignore(Path path, String context) { public void ignore(Path path, String context) {
LOGGER.info("Ignored {} ({})", path.toString(), context); LOGGER.info("Ignored {} ({})", path, context);
} }
@Override @Override
public void skip(Path path, String context) { public void skip(Path path, String context) {
LOGGER.info("Skipped {} ({})", path.toString(), context); LOGGER.info("Skipped {} ({})", path, context);
} }
/** /**
@ -189,23 +216,23 @@ public class TemplateManager implements TemplatingExecutor, TemplateProcessor {
try { try {
tempFile = writeToFileRaw(tempFilename, contents); tempFile = writeToFileRaw(tempFilename, contents);
if (!filesEqual(tempFile, outputFile)) { if (!filesEqual(tempFile, outputFile)) {
LOGGER.info("writing file " + filename); LOGGER.info("writing file {}", filename);
Files.move(tempFile.toPath(), outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING); Files.move(tempFile.toPath(), outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
tempFile = null; tempFile = null;
} else { } else {
LOGGER.info("skipping unchanged file " + filename); LOGGER.info("skipping unchanged file {}", filename);
} }
} finally { } finally {
if (tempFile != null && tempFile.exists()) { if (tempFile != null && tempFile.exists()) {
try { try {
tempFile.delete(); Files.delete(tempFile.toPath());
} catch (Exception ex) { } catch (Exception ex) {
LOGGER.error("Error removing temporary file " + tempFile, ex); LOGGER.error("Error removing temporary file {}", tempFile, ex);
} }
} }
} }
} else { } else {
LOGGER.info("writing file " + filename); LOGGER.info("writing file {}", filename);
outputFile = writeToFileRaw(filename, contents); outputFile = writeToFileRaw(filename, contents);
} }
@ -216,7 +243,7 @@ public class TemplateManager implements TemplatingExecutor, TemplateProcessor {
// Use Paths.get here to normalize path (for Windows file separator, space escaping on Linux/Mac, etc) // Use Paths.get here to normalize path (for Windows file separator, space escaping on Linux/Mac, etc)
File output = Paths.get(filename).toFile(); File output = Paths.get(filename).toFile();
if (this.options.isSkipOverwrite() && output.exists()) { if (this.options.isSkipOverwrite() && output.exists()) {
LOGGER.info("skip overwrite of file " + filename); LOGGER.info("skip overwrite of file {}", filename);
return output; return output;
} }

View File

@ -22,6 +22,11 @@ import java.util.Locale;
import java.util.ServiceLoader; import java.util.ServiceLoader;
public class TemplatingEngineLoader { public class TemplatingEngineLoader {
private TemplatingEngineLoader() {
throw new IllegalStateException("Utility class");
}
@SuppressWarnings({"java:S112"}) // ignore java:S112 as generic RuntimeException is acceptable here
public static TemplatingEngineAdapter byIdentifier(String id) { public static TemplatingEngineAdapter byIdentifier(String id) {
ServiceLoader<TemplatingEngineAdapter> loader = ServiceLoader.load(TemplatingEngineAdapter.class, TemplatingEngineLoader.class.getClassLoader()); ServiceLoader<TemplatingEngineAdapter> loader = ServiceLoader.load(TemplatingEngineAdapter.class, TemplatingEngineLoader.class.getClassLoader());
@ -37,7 +42,7 @@ public class TemplatingEngineLoader {
// Attempt to load skipping SPI // Attempt to load skipping SPI
return (TemplatingEngineAdapter) Class.forName(id).getDeclaredConstructor().newInstance(); return (TemplatingEngineAdapter) Class.forName(id).getDeclaredConstructor().newInstance();
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(String.format(Locale.ROOT, "Couldn't load template engine adapter %s. Available options: \n%s", id, sb.toString()), e); throw new RuntimeException(String.format(Locale.ROOT, "Couldn't load template engine adapter %s. Available options: %n%s", id, sb.toString()), e);
} }
} }
} }

View File

@ -129,35 +129,35 @@ public class PhpLumenServerCodegen extends AbstractPhpCodegen {
supportingFiles.add(new SupportingFile("storage_logs_gitignore", srcBasePath + File.separator + "storage" + File.separator + "framework" + File.separator + "views", ".gitignore")); supportingFiles.add(new SupportingFile("storage_logs_gitignore", srcBasePath + File.separator + "storage" + File.separator + "framework" + File.separator + "views", ".gitignore"));
supportingFiles.add(new SupportingFile("storage_framework_cache_gitignore", srcBasePath + File.separator + "storage" + File.separator + "framework" + File.separator + "cache", ".gitignore")); supportingFiles.add(new SupportingFile("storage_framework_cache_gitignore", srcBasePath + File.separator + "storage" + File.separator + "framework" + File.separator + "cache", ".gitignore"));
supportingFiles.add(new SupportingFile("storage_logs_gitignore", srcBasePath + File.separator + "storage" + File.separator + "framework" + File.separator + "cache" + File.separator + "data", ".gitignore")); supportingFiles.add(new SupportingFile("storage_logs_gitignore", srcBasePath + File.separator + "storage" + File.separator + "framework" + File.separator + "cache" + File.separator + "data", ".gitignore"));
supportingFiles.add(new SupportingFile("artisan", srcBasePath, "artisan")); supportingFiles.add(new SupportingFile("artisan.mustache", srcBasePath, "artisan"));
supportingFiles.add(new SupportingFile("composer.mustache", srcBasePath, "composer.json")); supportingFiles.add(new SupportingFile("composer.mustache", srcBasePath, "composer.json"));
supportingFiles.add(new SupportingFile("readme.md", srcBasePath, "readme.md")); supportingFiles.add(new SupportingFile("readme.md", srcBasePath, "readme.md"));
supportingFiles.add(new SupportingFile("User.php", srcBasePath + File.separator + "app", "User.php")); supportingFiles.add(new SupportingFile("User.php.mustache", srcBasePath + File.separator + "app", "User.php"));
supportingFiles.add(new SupportingFile("Kernel.php", srcBasePath + File.separator + "app" + File.separator + "Console", "Kernel.php")); supportingFiles.add(new SupportingFile("Kernel.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Console", "Kernel.php"));
supportingFiles.add(new SupportingFile(".gitkeep", srcBasePath + File.separator + "app" + File.separator + "Console" + File.separator + "Commands", ".gitkeep")); supportingFiles.add(new SupportingFile(".gitkeep", srcBasePath + File.separator + "app" + File.separator + "Console" + File.separator + "Commands", ".gitkeep"));
supportingFiles.add(new SupportingFile("Event.php", srcBasePath + File.separator + "app" + File.separator + "Events", "Event.php")); supportingFiles.add(new SupportingFile("Event.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Events", "Event.php"));
supportingFiles.add(new SupportingFile("ExampleEvent.php", srcBasePath + File.separator + "app" + File.separator + "Events", "ExampleEvent.php")); supportingFiles.add(new SupportingFile("ExampleEvent.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Events", "ExampleEvent.php"));
supportingFiles.add(new SupportingFile("Handler.php", srcBasePath + File.separator + "app" + File.separator + "Exceptions", "Handler.php")); supportingFiles.add(new SupportingFile("Handler.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Exceptions", "Handler.php"));
supportingFiles.add(new SupportingFile("Controller.php", srcBasePath + File.separator + "app" + File.separator + "Http" + File.separator + "Controllers" + File.separator, "Controller.php")); supportingFiles.add(new SupportingFile("Controller.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Http" + File.separator + "Controllers" + File.separator, "Controller.php"));
supportingFiles.add(new SupportingFile("ExampleController.php", srcBasePath + File.separator + "app" + File.separator + "Http" + File.separator + "Controllers" + File.separator, "ExampleController.php")); supportingFiles.add(new SupportingFile("ExampleController.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Http" + File.separator + "Controllers" + File.separator, "ExampleController.php"));
supportingFiles.add(new SupportingFile("Authenticate.php", srcBasePath + File.separator + "app" + File.separator + "Http" + File.separator + "Middleware" + File.separator, "Authenticate.php")); supportingFiles.add(new SupportingFile("Authenticate.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Http" + File.separator + "Middleware" + File.separator, "Authenticate.php"));
supportingFiles.add(new SupportingFile("ExampleMiddleware.php", srcBasePath + File.separator + "app" + File.separator + "Http" + File.separator + "Middleware" + File.separator, "ExampleMiddleware.php")); supportingFiles.add(new SupportingFile("ExampleMiddleware.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Http" + File.separator + "Middleware" + File.separator, "ExampleMiddleware.php"));
supportingFiles.add(new SupportingFile("ExampleJob.php", srcBasePath + File.separator + "app" + File.separator + "Jobs", "ExampleJob.php")); supportingFiles.add(new SupportingFile("ExampleJob.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Jobs", "ExampleJob.php"));
supportingFiles.add(new SupportingFile("Job.php", srcBasePath + File.separator + "app" + File.separator + "Jobs", "Job.php")); supportingFiles.add(new SupportingFile("Job.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Jobs", "Job.php"));
supportingFiles.add(new SupportingFile("ExampleListener.php", srcBasePath + File.separator + "app" + File.separator + "Listeners", "ExampleListener.php")); supportingFiles.add(new SupportingFile("ExampleListener.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Listeners", "ExampleListener.php"));
supportingFiles.add(new SupportingFile("AppServiceProvider.php", srcBasePath + File.separator + "app" + File.separator + "Providers", "AppServiceProvider.php")); supportingFiles.add(new SupportingFile("AppServiceProvider.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Providers", "AppServiceProvider.php"));
supportingFiles.add(new SupportingFile("AuthServiceProvider.php", srcBasePath + File.separator + "app" + File.separator + "Providers", "AuthServiceProvider.php")); supportingFiles.add(new SupportingFile("AuthServiceProvider.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Providers", "AuthServiceProvider.php"));
supportingFiles.add(new SupportingFile("EventServiceProvider.php", srcBasePath + File.separator + "app" + File.separator + "Providers", "EventServiceProvider.php")); supportingFiles.add(new SupportingFile("EventServiceProvider.php.mustache", srcBasePath + File.separator + "app" + File.separator + "Providers", "EventServiceProvider.php"));
supportingFiles.add(new SupportingFile("app.php", srcBasePath + File.separator + "bootstrap", "app.php")); supportingFiles.add(new SupportingFile("app.php.mustache", srcBasePath + File.separator + "bootstrap", "app.php"));
supportingFiles.add(new SupportingFile("ModelFactory.php", srcBasePath + File.separator + "database" + File.separator + "factories", "ModelFactory.php")); supportingFiles.add(new SupportingFile("ModelFactory.php.mustache", srcBasePath + File.separator + "database" + File.separator + "factories", "ModelFactory.php"));
supportingFiles.add(new SupportingFile(".gitkeep", srcBasePath + File.separator + "database" + File.separator + "migrations", ".gitkeep")); supportingFiles.add(new SupportingFile(".gitkeep", srcBasePath + File.separator + "database" + File.separator + "migrations", ".gitkeep"));
supportingFiles.add(new SupportingFile("DatabaseSeeder.php", srcBasePath + File.separator + "database" + File.separator + "seeds", "DatabaseSeeder.php")); supportingFiles.add(new SupportingFile("DatabaseSeeder.php.mustache", srcBasePath + File.separator + "database" + File.separator + "seeds", "DatabaseSeeder.php"));
supportingFiles.add(new SupportingFile(".htaccess", srcBasePath + File.separator + "public", ".htaccess")); supportingFiles.add(new SupportingFile(".htaccess", srcBasePath + File.separator + "public", ".htaccess"));
supportingFiles.add(new SupportingFile("index.php", srcBasePath + File.separator + "public", "index.php")); supportingFiles.add(new SupportingFile("index.php.mustache", srcBasePath + File.separator + "public", "index.php"));
supportingFiles.add(new SupportingFile(".gitkeep", srcBasePath + File.separator + "resources" + File.separator + "views", ".gitkeep")); supportingFiles.add(new SupportingFile(".gitkeep", srcBasePath + File.separator + "resources" + File.separator + "views", ".gitkeep"));
supportingFiles.add(new SupportingFile("routes.mustache", srcBasePath + File.separator + "routes", "web.php")); supportingFiles.add(new SupportingFile("routes.mustache", srcBasePath + File.separator + "routes", "web.php"));
supportingFiles.add(new SupportingFile("ExampleTest.php", srcBasePath + File.separator + "tests", "ExampleTest.php")); supportingFiles.add(new SupportingFile("ExampleTest.php.mustache", srcBasePath + File.separator + "tests", "ExampleTest.php"));
supportingFiles.add(new SupportingFile("TestCase.php", srcBasePath + File.separator + "tests", "TestCase.php")); supportingFiles.add(new SupportingFile("TestCase.php.mustache", srcBasePath + File.separator + "tests", "TestCase.php"));
supportingFiles.add(new SupportingFile("editorconfig", srcBasePath, ".editorconfig")); supportingFiles.add(new SupportingFile("editorconfig", srcBasePath, ".editorconfig"));
supportingFiles.add(new SupportingFile("styleci", srcBasePath, ".styleci.yml")); supportingFiles.add(new SupportingFile("styleci", srcBasePath, ".styleci.yml"));
supportingFiles.add(new SupportingFile("phpunit.xml", srcBasePath, "phpunit.xml")); supportingFiles.add(new SupportingFile("phpunit.xml", srcBasePath, "phpunit.xml"));

View File

@ -236,7 +236,8 @@ public class PythonBluePlanetServerCodegen extends AbstractPythonConnexionServer
@Override @Override
public String modelDocFileFolder() { public String modelDocFileFolder() {
return (outputFolder + File.separator + modelDocPath).replace('.', File.separatorChar); // character replaces should _only_ occur on paths we define. Don't replace on outputFolder (which is supplied by the user and should always be considered correct)
return outputFolder + File.separator + modelDocPath.replace('.', File.separatorChar);
} }
@Override @Override

View File

@ -35,13 +35,17 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
public class HandlebarsEngineAdapter extends AbstractTemplatingEngineAdapter { public class HandlebarsEngineAdapter extends AbstractTemplatingEngineAdapter {
static Logger LOGGER = LoggerFactory.getLogger(HandlebarsEngineAdapter.class); static final Logger LOGGER = LoggerFactory.getLogger(HandlebarsEngineAdapter.class);
private final String[] extensions = new String[]{"handlebars", "hbs"}; private final String[] extensions = new String[]{"handlebars", "hbs"};
// We use this as a simple lookup for valid file name extensions. This adapter will inspect .mustache (built-in) and infer the relevant handlebars filename
private final String[] canCompileFromExtensions = new String[]{".handlebars",".hbs",".mustache"};
/** /**
* Provides an identifier used to load the adapter. This could be a name, uuid, or any other string. * Provides an identifier used to load the adapter. This could be a name, uuid, or any other string.
* *
@ -71,7 +75,7 @@ public class HandlebarsEngineAdapter extends AbstractTemplatingEngineAdapter {
Handlebars handlebars = new Handlebars(loader); Handlebars handlebars = new Handlebars(loader);
handlebars.registerHelperMissing((obj, options) -> { handlebars.registerHelperMissing((obj, options) -> {
LOGGER.warn(String.format(Locale.ROOT, "Unregistered helper name '%s', processing template:\n%s", options.helperName, options.fn.text())); LOGGER.warn(String.format(Locale.ROOT, "Unregistered helper name '%s', processing template:%n%s", options.helperName, options.fn.text()));
return ""; return "";
}); });
handlebars.registerHelper("json", Jackson2Helper.INSTANCE); handlebars.registerHelper("json", Jackson2Helper.INSTANCE);
@ -82,6 +86,7 @@ public class HandlebarsEngineAdapter extends AbstractTemplatingEngineAdapter {
return tmpl.apply(context); return tmpl.apply(context);
} }
@SuppressWarnings({"java:S108"})
public TemplateSource findTemplate(TemplatingExecutor generator, String templateFile) { public TemplateSource findTemplate(TemplatingExecutor generator, String templateFile) {
String[] possibilities = getModifiedFileLocation(templateFile); String[] possibilities = getModifiedFileLocation(templateFile);
for (String file : possibilities) { for (String file : possibilities) {
@ -104,5 +109,17 @@ public class HandlebarsEngineAdapter extends AbstractTemplatingEngineAdapter {
public String[] getFileExtensions() { public String[] getFileExtensions() {
return extensions; return extensions;
} }
/**
* Determine if the adapter handles compilation of the file
*
* @param filename The template filename
* @return True if the file should be compiled by this adapter, else false.
*/
@Override
public boolean handlesFile(String filename) {
// disallow any extension-only files like ".hbs" or ".mustache", and only consider a file compilable if it's handlebars or mustache (from which we later infer the handlebars filename)
return Arrays.stream(canCompileFromExtensions).anyMatch(suffix -> !suffix.equalsIgnoreCase(filename) && filename.endsWith(suffix));
}
} }

View File

@ -39,7 +39,7 @@ public class MustacheEngineAdapter implements TemplatingEngineAdapter {
return "mustache"; return "mustache";
} }
public String[] extensions = new String[]{"mustache"}; private final String[] extensions = new String[]{"mustache"};
Mustache.Compiler compiler = Mustache.compiler(); Mustache.Compiler compiler = Mustache.compiler();
/** /**
@ -61,6 +61,7 @@ public class MustacheEngineAdapter implements TemplatingEngineAdapter {
return tmpl.execute(bundle); return tmpl.execute(bundle);
} }
@SuppressWarnings({"java:S108"}) // catch-all is expected, and is later thrown
public Reader findTemplate(TemplatingExecutor generator, String name) { public Reader findTemplate(TemplatingExecutor generator, String name) {
for (String extension : extensions) { for (String extension : extensions) {
try { try {
@ -69,12 +70,6 @@ public class MustacheEngineAdapter implements TemplatingEngineAdapter {
} }
} }
// support files without targeted extension (e.g. .gitignore, README.md), etc.
try {
return new StringReader(generator.getFullTemplateContents(name));
} catch (Exception ignored) {
}
throw new TemplateNotFoundException(name); throw new TemplateNotFoundException(name);
} }

View File

@ -113,7 +113,7 @@ impl Request {
let mut path = self.path; let mut path = self.path;
for (k, v) in self.path_params { for (k, v) in self.path_params {
// replace {id} with the value of the id path param // replace {id} with the value of the id path param
{{=<% %>=}}path = path.replace(&format!("{{{}}}", k), &v);<%={{ }}=%> path = path.replace(&format!("{{{}}}", k), &v);
} }
for (k, v) in self.header_params { for (k, v) in self.header_params {

View File

@ -47,6 +47,15 @@ public class TemplateManagerTest {
assertEquals(manager.getFullTemplateContents("simple.mustache"), "{{name}} and {{age}}"); assertEquals(manager.getFullTemplateContents("simple.mustache"), "{{name}} and {{age}}");
} }
@Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Template location must be constrained to template directory\\.")
public void loadTemplateContentsThrowsForEscapingTemplates(){
TemplateManagerOptions opts = new TemplateManagerOptions(false,false);
TemplateManager manager = new TemplateManager(opts, mustacheEngineAdapter, new TemplatePathLocator[]{ locator });
manager.getFullTemplateContents("../simple.mustache");
fail("Expected an exception that did not occur");
}
@Test @Test
public void readTemplate(){ public void readTemplate(){
TemplateManagerOptions opts = new TemplateManagerOptions(false,false); TemplateManagerOptions opts = new TemplateManagerOptions(false,false);
@ -71,6 +80,15 @@ public class TemplateManagerTest {
assertTrue(manager.getTemplateReader("templating/templates/simple.mustache") instanceof InputStreamReader); assertTrue(manager.getTemplateReader("templating/templates/simple.mustache") instanceof InputStreamReader);
} }
@Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Template location must be constrained to template directory\\.")
public void getTemplateReaderThrowsForEscapingTemplates(){
TemplateManagerOptions opts = new TemplateManagerOptions(false,false);
TemplateManager manager = new TemplateManager(opts, mustacheEngineAdapter, new TemplatePathLocator[]{ locator });
manager.getTemplateReader("../templating/templates/simple.mustache");
fail("Expected an exception that did not occur");
}
@Test @Test
public void writeViaMustacheAdapter() throws IOException { public void writeViaMustacheAdapter() throws IOException {
TemplateManagerOptions opts = new TemplateManagerOptions(false,false); TemplateManagerOptions opts = new TemplateManagerOptions(false,false);
@ -91,6 +109,28 @@ public class TemplateManagerTest {
} }
} }
@Test
public void writeUsingMustacheAdapterSkipsNonMustache() throws IOException {
TemplateManagerOptions opts = new TemplateManagerOptions(false,false);
TemplateManager manager = new TemplateManager(opts, mustacheEngineAdapter, new TemplatePathLocator[]{ locator });
Map<String, Object> data = new HashMap<>();
data.put("this", "this");
data.put("that", "1234");
Path target = Files.createTempDirectory("test-templatemanager");
try {
File output = new File(target.toFile(), ".gitignore");
File written = manager.write(data, ".gitignore", output);
assertEquals(Files.readAllLines(written.toPath()).get(0), "# Should not escape {{this}} or that: {{{that}}}");
output = new File(target.toFile(), "README.md");
written = manager.write(data, "README.md", output);
assertEquals(Files.readAllLines(written.toPath()).get(0), "This should not escape `{{this}}` or `{{{that}}}` or `{{name}} counts{{#each numbers}} {{.}}{{/each}}`");
} finally {
target.toFile().delete();
}
}
@Test @Test
public void skipOverwriteViaOption() throws IOException { public void skipOverwriteViaOption() throws IOException {
TemplateManagerOptions opts = new TemplateManagerOptions(false,true); TemplateManagerOptions opts = new TemplateManagerOptions(false,true);
@ -186,4 +226,26 @@ public class TemplateManagerTest {
target.toFile().delete(); target.toFile().delete();
} }
} }
@Test
public void writeUsingHandlebarsAdapterSkipsNonHandlebars() throws IOException {
TemplateManagerOptions opts = new TemplateManagerOptions(false,false);
TemplateManager manager = new TemplateManager(opts, handlebarsEngineAdapter, new TemplatePathLocator[]{ locator });
Map<String, Object> data = new HashMap<>();
data.put("this", "this");
data.put("that", "1234");
Path target = Files.createTempDirectory("test-templatemanager");
try {
File output = new File(target.toFile(), ".gitignore");
File written = manager.write(data, ".gitignore", output);
assertEquals(Files.readAllLines(written.toPath()).get(0), "# Should not escape {{this}} or that: {{{that}}}");
output = new File(target.toFile(), "README.md");
written = manager.write(data, "README.md", output);
assertEquals(Files.readAllLines(written.toPath()).get(0), "This should not escape `{{this}}` or `{{{that}}}` or `{{name}} counts{{#each numbers}} {{.}}{{/each}}`");
} finally {
target.toFile().delete();
}
}
} }

View File

@ -0,0 +1,36 @@
package org.openapitools.codegen.templating;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
public class HandlebarsEngineAdapterTest {
@Test(dataProvider = "handlesFileExpectations")
public void checkHandleFiles(String input, boolean shouldHandle, String message) {
HandlebarsEngineAdapter adapter = new HandlebarsEngineAdapter();
boolean handles = adapter.handlesFile(input);
assertEquals(handles, shouldHandle, message);
}
@DataProvider(name = "handlesFileExpectations")
public Object[][] handlesFileExpectations() {
// input, shouldHandle, message
return new Object[][]{
{"api.handlebars", true, "Expected to support handlebars extension"},
{"api.hbs", true, "Expected to support hbs extension"},
{"model.handlebars", true, "Expected to support handlebars extension"},
{"model.hbs", true, "Expected to support hbs extension"},
{"libraries/some/api.handlebars", true, "Expected to support handlebars extension for libraries"},
{"libraries/some/api.hbs", true, "Expected to support hbs extension for libraries"},
{"api.mustache", true, "Expected to support inferring handlebars extension from a mustache input"},
{"model.mustache", true, "Expected to support inferring handlebars extension from a mustache input"},
{"libraries/some/api.mustache", true, "Expected to support inferring handlebars extension from a mustache input for libraries"},
{"libraries/some/model.mustache", true, "Expected to support inferring handlebars extension from a mustache input for libraries"},
{".hbs", false, "Should not consider .hbs a valid file to process"},
{".handlebars", false, "Should not consider .handlebars a valid file to process"},
{".gitignore", false, "Should not attempt to handle .gitignore"},
{"README.md", false, "Should not attempt to handle non-handlebars extensions (other than mustache)"}
};
}
}

View File

@ -0,0 +1 @@
# Should not escape {{this}} or that: {{{that}}}

View File

@ -0,0 +1 @@
This should not escape `{{this}}` or `{{{that}}}` or `{{name}} counts{{#each numbers}} {{.}}{{/each}}`

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang=""> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@ -68,12 +68,12 @@
@if (Route::has('login')) @if (Route::has('login'))
<div class="top-right links"> <div class="top-right links">
@auth @auth
<a href="">Home</a> <a href="{{ url('/home') }}">Home</a>
@else @else
<a href="">Login</a> <a href="{{ route('login') }}">Login</a>
@if (Route::has('register')) @if (Route::has('register'))
<a href="">Register</a> <a href="{{ route('register') }}">Register</a>
@endif @endif
@endauth @endauth
</div> </div>