| /* |
| * 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; |
| |
| /** |
| * Symbolized template name format. 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 myschme:/x} is normalized to {@code myschme:x}, while {@code myschme:///x} is normalized |
| * to {@code myschme://x}, but {@code myschme://x} or {@code myschme:/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(); |
| |
| /** |
| * @param baseName Maybe {@code null}, maybe a "file" name instead of a "directory" name. |
| * @param targetName No {@code null}. Maybe relative, maybe absolute. |
| */ |
| abstract String toAbsoluteName(String baseName, String targetName) throws MalformedTemplateNameException; |
| |
| /** |
| * @return For backward compatibility only, {@code null} is allowed and will be treated as if the template doesn't |
| * exist (despite that a normalizer doesn't access the storage, so it's not its duty to decide that). |
| */ |
| abstract String normalizeAbsoluteName(String name) throws MalformedTemplateNameException; |
| |
| private static final class Default020300 extends TemplateNameFormat { |
| @Override |
| String toAbsoluteName(String baseName, String targetName) { |
| if (baseName == null) { |
| return 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 normalizeAbsoluteName(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 |
| public String toString() { |
| return "TemplateNameFormat.DEFAULT_2_3_0"; |
| } |
| |
| } |
| |
| private static final class Default020400 extends TemplateNameFormat { |
| @Override |
| String toAbsoluteName(String baseName, String targetName) { |
| if (baseName == null) { |
| return 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 normalizeAbsoluteName(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 |
| 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"); |
| } |
| |
| } |