blob: 774ae88778bc51ce0a91f530215f61540226f599 [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 org.apache.ofbiz.base.util;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.apache.commons.io.FileUtils;
import org.apache.ofbiz.base.location.ComponentLocationResolver;
import net.lingala.zip4j.ZipFile;
/**
* File Utilities
*/
public final class FileUtil {
private static final String MODULE = FileUtil.class.getName();
private FileUtil() { }
public static File getFile(String path) {
return getFile(null, path);
}
public static File getFile(File root, String path) {
if (path.startsWith("component://")) {
try {
path = ComponentLocationResolver.getBaseLocation(path).toString();
} catch (MalformedURLException e) {
Debug.logError(e, MODULE);
return null;
}
}
return new File(root, localizePath(path));
}
/**
* Converts a file path to one that is compatible with the host operating system.
* @param path The file path to convert.
* @return The converted file path.
*/
private static String localizePath(String path) {
String fileNameSeparator = ("\\".equals(File.separator) ? "\\" + File.separator : File.separator);
return path.replaceAll("/+|\\\\+", fileNameSeparator);
}
public static void writeString(String fileName, String s) throws IOException {
writeString(null, fileName, s);
}
private static void writeString(String path, String name, String s) {
try (Writer out = getBufferedWriter(path, name);) {
out.write(s + System.getProperty("line.separator"));
} catch (IOException e) {
Debug.logError(e, MODULE);
}
}
/**
* Writes a file from a string with a specified encoding.
* @param path
* @param name
* @param encoding
* @param s
* @throws IOException
*/
public static void writeString(String path, String name, String encoding, String s) throws IOException {
String fileName = getPatchedFileName(path, name);
if (UtilValidate.isEmpty(fileName)) {
throw new IOException("Cannot obtain buffered writer for an empty filename!");
}
try {
FileUtils.writeStringToFile(new File(fileName), s, encoding);
} catch (IOException e) {
Debug.logError(e, MODULE);
throw e;
}
}
public static void writeString(String encoding, String s, File outFile) throws IOException {
try {
FileUtils.writeStringToFile(outFile, s, encoding);
} catch (IOException e) {
Debug.logError(e, MODULE);
throw e;
}
}
private static Writer getBufferedWriter(String path, String name) throws IOException {
String fileName = getPatchedFileName(path, name);
if (UtilValidate.isEmpty(fileName)) {
throw new IOException("Cannot obtain buffered writer for an empty filename!");
}
return new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), StandardCharsets.UTF_8));
}
public static OutputStream getBufferedOutputStream(String path, String name) throws IOException {
String fileName = getPatchedFileName(path, name);
if (UtilValidate.isEmpty(fileName)) {
throw new IOException("Cannot obtain buffered writer for an empty filename!");
}
return new BufferedOutputStream(new FileOutputStream(fileName));
}
private static String getPatchedFileName(String path, String fileName) throws IOException {
// make sure the export directory exists
if (UtilValidate.isNotEmpty(path)) {
path = path.replace("\\", "/");
File parentDir = new File(path);
if (!parentDir.exists()) {
if (!parentDir.mkdir()) {
throw new IOException("Cannot create directory for path: " + path);
}
}
// build the filename with the path
if (!path.endsWith("/")) {
path = path + "/";
}
if (fileName.startsWith("/")) {
fileName = fileName.substring(1);
}
fileName = path + fileName;
}
return fileName;
}
private static StringBuffer readTextFile(File file, boolean newline) throws FileNotFoundException, IOException {
if (!file.exists()) {
throw new FileNotFoundException();
}
StringBuffer buf = new StringBuffer();
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8));) {
String str;
while ((str = in.readLine()) != null) {
buf.append(str);
if (newline) {
buf.append(System.getProperty("line.separator"));
}
}
} catch (IOException e) {
Debug.logError(e, MODULE);
}
return buf;
}
public static StringBuffer readTextFile(String fileName, boolean newline) throws FileNotFoundException, IOException {
File file = new File(fileName);
return readTextFile(file, newline);
}
public static String readString(String encoding, File inFile) throws IOException {
String readString = "";
try {
readString = FileUtils.readFileToString(inFile, encoding);
} catch (IOException e) {
Debug.logError(e, MODULE);
throw e;
}
return readString;
}
private static void searchFiles(List<File> fileList, File path, FilenameFilter filter, boolean includeSubfolders) throws IOException {
// Get filtered files in the current path
File[] files = path.listFiles(filter);
if (files == null) {
return;
}
// Process each filtered entry
for (int i = 0; i < files.length; i++) {
// recurse if the entry is a directory
if (files[i].isDirectory() && includeSubfolders && !files[i].getName().startsWith(".")) {
searchFiles(fileList, files[i], filter, true);
} else {
// add the filtered file to the list
fileList.add(files[i]);
}
}
}
public static List<File> findFiles(String fileExt, String basePath, String partialPath, String stringToFind) throws IOException {
if (basePath == null) {
basePath = System.getProperty("ofbiz.home");
}
Set<String> stringsToFindInPath = new HashSet<>();
Set<String> stringsToFindInFile = new HashSet<>();
if (partialPath != null) {
stringsToFindInPath.add(partialPath);
}
if (stringToFind != null) {
stringsToFindInFile.add(stringToFind);
}
List<File> fileList = new LinkedList<>();
FileUtil.searchFiles(fileList, new File(basePath), new SearchTextFilesFilter(fileExt, stringsToFindInPath, stringsToFindInFile), true);
return fileList;
}
public static List<File> findXmlFiles(String basePath, String partialPath, String rootElementName, String xsdOrDtdName) throws IOException {
if (basePath == null) {
basePath = System.getProperty("ofbiz.home");
}
Set<String> stringsToFindInPath = new HashSet<>();
Set<String> stringsToFindInFile = new HashSet<>();
if (partialPath != null) {
stringsToFindInPath.add(partialPath);
}
if (rootElementName != null) {
stringsToFindInFile.add("<" + rootElementName + " ");
}
if (xsdOrDtdName != null) {
stringsToFindInFile.add(xsdOrDtdName);
}
List<File> fileList = new LinkedList<>();
FileUtil.searchFiles(fileList, new File(basePath), new SearchTextFilesFilter("xml", stringsToFindInPath, stringsToFindInFile), true);
return fileList;
}
/**
* Search for the specified <code>searchString</code> in the given
* {@link Reader}.
* @param reader A Reader in which the String will be searched.
* @param searchString The String to search for
* @return <code>TRUE</code> if the <code>searchString</code> is found;
* <code>FALSE</code> otherwise.
* @throws IOException
*/
public static boolean containsString(Reader reader, final String searchString) throws IOException {
char[] buffer = new char[1024];
int numCharsRead;
int count = 0;
while ((numCharsRead = reader.read(buffer)) > 0) {
for (int c = 0; c < numCharsRead; ++c) {
if (buffer[c] == searchString.charAt(count)) {
count++;
} else {
count = 0;
}
if (count == searchString.length()) {
return true;
}
}
}
return false;
}
/**
* Search for the specified <code>searchString</code> in the given
* filename. If the specified file doesn't exist, <code>FALSE</code>
* returns.
* @param fileName A full path to a file in which the String will be searched.
* @param searchString The String to search for
* @return <code>TRUE</code> if the <code>searchString</code> is found;
* <code>FALSE</code> otherwise.
* @throws IOException
*/
public static boolean containsString(final String fileName, final String searchString) throws IOException {
File inFile = new File(fileName);
if (inFile.exists()) {
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(inFile), StandardCharsets.UTF_8));) {
return containsString(in, searchString);
}
}
return false;
}
/**
* Check if the specified <code>fileName</code> exists and is a file (not a directory)
* If the specified file doesn't exist or is a directory <code>FALSE</code> returns.
* @param fileName A full path to a file in which the String will be searched.
* @return <code>TRUE</code> if the <code>fileName</code> exists and is a file (not a directory)
* <code>FALSE</code> otherwise.
*/
public static boolean isFile(String fileName) {
File f = new File(fileName);
return f.isFile();
}
/**
* For an inputStream and a file name, create a zip stream containing only one entry with the inputStream set to fileName
* If fileName is empty, generate a unique one.
* @param fileStream
* @param fileName
* @return zipStream
* @throws IOException
*/
public static ByteArrayInputStream zipFileStream(InputStream fileStream, String fileName) throws IOException {
if (fileStream == null) {
return null;
}
if (fileName == null) {
fileName = UUID.randomUUID().toString();
}
// Create zip file from content input stream
return zipFileStreams(Map.of(fileName, fileStream));
}
/**
* For map with entries as [fileName: inputStream], create a zip stream containing all given files
* @param files
* @return
* @throws IOException
*/
public static ByteArrayInputStream zipFileStreams(Map<String, InputStream> files) throws IOException {
if (files == null) {
return null;
}
// Create zip file from content input stream
String zipFileName = UUID.randomUUID().toString() + ".zip";
String zipFilePath = UtilProperties.getPropertyValue("general", "http.upload.tmprepository", "runtime/tmp");
FileOutputStream fos = new FileOutputStream(new File(zipFilePath, zipFileName));
ZipOutputStream zos = new ZipOutputStream(fos);
zos.setMethod(ZipOutputStream.DEFLATED);
zos.setLevel(Deflater.BEST_COMPRESSION);
// parse all map to set in the zip stream
for (String fileName : files.keySet()) {
ZipEntry ze = new ZipEntry(fileName);
zos.putNextEntry(ze);
int len;
byte[] bufferData = new byte[8192];
while ((len = files.get(fileName).read(bufferData)) > 0) {
zos.write(bufferData, 0, len);
}
}
zos.closeEntry();
zos.close();
fos.close();
//prepare zip stream
File tmpZipFile = new File(zipFilePath, zipFileName);
ByteArrayInputStream zipStream = new ByteArrayInputStream(FileUtils.readFileToByteArray(tmpZipFile));
//Then delete zip file
tmpZipFile.delete();
return zipStream;
}
/**
* This useful to prevents Zip slip vulnerability cf. https://snyk.io/research/zip-slip-vulnerability
* @param destinationDirName The file path name to normalize
* @param zipEntry Zip entry to check before creating
* @return A file in destinationDir
*/
public static File newFile(String destinationDirName, ZipEntry zipEntry) throws IOException {
File destinationDir = new File(destinationDirName);
File destFile = new File(destinationDir, zipEntry.getName());
String destDirPath = destinationDir.getCanonicalPath();
String destFilePath = destFile.getCanonicalPath();
if (!destFilePath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
}
return destFile;
}
/**
* Unzip file structure of the given zipFile to specified outputFolder
* @param zipFile
* @param outputFolder
* @param handleZipSlip if true FileUtil::newFile is used
* @throws IOException
*/
public static boolean unzipFileToFolder(File zipFile, String outputFolder, boolean handleZipSlip) throws IOException {
byte[] buffer = new byte[8192];
//create output directory if not exists
File folder = new File(outputFolder);
if (!folder.exists()) {
folder.mkdir();
}
// get the Zip file content
ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile));
// get the zipped file list entry
ZipEntry ze = zis.getNextEntry();
while (ze != null) {
File newFile = null;
if (handleZipSlip) {
newFile = newFile(outputFolder, ze); // Prevents Zip slip vulnerability
if (null == newFile) {
zis.closeEntry();
zis.close();
return false;
}
} else {
newFile = new File(outputFolder, ze.getName());
}
//create all non existing folders
//else you will hit FileNotFoundException for compressed folder
new File(newFile.getParent()).mkdirs();
FileOutputStream fos = new FileOutputStream(newFile);
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
fos.close();
ze = zis.getNextEntry();
}
zis.closeEntry();
zis.close();
return true;
}
/**
* Creates a File with a normalized file path
* This useful to prevent path traversal security issues
* cf. OFBIZ-9973 for more details
* @param filePath The file path to normalize
* @return A File with a normalized file path
*/
public static File createFileWithNormalizedPath(String filePath) {
return new File(filePath).toPath().normalize().toFile();
}
/**
* Normalizes a file path
* This useful to prevent path traversal security issues
* @param filePath The file path to normalize
* @return A normalized file path
*/
public static String normalizeFilePath(String filePath) {
return createFileWithNormalizedPath(filePath).toString();
}
private static class SearchTextFilesFilter implements FilenameFilter {
private String fileExtension;
private Set<String> stringsToFindInFile = new HashSet<>();
private Set<String> stringsToFindInPath = new HashSet<>();
SearchTextFilesFilter(String fileExtension, Set<String> stringsToFindInPath, Set<String> stringsToFindInFile) {
this.fileExtension = fileExtension;
if (stringsToFindInPath != null) {
this.stringsToFindInPath.addAll(stringsToFindInPath);
}
if (stringsToFindInFile != null) {
this.stringsToFindInFile.addAll(stringsToFindInFile);
}
}
@Override
public boolean accept(File dir, String name) {
File file = new File(dir, name);
if (file.getName().startsWith(".")) {
return false;
}
if (file.isDirectory()) {
return true;
}
boolean hasAllPathStrings = true;
String fullPath = dir.getPath().replace('\\', '/');
for (String pathString : stringsToFindInPath) {
if (fullPath.indexOf(pathString) < 0) {
hasAllPathStrings = false;
break;
}
}
if (hasAllPathStrings && name.endsWith("." + fileExtension)) {
if (stringsToFindInFile.isEmpty()) {
return true;
}
StringBuffer xmlFileBuffer = null;
try {
xmlFileBuffer = FileUtil.readTextFile(file, true);
} catch (IOException e) {
Debug.logWarning("Error reading xml file [" + file + "] for file search: " + e.toString(), MODULE);
return false;
}
if (UtilValidate.isNotEmpty(xmlFileBuffer)) {
boolean hasAllStrings = true;
for (String stringToFile : stringsToFindInFile) {
if (xmlFileBuffer.indexOf(stringToFile) < 0) {
hasAllStrings = false;
break;
}
}
return hasAllStrings;
}
} else {
return false;
}
return false;
}
}
/**
* Unzip file structure of the given zipFile to specified outputFolder The Zip slip vulnerability is handled since version 1.3.3 of
* net.lingala.zip4j.ZipFile; unzipFileToFolder is not as reliable and does not handle passwords
* @param source
* @param destination
* @param password optional
* @return true if OK
*/
public static boolean unZip(String source, String destination, String password) {
try (ZipFile zipSource = new ZipFile(source);
ZipFile zipSourcePwd = new ZipFile(source, password.toCharArray())) {
if (password.isEmpty()) {
zipSource.extractAll(destination);
} else {
zipSourcePwd.extractAll(destination);
}
} catch (IOException e) {
Debug.logError("Error extracting [" + source + "] file to dir destination: " + destination, e.toString(), MODULE);
return false;
}
return true;
}
}