/*
* 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 java.io.
File;
import java.io.
FileInputStream;
import java.io.
FileNotFoundException;
import java.io.
IOException;
import java.io.
InputStreamReader;
import java.io.
Reader;
import java.security.
AccessController;
import java.security.
PrivilegedAction;
import java.security.
PrivilegedActionException;
import java.security.
PrivilegedExceptionAction;
import freemarker.log.
Logger;
import freemarker.template.
Configuration;
import freemarker.template.utility.
SecurityUtilities;
import freemarker.template.utility.
StringUtil;
/**
* A {@link TemplateLoader} that uses files inside a specified directory as the source of templates. By default it does
* security checks on the <em>canonical</em> path that will prevent it serving templates outside that specified
* directory. If you want symbolic links that point outside the template directory to work, you need to disable this
* feature by using {@link #FileTemplateLoader(File, boolean)} with {@code true} second argument, but before that, check
* the security implications there!
*/
public class
FileTemplateLoader implements
TemplateLoader {
/**
* By setting this Java system property to {@code true}, you can change the default of
* {@code #getEmulateCaseSensitiveFileSystem()}.
*/
public static
String SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM
= "org.freemarker.emulateCaseSensitiveFileSystem";
private static final boolean
EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT;
static {
final
String s =
SecurityUtilities.
getSystemProperty(
SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM,
"false");
boolean
emuCaseSensFS;
try {
emuCaseSensFS =
StringUtil.
getYesNo(
s);
} catch (
Exception e) {
emuCaseSensFS = false;
}
EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT =
emuCaseSensFS;
}
private static final int
CASE_CHECH_CACHE_HARD_SIZE = 50;
private static final int
CASE_CHECK_CACHE__SOFT_SIZE = 1000;
private static final boolean
SEP_IS_SLASH =
File.
separatorChar == '/';
private static final
Logger LOG =
Logger.
getLogger("freemarker.cache");
public final
File baseDir;
private final
String canonicalBasePath;
private boolean
emulateCaseSensitiveFileSystem;
private
MruCacheStorage correctCasePaths;
/**
* Creates a new file template cache that will use the current directory (the value of the system property
* <code>user.dir</code> as the base directory for loading templates. It will not allow access to template files
* that are accessible through symlinks that point outside the base directory.
*
* @deprecated Relying on what the current directory is is a bad practice; use
* {@link FileTemplateLoader#FileTemplateLoader(File)} instead.
*/
@
Deprecated
public
FileTemplateLoader() throws
IOException {
this(new
File(
SecurityUtilities.
getSystemProperty("user.dir")));
}
/**
* Creates a new file template loader that will use the specified directory
* as the base directory for loading templates. It will not allow access to
* template files that are accessible through symlinks that point outside
* the base directory.
* @param baseDir the base directory for loading templates
*/
public
FileTemplateLoader(final
File baseDir) throws
IOException {
this(
baseDir, false);
}
/**
* Creates a new file template loader that will use the specified directory as the base directory for loading
* templates. See the parameters for allowing symlinks that point outside the base directory.
*
* @param baseDir
* the base directory for loading templates
*
* @param disableCanonicalPathCheck
* If {@code true}, it will not check if the file to be loaded is inside the {@code baseDir} or not,
* according the <em>canonical</em> paths of the {@code baseDir} and the file to load. Note that
* {@link Configuration#getTemplate(String)} and (its overloads) already prevents backing out from the
* template directory with paths like {@code /../../../etc/password}, however, that can be circumvented
* with symbolic links or other file system features. If you really want to use symbolic links that point
* outside the {@code baseDir}, set this parameter to {@code true}, but then be very careful with
* template paths that are supplied by the visitor or an external system.
*/
public
FileTemplateLoader(final
File baseDir, final boolean
disableCanonicalPathCheck) throws
IOException {
try {
Object[]
retval = (
Object[])
AccessController.
doPrivileged(new
PrivilegedExceptionAction<
Object[]>() {
public
Object[]
run() throws
IOException {
if (!
baseDir.
exists()) {
throw new
FileNotFoundException(
baseDir + " does not exist.");
}
if (!
baseDir.
isDirectory()) {
throw new
IOException(
baseDir + " is not a directory.");
}
Object[]
retval = new
Object[2];
if (
disableCanonicalPathCheck) {
retval[0] =
baseDir;
retval[1] = null;
} else {
retval[0] =
baseDir.
getCanonicalFile();
String basePath = ((
File)
retval[0]).
getPath();
// Most canonical paths don't end with File.separator,
// but some does. Like, "C:\" VS "C:\templates".
if (!
basePath.
endsWith(
File.
separator)) {
basePath +=
File.
separatorChar;
}
retval[1] =
basePath;
}
return
retval;
}
});
this.
baseDir = (
File)
retval[0];
this.
canonicalBasePath = (
String)
retval[1];
setEmulateCaseSensitiveFileSystem(
getEmulateCaseSensitiveFileSystemDefault());
} catch (
PrivilegedActionException e) {
throw (
IOException)
e.
getException();
}
}
public
Object findTemplateSource(final
String name) throws
IOException {
try {
return
AccessController.
doPrivileged(new
PrivilegedExceptionAction<
File>() {
public
File run() throws
IOException {
File source = new
File(
baseDir,
SEP_IS_SLASH ?
name :
name.
replace('/',
File.
separatorChar));
if (!
source.
isFile()) {
return null;
}
// Security check for inadvertently returning something
// outside the template directory when linking is not
// allowed.
if (
canonicalBasePath != null) {
String normalized =
source.
getCanonicalPath();
if (!
normalized.
startsWith(
canonicalBasePath)) {
throw new
SecurityException(
source.
getAbsolutePath()
+ " resolves to " +
normalized + " which "
+ " doesn't start with " +
canonicalBasePath);
}
}
if (
emulateCaseSensitiveFileSystem && !
isNameCaseCorrect(
source)) {
return null;
}
return
source;
}
});
} catch (
PrivilegedActionException e) {
throw (
IOException)
e.
getException();
}
}
public long
getLastModified(final
Object templateSource) {
return (
AccessController.
doPrivileged(new
PrivilegedAction<
Long>() {
public
Long run() {
return
Long.
valueOf(((
File)
templateSource).
lastModified());
}
})).
longValue();
}
public
Reader getReader(final
Object templateSource, final
String encoding) throws
IOException {
try {
return
AccessController.
doPrivileged(new
PrivilegedExceptionAction<
Reader>() {
public
Reader run() throws
IOException {
if (!(
templateSource instanceof
File)) {
throw new
IllegalArgumentException(
"templateSource wasn't a File, but a: " +
templateSource.
getClass().
getName());
}
return new
InputStreamReader(new
FileInputStream((
File)
templateSource),
encoding);
}
});
} catch (
PrivilegedActionException e) {
throw (
IOException)
e.
getException();
}
}
/**
* Called by {@link #findTemplateSource(String)} when {@link #getEmulateCaseSensitiveFileSystem()} is {@code true}.
*/
private boolean
isNameCaseCorrect(
File source) throws
IOException {
final
String sourcePath =
source.
getPath();
synchronized (
correctCasePaths) {
if (
correctCasePaths.
get(
sourcePath) != null) {
return true;
}
}
final
File parentDir =
source.
getParentFile();
if (
parentDir != null) {
if (!
baseDir.
equals(
parentDir) && !
isNameCaseCorrect(
parentDir)) {
return false;
}
final
String[]
listing =
parentDir.
list();
if (
listing != null) {
final
String fileName =
source.
getName();
boolean
identicalNameFound = false;
for (int
i = 0; !
identicalNameFound &&
i <
listing.length;
i++) {
if (
fileName.
equals(
listing[
i])) {
identicalNameFound = true;
}
}
if (!
identicalNameFound) {
// If we find a similarly named file that only differs in case, then this is a file-not-found.
for (int
i = 0;
i <
listing.length;
i++) {
final
String listingEntry =
listing[
i];
if (
fileName.
equalsIgnoreCase(
listingEntry)) {
if (
LOG.
isDebugEnabled()) {
LOG.
debug("Emulating file-not-found because of letter case differences to the "
+ "real file, for: " +
sourcePath);
}
return false;
}
}
}
}
}
synchronized (
correctCasePaths) {
correctCasePaths.
put(
sourcePath,
Boolean.
TRUE);
}
return true;
}
public void
closeTemplateSource(
Object templateSource) {
// Do nothing.
}
/**
* Returns the base directory in which the templates are searched. This comes from the constructor argument, but
* it's possibly a canonicalized version of that.
*
* @since 2.3.21
*/
public
File getBaseDirectory() {
return
baseDir;
}
/**
* Intended for development only, checks if the template name matches the case (upper VS lower case letters) of the
* actual file name, and if it doesn't, it emulates a file-not-found even if the file system is case insensitive.
* This is useful when developing application on Windows, which will be later installed on Linux, OS X, etc. This
* check can be resource intensive, as to check the file name the directories involved, up to the
* {@link #getBaseDirectory()} directory, must be listed. Positive results (matching case) will be cached without
* expiration time.
*
* <p>The default in {@link FileTemplateLoader} is {@code false}, but subclasses may change they by overriding
* {@link #getEmulateCaseSensitiveFileSystemDefault()}.
*
* @since 2.3.23
*/
public void
setEmulateCaseSensitiveFileSystem(boolean
nameCaseChecked) {
// Ensure that the cache exists exactly when needed:
if (
nameCaseChecked) {
if (
correctCasePaths == null) {
correctCasePaths = new
MruCacheStorage(
CASE_CHECH_CACHE_HARD_SIZE,
CASE_CHECK_CACHE__SOFT_SIZE);
}
} else {
correctCasePaths = null;
}
this.
emulateCaseSensitiveFileSystem =
nameCaseChecked;
}
/**
* Getter pair of {@link #setEmulateCaseSensitiveFileSystem(boolean)}.
*
* @since 2.3.23
*/
public boolean
getEmulateCaseSensitiveFileSystem() {
return
emulateCaseSensitiveFileSystem;
}
/**
* Returns the default of {@link #getEmulateCaseSensitiveFileSystem()}. In {@link FileTemplateLoader} it's
* {@code false}, unless the {@link #SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM} system property was
* set to {@code true}, but this can be overridden here in custom subclasses. For example, if your environment
* defines something like developer mode, you may want to override this to return {@code true} on Windows.
*
* @since 2.3.23
*/
protected boolean
getEmulateCaseSensitiveFileSystemDefault() {
return
EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT;
}
/**
* Show class name and some details that are useful in template-not-found errors.
*
* @since 2.3.21
*/
@
Override
public
String toString() {
// We don't StringUtil.jQuote paths here, because on Windows there will be \\-s then that some may find
// confusing.
return
TemplateLoaderUtils.
getClassNameForToString(this) + "("
+ "baseDir=\"" +
baseDir + "\""
+ (
canonicalBasePath != null ? ", canonicalBasePath=\"" +
canonicalBasePath + "\"" : "")
+ (
emulateCaseSensitiveFileSystem ? ", emulateCaseSensitiveFileSystem=true" : "")
+ ")";
}
}