/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package freemarker.cache;

import freemarker.template.Configuration;
import freemarker.template.MalformedTemplateNameException;
import freemarker.template.TemplateNotFoundException;
import freemarker.template.Version;
import freemarker.template.utility.StringUtil;

Symbolizes a template name format, which defines the basic syntax of names through algorithms such as normalization. The API of this class isn't exposed as it's too immature, so custom template name formats aren't possible yet.
Since:2.3.22
/** * Symbolizes a template name format, which defines the basic syntax of names through algorithms such as normalization. * The API of this class isn't exposed as it's too immature, so custom template name formats aren't possible yet. * * @since 2.3.22 */
public abstract class TemplateNameFormat { private TemplateNameFormat() { // Currently can't be instantiated from outside }
The default template name format when incompatible_improvements is below 2.4.0. As of FreeMarker 2.4.0, the default incompatible_improvements is still 2.3.0, and it will certainly remain so for a very long time. In new projects it's highly recommended to use DEFAULT_2_4_0 instead.
/** * The default template name format when {@link Configuration#Configuration(Version) incompatible_improvements} is * below 2.4.0. As of FreeMarker 2.4.0, the default {@code incompatible_improvements} is still {@code 2.3.0}, and it * will certainly remain so for a very long time. In new projects it's highly recommended to use * {@link #DEFAULT_2_4_0} instead. */
public static final TemplateNameFormat DEFAULT_2_3_0 = new Default020300();
The default template name format only when incompatible_improvements is set to 2.4.0 (or higher). This is not the out-of-the-box default format of FreeMarker 2.4.x, because the default incompatible_improvements is still 2.3.0 there.

Differences to the DEFAULT_2_3_0 format:

