| /* |
| * 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() { |
| 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() { |
| public Object 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 ((Long) (AccessController.doPrivileged(new PrivilegedAction() |
| { |
| public Object run() { |
| return Long.valueOf(((File) templateSource).lastModified()); |
| } |
| }))).longValue(); |
| |
| |
| } |
| |
| public Reader getReader(final Object templateSource, final String encoding) |
| throws IOException { |
| try { |
| return (Reader) AccessController.doPrivileged(new PrivilegedExceptionAction() |
| { |
| public Object 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}. Should throw |
| * {@link FileNotFoundException} if there's a mismatch; the error message should contain both the requested and the |
| * correct file name. |
| */ |
| 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" : "") |
| + ")"; |
| } |
| |
| } |