| /* |
| * 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 |
| */ |
| public abstract class TemplateNameFormat { |
| |
| private TemplateNameFormat() { |
| // Currently can't be instantiated from outside |
| } |
| |
| /** |
| * 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 {@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". |
| * |
| * @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 {@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 {@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); |
| } |
| } |
| } |
| |
| /** |
| * @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"); |
| } |
| |
| } |