  • The scheme and the path need not be separated with "://" anymore, only with ":". This makes template names like "classpath:foo.ftl" interpreted as an absolute name with scheme "classpath" and absolute path "foo.ftl". The scheme name before the ":" can't contain "/", or else it's treated as a malformed name. The scheme part can be separated either with "://" or just ":" from the path. Hence, myscheme:/x is normalized to myscheme:x, while myscheme:///x is normalized to myscheme://x, but myscehme://x or myscheme:/x aren't changed by normalization. It's up the TemplateLoader to which the normalized names are passed to decide which of these scheme separation conventions are valid (maybe both).
  • ":" is not allowed in template names, except as the scheme separator (see previous point).
  • Malformed paths throw MalformedTemplateNameException instead of acting like if the template wasn't found.
  • "\" (backslash) is not allowed in template names, and causes MalformedTemplateNameException. With DEFAULT_2_3_0 you would certainly end up with a TemplateNotFoundException (or worse, it would work, but steps like ".." wouldn't be normalized by FreeMarker).
  • Template names might end with /, like "foo/", and the presence or lack of the terminating / is seen as significant. While their actual interpretation is up to the TemplateLoader, operations that manipulate template names assume that the last step refers to a "directory" as opposed to a "file" exactly if the terminating / is present. Except, the empty name is assumed to refer to the root "directory" (despite that it doesn't end with /).
  • // is normalized to /, except of course if it's in the scheme name terminator. Like foo//bar///baaz.ftl is normalized to foo/bar/baaz.ftl. (In general, 0 long step names aren't possible anymore.)
  • The ".." bugs of the legacy normalizer are fixed: ".." steps has removed the preceding "." or "*" or scheme steps, not treating them specially as they should be. Now these work as expected. Examples: "a/./../c" has become to "a/c", now it will be "c"; "a/b/* /../c" has become to "a/b/c", now it will be "a/*/c"; "scheme://.." has become to "scheme:/", now it will be null (TemplateNotFoundException) for backing out of the root directory.
  • As now directory paths has to be handled as well, it recognizes terminating, leading, and lonely ".." and "." steps. For example, "foo/bar/.." now becomes to "foo/"
  • Multiple consecutive * steps are normalized to one
/** * The default template name format only when {@link Configuration#Configuration(Version) incompatible_improvements} * is set to 2.4.0 (or higher). This is not the out-of-the-box default format of FreeMarker 2.4.x, because the * default {@code incompatible_improvements} is still 2.3.0 there. * * <p> * Differences to the {@link #DEFAULT_2_3_0} format: * * <ul> * * <li>The scheme and the path need not be separated with {@code "://"} anymore, only with {@code ":"}. This makes * template names like {@code "classpath:foo.ftl"} interpreted as an absolute name with scheme {@code "classpath"} * and absolute path "foo.ftl". The scheme name before the {@code ":"} can't contain {@code "/"}, or else it's * treated as a malformed name. The scheme part can be separated either with {@code "://"} or just {@code ":"} from * the path. Hence, {@code myscheme:/x} is normalized to {@code myscheme:x}, while {@code myscheme:///x} is * normalized to {@code myscheme://x}, but {@code myscehme://x} or {@code myscheme:/x} aren't changed by * normalization. It's up the {@link TemplateLoader} to which the normalized names are passed to decide which of * these scheme separation conventions are valid (maybe both).</li> * * <li>{@code ":"} is not allowed in template names, except as the scheme separator (see previous point). * * <li>Malformed paths throw {@link MalformedTemplateNameException} instead of acting like if the template wasn't * found. * * <li>{@code "\"} (backslash) is not allowed in template names, and causes {@link MalformedTemplateNameException}. * With {@link #DEFAULT_2_3_0} you would certainly end up with a {@link TemplateNotFoundException} (or worse, * it would work, but steps like {@code ".."} wouldn't be normalized by FreeMarker). * * <li>Template names might end with {@code /}, like {@code "foo/"}, and the presence or lack of the terminating * {@code /} is seen as significant. While their actual interpretation is up to the {@link TemplateLoader}, * operations that manipulate template names assume that the last step refers to a "directory" as opposed to a * "file" exactly if the terminating {@code /} is present. Except, the empty name is assumed to refer to the root * "directory" (despite that it doesn't end with {@code /}). * * <li>{@code //} is normalized to {@code /}, except of course if it's in the scheme name terminator. Like * {@code foo//bar///baaz.ftl} is normalized to {@code foo/bar/baaz.ftl}. (In general, 0 long step names aren't * possible anymore.)</li> * * <li>The {@code ".."} bugs of the legacy normalizer are fixed: {@code ".."} steps has removed the preceding * {@code "."} or {@code "*"} or scheme steps, not treating them specially as they should be. Now these work as * expected. Examples: {@code "a/./../c"} has become to {@code "a/c"}, now it will be {@code "c"}; {@code "a/b/*} * {@code /../c"} has become to {@code "a/b/c"}, now it will be {@code "a/*}{@code /c"}; {@code "scheme://.."} has * become to {@code "scheme:/"}, now it will be {@code null} ({@link TemplateNotFoundException}) for backing out of * the root directory.</li> * * <li>As now directory paths has to be handled as well, it recognizes terminating, leading, and lonely {@code ".."} * and {@code "."} steps. For example, {@code "foo/bar/.."} now becomes to {@code "foo/"}</li> * * <li>Multiple consecutive {@code *} steps are normalized to one</li> * * </ul> */
public static final TemplateNameFormat DEFAULT_2_4_0 = new Default020400();
Converts a name to a template root directory based name, so that it can be used to find a template without knowing what (like which template) has referred to it. The rules depend on the name format, but a typical example is converting "t.ftl" with base "sub/contex.ftl" to "sub/t.ftl".
Params:
  • baseName – Maybe a file name, maybe a directory name. The meaning of file name VS directory name depends on the name format, but typically, something like "foo/bar/" is a directory name, and something like "foo/bar" is a file name, and thus in the last case the effective base is "foo/" (i.e., the directory that contains the file). Not null.
  • targetName – The name to convert. This usually comes from a template that refers to another template by name. It can be a relative name, or an absolute name. (In typical name formats absolute names start with "/" or maybe with an URL scheme, and all others are relative). Not null.
Returns:The path in template root directory relative format, or even an absolute name (where the root directory is not the real root directory of the file system, but the imaginary directory that exists to store the templates). The standard implementations shipped with FreeMarker always return a root relative path (except if the name starts with an URI schema, in which case a full URI is returned).
/** * Converts a name to a template root directory based name, so that it can be used to find a template without * knowing what (like which template) has referred to it. The rules depend on the name format, but a typical example * is converting "t.ftl" with base "sub/contex.ftl" to "sub/t.ftl". * * @param baseName * Maybe a file name, maybe a directory name. The meaning of file name VS directory name depends on the * name format, but typically, something like "foo/bar/" is a directory name, and something like * "foo/bar" is a file name, and thus in the last case the effective base is "foo/" (i.e., the directory * that contains the file). Not {@code null}. * @param targetName * The name to convert. This usually comes from a template that refers to another template by name. It * can be a relative name, or an absolute name. (In typical name formats absolute names start with * {@code "/"} or maybe with an URL scheme, and all others are relative). Not {@code null}. * * @return The path in template root directory relative format, or even an absolute name (where the root directory * is not the real root directory of the file system, but the imaginary directory that exists to store the * templates). The standard implementations shipped with FreeMarker always return a root relative path * (except if the name starts with an URI schema, in which case a full URI is returned). */
abstract String toRootBasedName(String baseName, String targetName) throws MalformedTemplateNameException;
Normalizes a template root directory based name (relative to the root or absolute), so that equivalent names become equivalent according String.equals(Object) too. The rules depend on the name format, but typical examples are "sub/../t.ftl" to "t.ftl", "sub/./t.ftl" to "sub/t.ftl" and "/t.ftl" to "t.ftl".

The standard implementations shipped with FreeMarker always returns a root relative path (except if the name starts with an URI schema, in which case a full URI is returned), for example, "/foo.ftl" becomes to "foo.ftl".

Params:
  • name – The root based name (a name that's either absolute or relative to the root). Not null.
Returns:The normalized root based name. Not null.
/** * Normalizes a template root directory based name (relative to the root or absolute), so that equivalent names * become equivalent according {@link String#equals(Object)} too. The rules depend on the name format, but typical * examples are "sub/../t.ftl" to "t.ftl", "sub/./t.ftl" to "sub/t.ftl" and "/t.ftl" to "t.ftl". * * <p>The standard implementations shipped with FreeMarker always returns a root relative path * (except if the name starts with an URI schema, in which case a full URI is returned), for example, "/foo.ftl" * becomes to "foo.ftl". * * @param name * The root based name (a name that's either absolute or relative to the root). Not {@code null}. * * @return The normalized root based name. Not {@code null}. */
abstract String normalizeRootBasedName(String name) throws MalformedTemplateNameException;
Converts a root based name to an absolute name, which is useful if you need to pass a name to something that doesn't necessary resolve relative paths relative to the root (like the #include directive).
Params:
  • name – The root based name (a name that's either absolute or relative to the root). Not null.
/** * Converts a root based name to an absolute name, which is useful if you need to pass a name to something that * doesn't necessary resolve relative paths relative to the root (like the {@code #include} directive). * * @param name * The root based name (a name that's either absolute or relative to the root). Not {@code null}. */
// TODO [FM3] This is the kind of complication why normalized template names should just be absolute paths. abstract String rootBasedNameToAbsoluteName(String name) throws MalformedTemplateNameException; private static final class Default020300 extends TemplateNameFormat { @Override String toRootBasedName(String baseName, String targetName) { if (targetName.indexOf("://") > 0) { return targetName; } else if (targetName.startsWith("/")) { int schemeSepIdx = baseName.indexOf("://"); if (schemeSepIdx > 0) { return baseName.substring(0, schemeSepIdx + 2) + targetName; } else { return targetName.substring(1); } } else { if (!baseName.endsWith("/")) { baseName = baseName.substring(0, baseName.lastIndexOf("/") + 1); } return baseName + targetName; } } @Override String normalizeRootBasedName(final String name) throws MalformedTemplateNameException { // Disallow 0 for security reasons. checkNameHasNoNullCharacter(name); // The legacy algorithm haven't considered schemes, so the name is in effect a path. // Also, note that `path` will be repeatedly replaced below, while `name` is final. String path = name; for (; ; ) { int parentDirPathLoc = path.indexOf("/../"); if (parentDirPathLoc == 0) { // If it starts with /../, then it reaches outside the template // root. throw newRootLeavingException(name); } if (parentDirPathLoc == -1) { if (path.startsWith("../")) { throw newRootLeavingException(name); } break; } int previousSlashLoc = path.lastIndexOf('/', parentDirPathLoc - 1); path = path.substring(0, previousSlashLoc + 1) + path.substring(parentDirPathLoc + "/../".length()); } for (; ; ) { int currentDirPathLoc = path.indexOf("/./"); if (currentDirPathLoc == -1) { if (path.startsWith("./")) { path = path.substring("./".length()); } break; } path = path.substring(0, currentDirPathLoc) + path.substring(currentDirPathLoc + "/./".length() - 1); } // Editing can leave us with a leading slash; strip it. if (path.length() > 1 && path.charAt(0) == '/') { path = path.substring(1); } return path; } @Override String rootBasedNameToAbsoluteName(String name) throws MalformedTemplateNameException { if (name.indexOf("://") > 0) { return name; } if (!name.startsWith("/")) { return "/" + name; } return name; } @Override public String toString() { return "TemplateNameFormat.DEFAULT_2_3_0"; } } private static final class Default020400 extends TemplateNameFormat { @Override String toRootBasedName(String baseName, String targetName) { if (findSchemeSectionEnd(targetName) != 0) { return targetName; } else if (targetName.startsWith("/")) { // targetName is an absolute path final String targetNameAsRelative = targetName.substring(1); final int schemeSectionEnd = findSchemeSectionEnd(baseName); if (schemeSectionEnd == 0) { return targetNameAsRelative; } else { // Prepend the scheme of baseName: return baseName.substring(0, schemeSectionEnd) + targetNameAsRelative; } } else { // targetName is a relative path if (!baseName.endsWith("/")) { // Not a directory name => get containing directory name int baseEnd = baseName.lastIndexOf("/") + 1; if (baseEnd == 0) { // For something like "classpath:t.ftl", must not remove the scheme part: baseEnd = findSchemeSectionEnd(baseName); } baseName = baseName.substring(0, baseEnd); } return baseName + targetName; } } @Override String normalizeRootBasedName(final String name) throws MalformedTemplateNameException { // Disallow 0 for security reasons. checkNameHasNoNullCharacter(name); if (name.indexOf('\\') != -1) { throw new MalformedTemplateNameException( name, "Backslash (\"\\\") is not allowed in template names. Use slash (\"/\") instead."); } // Split name to a scheme and a path: final String scheme; String path; { int schemeSectionEnd = findSchemeSectionEnd(name); if (schemeSectionEnd == 0) { scheme = null; path = name; } else { scheme = name.substring(0, schemeSectionEnd); path = name.substring(schemeSectionEnd); } } if (path.indexOf(':') != -1) { throw new MalformedTemplateNameException(name, "The ':' character can only be used after the scheme name (if there's any), " + "not in the path part"); } path = removeRedundantSlashes(path); // path now doesn't start with "/" path = removeDotSteps(path); path = resolveDotDotSteps(path, name); path = removeRedundantStarSteps(path); return scheme == null ? path : scheme + path; } private int findSchemeSectionEnd(String name) { int schemeColonIdx = name.indexOf(":"); if (schemeColonIdx == -1 || name.lastIndexOf('/', schemeColonIdx - 1) != -1) { return 0; } else { // If there's a following "//", it's treated as the part of the scheme section: if (schemeColonIdx + 2 < name.length() && name.charAt(schemeColonIdx + 1) == '/' && name.charAt(schemeColonIdx + 2) == '/') { return schemeColonIdx + 3; } else { return schemeColonIdx + 1; } } } private String removeRedundantSlashes(String path) { String prevName; do { prevName = path; path = StringUtil.replace(path, "//", "/"); } while (prevName != path); return path.startsWith("/") ? path.substring(1) : path; } private String removeDotSteps(String path) { int nextFromIdx = path.length() - 1; findDotSteps: while (true) { final int dotIdx = path.lastIndexOf('.', nextFromIdx); if (dotIdx < 0) { return path; } nextFromIdx = dotIdx - 1; if (dotIdx != 0 && path.charAt(dotIdx - 1) != '/') { // False alarm continue findDotSteps; } final boolean slashRight; if (dotIdx + 1 == path.length()) { slashRight = false; } else if (path.charAt(dotIdx + 1) == '/') { slashRight = true; } else { // False alarm continue findDotSteps; } if (slashRight) { // "foo/./bar" or "./bar" path = path.substring(0, dotIdx) + path.substring(dotIdx + 2); } else { // "foo/." or "." path = path.substring(0, path.length() - 1); } } }
Params:
  • name – The original name, needed for exception error messages.
/** * @param name The original name, needed for exception error messages. */
private String resolveDotDotSteps(String path, final String name) throws MalformedTemplateNameException { int nextFromIdx = 0; findDotDotSteps: while (true) { final int dotDotIdx = path.indexOf("..", nextFromIdx); if (dotDotIdx < 0) { return path; } if (dotDotIdx == 0) { throw newRootLeavingException(name); } else if (path.charAt(dotDotIdx - 1) != '/') { // False alarm nextFromIdx = dotDotIdx + 3; continue findDotDotSteps; } // Here we know that it has a preceding "/". final boolean slashRight; if (dotDotIdx + 2 == path.length()) { slashRight = false; } else if (path.charAt(dotDotIdx + 2) == '/') { slashRight = true; } else { // False alarm nextFromIdx = dotDotIdx + 3; continue findDotDotSteps; } int previousSlashIdx; boolean skippedStarStep = false; { int searchSlashBacwardsFrom = dotDotIdx - 2; // before the "/.." scanBackwardsForSlash: while (true) { if (searchSlashBacwardsFrom == -1) { throw newRootLeavingException(name); } previousSlashIdx = path.lastIndexOf('/', searchSlashBacwardsFrom); if (previousSlashIdx == -1) { if (searchSlashBacwardsFrom == 0 && path.charAt(0) == '*') { // "*/.." throw newRootLeavingException(name); } break scanBackwardsForSlash; } if (path.charAt(previousSlashIdx + 1) == '*' && path.charAt(previousSlashIdx + 2) == '/') { skippedStarStep = true; searchSlashBacwardsFrom = previousSlashIdx - 1; } else { break scanBackwardsForSlash; } } } // Note: previousSlashIdx is possibly -1 // Removed part in {}: "a/{b/*/../}c" or "a/{b/*/..}" path = path.substring(0, previousSlashIdx + 1) + (skippedStarStep ? "*/" : "") + path.substring(dotDotIdx + (slashRight ? 3 : 2)); nextFromIdx = previousSlashIdx + 1; } } private String removeRedundantStarSteps(String path) { String prevName; removeDoubleStarSteps: do { int supiciousIdx = path.indexOf("*/*"); if (supiciousIdx == -1) { break removeDoubleStarSteps; } prevName = path; // Is it delimited on both sided by "/" or by the string boundaires? if ((supiciousIdx == 0 || path.charAt(supiciousIdx - 1) == '/') && (supiciousIdx + 3 == path.length() || path.charAt(supiciousIdx + 3) == '/')) { path = path.substring(0, supiciousIdx) + path.substring(supiciousIdx + 2); } } while (prevName != path); // An initial "*" step is redundant: if (path.startsWith("*")) { if (path.length() == 1) { path = ""; } else if (path.charAt(1) == '/') { path = path.substring(2); } // else: it's wasn't a "*" step. } return path; } @Override String rootBasedNameToAbsoluteName(String name) throws MalformedTemplateNameException { if (findSchemeSectionEnd(name) != 0) { return name; } if (!name.startsWith("/")) { return "/" + name; } return name; } @Override public String toString() { return "TemplateNameFormat.DEFAULT_2_4_0"; } } private static void checkNameHasNoNullCharacter(final String name) throws MalformedTemplateNameException { if (name.indexOf(0) != -1) { throw new MalformedTemplateNameException(name, "Null character (\\u0000) in the name; possible attack attempt"); } } private static MalformedTemplateNameException newRootLeavingException(final String name) { return new MalformedTemplateNameException(name, "Backing out from the root directory is not allowed"); } }