blob: 2e06795b17bf322bacabf6996b9b3fbfe1edeea0 [file] [log] [blame]
/*
* 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");
}
}