WW-5388 Uses the latest JakartaEE FileUpload Servlet 6 package

Also refactors the Jakarta based parsers as they have a lot in common
This commit is contained in:
Lukasz Lenart
2024-01-24 09:09:56 +01:00
parent 00533db153
commit 8c161f431d
7 changed files with 284 additions and 446 deletions
+2 -2
View File
@@ -216,8 +216,8 @@
<!-- File upload -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-fileupload2-jakarta</artifactId>
<version>2.0.0-M1</version>
<artifactId>commons-fileupload2-jakarta-servlet6</artifactId>
<version>2.0.0-M2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
@@ -20,12 +20,12 @@ package org.apache.struts2.dispatcher.multipart;
import com.opensymphony.xwork2.LocaleProviderFactory;
import com.opensymphony.xwork2.inject.Inject;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.dispatcher.LocalizedMessage;
import jakarta.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -125,7 +125,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
/**
* @param request Inspect the servlet request and set the locale if one wasn't provided by
* the Struts2 framework.
* the Struts2 framework.
*/
protected void setLocale(HttpServletRequest request) {
if (defaultLocale == null) {
@@ -136,7 +136,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
/**
* Build error message.
*
* @param e the Throwable/Exception
* @param e the Throwable/Exception
* @param args arguments
* @return error message
*/
@@ -149,7 +149,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getErrors()
*/
*/
public List<LocalizedMessage> getErrors() {
return errors;
}
@@ -171,4 +171,8 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
return fileName;
}
protected String sanitizeNewlines(String before) {
return before.replaceAll("[\n\r]", "_");
}
}
@@ -19,25 +19,23 @@
package org.apache.struts2.dispatcher.multipart;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload2.core.AbstractFileUpload;
import org.apache.commons.fileupload2.core.DiskFileItem;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.core.FileItem;
import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException;
import org.apache.commons.fileupload2.core.FileUploadContentTypeException;
import org.apache.commons.fileupload2.core.FileUploadException;
import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException;
import org.apache.commons.fileupload2.core.FileUploadSizeException;
import org.apache.commons.fileupload2.core.RequestContext;
import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.dispatcher.LocalizedMessage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
@@ -52,12 +50,13 @@ import java.util.Set;
*/
public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
static final Logger LOG = LogManager.getLogger(JakartaMultiPartRequest.class);
private static final Logger LOG = LogManager.getLogger(JakartaMultiPartRequest.class);
// maps parameter name -> List of FileItem objects
protected Map<String, List<FileItem>> files = new HashMap<>();
protected Map<String, List<UploadedFile>> uploadedFiles = new HashMap<>();
// maps parameter name -> List of param values
/**
* Keeps info about normal form fields
*/
protected Map<String, List<String>> params = new HashMap<>();
/**
@@ -112,40 +111,46 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
}
protected void processUpload(HttpServletRequest request, String saveDir) throws IOException {
if (JakartaServletFileUpload.isMultipartContent(request)) {
for (FileItem item : parseRequest(request, saveDir)) {
LOG.debug("Found file item: [{}]", sanitizeNewlines(item.getFieldName()));
if (item.isFormField()) {
processNormalFormField(item, request.getCharacterEncoding());
} else {
processFileField(item);
}
if (!JakartaServletFileUpload.isMultipartContent(request)) {
LOG.debug("Http request isn't: {}, stop processing", AbstractFileUpload.MULTIPART_FORM_DATA);
return;
}
for (DiskFileItem item : parseRequest(request, saveDir)) {
LOG.debug("Processing a form field: {}", item.getFieldName());
if (item.isFormField()) {
processNormalFormField(item, request.getCharacterEncoding());
} else {
LOG.debug("Processing a file: {}", item.getFieldName());
processFileField(item);
}
}
}
protected void processFileField(FileItem item) {
LOG.debug("Item is a file upload");
protected void processFileField(DiskFileItem item) {
// Skip file uploads that don't have a file name - meaning that no file was selected.
if (item.getName() == null || item.getName().trim().isEmpty()) {
LOG.debug("No file has been uploaded for the field: {}", sanitizeNewlines(item.getFieldName()));
return;
}
List<FileItem> values;
if (files.get(item.getFieldName()) != null) {
values = files.get(item.getFieldName());
List<UploadedFile> values;
if (uploadedFiles.get(item.getFieldName()) != null) {
values = uploadedFiles.get(item.getFieldName());
} else {
values = new ArrayList<>();
}
values.add(item);
files.put(item.getFieldName(), values);
UploadedFile uploadedFile = StrutsUploadedFile.Builder
.create(item.getPath().toFile())
.withOriginalName(item.getName())
.withContentType(item.getContentType())
.build();
values.add(uploadedFile);
uploadedFiles.put(item.getFieldName(), values);
}
protected void processNormalFormField(FileItem item, String charset) throws IOException {
protected void processNormalFormField(DiskFileItem item, String charset) throws IOException {
try {
LOG.debug("Item is a normal form field");
Charset encoding = Charset.forName(charset);
@@ -159,7 +164,7 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
long size = item.getSize();
if (size > maxStringLength) {
LOG.debug("Form field {} of size {} bytes exceeds limit of {}.", sanitizeNewlines(item.getFieldName()), size, maxStringLength);
LOG.debug("Form field: {} of size: {} bytes exceeds limit of: {}.", sanitizeNewlines(item.getFieldName()), size, maxStringLength);
LocalizedMessage localizedMessage = new LocalizedMessage(this.getClass(),
STRUTS_MESSAGES_UPLOAD_ERROR_PARAMETER_TOO_LONG_KEY, null,
new Object[]{item.getFieldName(), maxStringLength, size});
@@ -170,8 +175,6 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
}
if (size == 0) {
values.add(StringUtils.EMPTY);
} else if (charset == null) {
values.add(item.getString()); // WW-633
} else {
values.add(item.getString(encoding));
}
@@ -181,140 +184,99 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
}
}
protected List<FileItem> parseRequest(HttpServletRequest servletRequest, String saveDir) throws FileUploadException {
DiskFileItemFactory fac = createDiskFileItemFactory(saveDir);
JakartaServletFileUpload upload = createServletFileUpload(fac);
protected List<DiskFileItem> parseRequest(HttpServletRequest servletRequest, String saveDir) throws FileUploadException {
DiskFileItemFactory fileItemFactory = createDiskFileItemFactory(saveDir);
JakartaServletFileUpload<DiskFileItem, DiskFileItemFactory> upload = createServletFileUpload(fileItemFactory);
return upload.parseRequest(createRequestContext(servletRequest));
}
protected JakartaServletFileUpload createServletFileUpload(DiskFileItemFactory fac) {
JakartaServletFileUpload upload = new JakartaServletFileUpload(fac);
protected JakartaServletFileUpload<DiskFileItem, DiskFileItemFactory> createServletFileUpload(DiskFileItemFactory fileItemFactory) {
JakartaServletFileUpload<DiskFileItem, DiskFileItemFactory> servletFileUpload = new JakartaServletFileUpload<>(fileItemFactory);
if (maxSize != null) {
upload.setSizeMax(maxSize);
servletFileUpload.setSizeMax(maxSize);
}
if (maxFiles != null) {
upload.setFileCountMax(maxFiles);
servletFileUpload.setFileCountMax(maxFiles);
}
if (maxFileSize != null) {
upload.setFileSizeMax(maxFileSize);
servletFileUpload.setFileSizeMax(maxFileSize);
}
return upload;
return servletFileUpload;
}
protected DiskFileItemFactory createDiskFileItemFactory(String saveDir) {
DiskFileItemFactory.Builder fac = DiskFileItemFactory.builder();
// Make sure that the data is written to file, even if the file is empty.
//setting 0 or -1 no longer seems to work for fileupload buffer size, so using 1 instead.
fac.setBufferSize(1);
DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();
if (saveDir != null) {
fac.setPath(saveDir);
LOG.debug("Using file save directory: {}", saveDir);
builder.setPath(saveDir);
}
return fac.get();
return builder.get();
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames()
*/
public Enumeration<String> getFileParameterNames() {
return Collections.enumeration(files.keySet());
return Collections.enumeration(uploadedFiles.keySet());
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getContentType(java.lang.String)
*/
public String[] getContentType(String fieldName) {
List<FileItem> items = files.get(fieldName);
List<UploadedFile> uploadedFilesList = uploadedFiles.get(fieldName);
if (items == null) {
if (uploadedFilesList == null) {
return null;
}
List<String> contentTypes = new ArrayList<>(items.size());
for (FileItem fileItem : items) {
contentTypes.add(fileItem.getContentType());
}
return contentTypes.toArray(new String[0]);
return uploadedFilesList.stream().map(UploadedFile::getContentType).toArray(String[]::new);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFile(java.lang.String)
*/
public UploadedFile[] getFile(String fieldName) {
List<FileItem> items = files.get(fieldName);
List<UploadedFile> uploadedFileList = uploadedFiles.get(fieldName);
if (items == null) {
if (uploadedFileList == null) {
return null;
}
List<UploadedFile> fileList = new ArrayList<>(items.size());
for (FileItem fileItem : items) {
DiskFileItem diskFileItem = (DiskFileItem) fileItem;
File storeLocation = diskFileItem.getPath().toFile();
// Ensure file exists even if it is empty.
if (diskFileItem.getSize() == 0 && !storeLocation.exists()) {
try {
storeLocation.createNewFile();
} catch (IOException e) {
LOG.error("Cannot write uploaded empty file to disk: {}", storeLocation.getAbsolutePath(), e);
}
}
UploadedFile uploadedFile = StrutsUploadedFile.Builder.create(storeLocation)
.withContentType(fileItem.getContentType())
.withOriginalName(fileItem.getName())
.build();
fileList.add(uploadedFile);
}
return fileList.toArray(new UploadedFile[0]);
return uploadedFileList.toArray(UploadedFile[]::new);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileNames(java.lang.String)
*/
public String[] getFileNames(String fieldName) {
List<FileItem> items = files.get(fieldName);
List<UploadedFile> uploadedFileList = uploadedFiles.get(fieldName);
if (items == null) {
if (uploadedFileList == null) {
return null;
}
List<String> fileNames = new ArrayList<>(items.size());
for (FileItem fileItem : items) {
fileNames.add(getCanonicalName(fileItem.getName()));
}
return fileNames.toArray(new String[0]);
return uploadedFileList.stream()
.map(file -> getCanonicalName(file.getName()))
.toArray(String[]::new);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFilesystemName(java.lang.String)
*/
public String[] getFilesystemName(String fieldName) {
List<FileItem> items = files.get(fieldName);
List<UploadedFile> uploadedFileList = uploadedFiles.get(fieldName);
if (items == null) {
if (uploadedFileList == null) {
return null;
}
List<String> fileNames = new ArrayList<>(items.size());
for (FileItem fileItem : items) {
fileNames.add(((DiskFileItem) fileItem).getPath().toFile().getName());
}
return fileNames.toArray(new String[0]);
return uploadedFileList.stream().map(UploadedFile::getAbsolutePath).toArray(String[]::new);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameter(java.lang.String)
*/
public String getParameter(String name) {
List<String> v = params.get(name);
if (v != null && !v.isEmpty()) {
return v.get(0);
List<String> paramValue = params.get(name);
if (paramValue != null && !paramValue.isEmpty()) {
return paramValue.get(0);
}
return null;
@@ -373,23 +335,14 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp()
*/
public void cleanUp() {
Set<String> names = files.keySet();
for (String name : names) {
List<FileItem> items = files.get(name);
for (FileItem item : items) {
LOG.debug("Removing file {} {}", name, item);
if (!item.isInMemory()) {
try {
item.delete();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
}
Set<String> names = uploadedFiles.keySet();
names.forEach(name -> {
List<UploadedFile> uploadedFileList = uploadedFiles.get(name);
uploadedFileList.forEach(file -> {
LOG.debug("Removing file: {}", file.getName());
file.delete();
});
});
}
private String sanitizeNewlines(String before) {
return before.replaceAll("[\n\r]", "_");
}
}
@@ -18,23 +18,22 @@
*/
package org.apache.struts2.dispatcher.multipart;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload2.core.AbstractFileUpload;
import org.apache.commons.fileupload2.core.DiskFileItem;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload;
import org.apache.commons.fileupload2.core.FileUploadSizeException;
import org.apache.commons.fileupload2.core.FileItemInputIterator;
import org.apache.commons.fileupload2.core.FileItemInput;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.struts2.dispatcher.LocalizedMessage;
import jakarta.servlet.http.HttpServletRequest;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
@@ -54,12 +53,12 @@ import java.util.UUID;
*/
public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
static final Logger LOG = LogManager.getLogger(JakartaStreamMultiPartRequest.class);
private static final Logger LOG = LogManager.getLogger(JakartaStreamMultiPartRequest.class);
/**
* Map between file fields and file data.
*/
protected Map<String, List<FileInfo>> fileInfos = new HashMap<>();
protected Map<String, List<UploadedFile>> uploadedFiles = new HashMap<>();
/**
* Map between non-file fields and values.
@@ -71,12 +70,11 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
*/
public void cleanUp() {
LOG.debug("Performing File Upload temporary storage cleanup.");
for (List<FileInfo> fileInfoList : fileInfos.values()) {
for (FileInfo fileInfo : fileInfoList) {
File file = fileInfo.getFile();
LOG.debug("Deleting file '{}'.", file.getName());
if (!file.delete()) {
LOG.warn("There was a problem attempting to delete file '{}'.", file.getName());
for (List<UploadedFile> uploadedFileList : uploadedFiles.values()) {
for (UploadedFile uploadedFile : uploadedFileList) {
LOG.debug("Deleting file '{}'.", uploadedFile.getName());
if (!uploadedFile.delete()) {
LOG.warn("There was a problem attempting to delete file '{}'.", uploadedFile.getName());
}
}
}
@@ -86,14 +84,14 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getContentType(java.lang.String)
*/
public String[] getContentType(String fieldName) {
List<FileInfo> infos = fileInfos.get(fieldName);
if (infos == null) {
List<UploadedFile> uploadedFileList = uploadedFiles.get(fieldName);
if (uploadedFileList == null) {
return null;
}
List<String> types = new ArrayList<>(infos.size());
for (FileInfo fileInfo : infos) {
types.add(fileInfo.getContentType());
List<String> types = new ArrayList<>(uploadedFileList.size());
for (UploadedFile uploadedFile : uploadedFileList) {
types.add(uploadedFile.getContentType());
}
return types.toArray(new String[0]);
@@ -103,31 +101,25 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFile(java.lang.String)
*/
public UploadedFile[] getFile(String fieldName) {
List<FileInfo> infos = fileInfos.get(fieldName);
if (infos == null) {
List<UploadedFile> uploadedFileList = uploadedFiles.get(fieldName);
if (uploadedFileList == null) {
return null;
}
return infos.stream().map(fileInfo ->
StrutsUploadedFile.Builder.create(fileInfo.getFile())
.withContentType(fileInfo.contentType)
.withOriginalName(fileInfo.originalName)
.build()
).toArray(UploadedFile[]::new);
return uploadedFileList.toArray(UploadedFile[]::new);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileNames(java.lang.String)
*/
public String[] getFileNames(String fieldName) {
List<FileInfo> infos = fileInfos.get(fieldName);
if (infos == null) {
List<UploadedFile> uploadedFileList = uploadedFiles.get(fieldName);
if (uploadedFileList == null) {
return null;
}
List<String> names = new ArrayList<>(infos.size());
for (FileInfo fileInfo : infos) {
names.add(getCanonicalName(fileInfo.getOriginalName()));
List<String> names = new ArrayList<>(uploadedFileList.size());
for (UploadedFile uploadedFile : uploadedFileList) {
names.add(getCanonicalName(uploadedFile.getOriginalName()));
}
return names.toArray(new String[0]);
@@ -137,24 +129,18 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames()
*/
public Enumeration<String> getFileParameterNames() {
return Collections.enumeration(fileInfos.keySet());
return Collections.enumeration(uploadedFiles.keySet());
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFilesystemName(java.lang.String)
*/
public String[] getFilesystemName(String fieldName) {
List<FileInfo> infos = fileInfos.get(fieldName);
if (infos == null) {
List<UploadedFile> uploadedFileList = uploadedFiles.get(fieldName);
if (uploadedFileList == null) {
return null;
}
List<String> names = new ArrayList<>(infos.size());
for (FileInfo fileInfo : infos) {
names.add(fileInfo.getFile().getName());
}
return names.toArray(new String[0]);
return uploadedFileList.stream().map(UploadedFile::getAbsolutePath).toArray(String[]::new);
}
/* (non-Javadoc)
@@ -209,111 +195,51 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
* @param saveDir location of the save dir
*/
protected void processUpload(HttpServletRequest request, String saveDir) throws Exception {
// Sanity check that the request is a multi-part/form-data request.
if (JakartaServletFileUpload.isMultipartContent(request)) {
// Sanity check on request size.
boolean requestSizePermitted = isRequestSizePermitted(request);
// Interface with Commons FileUpload API
// Using the Streaming API
JakartaServletFileUpload<DiskFileItem, DiskFileItemFactory> servletFileUpload = new JakartaServletFileUpload<>();
if (maxSize != null) {
servletFileUpload.setSizeMax(maxSize);
}
if (maxFiles != null) {
servletFileUpload.setFileCountMax(maxFiles);
}
if (maxFileSize != null) {
servletFileUpload.setFileSizeMax(maxFileSize);
}
FileItemInputIterator i = servletFileUpload.getItemIterator(request);
// Iterate the file items
while (i.hasNext()) {
try {
FileItemInput itemStream = i.next();
// If the file item stream is a form field, delegate to the
// field item stream handler
if (itemStream.isFormField()) {
processFileItemStreamAsFormField(itemStream);
}
// Delegate the file item stream for a file field to the
// file item stream handler, but delegation is skipped
// if the requestSizePermitted check failed based on the
// complete content-size of the request.
else {
// prevent processing file field item if request size not allowed.
if (!requestSizePermitted) {
addFileSkippedError(itemStream.getName(), request);
LOG.debug("Skipped stream '{}', request maximum size ({}) exceeded.", itemStream.getName(), maxSize);
continue;
}
processFileItemStreamAsFileField(itemStream, saveDir);
}
} catch (IOException e) {
LOG.warn("Error occurred during process upload", e);
}
}
if (!JakartaServletFileUpload.isMultipartContent(request)) {
LOG.debug("Http request isn't: {}, stop processing", AbstractFileUpload.MULTIPART_FORM_DATA);
return;
}
}
/**
* Defines whether the request allowed based on content length.
*
* @param request the servlet request
* @return true if request size is permitted
*/
protected boolean isRequestSizePermitted(HttpServletRequest request) {
// if maxSize is specified as -1, there is no sanity check and it's
// safe to return true for any request, delegating the failure
// checks later in the upload process.
if (maxSize == null || maxSize == -1 || request == null) {
return true;
// Interface with Commons FileUpload API
// Using the Streaming API
JakartaServletFileUpload<DiskFileItem, DiskFileItemFactory> servletFileUpload = new JakartaServletFileUpload<>();
if (maxSize != null) {
LOG.debug("Applies max size: {} to file upload request", maxSize);
servletFileUpload.setSizeMax(maxSize);
}
return request.getContentLength() < maxSize;
}
/**
* @param request the servlet request
* @return the request content length.
*/
protected long getRequestSize(HttpServletRequest request) {
return request != null ? request.getContentLength() : 0;
}
/**
* Add a file skipped message notification for action messages.
*
* @param fileName file name
* @param request the servlet request
*/
protected void addFileSkippedError(String fileName, HttpServletRequest request) {
String exceptionMessage = "Skipped file " + fileName + "; request size limit exceeded.";
long allowedMaxSize = maxSize != null ? maxSize : -1;
FileUploadSizeException exception = new FileUploadSizeException(exceptionMessage, getRequestSize(request), allowedMaxSize);
LocalizedMessage message = buildErrorMessage(exception, new Object[]{fileName, getRequestSize(request), allowedMaxSize});
if (!errors.contains(message)) {
errors.add(message);
if (maxFiles != null) {
LOG.debug("Applies max files number: {} to file upload request", maxFiles);
servletFileUpload.setFileCountMax(maxFiles);
}
if (maxFileSize != null) {
LOG.debug("Applies max size of single file: {} to file upload request", maxFileSize);
servletFileUpload.setFileSizeMax(maxFileSize);
}
// Iterate the file items
servletFileUpload.getItemIterator(request).forEachRemaining(item -> {
if (item.isFormField()) {
LOG.debug("Processing a form field: {}", sanitizeNewlines(item.getFieldName()));
processFileItemAsFormField(item);
} else {
LOG.debug("Processing a file: {}", sanitizeNewlines(item.getFieldName()));
processFileItemAsFileField(item, saveDir);
}
});
}
/**
* Processes the FileItemStream as a Form Field.
* Processes the FileItem as a normal form field.
*
* @param itemStream file item stream
* @param fileItemInput a form field item input
*/
protected void processFileItemStreamAsFormField(FileItemInput itemStream) {
String fieldName = itemStream.getFieldName();
protected void processFileItemAsFormField(FileItemInput fileItemInput) {
String fieldName = fileItemInput.getFieldName();
try {
List<String> values;
String fieldValue = itemStream.getInputStream().toString();
String fieldValue = fileItemInput.getInputStream().toString();
if (!parameters.containsKey(fieldName)) {
values = new ArrayList<>();
parameters.put(fieldName, values);
@@ -322,35 +248,37 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
}
values.add(fieldValue);
} catch (IOException e) {
LOG.warn("Failed to handle form field '{}'.", fieldName, e);
LOG.warn(new ParameterizedMessage("Failed to handle form field: '{}'", sanitizeNewlines(fieldName)), e);
}
}
/**
* Processes the FileItemStream as a file field.
* Processes the FileItem as a file field.
*
* @param itemStream file item stream
* @param location location
* @param fileItemInput file item representing upload file
* @param location location
*/
protected void processFileItemStreamAsFileField(FileItemInput itemStream, String location) {
protected void processFileItemAsFileField(FileItemInput fileItemInput, String location) {
// Skip file uploads that don't have a file name - meaning that no file was selected.
if (itemStream.getName() == null || itemStream.getName().trim().isEmpty()) {
LOG.debug("No file has been uploaded for the field: {}", itemStream.getFieldName());
if (fileItemInput.getName() == null || fileItemInput.getName().trim().isEmpty()) {
LOG.debug("No file has been uploaded for the field: {}", sanitizeNewlines(fileItemInput.getFieldName()));
return;
}
File file = null;
try {
// Create the temporary upload file.
file = createTemporaryFile(itemStream.getName(), location);
file = createTemporaryFile(fileItemInput.getName(), location);
if (streamFileToDisk(itemStream, file)) {
createFileInfoFromItemStream(itemStream, file);
if (streamFileToDisk(fileItemInput, file)) {
createUploadFile(fileItemInput, file);
}
} catch (IOException e) {
if (file != null) {
try {
file.delete();
if (!file.delete()) {
LOG.warn("Could not delete the file: {}", file.getAbsoluteFile());
}
} catch (SecurityException se) {
LOG.warn("Failed to delete '{}' due to security exception above.", file.getName(), se);
}
@@ -363,13 +291,13 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
*
* @param fileName file name
* @param location location
* @return temporary file based on the given filename and location
* @return a temporary file based on the given filename and location
* @throws IOException in case of IO errors
*/
protected File createTemporaryFile(String fileName, String location) throws IOException {
String name = fileName
.substring(fileName.lastIndexOf('/') + 1)
.substring(fileName.lastIndexOf('\\') + 1);
.substring(fileName.lastIndexOf('/') + 1)
.substring(fileName.lastIndexOf('\\') + 1);
String prefix = name;
String suffix = "";
@@ -391,93 +319,50 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
/**
* Streams the file upload stream to the specified file.
*
* @param itemStream file item stream
* @param file the file
* @param fileItemInput file item input
* @param file the file
* @return true if stream was successfully
* @throws IOException in case of IO errors
*/
protected boolean streamFileToDisk(FileItemInput itemStream, File file) throws IOException {
boolean result;
try (InputStream input = itemStream.getInputStream();
OutputStream output = new BufferedOutputStream(Files.newOutputStream(file.toPath()), bufferSize)) {
protected boolean streamFileToDisk(FileItemInput fileItemInput, File file) throws IOException {
try (InputStream input = fileItemInput.getInputStream();
OutputStream output = new BufferedOutputStream(Files.newOutputStream(file.toPath()), bufferSize)) {
byte[] buffer = new byte[bufferSize];
LOG.debug("Streaming file using buffer size {}.", bufferSize);
LOG.debug("Streaming file using buffer size: {}", bufferSize);
for (int length; ((length = input.read(buffer)) > 0); ) {
output.write(buffer, 0, length);
}
result = true;
} catch (IOException e) {
LOG.error(new ParameterizedMessage("Cannot write input file: {} into file stream: {}",
fileItemInput.getName(), file.getAbsolutePath()), e);
return false;
}
return result;
return true;
}
/**
* Creates an internal <code>FileInfo</code> structure used to pass information
* to the <code>FileUploadInterceptor</code> during the interceptor stack
* invocation process.
* Create {@link UploadedFile} abstraction over uploaded file
*
* @param itemStream file item stream
* @param file the file
* @param fileItemInput file item stream
* @param file the file
*/
protected void createFileInfoFromItemStream(FileItemInput itemStream, File file) {
protected void createUploadFile(FileItemInput fileItemInput, File file) {
// gather attributes from file upload stream.
String fileName = itemStream.getName();
String fieldName = itemStream.getFieldName();
String fileName = fileItemInput.getName();
String fieldName = fileItemInput.getFieldName();
// create internal structure
FileInfo fileInfo = new FileInfo(file, itemStream.getContentType(), fileName);
UploadedFile uploadedFile = StrutsUploadedFile.Builder
.create(file)
.withOriginalName(fileName)
.withContentType(fileItemInput.getContentType())
.build();
// append or create new entry.
if (!fileInfos.containsKey(fieldName)) {
List<FileInfo> infos = new ArrayList<>();
infos.add(fileInfo);
fileInfos.put(fieldName, infos);
if (!uploadedFiles.containsKey(fieldName)) {
List<UploadedFile> infos = new ArrayList<>();
infos.add(uploadedFile);
uploadedFiles.put(fieldName, infos);
} else {
fileInfos.get(fieldName).add(fileInfo);
}
}
/**
* Internal data structure used to store a reference to information needed
* to later pass post processing data to the <code>FileUploadInterceptor</code>.
*
* @since 7.0.0
*/
public static class FileInfo implements Serializable {
private final File file;
private final String contentType;
private final String originalName;
/**
* Default constructor.
*
* @param file the file
* @param contentType content type
* @param originalName original file name
*/
public FileInfo(File file, String contentType, String originalName) {
this.file = file;
this.contentType = contentType;
this.originalName = originalName;
}
/**
* @return the file
*/
public File getFile() {
return file;
}
/**
* @return content type
*/
public String getContentType() {
return contentType;
}
/**
* @return original file name
*/
public String getOriginalName() {
return originalName;
uploadedFiles.get(fieldName).add(uploadedFile);
}
}
@@ -26,8 +26,8 @@ import com.opensymphony.xwork2.mock.MockActionInvocation;
import com.opensymphony.xwork2.mock.MockActionProxy;
import com.opensymphony.xwork2.util.ClassLoaderUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload2.jakarta.JakartaServletDiskFileUpload;
import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.action.UploadedFilesAware;
@@ -221,10 +221,10 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
String msg = errors.get(0);
// the error message should contain at least this test
assertThat(msg).contains(
"The file is too large to be uploaded",
"inputName",
"log4j2.xml",
"allowed mx size is 10"
"The file is too large to be uploaded",
"inputName",
"log4j2.xml",
"allowed mx size is 10"
);
}
@@ -331,15 +331,15 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
req.setMethod("POST");
req.addHeader("Content-type", "multipart/form-data; boundary=" + boundary);
String content = encodeTextFile("test.html", "text/plain", plainContent) +
encodeTextFile("test1.html", "text/html", htmlContent) +
encodeTextFile("test2.html", "text/html", htmlContent) +
endline +
endline +
endline +
"--" +
boundary +
"--" +
endline;
encodeTextFile("test1.html", "text/html", htmlContent) +
encodeTextFile("test2.html", "text/html", htmlContent) +
endline +
endline +
endline +
"--" +
boundary +
"--" +
endline;
req.setContent(content.getBytes());
assertTrue(JakartaServletDiskFileUpload.isMultipartContent(req));
@@ -369,14 +369,14 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
req.setMethod("POST");
req.addHeader("Content-type", "multipart/form-data; boundary=" + boundary);
String content = encodeTextFile("test.html", "text/plain", plainContent) +
encodeTextFile("test1.html", "text/html", htmlContent) +
encodeTextFile("test2.html", "text/html", htmlContent) +
encodeTextFile("test3.html", "text/html", htmlContent) +
endline +
"--" +
boundary +
"--" +
endline;
encodeTextFile("test1.html", "text/html", htmlContent) +
encodeTextFile("test2.html", "text/html", htmlContent) +
encodeTextFile("test3.html", "text/html", htmlContent) +
endline +
"--" +
boundary +
"--" +
endline;
req.setContent(content.getBytes());
assertTrue(JakartaServletFileUpload.isMultipartContent(req));
@@ -424,7 +424,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext()
.withServletRequest(createMultipartRequestMaxFileSize(req));
.withServletRequest(createMultipartRequestMaxFileSize(req));
interceptor.intercept(mai);
@@ -435,8 +435,8 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
String msg = errors.iterator().next();
// FIXME: the expected size is 40 - length of the string
assertEquals(
"File deleteme.txt assigned to file exceeded allowed size limit! Max size allowed is: 10 but file was: 10!",
msg);
"File deleteme.txt assigned to file exceeded allowed size limit! Max size allowed is: 10 but file was: 10!",
msg);
}
public void testMultipartRequestMaxStringLength() throws Exception {
@@ -471,7 +471,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext()
.withServletRequest(createMultipartRequestMaxStringLength(req));
.withServletRequest(createMultipartRequestMaxStringLength(req));
interceptor.intercept(mai);
@@ -481,8 +481,8 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
assertEquals(1, errors.size());
String msg = errors.iterator().next();
assertEquals(
"The request parameter \"normalFormField2\" was too long. Max length allowed is 20, but found 27!",
msg);
"The request parameter \"normalFormField2\" was too long. Max length allowed is 20, but found 27!",
msg);
}
public void testMultipartRequestLocalizedError() throws Exception {
@@ -509,8 +509,8 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext()
.withLocale(Locale.GERMAN)
.withServletRequest(createMultipartRequestMaxSize(req, 10));
.withLocale(Locale.GERMAN)
.withServletRequest(createMultipartRequestMaxSize(req, 10));
interceptor.intercept(mai);
@@ -525,19 +525,19 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
private String encodeTextFile(String filename, String contentType, String content) {
return "\r\n" +
"--" +
"simple boundary" +
"\r\n" +
"Content-Disposition: form-data; name=\"" +
"file" +
"\"; filename=\"" +
filename +
"\r\n" +
"Content-Type: " +
contentType +
"\r\n" +
"\r\n" +
content;
"--" +
"simple boundary" +
"\r\n" +
"Content-Disposition: form-data; name=\"" +
"file" +
"\"; filename=\"" +
filename +
"\r\n" +
"Content-Type: " +
contentType +
"\r\n" +
"\r\n" +
content;
}
private MultiPartRequestWrapper createMultipartRequestMaxFileSize(HttpServletRequest req) {
@@ -18,8 +18,24 @@
*/
package org.apache.struts2.interceptor;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.DefaultLocaleProvider;
import com.opensymphony.xwork2.ValidationAwareSupport;
import com.opensymphony.xwork2.mock.MockActionInvocation;
import com.opensymphony.xwork2.util.ClassLoaderUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.dispatcher.HttpParameters;
import org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest;
import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
import org.apache.struts2.dispatcher.multipart.StrutsUploadedFile;
import org.apache.struts2.dispatcher.multipart.UploadedFile;
import org.springframework.mock.web.MockHttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
@@ -29,26 +45,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.TestAction;
import org.apache.struts2.dispatcher.HttpParameters;
import org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest;
import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
import org.apache.struts2.dispatcher.multipart.StrutsUploadedFile;
import org.apache.struts2.dispatcher.multipart.UploadedFile;
import org.springframework.mock.web.MockHttpServletRequest;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.DefaultLocaleProvider;
import com.opensymphony.xwork2.ValidationAwareSupport;
import com.opensymphony.xwork2.mock.MockActionInvocation;
import com.opensymphony.xwork2.util.ClassLoaderUtil;
import jakarta.servlet.http.HttpServletRequest;
import static org.assertj.core.api.Assertions.assertThat;
/**
@@ -221,10 +217,10 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
String msg = errors.get(0);
// the error message should contain at least this test
assertThat(msg).contains(
"The file is too large to be uploaded",
"inputName",
"log4j2.xml",
"allowed mx size is 10"
"The file is too large to be uploaded",
"inputName",
"log4j2.xml",
"allowed mx size is 10"
);
}
@@ -343,15 +339,15 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
req.setMethod("POST");
req.addHeader("Content-type", "multipart/form-data; boundary=" + bondary);
String content = encodeTextFile("test.html", "text/plain", plainContent) +
encodeTextFile("test1.html", "text/html", htmlContent) +
encodeTextFile("test2.html", "text/html", htmlContent) +
endline +
endline +
endline +
"--" +
bondary +
"--" +
endline;
encodeTextFile("test1.html", "text/html", htmlContent) +
encodeTextFile("test2.html", "text/html", htmlContent) +
endline +
endline +
endline +
"--" +
bondary +
"--" +
endline;
req.setContent(content.getBytes());
assertTrue(JakartaServletFileUpload.isMultipartContent(req));
@@ -396,14 +392,14 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
req.setMethod("POST");
req.addHeader("Content-type", "multipart/form-data; boundary=" + boundary);
String content = encodeTextFile("test.html", "text/plain", plainContent) +
encodeTextFile("test1.html", "text/html", htmlContent) +
encodeTextFile("test2.html", "text/html", htmlContent) +
encodeTextFile("test3.html", "text/html", htmlContent) +
endline +
"--" +
boundary +
"--" +
endline;
encodeTextFile("test1.html", "text/html", htmlContent) +
encodeTextFile("test2.html", "text/html", htmlContent) +
encodeTextFile("test3.html", "text/html", htmlContent) +
endline +
"--" +
boundary +
"--" +
endline;
req.setContent(content.getBytes());
assertTrue(JakartaServletFileUpload.isMultipartContent(req));
@@ -543,9 +539,9 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
mai.setInvocationContext(ActionContext.getContext());
Map<String, Object> param = new HashMap<>();
ActionContext.getContext()
.withParameters(HttpParameters.create(param).build())
.withLocale(Locale.GERMAN)
.withServletRequest(createMultipartRequestMaxSize(req, 10));
.withParameters(HttpParameters.create(param).build())
.withLocale(Locale.GERMAN)
.withServletRequest(createMultipartRequestMaxSize(req, 10));
interceptor.intercept(mai);
@@ -560,19 +556,19 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
private String encodeTextFile(String filename, String contentType, String content) {
return "\r\n" +
"--" +
"simple boundary" +
"\r\n" +
"Content-Disposition: form-data; name=\"" +
"file" +
"\"; filename=\"" +
filename +
"\r\n" +
"Content-Type: " +
contentType +
"\r\n" +
"\r\n" +
content;
"--" +
"simple boundary" +
"\r\n" +
"Content-Disposition: form-data; name=\"" +
"file" +
"\"; filename=\"" +
filename +
"\r\n" +
"Content-Type: " +
contentType +
"\r\n" +
"\r\n" +
content;
}
private MultiPartRequestWrapper createMultipartRequestMaxFileSize(HttpServletRequest req) {
+2 -2
View File
@@ -823,8 +823,8 @@
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-fileupload2-jakarta</artifactId>
<version>2.0.0-M1</version>
<artifactId>commons-fileupload2-jakarta-servlet6</artifactId>
<version>2.0.0-M2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>