/*
* Copyright (C) 2000 - 2011 TagServlet Ltd
*
* This file is part of Open BlueDragon (OpenBD) CFML Server Engine.
*
* OpenBD is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* Free Software Foundation,version 3.
*
* OpenBD is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenBD. If not, see http://www.gnu.org/licenses/
*
* Additional permission under GNU GPL version 3 section 7
*
* If you modify this Program, or any covered work, by linking or combining
* it with any of the JARS listed in the README.txt (or a modified version of
* (that library), containing parts covered by the terms of that JAR, the
* licensors of this Program grant you additional permission to convey the
* resulting work.
* README.txt @ http://www.openbluedragon.org/license/README.txt
*
* http://openbd.org/
* $Id: FileUtils.java 2374 2013-06-10 22:14:24Z alan $
*/
package com.nary.io;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.vfs.FileObject;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;
import org.aw20.io.StreamUtil;
import com.nary.util.FastMap;
import com.naryx.tagfusion.cfm.engine.cfData;
import com.naryx.tagfusion.cfm.engine.cfDateData;
import com.naryx.tagfusion.cfm.engine.cfEngine;
import com.naryx.tagfusion.cfm.engine.cfNumberData;
import com.naryx.tagfusion.cfm.engine.cfSession;
import com.naryx.tagfusion.cfm.engine.cfStringData;
import com.naryx.tagfusion.cfm.file.cfmlFileCache;
import com.naryx.tagfusion.cfm.file.cfmlURI;
/**
* Utility class to handle file manipulation.
*/
public class FileUtils extends Object {
private static AtomicInteger counter = new AtomicInteger(new Random().nextInt() & 0xffff);
public static int LIST_TYPE_ALL = 0;
public static int LIST_TYPE_DIR = 1;
public static int LIST_TYPE_FILE = 2;
public static int LIST_INFO_ALL = 0;
public static int LIST_INFO_NAME = 1;
/**
* Delete all files in the directory whose names match the specified pattern.
* @throws MalformedPatternException
*/
public static void deleteFiles(String _dir, String _pattern) throws IOException, MalformedPatternException {
org.apache.oro.text.regex.Perl5Compiler perl = new org.apache.oro.text.regex.Perl5Compiler();
Pattern pattern = perl.compile( escapeFilter( _pattern ), Perl5Compiler.CASE_INSENSITIVE_MASK );
try {
File[] filesToDelete = new File(_dir).listFiles((FileFilter) new CustomFileFilter(pattern, false));
for (int i = 0; i < filesToDelete.length; i++) {
filesToDelete[i].delete();
}
} catch (Throwable t) {
throw new IOException(t.getMessage());
}
}
/**
* These methods are for use by the CFDIRECTORY tag.
* @throws MalformedPatternException
*/
public static List<Map<String, cfData>> createFileVector(File dir, String _pattern, boolean _recurse, int listType, int listInfo) throws MalformedPatternException {
Pattern pattern = null;
if ( _pattern != null ){
org.apache.oro.text.regex.Perl5Compiler perl = new org.apache.oro.text.regex.Perl5Compiler();
pattern = perl.compile( escapeFilter( _pattern ), Perl5Compiler.CASE_INSENSITIVE_MASK );
}
if (listInfo == LIST_INFO_NAME) {
return createFilenameVector(listFilenames(dir, "", pattern, _recurse, listType));
} else {
return createFileVector(listFiles(dir, pattern, _recurse, listType), dir, _recurse);
}
}
public static List<Map<String, cfData>> createFileVector(File dir, boolean _recurse, int listType, int listInfo) {
if (listInfo == LIST_INFO_NAME) {
return createFilenameVector(listFilenames(dir, "", null, _recurse, listType));
} else {
return createFileVector(listFiles(dir, null, _recurse, listType), dir, _recurse);
}
}
private static List<File> listFiles(File dir, Pattern _pattern, boolean _recurse, int listType) {
File[] files = (_pattern == null ? dir.listFiles() : listFiles(dir, _pattern, listType));
List<File> filesList = new ArrayList<File>();
Perl5Matcher matcher = ( _pattern == null ? null : new Perl5Matcher() );
if (files != null) {
for (int i = 0; i < files.length; i++) {
boolean isDir = files[i].isDirectory();
if (isDir && _recurse) {
filesList.addAll(listFiles(files[i], _pattern, _recurse, listType));
}
if ((listType == LIST_TYPE_DIR && !isDir) || (listType == LIST_TYPE_FILE && isDir))
continue;
if ( _pattern == null || matcher.matches( files[i].getName(), _pattern ) ){
filesList.add(files[i]);
}
}
}
return filesList;
}
private static List<String> listFilenames(File dir, String _parentDir, Pattern _pattern, boolean _recurse, int listType) {
File[] files = (_pattern == null ? dir.listFiles() : listFiles(dir, _pattern, listType));
List<String> filesList = new ArrayList<String>();
if (files != null) {
for (int i = 0; i < files.length; i++) {
boolean isDir = files[i].isDirectory();
if (isDir && _recurse) {
filesList.addAll(listFilenames(files[i], _parentDir + files[i].getName() + "/", _pattern, _recurse, listType));
}
if ((listType == LIST_TYPE_DIR && !isDir) || (listType == LIST_TYPE_FILE && isDir))
continue;
if (_pattern == null || listType == LIST_TYPE_ALL ) {
filesList.add(_parentDir + files[i].getName());
}
}
}
return filesList;
}
private static List<Map<String, cfData>> createFilenameVector(List<String> files) {
if (files == null)
return null;
List<Map<String, cfData>> resultVector = new ArrayList<Map<String, cfData>>();
for (int i = 0; i < files.size(); i++) {
Map<String, cfData> hm = new FastMap<String, cfData>();
hm.put("name", new cfStringData(files.get(i)));
hm.put("size", new cfNumberData(-1));
hm.put("directory", new cfStringData(""));
hm.put("type", new cfStringData(""));
hm.put("datelastmodified", new cfNumberData(-1));
hm.put("attributes", new cfStringData(""));
hm.put("mode", new cfStringData(""));
resultVector.add(hm);
}
return resultVector;
}
private static List<Map<String, cfData>> createFileVector(List<File> files, File rootdir, boolean recurse) {
if (files == null)
return null;
String rootDirString = rootdir.getAbsolutePath();
List<Map<String, cfData>> resultVector = new ArrayList<Map<String, cfData>>();
int rootprefix = 1 + rootDirString.length();
for (int i = 0; i < files.size(); i++) {
File f = files.get(i);
Map<String, cfData> hm = new FastMap<String, cfData>();
if ( recurse ){
// Make this a relative path
hm.put("name", new cfStringData(f.getAbsolutePath().substring(rootprefix)));
hm.put("directory", new cfStringData(rootDirString));
}else{
hm.put("name", new cfStringData(f.getName()));
hm.put("directory", new cfStringData(f.getParent()));
}
hm.put("size", new cfNumberData(f.length()));
if (f.isDirectory()) {
hm.put("type", new cfStringData("Dir"));
} else {
hm.put("type", new cfStringData("File"));
}
hm.put("datelastmodified", new cfDateData(f.lastModified()));
StringBuilder attrs = new StringBuilder();
if (!f.canWrite())
attrs.append('R');
if (f.isHidden())
attrs.append('H');
hm.put("attributes", new cfStringData(attrs.toString()));
hm.put("mode", new cfStringData(""));
resultVector.add(hm);
}
return resultVector;
}
/**
* The "pattern" string recognizes two wildcards characters:
*
* * - matches 0 or more characters ? - matches exactly one character
*
* Characters that are not wildcards are taken to be literal.
*/
private static File[] listFiles(File dir, Pattern _pattern, int listType) {
return dir.listFiles((FilenameFilter) new CustomFileFilter( _pattern, true));
}
private static class CustomFileFilter implements java.io.FilenameFilter {
private boolean includeDirs = true;
private Pattern pattern;
private Perl5Matcher matcher;
public CustomFileFilter( Pattern _pattern, boolean _includeDirs) {
this.includeDirs = _includeDirs;
pattern = _pattern;
matcher = new Perl5Matcher();
}
public boolean accept(File _path) {
if (_path.isDirectory()) {
return this.includeDirs;
} else {
boolean match = matcher.matches( _path.getName(), pattern );
return match;
}
}
@Override
public boolean accept(File _path, String _filename) {
return accept(new File(_path, _filename));
}
}
private static String escapeFilter(String _filter) {
String filter = _filter;
filter = com.nary.util.string.replaceString(filter, "?", "\\?");
filter = com.nary.util.string.replaceString(filter, "+", "\\+");
filter = com.nary.util.string.replaceString(filter, ".", "\\.");
filter = com.nary.util.string.replaceString(filter, "$", "\\$");
filter = com.nary.util.string.replaceString(filter, "^", "\\^");
filter = com.nary.util.string.replaceString(filter, "\\?", ".");
filter = com.nary.util.string.replaceString(filter, "(", "\\(");
filter = com.nary.util.string.replaceString(filter, ")", "\\)");
filter = com.nary.util.string.replaceString(filter, "[", "\\[");
filter = com.nary.util.string.replaceString(filter, "]", "\\]");
filter = com.nary.util.string.replaceString(filter, "{", "\\{");
filter = com.nary.util.string.replaceString(filter, "}", "\\}");
return com.nary.util.string.replaceString(filter, "*", ".*");
}
/**
* resolveNativeLibPath
*/
public static String resolveNativeLibPath(String nativeLibPath) throws IOException {
if (new cfmlURI(nativeLibPath).isRealFile()) {
File nativeLib = cfEngine.getResolvedFile(nativeLibPath);
if (!nativeLib.exists()) {
throw new IOException("Native library does not exist: " + nativeLibPath);
}
return nativeLib.getCanonicalPath();
}
// copy native lib to temporary file, and return path to temp file
InputStream is = cfEngine.thisServletContext.getResourceAsStream(nativeLibPath);
if (is == null) {
throw new IOException("Could not load native library: " + nativeLibPath);
}
File tempFile = File.createTempFile("LIB", (cfEngine.WINDOWS ? ".dll" : ".so"), cfEngine.thisPlatform.getFileIO().getTempDirectory());
OutputStream fos = cfEngine.thisPlatform.getFileIO().getFileOutputStream(tempFile);
StreamUtil.copyTo(is, fos);
return tempFile.getCanonicalPath();
}
/**
* The following methods were provided by Montara for use by the CFSEARCH classes.
*/
public static void recursiveDelete(File file, boolean dirToo) throws IOException {
File[] files = file.listFiles();
if (files != null) {
for (int i = 0; i < files.length; i++) {
if (files[i].isDirectory())
recursiveDelete(files[i], true);
else
files[i].delete();
}
}
if (dirToo) {
file.delete();
}
}
public static File getRealFile(HttpServletRequest request, String path) {
String realPath = getRealPath(request, path);
if (realPath == null) {
return null;
}
return new File(realPath);
}
/**
* This method should be used instead of HttpServletRequest.getRealPath()
*
* If we can't get the path, then lets drop to use the ServletContext as a final resort
*/
@SuppressWarnings("deprecation")
public static String getRealPath(HttpServletRequest request, String path) {
path = path.replace('\\', '/');
String realPath = request.getRealPath(path);
if (realPath == null) { // WebLogic packed WAR
realPath = getWebLogicRealPath(path);
if (realPath == null) {
realPath = cfEngine.thisServletContext.getRealPath(path);
}
}
return realPath;
}
/**
* This method should be used instead of ServletContext.getRealPath()
*/
public static String getRealPath(String path) {
path = path.replace('\\', '/');
String realPath = cfEngine.thisServletContext.getRealPath(path);
if (realPath == null) { // WebLogic packed WAR
realPath = getWebLogicRealPath(path);
}
return realPath;
}
/**
* This only works on WLS 9.x, which unpacks WARs to a temp directory, but still returns null from context.getRealPath() and request.getRealPath(). This method returns null if the resource does not exist, unlike context.getRealPath() and request.getRealPath()
*/
private static String getWebLogicRealPath(String path) {
try {
java.net.URL url = cfEngine.thisServletContext.getResource(path);
if ((url != null) && (url.getProtocol().equalsIgnoreCase("file"))) {
return url.getPath();
}
} catch (java.net.MalformedURLException ignore) {
}
return null;
}
public static String getRealPath(File root, File file) throws IOException {
String rtn = null;
String rootAbs = root.getCanonicalPath();
String abs = file.getCanonicalPath();
if (abs.startsWith(rootAbs)) {
if (abs.equals(rootAbs))
return "";
abs = abs.substring(rootAbs.length());
if (abs.startsWith(File.separator)) {
abs = abs.substring(File.separator.length());
}
rtn = abs;
}
return rtn;
}
public static long getLastModified(String realPath) {
if (realPath != null) {
return new File(realPath).lastModified();
}
return 0;
}
/**
* Given a directory and filename, combine them and return the full canonical path.
*/
public static String getCanonicalPath(String directory, String fileName) {
File f = new File(directory, fileName);
try {
return f.getCanonicalPath();
} catch (IOException e) {
return f.getPath();
}
}
public static String getCanonicalPath(String path) {
if (path != null) {
try {
return new File(path).getCanonicalPath();
} catch (java.io.IOException ignore) {
}
}
return path;
}
public static String combine(String path1, String path2) {
return new File(path1, path2).getPath();
}
public static boolean exists(String path) {
return new File(path).exists();
}
/**
* Replacement for File.exists() method designed to provide an accurate and efficient check that a file really exists. This method is necessary because of an error in the Windows JavaVM in which file names beginning with the same name as a MS-DOS device (e.g. con.cfm, or com1.cfm, etc) will be reported as existing by File.exists(). [ This causes the current thread to hang while attempting to
* read from the DOS device thus [ preventing further requests to be processed; thus a "denial of service" vulnerability. This alternative method is designed to return quickly for the typical case where the URL is supported with a real file. Note however that empty real files cannot be confirmed without completing all the validation steps required to absolutely confirm the MS-DOS device file
* condition. This should be a rare case. Note: - getCanonicalPath() provides inconsistent results for non-existing file dependent upon whether a previous call was made with an existant file! (yes, I tested this!) - DOS device names cannot be used as file names on Windows XP nor 2003; i.e. aux.cfm or con.cfm cannot be created. - getCanonicalPath() will sometimes return the pathname with the
* ".cfm" extension truncated for these "DOS device" files.
*/
public static boolean exists(File _thefile, cfmlURI _uri) {
if (_thefile.length() != 0) { // all DOS devices have zero length...
return true; // therefore most valid files will satisfy these tests and return here.
}
if (_thefile.lastModified() == 0) { // all valid files have a non-zero value, but DOS devices may also ...
return false;
}
// if we make it here then _thefile.length() == 0, and _thefile.lastModified() != 0,
// so now we need to make sure it's a valid empty file and not a DOS device name
try {
// _thefile.getCanonicalPath() has three possible results:
// 1. throw an IOException, in which case the file doesn't exist
// 2. returns a path including the file extension, in which case the file exists
// 3. returns a path that doesn't have the file extension, in which case the file doesn't exist
String filename = _thefile.getCanonicalPath();
// Check for the case that filename has had the ".cfm" extension truncated by comparing against the original URI
String uri = _uri.getURI();
if (filename.endsWith(uri.substring(uri.lastIndexOf('.')))) {
return true;
}
return false;
} catch (IOException e) {
return false; // Exception guarantees that file does not exist.
}
}
/**
* Starting with the current directory of the requested template, look for Application.cfc and Application.cfm, searching parent directories until one is found or we reach the root of the file system.
*
* The "requestPath" parameter is expected to be a full physical path that includes the name of the requested template.
*/
public static String findApplicationFile(String requestPath) {
File f = null;
String parentPath = requestPath; // start in the current directory
try {
do {
parentPath = new File(parentPath).getParent();
if ((parentPath == null) || (parentPath.length() == 0)) {
return null; // reached file system root
}
// look for Application.cfc first, then Application.cfm
f = new File(parentPath, cfSession.APPLICATION_CFC);
if (f.exists()) {
return f.getPath();
}
f = new File(parentPath, cfSession.APPLICATION_CFM);
} while (!f.exists());
return f.getPath();
} catch (Exception e) { // GoogleAppEngine throws java.security.AccessControlException if we go outside the file root
return null;
}
}
public static String getOnRequestEndCfm(String path) {
File f = new File(new File(path).getParent(), cfSession.ON_REQUEST_END_CFM);
if (f.exists()) {
return f.getPath();
}
return null;
}
public static String getExtension(String fileName) {
return ("." + org.apache.commons.io.FilenameUtils.getExtension(fileName));
}
public static void copy(String sourceFile, String destFile) throws IOException {
org.apache.commons.io.FileUtils.copyFile(new File(sourceFile), new File(destFile));
}
/**
* This method takes a root directory and a directory name, and will then create a series of nested directories to ensure maximum spread throughout a directory structure for files that may have a large number of files. The maximum size a subdirectory will be is 2 characters.
*
* @param root
* @param dirname
* @return
*/
public static File deepMakedirs(File root, String dirname) {
File newdir;
// get the end piece
if (dirname.length() == 1)
newdir = new File(root, dirname);
else
newdir = new File(root, dirname.substring(0, 2));
// Create the directory
if (!newdir.isDirectory()) {
newdir.mkdir();
}
if (dirname.length() == 1)
return newdir;
dirname = dirname.substring(2);
if (dirname.length() == 0) {
return newdir;
} else
return deepMakedirs(newdir, dirname);
}
/**
* Cleans up the path, removing any /./ and /../
*
* @param pathIn
* @return
*/
public static String cleanPath(String pathIn) {
return cleanPath(pathIn, '/');
}
public static String cleanPath(String pathIn, char fileseparator) {
String tmp = pathIn;
String slashdotslash = fileseparator + "." + fileseparator;
String slashdotdotslash = fileseparator + ".." + fileseparator;
// Sort out the /./ combinations
int c1 = tmp.indexOf(slashdotslash);
while (c1 != -1) {
tmp = tmp.substring(0, c1) + fileseparator + tmp.substring(c1 + 3);
c1 = tmp.indexOf(slashdotslash);
}
c1 = tmp.indexOf(slashdotdotslash);
while (c1 != -1) {
int c2 = tmp.lastIndexOf(fileseparator, c1 - 1);
if (c2 != -1) {
tmp = tmp.substring(0, c2 + 1) + tmp.substring(c1 + 4);
} else
break;
c1 = tmp.indexOf(slashdotdotslash);
}
return tmp;
}
/*
* Helper Function to write content to a given file
*/
public static void writeFile(File outFile, String content) throws IOException {
Writer outWriter = null;
try {
outWriter = cfEngine.thisPlatform.getFileIO().getFileWriter(outFile);
outWriter.write(content);
outWriter.flush();
} finally {
try {
if (outWriter != null)
outWriter.close();
} catch (Exception ignoreCloseException) {
}
}
}
/*
* Reads a file into the given String Builder
*/
public static void readFile(File filePath, StringBuilder buffer) throws IOException {
BufferedReader reader = null;
Reader inreader = null;
try {
inreader = new FileReader(filePath);
reader = new BufferedReader(inreader);
char[] chars = new char[8096];
int read;
while ((read = reader.read(chars, 0, chars.length)) != -1) {
buffer.append(chars, 0, read);
}
} finally {
if (reader != null)
try {
reader.close();
} catch (IOException ignored) {
}
if (inreader != null)
try {
inreader.close();
} catch (IOException ignored) {
}
}
}
/*
* Checks to see that a given directory exists, and if it does, optional delete its contents
*/
public static File checkAndCreateDirectory(File rootDirectory, String subsdir, boolean deleteContents) throws Exception {
File newDir = new File(rootDirectory, subsdir);
if (!newDir.isDirectory()) {
newDir.mkdirs();
if (!newDir.isDirectory())
throw new Exception("failed to create the directory: " + newDir);
}
if (deleteContents) {
recursiveDelete(newDir, false);
}
return newDir;
}
/**
* Performs a MIME comparison against the file name
*/
public static boolean acceptContent(String _contentType, String _acceptable) {
if (_contentType == null) {
return false;
}
String[] acceptables = com.nary.util.string.convertToList(_acceptable, ',');
String contentType = _contentType;
int charsetSeparatorIndx = contentType.indexOf(';');
if (charsetSeparatorIndx != -1) {
contentType = contentType.substring(0, charsetSeparatorIndx);
}
int slashIndx = _contentType.indexOf('/');
if (slashIndx != -1) { // check if we are fed a bad mimetype for whatever reason
// breakdown the mime type into it's 2 parts
String main = contentType.substring(0, slashIndx);
String subtype = contentType.substring(slashIndx + 1);
for (int i = 0; i < acceptables.length; i++) {
String nextAcceptable = acceptables[i].trim();
if (nextAcceptable.equals("*") || nextAcceptable.equals("*/*"))
return true;
int starIndx = nextAcceptable.indexOf('*');
if (starIndx != -1) { // we're trying to match against mime types like text/* or */html
slashIndx = nextAcceptable.indexOf('/');
if (slashIndx != -1) {
String acceptMain = nextAcceptable.substring(0, slashIndx);
String acceptSubtype = nextAcceptable.substring(slashIndx + 1);
if ((acceptMain.equalsIgnoreCase("*") && acceptSubtype.equalsIgnoreCase(subtype)) || (acceptMain.equalsIgnoreCase(main) && acceptSubtype.equalsIgnoreCase("*"))) {
return true;
}
}
} else if (nextAcceptable.equalsIgnoreCase(contentType)) {
return true;
}
}
}
return false;
}
public static String getCleanFilePath(cfSession _Session, String _path, boolean _bURI) {
String cleanPath = _path;
if (File.separatorChar == '/') {
cleanPath = cleanPath.replace('\\', '/');
} else {
cleanPath = cleanPath.replace('/', '\\');
}
if (_bURI)
cleanPath = FileUtils.getRealPath(_Session.REQ, cleanPath);
return cleanPath;
}
public static void removeFromFileCache(cfSession _Session, String _filepath) {
String filename = _filepath;
int filenameIndx = filename.lastIndexOf(File.separatorChar);
filename = filename.substring(filenameIndx + 1);
_Session.removeFromFileCache(filename);
cfmlFileCache.flushFile(filename);
}
public static File getFile(cfSession _Session, String _path, boolean _bURI) {
File file = new File(getCleanFilePath(_Session, _path, _bURI));
if (!file.isAbsolute()) {
file = new File(cfEngine.thisPlatform.getFileIO().getTempDirectory(), file.getPath());
}
return file;
}
public static FileObject createTempFile(String prefix, String suffix, FileObject directory) throws IOException {
if (prefix == null) {
throw new NullPointerException();
}
if (prefix.length() < 3) {
throw new IllegalArgumentException("Prefix string too short");
}
return directory.resolveFile(prefix + Integer.toString(counter.getAndIncrement()) + (suffix == null ? ".tmp" : suffix));
}
}