/*
 * 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 TemplateLoader that uses files inside a specified directory as the source of templates. By default it does security checks on the canonical 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 FileTemplateLoader(File, boolean) with true second argument, but before that, check the security implications there!
/** * 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 true, you can change the default of #getEmulateCaseSensitiveFileSystem().
/** * 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 user.dir 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 FileTemplateLoader(File) instead.
/** * 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.
Params:
  • baseDir – the base directory for loading templates
/** * 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.
Params:
  • baseDir – the base directory for loading templates
  • disableCanonicalPathCheck – If true, it will not check if the file to be loaded is inside the baseDir or not, according the canonical paths of the baseDir and the file to load. Note that Configuration.getTemplate(String) and (its overloads) already prevents backing out from the template directory with paths like /../../../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 baseDir, set this parameter to true, but then be very careful with template paths that are supplied by the visitor or an external system.
/** * 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
/** * 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 getBaseDirectory() directory, must be listed. Positive results (matching case) will be cached without expiration time.

The default in FileTemplateLoader is false, but subclasses may change they by overriding getEmulateCaseSensitiveFileSystemDefault().

Since:2.3.23
/** * 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; }
Since:2.3.23
/** * Getter pair of {@link #setEmulateCaseSensitiveFileSystem(boolean)}. * * @since 2.3.23 */
public boolean getEmulateCaseSensitiveFileSystem() { return emulateCaseSensitiveFileSystem; }
Returns the default of getEmulateCaseSensitiveFileSystem(). In FileTemplateLoader it's false, unless the SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM system property was set to 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 true on Windows.
Since:2.3.23
/** * 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
/** * 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" : "") + ")"; } }