Merge pull request #861 from apache/feature/WW-5388-upload-servlet6

[WW-5388] Uses the latest JakartaEE FileUpload Servlet 6 package
This commit is contained in:
Lukasz Lenart
2024-02-01 09:23:26 +01:00
committed by GitHub
25 changed files with 1308 additions and 985 deletions
@@ -24,6 +24,7 @@ import com.opensymphony.xwork2.ActionSupport;
import org.apache.struts2.action.UploadedFilesAware;
import org.apache.struts2.dispatcher.multipart.UploadedFile;
import java.io.File;
import java.util.List;
/**
@@ -32,7 +33,7 @@ import java.util.List;
public class FileUploadAction extends ActionSupport implements UploadedFilesAware {
private String contentType;
private UploadedFile uploadedFile;
private UploadedFile<File> uploadedFile;
private String fileName;
private String caption;
private String originalName;
@@ -78,7 +79,7 @@ public class FileUploadAction extends ActionSupport implements UploadedFilesAwar
}
@Override
public void withUploadedFiles(List<UploadedFile> uploadedFiles) {
public void withUploadedFiles(List<UploadedFile<File>> uploadedFiles) {
this.uploadedFile = uploadedFiles.get(0);
this.fileName = uploadedFile.getName();
this.contentType = uploadedFile.getContentType();
@@ -24,6 +24,9 @@
"https://struts.apache.org/dtds/struts-6.0.dtd">
<struts>
<constant name="struts.multipart.maxSize" value="10240" />
<package name="fileupload" extends="struts-default" namespace="/fileupload">
<action name="upload" class="org.apache.struts2.showcase.fileupload.FileUploadAction" method="input">
@@ -41,7 +41,8 @@
<li>FileName: <s:property value="fileName" /></li>
<li>Original FileName: <s:property value="originalName" /></li>
<li>File: <s:property value="uploadedFile" /></li>
<li>Caption:<s:property value="caption" /></li>
<li>Size: <s:property value="uploadSize" /></li>
<li>Caption: <s:property value="caption" /></li>
</ul>
</div>
</div>
@@ -18,9 +18,6 @@
*/
package it.org.apache.struts2.showcase;
import java.io.File;
import java.io.FileWriter;
import org.htmlunit.WebClient;
import org.htmlunit.html.HtmlFileInput;
import org.htmlunit.html.HtmlForm;
@@ -29,6 +26,10 @@ import org.htmlunit.html.HtmlPage;
import org.htmlunit.html.HtmlSubmitInput;
import org.junit.Test;
import java.io.File;
import java.io.FileWriter;
import java.security.SecureRandom;
import static org.assertj.core.api.Assertions.assertThat;
public class FileUploadTest {
@@ -46,6 +47,42 @@ public class FileUploadTest {
try (FileWriter writer = new FileWriter(tempFile)) {
writer.append("Some strings");
writer.flush();
}
uploadInput.setValue(tempFile.getAbsolutePath());
final HtmlSubmitInput button = form.getInputByValue("Submit");
final HtmlPage resultPage = button.click();
String content = resultPage.getVisibleText();
assertThat(content).contains(
"ContentType: text/plain",
"Original FileName: " + tempFile.getName(),
"Caption: some caption",
"Size: 12"
);
}
}
@Test
public void testUploadOverMaxSize() throws Exception {
try (final WebClient webClient = new WebClient()) {
final HtmlPage page = webClient.getPage(ParameterUtils.getBaseUrl() + "/fileupload/doUpload.action");
final HtmlForm form = page.getFormByName("doUpload");
HtmlInput captionInput = form.getInputByName("caption");
HtmlFileInput uploadInput = form.getInputByName("upload");
captionInput.type("Large file");
File tempFile = File.createTempFile("testEmptyFile", ".txt");
SecureRandom rng = new SecureRandom();
tempFile.deleteOnExit();
try (FileWriter writer = new FileWriter(tempFile)) {
for (int i = 0; i < 10240; ++i) {
String line = String.format("%s %s%n", rng.nextInt(), rng.nextInt());
writer.append(line);
}
writer.flush();
}
uploadInput.setValue(tempFile.getAbsolutePath());
@@ -55,9 +92,7 @@ public class FileUploadTest {
String content = resultPage.getVisibleText();
System.out.println(content);
assertThat(content).contains(
"ContentType: text/plain",
"Original FileName: " + tempFile.getName(),
"Caption:some caption"
"Request exceeded allowed size limit! Max size allowed is: 10,240!"
);
}
}
+3 -3
View File
@@ -19,7 +19,8 @@
* under the License.
*/
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.struts</groupId>
@@ -216,8 +217,7 @@
<!-- 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>
</dependency>
<dependency>
<groupId>commons-io</groupId>
@@ -140,21 +140,28 @@ public final class StrutsConstants {
public static final String STRUTS_UI_ESCAPE_HTML_BODY = "struts.ui.escapeHtmlBody";
/** The maximum size of a multipart request (file upload) */
public static final String STRUTS_MULTIPART_MAXSIZE = "struts.multipart.maxSize";
public static final String STRUTS_MULTIPART_MAX_SIZE = "struts.multipart.maxSize";
/** The maximum size of all uploaded files.
Used only with {@link org.apache.struts2.dispatcher.multipart.JakartaStreamMultiPartRequest} */
public static final String STRUTS_MULTIPART_MAX_SIZE_OF_FILES = "struts.multipart.maxSizeOfFiles";
/** The maximum number of files allowed in a multipart request */
public static final String STRUTS_MULTIPART_MAXFILES = "struts.multipart.maxFiles";
public static final String STRUTS_MULTIPART_MAX_FILES = "struts.multipart.maxFiles";
/** The maximum length of a string parameter in a multipart request. */
public static final String STRUTS_MULTIPART_MAX_STRING_LENGTH = "struts.multipart.maxStringLength";
/** The maximum size per file in a multipart request */
public static final String STRUTS_MULTIPART_MAXFILESIZE = "struts.multipart.maxFileSize";
/** The directory to use for storing uploaded files */
public static final String STRUTS_MULTIPART_SAVEDIR = "struts.multipart.saveDir";
public static final String STRUTS_MULTIPART_MAX_FILE_SIZE = "struts.multipart.maxFileSize";
/** Declares the buffer size to be used during streaming multipart content to disk. Used only with {@link org.apache.struts2.dispatcher.multipart.JakartaStreamMultiPartRequest} */
public static final String STRUTS_MULTIPART_BUFFERSIZE = "struts.multipart.bufferSize";
/** The directory to use for storing uploaded files */
public static final String STRUTS_MULTIPART_SAVE_DIR = "struts.multipart.saveDir";
/** Declares the buffer size to be used during streaming multipart content to disk.
* Used only with {@link org.apache.struts2.dispatcher.multipart.JakartaStreamMultiPartRequest}
*/
public static final String STRUTS_MULTIPART_BUFFER_SIZE = "struts.multipart.bufferSize";
/**
* The org.apache.struts2.dispatcher.multipart.MultiPartRequest parser implementation
@@ -20,6 +20,7 @@ package org.apache.struts2.action;
import org.apache.struts2.dispatcher.multipart.UploadedFile;
import java.io.File;
import java.util.List;
/**
@@ -35,6 +36,6 @@ public interface UploadedFilesAware {
*
* @param uploadedFiles a list of {@link UploadedFile}, cannot be null. It can be empty.
*/
void withUploadedFiles(List<UploadedFile> uploadedFiles);
void withUploadedFiles(List<UploadedFile<File>> uploadedFiles);
}
@@ -199,12 +199,12 @@ public class ConstantConfig {
map.put(StrutsConstants.STRUTS_UI_TEMPLATEDIR, uiTemplateDir);
map.put(StrutsConstants.STRUTS_UI_THEME, uiTheme);
map.put(StrutsConstants.STRUTS_UI_THEME_EXPANSION_TOKEN, uiThemeExpansionToken);
map.put(StrutsConstants.STRUTS_MULTIPART_MAXSIZE, Objects.toString(multipartMaxSize, null));
map.put(StrutsConstants.STRUTS_MULTIPART_MAXFILES, Objects.toString(multipartMaxFiles, null));
map.put(StrutsConstants.STRUTS_MULTIPART_MAXFILESIZE, Objects.toString(multipartMaxFileSize, null));
map.put(StrutsConstants.STRUTS_MULTIPART_MAX_SIZE, Objects.toString(multipartMaxSize, null));
map.put(StrutsConstants.STRUTS_MULTIPART_MAX_FILES, Objects.toString(multipartMaxFiles, null));
map.put(StrutsConstants.STRUTS_MULTIPART_MAX_FILE_SIZE, Objects.toString(multipartMaxFileSize, null));
map.put(StrutsConstants.STRUTS_MULTIPART_MAX_STRING_LENGTH, Objects.toString(multipartMaxStringLength, null));
map.put(StrutsConstants.STRUTS_MULTIPART_SAVEDIR, multipartSaveDir);
map.put(StrutsConstants.STRUTS_MULTIPART_BUFFERSIZE, Objects.toString(multipartBufferSize, null));
map.put(StrutsConstants.STRUTS_MULTIPART_SAVE_DIR, multipartSaveDir);
map.put(StrutsConstants.STRUTS_MULTIPART_BUFFER_SIZE, Objects.toString(multipartBufferSize, null));
map.put(StrutsConstants.STRUTS_MULTIPART_PARSER, beanConfToString(multipartParser));
map.put(StrutsConstants.STRUTS_MULTIPART_ENABLED, Objects.toString(multipartEnabled, null));
map.put(StrutsConstants.STRUTS_MULTIPART_VALIDATION_REGEX, Objects.toString(multipartValidationRegex, null));
@@ -147,7 +147,7 @@ public class Dispatcher {
private String defaultLocale;
/**
* Store state of StrutsConstants.STRUTS_MULTIPART_SAVEDIR setting.
* Store state of {@link StrutsConstants#STRUTS_MULTIPART_SAVE_DIR} setting.
*/
private String multipartSaveDir;
@@ -315,7 +315,7 @@ public class Dispatcher {
*
* @param val New setting
*/
@Inject(StrutsConstants.STRUTS_MULTIPART_SAVEDIR)
@Inject(StrutsConstants.STRUTS_MULTIPART_SAVE_DIR)
public void setMultipartSaveDir(String val) {
multipartSaveDir = val;
}
@@ -323,7 +323,7 @@ public class Dispatcher {
/**
* @deprecated since 6.4.0, no replacement.
*/
@Deprecated(since = "6.4.9", forRemoval = true)
@Deprecated(since = "6.4.0", forRemoval = true)
public void setMultipartHandler(String val) {
// no-op
}
@@ -868,11 +868,12 @@ public class Dispatcher {
* @return the path to save uploaded files to
*/
protected String getSaveDir() {
String saveDir = multipartSaveDir.trim();
String saveDir = Objects.toString(multipartSaveDir, "").trim();
if (saveDir.equals("")) {
File tempdir = (File) servletContext.getAttribute("jakarta.servlet.context.tempdir");
LOG.info("Unable to find 'struts.multipart.saveDir' property setting. Defaulting to jakarta.servlet.context.tempdir");
if (saveDir.isEmpty()) {
File tempdir = (File) servletContext.getAttribute(ServletContext.TEMPDIR);
LOG.info("Unable to find: {} property setting. Defaulting to: {}",
StrutsConstants.STRUTS_MULTIPART_SAVE_DIR, ServletContext.TEMPDIR);
if (tempdir != null) {
saveDir = tempdir.toString();
@@ -885,9 +886,9 @@ public class Dispatcher {
if (!multipartSaveDir.mkdirs()) {
String logMessage;
try {
logMessage = "Could not find create multipart save directory '" + multipartSaveDir.getCanonicalPath() + "'.";
logMessage = "Could not create multipart save directory '" + multipartSaveDir.getCanonicalPath() + "'.";
} catch (IOException e) {
logMessage = "Could not find create multipart save directory '" + multipartSaveDir.toString() + "'.";
logMessage = "Could not create multipart save directory '" + multipartSaveDir + "'.";
}
if (devMode) {
LOG.error(logMessage);
@@ -18,23 +18,35 @@
*/
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.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.jakarta.servlet6.JakartaServletDiskFileUpload;
import org.apache.commons.lang3.StringUtils;
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.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Abstract class with some helper methods, it should be used
* when starting development of another implementation of {@link MultiPartRequest}
*/
public abstract class AbstractMultiPartRequest implements MultiPartRequest {
public abstract class AbstractMultiPartRequest<T> implements MultiPartRequest {
protected static final String STRUTS_MESSAGES_UPLOAD_ERROR_PARAMETER_TOO_LONG_KEY = "struts.messages.upload.error.parameter.too.long";
@@ -55,6 +67,11 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
*/
protected Long maxSize;
/**
* Specifies the maximum size of all the uploaded files.
*/
protected Long maxSizeOfFiles;
/**
* Specifies the maximum number of files in one request.
*/
@@ -75,17 +92,25 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
*/
protected int bufferSize = BUFFER_SIZE;
/**
* Defines default encoding to encode data from request used if not provided with request
*/
protected String defaultEncoding;
/**
* Localization to be used regarding errors.
* Map between file fields and file data.
*/
protected Locale defaultLocale = Locale.ENGLISH;
protected Map<String, List<UploadedFile<T>>> uploadedFiles = new HashMap<>();
/**
* Map between non-file fields and values.
*/
protected Map<String, List<String>> parameters = new HashMap<>();
/**
* @param bufferSize Sets the buffer size to be used.
*/
@Inject(value = StrutsConstants.STRUTS_MULTIPART_BUFFERSIZE, required = false)
@Inject(value = StrutsConstants.STRUTS_MULTIPART_BUFFER_SIZE, required = false)
public void setBufferSize(String bufferSize) {
this.bufferSize = Integer.parseInt(bufferSize);
}
@@ -98,45 +123,154 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
/**
* @param maxSize Injects the Struts multipart request maximum size.
*/
@Inject(StrutsConstants.STRUTS_MULTIPART_MAXSIZE)
@Inject(StrutsConstants.STRUTS_MULTIPART_MAX_SIZE)
public void setMaxSize(String maxSize) {
this.maxSize = Long.parseLong(maxSize);
}
@Inject(StrutsConstants.STRUTS_MULTIPART_MAXFILES)
/**
* @param maxSizeOfFiles Injects the Struts maximum size of all uploaded files.
*/
@Inject(value = StrutsConstants.STRUTS_MULTIPART_MAX_SIZE_OF_FILES, required = false)
public void setMaxSizeOfFiles(String maxSizeOfFiles) {
this.maxSizeOfFiles = Long.parseLong(maxSizeOfFiles);
}
/**
* @param maxFiles Injects the Struts maximum size of an individual file uploaded.
*/
@Inject(StrutsConstants.STRUTS_MULTIPART_MAX_FILES)
public void setMaxFiles(String maxFiles) {
this.maxFiles = Long.parseLong(maxFiles);
}
@Inject(value = StrutsConstants.STRUTS_MULTIPART_MAXFILESIZE, required = false)
/**
* @param maxFileSize Injects the Struts maximum number of files, which can be uploaded.
*/
@Inject(value = StrutsConstants.STRUTS_MULTIPART_MAX_FILE_SIZE, required = false)
public void setMaxFileSize(String maxFileSize) {
this.maxFileSize = Long.parseLong(maxFileSize);
}
/**
* @param maxStringLength Injects the Struts maximum size of single form field.
*/
@Inject(StrutsConstants.STRUTS_MULTIPART_MAX_STRING_LENGTH)
public void setMaxStringLength(String maxStringLength) {
this.maxStringLength = Long.parseLong(maxStringLength);
}
@Inject
public void setLocaleProviderFactory(LocaleProviderFactory localeProviderFactory) {
defaultLocale = localeProviderFactory.createLocaleProvider().getLocale();
/**
* Process the request extract file upload data
*
* @param request current {@link HttpServletRequest}
* @param saveDir a temporary directory to store files
*/
protected abstract void processUpload(HttpServletRequest request, String saveDir) throws IOException;
/**
* @param request multipart request
* @return character encoding from request or {@link #defaultEncoding}
*/
protected Charset readCharsetEncoding(HttpServletRequest request) {
String charsetStr = StringUtils.isBlank(request.getCharacterEncoding())
? defaultEncoding
: request.getCharacterEncoding();
return Charset.forName(charsetStr);
}
/**
* @param request Inspect the servlet request and set the locale if one wasn't provided by
* the Struts2 framework.
* Creates an instance of {@link JakartaServletDiskFileUpload} used by the parser to extract uploaded files
*
* @param charset used charset from incoming request
* @param saveDir a temporary folder to store uploaded files (not always needed)
*/
protected void setLocale(HttpServletRequest request) {
if (defaultLocale == null) {
defaultLocale = request.getLocale();
protected abstract JakartaServletDiskFileUpload createJakartaFileUpload(Charset charset, Path saveDir);
protected JakartaServletDiskFileUpload prepareServletFileUpload(Charset charset, Path saveDir) {
JakartaServletDiskFileUpload servletFileUpload = createJakartaFileUpload(charset, saveDir);
if (maxSize != null) {
LOG.debug("Applies max size: {} to file upload request", maxSize);
servletFileUpload.setSizeMax(maxSize);
}
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);
}
return servletFileUpload;
}
protected boolean exceedsMaxStringLength(String fieldName, String fieldValue) {
if (maxStringLength != null && fieldValue.length() > maxStringLength) {
if (LOG.isDebugEnabled()) {
LOG.debug("Form field: {} of size: {} bytes exceeds limit of: {}.",
sanitizeNewlines(fieldName), fieldValue.length(), maxStringLength);
}
LocalizedMessage localizedMessage = new LocalizedMessage(this.getClass(),
STRUTS_MESSAGES_UPLOAD_ERROR_PARAMETER_TOO_LONG_KEY, null,
new Object[]{fieldName, maxStringLength, fieldValue.length()});
if (!errors.contains(localizedMessage)) {
errors.add(localizedMessage);
}
return true;
}
return false;
}
/**
* Processes the upload.
*
* @param request the servlet request
* @param saveDir location of the save dir
*/
public void parse(HttpServletRequest request, String saveDir) throws IOException {
try {
processUpload(request, saveDir);
} catch (FileUploadException e) {
LOG.debug("Request exceeded size limit!", e);
LocalizedMessage errorMessage;
if (e instanceof FileUploadByteCountLimitException ex) {
errorMessage = buildErrorMessage(e, new Object[]{
ex.getFieldName(), ex.getFileName(), ex.getPermitted(), ex.getActualSize()
});
} else if (e instanceof FileUploadFileCountLimitException ex) {
errorMessage = buildErrorMessage(e, new Object[]{
ex.getPermitted(), ex.getActualSize()
});
} else if (e instanceof FileUploadSizeException ex) {
errorMessage = buildErrorMessage(e, new Object[]{
ex.getPermitted(), ex.getActualSize()
});
} else if (e instanceof FileUploadContentTypeException ex) {
errorMessage = buildErrorMessage(e, new Object[]{
ex.getContentType()
});
} else {
errorMessage = buildErrorMessage(e, new Object[]{});
}
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
} catch (IOException e) {
LOG.debug("Unable to parse request", e);
LocalizedMessage errorMessage = buildErrorMessage(e, new Object[]{});
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
}
}
/**
* Build error message.
*
* @param e the Throwable/Exception
* @param e the Throwable/Exception
* @param args arguments
* @return error message
*/
@@ -147,13 +281,6 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
return new LocalizedMessage(this.getClass(), errorKey, e.getMessage(), args);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getErrors()
*/
public List<LocalizedMessage> getErrors() {
return errors;
}
/**
* @param originalFileName file name
* @return the canonical name based on the supplied filename
@@ -171,4 +298,109 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
return fileName;
}
protected String sanitizeNewlines(String before) {
return before.replaceAll("\\R", "_");
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getErrors()
*/
public List<LocalizedMessage> getErrors() {
return errors;
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames()
*/
public Enumeration<String> getFileParameterNames() {
return Collections.enumeration(uploadedFiles.keySet());
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getContentType(java.lang.String)
*/
public String[] getContentType(String fieldName) {
return uploadedFiles.getOrDefault(fieldName, Collections.emptyList()).stream()
.map(UploadedFile::getContentType)
.toArray(String[]::new);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFile(java.lang.String)
*/
@SuppressWarnings("unchecked")
public UploadedFile<T>[] getFile(String fieldName) {
return uploadedFiles.getOrDefault(fieldName, Collections.emptyList())
.toArray(UploadedFile[]::new);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileNames(java.lang.String)
*/
public String[] getFileNames(String fieldName) {
return uploadedFiles.getOrDefault(fieldName, Collections.emptyList()).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) {
return uploadedFiles.getOrDefault(fieldName, Collections.emptyList()).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> paramValue = parameters.getOrDefault(name, Collections.emptyList());
if (!paramValue.isEmpty()) {
return paramValue.get(0);
}
return null;
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterNames()
*/
public Enumeration<String> getParameterNames() {
return Collections.enumeration(parameters.keySet());
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterValues(java.lang.String)
*/
public String[] getParameterValues(String name) {
return parameters.getOrDefault(name, Collections.emptyList())
.toArray(String[]::new);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp()
*/
public void cleanUp() {
try {
LOG.debug("Performing File Upload temporary storage cleanup.");
for (List<UploadedFile<T>> uploadedFileList : uploadedFiles.values()) {
for (UploadedFile<T> uploadedFile : uploadedFileList) {
if (uploadedFile.isFile()) {
LOG.debug("Deleting file: {}", uploadedFile.getName());
if (!uploadedFile.delete()) {
LOG.warn("There was a problem attempting to delete file: {}", uploadedFile.getName());
}
} else {
LOG.debug("File: {} already deleted", uploadedFile.getName());
}
}
}
} finally {
uploadedFiles = new HashMap<>();
parameters = new HashMap<>();
}
}
}
@@ -21,375 +21,108 @@ package org.apache.struts2.dispatcher.multipart;
import jakarta.servlet.http.HttpServletRequest;
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.JakartaServletDiskFileUpload;
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.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Multipart form data request adapter for Jakarta Commons Fileupload package.
* Multipart form data request adapter for Jakarta Commons FileUpload package.
*/
public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
public class JakartaMultiPartRequest extends AbstractMultiPartRequest<File> {
static final Logger LOG = LogManager.getLogger(JakartaMultiPartRequest.class);
// maps parameter name -> List of FileItem objects
protected Map<String, List<FileItem>> files = new HashMap<>();
// maps parameter name -> List of param values
protected Map<String, List<String>> params = new HashMap<>();
/**
* Creates a new request wrapper to handle multipart data using methods adapted from Jason Pell's
* multipart classes (see class description).
*
* @param saveDir the directory to save off the file
* @param request the request containing the multipart
* @throws java.io.IOException is thrown if encoding fails.
*/
public void parse(HttpServletRequest request, String saveDir) throws IOException {
try {
setLocale(request);
processUpload(request, saveDir);
} catch (FileUploadException e) {
LOG.debug("Request exceeded size limit!", e);
LocalizedMessage errorMessage;
if (e instanceof FileUploadByteCountLimitException) {
FileUploadByteCountLimitException ex = (FileUploadByteCountLimitException) e;
errorMessage = buildErrorMessage(e, new Object[]{
ex.getFieldName(), ex.getFileName(), ex.getPermitted(), ex.getActualSize()
});
} else if (e instanceof FileUploadFileCountLimitException) {
FileUploadFileCountLimitException ex = (FileUploadFileCountLimitException) e;
errorMessage = buildErrorMessage(e, new Object[]{
ex.getPermitted(), ex.getActualSize()
});
} else if (e instanceof FileUploadSizeException) {
FileUploadSizeException ex = (FileUploadSizeException) e;
errorMessage = buildErrorMessage(e, new Object[]{
ex.getPermitted(), ex.getActualSize()
});
} else if (e instanceof FileUploadContentTypeException) {
FileUploadContentTypeException ex = (FileUploadContentTypeException) e;
errorMessage = buildErrorMessage(e, new Object[]{
ex.getContentType()
});
} else {
errorMessage = buildErrorMessage(e, new Object[]{});
}
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
} catch (Exception e) {
LOG.debug("Unable to parse request", e);
LocalizedMessage errorMessage = buildErrorMessage(e, new Object[]{});
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
}
}
private static final Logger LOG = LogManager.getLogger(JakartaMultiPartRequest.class);
@Override
protected void processUpload(HttpServletRequest request, String saveDir) throws IOException {
Charset charset = readCharsetEncoding(request);
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);
}
JakartaServletDiskFileUpload servletFileUpload =
prepareServletFileUpload(charset, Path.of(saveDir));
for (DiskFileItem item : servletFileUpload.parseRequest(request)) {
LOG.debug(() -> "Processing a form field: " + sanitizeNewlines(item.getFieldName()));
if (item.isFormField()) {
processNormalFormField(item, charset);
} else {
LOG.debug(() -> "Processing a file: " + sanitizeNewlines(item.getFieldName()));
processFileField(item);
}
}
}
protected void processFileField(FileItem item) {
LOG.debug("Item is a file upload");
protected JakartaServletDiskFileUpload createJakartaFileUpload(Charset charset, Path saveDir) {
DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();
// 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;
}
LOG.debug("Using file save directory: {}", saveDir);
builder.setPath(saveDir);
List<FileItem> values;
if (files.get(item.getFieldName()) != null) {
values = files.get(item.getFieldName());
LOG.debug("Sets minimal buffer size to always write file to disk");
builder.setBufferSize(1);
LOG.debug("Using charset: {}", charset);
builder.setCharset(charset);
DiskFileItemFactory factory = builder.get();
return new JakartaServletDiskFileUpload(factory);
}
protected void processNormalFormField(DiskFileItem item, Charset charset) throws IOException {
LOG.debug("Item: {} is a normal form field", item.getName());
List<String> values;
String fieldName = item.getFieldName();
if (parameters.get(fieldName) != null) {
values = parameters.get(fieldName);
} else {
values = new ArrayList<>();
}
values.add(item);
files.put(item.getFieldName(), values);
String fieldValue = item.getString(charset);
if (exceedsMaxStringLength(fieldName, fieldValue)) {
return;
}
if (item.getSize() == 0) {
values.add(StringUtils.EMPTY);
} else {
values.add(fieldValue);
}
parameters.put(fieldName, values);
}
protected void processNormalFormField(FileItem item, String charset) throws IOException {
try {
LOG.debug("Item is a normal form field");
Charset encoding = Charset.forName(charset);
List<String> values;
if (params.get(item.getFieldName()) != null) {
values = params.get(item.getFieldName());
} else {
values = new ArrayList<>();
}
long size = item.getSize();
if (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});
if (!errors.contains(localizedMessage)) {
errors.add(localizedMessage);
}
return;
}
if (size == 0) {
values.add(StringUtils.EMPTY);
} else if (charset == null) {
values.add(item.getString()); // WW-633
} else {
values.add(item.getString(encoding));
}
params.put(item.getFieldName(), values);
} finally {
item.delete();
}
}
protected List<FileItem> parseRequest(HttpServletRequest servletRequest, String saveDir) throws FileUploadException {
DiskFileItemFactory fac = createDiskFileItemFactory(saveDir);
JakartaServletFileUpload upload = createServletFileUpload(fac);
return upload.parseRequest(createRequestContext(servletRequest));
}
protected JakartaServletFileUpload createServletFileUpload(DiskFileItemFactory fac) {
JakartaServletFileUpload upload = new JakartaServletFileUpload(fac);
if (maxSize != null) {
upload.setSizeMax(maxSize);
}
if (maxFiles != null) {
upload.setFileCountMax(maxFiles);
}
if (maxFileSize != null) {
upload.setFileSizeMax(maxFileSize);
}
return upload;
}
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);
if (saveDir != null) {
fac.setPath(saveDir);
}
return fac.get();
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames()
*/
public Enumeration<String> getFileParameterNames() {
return Collections.enumeration(files.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);
if (items == null) {
return null;
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<String> contentTypes = new ArrayList<>(items.size());
for (FileItem fileItem : items) {
contentTypes.add(fileItem.getContentType());
List<UploadedFile<File>> values;
if (uploadedFiles.get(item.getFieldName()) != null) {
values = uploadedFiles.get(item.getFieldName());
} else {
values = new ArrayList<>();
}
return contentTypes.toArray(new String[0]);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFile(java.lang.String)
*/
public UploadedFile[] getFile(String fieldName) {
List<FileItem> items = files.get(fieldName);
if (items == 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())
if (item.isInMemory()) {
LOG.warn("Storing uploaded files just in memory isn't supported currently, skipping file: {}!", item.getName());
} else {
UploadedFile<File> uploadedFile = StrutsUploadedFile.Builder
.create(item.getPath().toFile())
.withOriginalName(item.getName())
.withContentType(item.getContentType())
.build();
fileList.add(uploadedFile);
values.add(uploadedFile);
}
return fileList.toArray(new UploadedFile[0]);
uploadedFiles.put(item.getFieldName(), values);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileNames(java.lang.String)
*/
public String[] getFileNames(String fieldName) {
List<FileItem> items = files.get(fieldName);
if (items == 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]);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFilesystemName(java.lang.String)
*/
public String[] getFilesystemName(String fieldName) {
List<FileItem> items = files.get(fieldName);
if (items == 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]);
}
/* (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);
}
return null;
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterNames()
*/
public Enumeration<String> getParameterNames() {
return Collections.enumeration(params.keySet());
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterValues(java.lang.String)
*/
public String[] getParameterValues(String name) {
List<String> v = params.get(name);
if (v != null && !v.isEmpty()) {
return v.toArray(new String[0]);
}
return null;
}
/**
* Creates a RequestContext needed by Jakarta Commons Upload.
*
* @param req the request.
* @return a new request context.
*/
protected RequestContext createRequestContext(final HttpServletRequest req) {
return new RequestContext() {
public String getCharacterEncoding() {
return req.getCharacterEncoding();
}
public String getContentType() {
return req.getContentType();
}
public long getContentLength() {
return req.getContentLength();
}
public InputStream getInputStream() throws IOException {
InputStream in = req.getInputStream();
if (in == null) {
throw new IOException("Missing content in the request");
}
return req.getInputStream();
}
};
}
/* (non-Javadoc)
* @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);
}
}
}
}
}
private String sanitizeNewlines(String before) {
return before.replaceAll("[\n\r]", "_");
}
}
@@ -18,30 +18,28 @@
*/
package org.apache.struts2.dispatcher.multipart;
import org.apache.commons.fileupload2.core.DiskFileItem;
import jakarta.servlet.http.HttpServletRequest;
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.core.FileUploadFileCountLimitException;
import org.apache.commons.fileupload2.core.FileUploadSizeException;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.dispatcher.LocalizedMessage;
import jakarta.servlet.http.HttpServletRequest;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
@@ -52,155 +50,9 @@ import java.util.UUID;
*
* @since 2.3.18
*/
public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest<File> {
static final Logger LOG = LogManager.getLogger(JakartaStreamMultiPartRequest.class);
/**
* Map between file fields and file data.
*/
protected Map<String, List<FileInfo>> fileInfos = new HashMap<>();
/**
* Map between non-file fields and values.
*/
protected Map<String, List<String>> parameters = new HashMap<>();
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp()
*/
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());
}
}
}
}
/* (non-Javadoc)
* @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) {
return null;
}
List<String> types = new ArrayList<>(infos.size());
for (FileInfo fileInfo : infos) {
types.add(fileInfo.getContentType());
}
return types.toArray(new String[0]);
}
/* (non-Javadoc)
* @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) {
return null;
}
return infos.stream().map(fileInfo ->
StrutsUploadedFile.Builder.create(fileInfo.getFile())
.withContentType(fileInfo.contentType)
.withOriginalName(fileInfo.originalName)
.build()
).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) {
return null;
}
List<String> names = new ArrayList<>(infos.size());
for (FileInfo fileInfo : infos) {
names.add(getCanonicalName(fileInfo.getOriginalName()));
}
return names.toArray(new String[0]);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getFileParameterNames()
*/
public Enumeration<String> getFileParameterNames() {
return Collections.enumeration(fileInfos.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) {
return null;
}
List<String> names = new ArrayList<>(infos.size());
for (FileInfo fileInfo : infos) {
names.add(fileInfo.getFile().getName());
}
return names.toArray(new String[0]);
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameter(java.lang.String)
*/
public String getParameter(String name) {
List<String> values = parameters.get(name);
if (values != null && !values.isEmpty()) {
return values.get(0);
}
return null;
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterNames()
*/
public Enumeration<String> getParameterNames() {
return Collections.enumeration(parameters.keySet());
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getParameterValues(java.lang.String)
*/
public String[] getParameterValues(String name) {
List<String> values = parameters.get(name);
if (values != null && !values.isEmpty()) {
return values.toArray(new String[0]);
}
return null;
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#parse(jakarta.servlet.http.HttpServletRequest, java.lang.String)
*/
public void parse(HttpServletRequest request, String saveDir) throws IOException {
try {
setLocale(request);
processUpload(request, saveDir);
} catch (Exception e) {
LOG.debug("Error occurred during parsing of multi part request", e);
LocalizedMessage errorMessage = buildErrorMessage(e, new Object[]{});
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
}
}
private static final Logger LOG = LogManager.getLogger(JakartaStreamMultiPartRequest.class);
/**
* Processes the upload.
@@ -208,153 +60,147 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
* @param request the servlet request
* @param saveDir location of the save dir
*/
protected void processUpload(HttpServletRequest request, String saveDir) throws Exception {
@Override
protected void processUpload(HttpServletRequest request, String saveDir) throws IOException {
Charset charset = readCharsetEncoding(request);
Path location = Path.of(saveDir);
// Sanity check that the request is a multi-part/form-data request.
if (JakartaServletFileUpload.isMultipartContent(request)) {
JakartaServletDiskFileUpload servletFileUpload =
prepareServletFileUpload(charset, location);
// 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);
}
}
}
}
/**
* 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;
}
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);
}
}
/**
* Processes the FileItemStream as a Form Field.
*
* @param itemStream file item stream
*/
protected void processFileItemStreamAsFormField(FileItemInput itemStream) {
String fieldName = itemStream.getFieldName();
try {
List<String> values;
String fieldValue = itemStream.getInputStream().toString();
if (!parameters.containsKey(fieldName)) {
values = new ArrayList<>();
parameters.put(fieldName, values);
LOG.debug("Using Jakarta Stream API to process request");
servletFileUpload.getItemIterator(request).forEachRemaining(item -> {
if (item.isFormField()) {
LOG.debug(() -> "Processing a form field: " + sanitizeNewlines(item.getFieldName()));
processFileItemAsFormField(item);
} else {
values = parameters.get(fieldName);
LOG.debug(() -> "Processing a file: " + sanitizeNewlines(item.getFieldName()));
processFileItemAsFileField(item, location);
}
values.add(fieldValue);
} catch (IOException e) {
LOG.warn("Failed to handle form field '{}'.", fieldName, e);
});
}
protected JakartaServletDiskFileUpload createJakartaFileUpload(Charset charset, Path location) {
DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();
LOG.debug("Using file save directory: {}", location);
builder.setPath(location);
LOG.debug("Sets buffer size: {}", bufferSize);
builder.setBufferSize(bufferSize);
LOG.debug("Using charset: {}", charset);
builder.setCharset(charset);
DiskFileItemFactory factory = builder.get();
return new JakartaServletDiskFileUpload(factory);
}
private String readStream(InputStream inputStream) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
for (int length; (length = inputStream.read(buffer)) != -1; ) {
result.write(buffer, 0, length);
}
return result.toString(StandardCharsets.UTF_8);
}
/**
* Processes the FileItemStream as a file field.
* Processes the FileItem as a normal form field.
*
* @param itemStream file item stream
* @param location location
* @param fileItemInput a form field item input
*/
protected void processFileItemStreamAsFileField(FileItemInput itemStream, 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());
protected void processFileItemAsFormField(FileItemInput fileItemInput) throws IOException {
String fieldName = fileItemInput.getFieldName();
String fieldValue = readStream(fileItemInput.getInputStream());
if (exceedsMaxStringLength(fieldName, fieldValue)) {
return;
}
File file = null;
try {
// Create the temporary upload file.
file = createTemporaryFile(itemStream.getName(), location);
List<String> values;
if (parameters.containsKey(fieldName)) {
values = parameters.get(fieldName);
} else {
values = new ArrayList<>();
parameters.put(fieldName, values);
}
values.add(fieldValue);
}
if (streamFileToDisk(itemStream, file)) {
createFileInfoFromItemStream(itemStream, file);
/**
* @return actual size of already uploaded files
*/
protected Long actualSizeOfUploadedFiles() {
return uploadedFiles.values().stream()
.map(files -> files.stream().map(UploadedFile::length).reduce(0L, Long::sum))
.reduce(0L, Long::sum);
}
private boolean exceedsMaxFiles(FileItemInput fileItemInput) {
if (maxFiles != null && maxFiles == uploadedFiles.size()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Cannot accept another file: {} as it will exceed max files: {}",
sanitizeNewlines(fileItemInput.getName()), maxFiles);
}
} catch (IOException e) {
if (file != null) {
try {
file.delete();
} catch (SecurityException se) {
LOG.warn("Failed to delete '{}' due to security exception above.", file.getName(), se);
}
LocalizedMessage errorMessage = buildErrorMessage(new FileUploadFileCountLimitException(
String.format("File %s exceeds allowed maximum number of files %s",
fileItemInput.getName(), maxFiles),
maxFiles, uploadedFiles.size()),
new Object[]{maxFiles, uploadedFiles.size()});
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
return true;
}
return false;
}
private void exceedsMaxSizeOfFiles(FileItemInput fileItemInput, File file, Long currentFilesSize) {
if (LOG.isDebugEnabled()) {
LOG.debug("File: {} of size: {} exceeds allowed max size: {}, actual size of already uploaded files: {}",
sanitizeNewlines(fileItemInput.getName()), file.length(), maxSizeOfFiles, currentFilesSize
);
}
LocalizedMessage errorMessage = buildErrorMessage(new FileUploadSizeException(
String.format("Size %s of file %s exceeds allowed max size %s",
file.length(), fileItemInput.getName(), maxSizeOfFiles),
maxSizeOfFiles, currentFilesSize),
new Object[]{maxSizeOfFiles, currentFilesSize});
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
if (!file.delete() && LOG.isWarnEnabled()) {
LOG.warn("Cannot delete file: {} which exceeds maximum size: {} of all files!",
sanitizeNewlines(fileItemInput.getName()), maxSizeOfFiles);
}
}
/**
* Processes the FileItem as a file field.
*
* @param fileItemInput file item representing upload file
* @param location location
*/
protected void processFileItemAsFileField(FileItemInput fileItemInput, Path location) throws IOException {
// Skip file uploads that don't have a file name - meaning that no file was selected.
if (fileItemInput.getName() == null || fileItemInput.getName().trim().isEmpty()) {
LOG.debug(() -> "No file has been uploaded for the field: " + sanitizeNewlines(fileItemInput.getFieldName()));
return;
}
if (exceedsMaxFiles(fileItemInput)) {
return;
}
File file = createTemporaryFile(fileItemInput.getName(), location);
streamFileToDisk(fileItemInput, file);
Long currentFilesSize = maxSizeOfFiles != null ? actualSizeOfUploadedFiles() : null;
if (maxSizeOfFiles != null && currentFilesSize + file.length() >= maxSizeOfFiles) {
exceedsMaxSizeOfFiles(fileItemInput, file, currentFilesSize);
} else {
createUploadedFile(fileItemInput, file);
}
}
@@ -363,121 +209,54 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
*
* @param fileName file name
* @param location location
* @return temporary file based on the given filename and location
* @throws IOException in case of IO errors
* @return a temporary file based on the given filename and location
*/
protected File createTemporaryFile(String fileName, String location) throws IOException {
String name = fileName
.substring(fileName.lastIndexOf('/') + 1)
.substring(fileName.lastIndexOf('\\') + 1);
String prefix = name;
String suffix = "";
if (name.contains(".")) {
prefix = name.substring(0, name.lastIndexOf('.'));
suffix = name.substring(name.lastIndexOf('.'));
}
if (prefix.length() < 3) {
prefix = UUID.randomUUID().toString();
}
File file = File.createTempFile(prefix + "_", suffix, new File(location));
LOG.debug("Creating temporary file '{}' (originally '{}').", file.getName(), fileName);
protected File createTemporaryFile(String fileName, Path location) {
String uid = UUID.randomUUID().toString().replace("-", "_");
File file = location.resolve("upload_" + uid + ".tmp").toFile();
LOG.debug("Creating temporary file: {} (originally: {})", file.getName(), fileName);
return file;
}
/**
* Streams the file upload stream to the specified file.
*
* @param itemStream file item stream
* @param file the file
* @return true if stream was successfully
* @throws IOException in case of IO errors
* @param fileItemInput file item input
* @param file the file
*/
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 void streamFileToDisk(FileItemInput fileItemInput, File file) throws IOException {
InputStream input = fileItemInput.getInputStream();
try (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: {}", fileItemInput.getName(), bufferSize);
for (int length; ((length = input.read(buffer)) > 0); ) {
output.write(buffer, 0, length);
}
result = true;
}
return result;
}
/**
* 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) {
// gather attributes from file upload stream.
String fileName = itemStream.getName();
String fieldName = itemStream.getFieldName();
// create internal structure
FileInfo fileInfo = new FileInfo(file, itemStream.getContentType(), fileName);
// append or create new entry.
if (!fileInfos.containsKey(fieldName)) {
List<FileInfo> infos = new ArrayList<>();
infos.add(fileInfo);
fileInfos.put(fieldName, infos);
protected void createUploadedFile(FileItemInput fileItemInput, File file) {
String fileName = fileItemInput.getName();
String fieldName = fileItemInput.getFieldName();
UploadedFile<File> uploadedFile = StrutsUploadedFile.Builder
.create(file)
.withOriginalName(fileName)
.withContentType(fileItemInput.getContentType())
.build();
if (uploadedFiles.containsKey(fieldName)) {
uploadedFiles.get(fieldName).add(uploadedFile);
} 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;
List<UploadedFile<File>> infos = new ArrayList<>();
infos.add(uploadedFile);
uploadedFiles.put(fieldName, infos);
}
}
@@ -32,7 +32,7 @@ import jakarta.servlet.http.HttpServletRequest;
public interface MultiPartRequest {
void parse(HttpServletRequest request, String saveDir) throws IOException;
/**
* Returns an enumeration of the parameter names for uploaded files
*
@@ -58,7 +58,7 @@ public interface MultiPartRequest {
* @param fieldName input field name
* @return a UploadedFile[] object for files associated with the specified input field name
*/
UploadedFile[] getFile(String fieldName);
<T> UploadedFile<T>[] getFile(String fieldName);
/**
* Returns a String[] of file names for files associated with the specified input field name
@@ -25,6 +25,8 @@ import org.apache.struts2.dispatcher.LocalizedMessage;
import org.apache.struts2.dispatcher.StrutsRequestWrapper;
import jakarta.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.*;
@@ -137,7 +139,7 @@ public class MultiPartRequestWrapper extends StrutsRequestWrapper {
* @param fieldName input field name
* @return a File[] object for files associated with the specified input field name
*/
public UploadedFile[] getFiles(String fieldName) {
public UploadedFile<File>[] getFiles(String fieldName) {
if (multi == null) {
return null;
}
@@ -20,7 +20,7 @@ package org.apache.struts2.dispatcher.multipart;
import java.io.File;
public class StrutsUploadedFile implements UploadedFile {
public class StrutsUploadedFile implements UploadedFile<File> {
private final File file;
private final String contentType;
@@ -28,6 +28,7 @@ public class StrutsUploadedFile implements UploadedFile {
/**
* Use builder instead of constructor
*
* @param file an uploaded file
* @deprecated since Struts 6.4.0
*/
@@ -87,9 +88,9 @@ public class StrutsUploadedFile implements UploadedFile {
@Override
public String toString() {
return "StrutsUploadedFile{" +
"contentType='" + contentType + '\'' +
", originalName='" + originalName + '\'' +
'}';
"contentType='" + contentType + '\'' +
", originalName='" + originalName + '\'' +
'}';
}
public static class Builder {
@@ -115,7 +116,7 @@ public class StrutsUploadedFile implements UploadedFile {
return this;
}
public UploadedFile build() {
public UploadedFile<File> build() {
return new StrutsUploadedFile(this.file, this.contentType, this.originalName);
}
}
@@ -21,24 +21,48 @@ package org.apache.struts2.dispatcher.multipart;
import java.io.Serializable;
/**
* Virtual representation of a uploaded file used by {@link MultiPartRequest}
* Virtual representation of an uploaded file used by {@link MultiPartRequest}
*/
public interface UploadedFile extends Serializable {
public interface UploadedFile<T> extends Serializable {
/**
* @return size of the content of file/stream/array
*/
Long length();
/**
* @return a local name of the file
*/
String getName();
/**
* @return original file name from upload source
*/
String getOriginalName();
/**
* @return indicates if this is a real file or maybe just in-memory stream
*/
boolean isFile();
/**
* @return removes a local copy of the uploaded file/stream
*/
boolean delete();
/**
* @return an absolute path of the file if possible
*/
String getAbsolutePath();
Object getContent();
/**
* @return content of the upload file
*/
T getContent();
/**
* @return content type of the uploaded file
*/
String getContentType();
}
@@ -28,6 +28,7 @@ import org.apache.struts2.action.UploadedFilesAware;
import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
import org.apache.struts2.dispatcher.multipart.UploadedFile;
import java.io.File;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
@@ -153,19 +154,19 @@ public class ActionFileUploadInterceptor extends AbstractFileUploadInterceptor {
// bind allowed Files
Enumeration<String> fileParameterNames = multiWrapper.getFileParameterNames();
List<UploadedFile> acceptedFiles = new ArrayList<>();
List<UploadedFile<File>> acceptedFiles = new ArrayList<>();
while (fileParameterNames != null && fileParameterNames.hasMoreElements()) {
// get the value of this input tag
String inputName = fileParameterNames.nextElement();
UploadedFile[] uploadedFiles = multiWrapper.getFiles(inputName);
UploadedFile<File>[] uploadedFiles = multiWrapper.getFiles(inputName);
if (uploadedFiles == null || uploadedFiles.length == 0) {
if (LOG.isWarnEnabled()) {
LOG.warn(getTextMessage(action, STRUTS_MESSAGES_INVALID_FILE_KEY, new String[]{inputName}));
}
} else {
for (UploadedFile uploadedFile : uploadedFiles) {
for (UploadedFile<File> uploadedFile : uploadedFiles) {
if (acceptFile(action, uploadedFile, uploadedFile.getOriginalName(), uploadedFile.getContentType(), inputName)) {
acceptedFiles.add(uploadedFile);
}
@@ -35,8 +35,8 @@ public class SettingsTest extends StrutsInternalTestCase {
public void testSettings() {
Settings settings = new DefaultSettings();
assertEquals("12345", settings.get(StrutsConstants.STRUTS_MULTIPART_MAXSIZE));
assertEquals("\\temp", settings.get(StrutsConstants.STRUTS_MULTIPART_SAVEDIR));
assertEquals("12345", settings.get(StrutsConstants.STRUTS_MULTIPART_MAX_SIZE));
assertEquals("\\temp", settings.get(StrutsConstants.STRUTS_MULTIPART_SAVE_DIR));
assertEquals("test,org/apache/struts2/othertest", settings.get( StrutsConstants.STRUTS_CUSTOM_PROPERTIES));
assertEquals("testvalue", settings.get("testkey"));
@@ -0,0 +1,485 @@
/*
* 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.struts2.dispatcher.multipart;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
import org.apache.struts2.dispatcher.LocalizedMessage;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.assertj.core.api.Assertions.assertThat;
abstract class AbstractMultiPartRequestTest {
protected static Path tempDir;
protected MockHttpServletRequest mockRequest;
protected final String boundary = "_boundary_";
protected final String endline = "\r\n";
protected AbstractMultiPartRequest<File> multiPart;
abstract protected AbstractMultiPartRequest<File> createMultipartRequest();
@BeforeClass
public static void beforeClass() {
String dirProp = System.getProperty("java.io.tmpdir");
if (Path.of(dirProp).toFile().exists()) {
tempDir = Path.of(dirProp, "multi-part-test");
} else {
tempDir = Path.of("target", "multi-part-test");
}
}
@Before
public void before() {
mockRequest = new MockHttpServletRequest();
mockRequest.setCharacterEncoding(StandardCharsets.UTF_8.name());
mockRequest.setMethod("post");
mockRequest.setContentType("multipart/form-data; boundary=" + boundary);
multiPart = createMultipartRequest();
}
@After
public void after() {
multiPart.cleanUp();
}
@Test
public void uploadedFilesToDisk() throws IOException {
// given
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formFile("file2", "test2.csv", "5,6,7,8") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
// when
multiPart.setBufferSize("1"); // always write files into disk
multiPart.parse(mockRequest, tempDir.toString());
// then
assertThat(multiPart.getErrors())
.isEmpty();
assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
.asList()
.containsOnly("file1", "file2");
assertThat(multiPart.getFile("file1")).allSatisfy(file -> {
assertThat(file.isFile())
.isTrue();
assertThat(file.getOriginalName())
.isEqualTo("test1.csv");
assertThat(file.getContentType())
.isEqualTo("text/csv");
assertThat(file.getContent()).asInstanceOf(InstanceOfAssertFactories.FILE)
.exists()
.content()
.isEqualTo("1,2,3,4");
});
assertThat(multiPart.getFile("file2")).allSatisfy(file -> {
assertThat(file.isFile())
.isTrue();
assertThat(file.getOriginalName())
.isEqualTo("test2.csv");
assertThat(file.getContentType())
.isEqualTo("text/csv");
assertThat(file.getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.exists()
.content()
.isEqualTo("5,6,7,8");
});
}
@Test
public void uploadedMultipleFilesToDisk() throws IOException {
// given
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formFile("file1", "test2.csv", "5,6,7,8") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
// when
multiPart.setBufferSize("1"); // always write files into disk
multiPart.parse(mockRequest, tempDir.toString());
// then
assertThat(multiPart.getErrors())
.isEmpty();
assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
.asList()
.containsOnly("file1");
assertThat(multiPart.getFile("file1")).allSatisfy(file -> {
if (Objects.equals(file.getName(), "test1.csv")) {
assertThat(file.isFile())
.isTrue();
assertThat(file.getOriginalName())
.isEqualTo("test1.csv");
assertThat(file.getContentType())
.isEqualTo("text/csv");
assertThat(file.getContent()).asInstanceOf(InstanceOfAssertFactories.FILE)
.exists()
.content()
.isEqualTo("1,2,3,4");
}
if (Objects.equals(file.getName(), "test2.csv")) {
assertThat(file.isFile())
.isTrue();
assertThat(file.getOriginalName())
.isEqualTo("test2.csv");
assertThat(file.getContentType())
.isEqualTo("text/csv");
assertThat(file.getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.exists()
.content()
.isEqualTo("5,6,7,8");
}
});
}
@Test
public void uploadedFilesWithLargeBuffer() throws IOException {
// given
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formFile("file2", "test2.csv", "5,6,7,8") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
// when
multiPart.setBufferSize("8192"); // streams files into disk using larger buffer
multiPart.parse(mockRequest, tempDir.toString());
// then
assertThat(multiPart.getErrors())
.isEmpty();
assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
.asList()
.containsOnly("file1", "file2");
assertThat(multiPart.getFile("file1")).allSatisfy(file -> {
assertThat(file.isFile())
.isTrue();
assertThat(file.getOriginalName())
.isEqualTo("test1.csv");
assertThat(file.getContentType())
.isEqualTo("text/csv");
assertThat(file.getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.exists()
.content()
.isEqualTo("1,2,3,4");
});
assertThat(multiPart.getFile("file2")).allSatisfy(file -> {
assertThat(file.isFile())
.isTrue();
assertThat(file.getOriginalName())
.isEqualTo("test2.csv");
assertThat(file.getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.exists()
.content()
.isEqualTo("5,6,7,8");
});
}
@Test
public void cleanUp() throws IOException {
// given
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formFile("file2", "test2.csv", "5,6,7,8") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
// when
multiPart.parse(mockRequest, tempDir.toString());
// then
assertThat(multiPart.getErrors())
.isEmpty();
assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
.asList()
.containsOnly("file1", "file2");
assertThat(multiPart.getFile("file1")).allSatisfy(file -> {
assertThat(file.isFile())
.isTrue();
assertThat(file.getOriginalName())
.isEqualTo("test1.csv");
assertThat(file.getContentType())
.isEqualTo("text/csv");
assertThat(file.getContent()).asInstanceOf(InstanceOfAssertFactories.FILE)
.exists()
.content()
.isEqualTo("1,2,3,4");
});
assertThat(multiPart.getFile("file2")).allSatisfy(file -> {
assertThat(file.isFile())
.isTrue();
assertThat(file.getOriginalName())
.isEqualTo("test2.csv");
assertThat(file.getContentType())
.isEqualTo("text/csv");
assertThat(file.getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.exists()
.content()
.isEqualTo("5,6,7,8");
});
List<UploadedFile<File>> uploadedFiles = new ArrayList<>();
for (Map.Entry<String, List<UploadedFile<File>>> entry : multiPart.uploadedFiles.entrySet()) {
uploadedFiles.addAll(entry.getValue());
}
// when
multiPart.cleanUp();
// then
assertThat(multiPart.uploadedFiles)
.isEmpty();
assertThat(multiPart.parameters)
.isEmpty();
assertThat(uploadedFiles).allSatisfy(file ->
assertThat(file.getContent()).asInstanceOf(InstanceOfAssertFactories.FILE)
.doesNotExist()
);
}
@Test
public void nonMultiPartUpload() throws IOException {
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formFile("file2", "test2.csv", "5,6,7,8") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// given
mockRequest.setContentType("");
// when
multiPart.parse(mockRequest, tempDir.toString());
// then
assertThat(multiPart.getErrors())
.map(LocalizedMessage::getTextKey)
.containsExactly("struts.messages.upload.error.FileUploadContentTypeException");
assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
.asList()
.isEmpty();
}
@Test
public void maxSize() throws IOException {
// given
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formFile("file2", "test2.csv", "5,6,7,8") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
// when
multiPart.setMaxSize("1");
multiPart.parse(mockRequest, tempDir.toString());
// then
assertThat(multiPart.uploadedFiles)
.isEmpty();
assertThat(multiPart.getErrors())
.map(LocalizedMessage::getTextKey)
.containsExactly("struts.messages.upload.error.FileUploadSizeException");
}
@Test
public void maxFilesSize() throws IOException {
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formFile("file2", "test2.csv", "5,6,7,8") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
multiPart.setMaxFileSize("1");
multiPart.parse(mockRequest, tempDir.toString());
assertThat(multiPart.getErrors())
.map(LocalizedMessage::getTextKey)
.containsExactly("struts.messages.upload.error.FileUploadByteCountLimitException");
}
@Test
public void maxFiles() throws IOException {
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formFile("file2", "test2.csv", "5,6,7,8") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.US_ASCII));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
multiPart.setMaxFiles("1");
multiPart.parse(mockRequest, tempDir.toString());
assertThat(multiPart.errors)
.map(LocalizedMessage::getTextKey)
.containsExactly("struts.messages.upload.error.FileUploadFileCountLimitException");
}
@Test
public void maxStringLength() throws IOException {
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formFile("file2", "test2.csv", "5,6,7,8") +
formField("longText", "very long text") +
formField("shortText", "short text") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
multiPart.setMaxStringLength("10");
multiPart.parse(mockRequest, tempDir.toString());
assertThat(multiPart.getErrors())
.map(LocalizedMessage::getTextKey)
.containsExactly("struts.messages.upload.error.parameter.too.long");
}
@Test
public void mismatchCharset() throws IOException {
// give
String content = formFile("file1", "test1.csv", "Ł,Ś,Ż,Ó") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
// when
mockRequest.setCharacterEncoding(null);
multiPart.setDefaultEncoding(StandardCharsets.ISO_8859_1.name());
multiPart.parse(mockRequest, tempDir.toString());
// then
assertThat(multiPart.getErrors())
.isEmpty();
assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
.asList()
.containsOnly("file1");
assertThat(multiPart.getFile("file1")).allSatisfy(file -> {
assertThat(file.isFile())
.isTrue();
assertThat(file.getOriginalName())
.isEqualTo("test1.csv");
assertThat(file.getContentType())
.isEqualTo("text/csv");
assertThat(file.getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.exists()
.content()
.isEqualTo("Ł,Ś,Ż,Ó");
});
}
@Test
public void normalFields() throws IOException {
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formFile("file2", "test2.csv", "5,6,7,8") +
formField("longText", "very long text") +
formField("shortText", "short text") +
formField("multi", "multi1") +
formField("multi", "multi2") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
multiPart.parse(mockRequest, tempDir.toString());
assertThat(multiPart.getErrors())
.isEmpty();
assertThat(multiPart.getParameterNames().asIterator()).toIterable()
.containsOnly("longText", "shortText", "multi");
assertThat(multiPart.getParameterValues("longText"))
.contains("very long text");
assertThat(multiPart.getParameterValues("shortText"))
.contains("short text");
assertThat(multiPart.getParameter("longText"))
.isEqualTo("very long text");
assertThat(multiPart.getParameter("shortText"))
.isEqualTo("short text");
assertThat(multiPart.getParameterValues("multi"))
.containsOnly("multi1", "multi2");
}
protected String formFile(String fieldName, String filename, String content) {
return endline +
"--" + boundary + endline +
"Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + filename + "\"" +
endline +
"Content-Type: text/csv" +
endline +
endline +
content;
}
protected String formField(String fieldName, String content) {
return endline +
"--" + boundary + endline +
"Content-Disposition: form-data; name=\"" + fieldName + "\"" +
endline +
endline +
content;
}
}
@@ -0,0 +1,30 @@
/*
* 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.struts2.dispatcher.multipart;
import java.io.File;
public class JakartaMultiPartRequestTest extends AbstractMultiPartRequestTest {
@Override
protected AbstractMultiPartRequest<File> createMultipartRequest() {
return new JakartaMultiPartRequest();
}
}
@@ -18,54 +18,57 @@
*/
package org.apache.struts2.dispatcher.multipart;
import java.io.ByteArrayInputStream;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
import org.apache.struts2.dispatcher.LocalizedMessage;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.struts2.dispatcher.LocalizedMessage;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.mock.web.DelegatingServletInputStream;
import static org.assertj.core.api.Assertions.assertThat;
import jakarta.servlet.http.HttpServletRequest;
public class JakartaStreamMultiPartRequestTest extends AbstractMultiPartRequestTest {
public class JakartaStreamMultiPartRequestTest {
private JakartaStreamMultiPartRequest multiPart;
private Path tempDir;
@Before
public void initialize() {
multiPart = new JakartaStreamMultiPartRequest();
tempDir = Paths.get("target", "multi-part-test");
@Override
protected AbstractMultiPartRequest createMultipartRequest() {
return new JakartaStreamMultiPartRequest();
}
/**
* Number of bytes in files greater than 2GB overflow the {@code int} primative.
* The {@link HttpServletRequest#getContentLength()} returns {@literal -1}
* when the header is not present or the size is greater than {@link Integer#MAX_VALUE}.
*/
@Test
public void unknownContentLength() throws IOException {
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
Mockito.when(request.getContentType()).thenReturn("multipart/form-data; charset=utf-8; boundary=__X_BOUNDARY__");
Mockito.when(request.getMethod()).thenReturn("POST");
Mockito.when(request.getContentLength()).thenReturn(-1);
String entity = "\r\n--__X_BOUNDARY__\r\n" +
"Content-Disposition: form-data; name=\"upload\"; filename=\"test.csv\"\r\n" +
"Content-Type: text/csv\r\n\r\n1,2\r\n\r\n" +
"--__X_BOUNDARY__\r\n" +
"Content-Disposition: form-data; name=\"upload2\"; filename=\"test2.csv\"\r\n" +
"Content-Type: text/csv\r\n\r\n3,4\r\n\r\n" +
"--__X_BOUNDARY__--\r\n";
Mockito.when(request.getInputStream()).thenReturn(new DelegatingServletInputStream(new ByteArrayInputStream(entity.getBytes(StandardCharsets.UTF_8))));
multiPart.setMaxSize("4");
multiPart.parse(request, tempDir.toString());
LocalizedMessage next = multiPart.getErrors().iterator().next();
Assert.assertEquals(next.getTextKey(), "struts.messages.upload.error.FileUploadSizeException");
public void maxSizeOfFiles() throws IOException {
// given
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formFile("file2", "test2.csv", "5,6,7,8") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
// when
multiPart.setMaxSizeOfFiles("10");
multiPart.parse(mockRequest, tempDir.toString());
// then
assertThat(multiPart.uploadedFiles)
.hasSize(1);
assertThat(multiPart.getFile("file1")).allSatisfy(file -> {
assertThat(file.isFile())
.isTrue();
assertThat(file.getOriginalName())
.isEqualTo("test1.csv");
assertThat(file.getContentType())
.isEqualTo("text/csv");
assertThat(file.getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.exists()
.content()
.isEqualTo("1,2,3,4");
});
assertThat(multiPart.getErrors())
.map(LocalizedMessage::getTextKey)
.containsExactly("struts.messages.upload.error.FileUploadSizeException");
}
}
@@ -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;
@@ -52,7 +52,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
public static final UploadedFile EMPTY_FILE = new UploadedFile() {
private static final UploadedFile<String> EMPTY_FILE = new UploadedFile<>() {
@Override
public Long length() {
return 0L;
@@ -79,8 +79,8 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
}
@Override
public byte[] getContent() {
return new byte[0];
public String getContent() {
return "";
}
@Override
@@ -210,7 +210,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
URL url = ClassLoaderUtil.getResource("log4j2.xml", ActionFileUploadInterceptorTest.class);
File file = new File(new URI(url.toString()));
assertTrue("log4j2.xml should be in src/test folder", file.exists());
UploadedFile uploadedFile = StrutsUploadedFile.Builder.create(file).withContentType("text/html").withOriginalName("filename").build();
UploadedFile<File> uploadedFile = StrutsUploadedFile.Builder.create(file).withContentType("text/html").withOriginalName("filename").build();
boolean notOk = interceptor.acceptFile(validation, uploadedFile, "filename", "text/html", "inputName");
assertFalse(notOk);
@@ -221,15 +221,15 @@ 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"
);
}
public void testNoMultipartRequest() throws Exception {
MyFileUploadAction action = new MyFileUploadAction();
MyFileUploadAction<File> action = new MyFileUploadAction<>();
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
@@ -250,13 +250,13 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
req.setContentType("multipart/form-data"); // not a multipart contentype
req.setMethod("post");
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
MyFileUploadAction<File> action = container.inject(MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000));
ActionContext.getContext().withServletRequest(createMultipartRequestMaxSize(req, 2000));
interceptor.intercept(mai);
@@ -271,13 +271,13 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
req.addHeader("Content-type", "multipart/form-data");
req.setContent(null); // there is no content
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
MyFileUploadAction<File> action = container.inject(MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000));
ActionContext.getContext().withServletRequest(createMultipartRequestMaxSize(req, 2000));
interceptor.intercept(mai);
@@ -301,19 +301,19 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
""");
req.setContent(content.getBytes(StandardCharsets.US_ASCII));
MyFileUploadAction action = new MyFileUploadAction();
MyFileUploadAction<File> action = new MyFileUploadAction<>();
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000));
ActionContext.getContext().withServletRequest(createMultipartRequestMaxSize(req, 2000));
interceptor.intercept(mai);
assertFalse(action.hasErrors());
List<UploadedFile> files = action.getUploadFiles();
List<UploadedFile<File>> files = action.getUploadFiles();
assertNotNull(files);
assertEquals(1, files.size());
@@ -329,33 +329,27 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
MockHttpServletRequest req = new MockHttpServletRequest();
req.setCharacterEncoding(StandardCharsets.UTF_8.name());
req.setMethod("POST");
req.addHeader("Content-type", "multipart/form-data; boundary=" + boundary);
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 + "--" + boundary + "--";
req.setContent(content.getBytes());
assertTrue(JakartaServletDiskFileUpload.isMultipartContent(req));
MyFileUploadAction action = new MyFileUploadAction();
MyFileUploadAction<File> action = new MyFileUploadAction<>();
container.inject(action);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000));
ActionContext.getContext().withServletRequest(createMultipartRequestMaxSize(req, 2000));
interceptor.setAllowedTypes("text/html");
interceptor.intercept(mai);
List<UploadedFile> files = action.getUploadFiles();
List<UploadedFile<File>> files = action.getUploadFiles();
assertNotNull(files);
assertEquals("files accepted ", 2, files.size());
@@ -369,19 +363,19 @@ 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));
MyFileUploadAction action = new MyFileUploadAction();
MyFileUploadAction<File> action = new MyFileUploadAction<>();
container.inject(action);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
@@ -417,14 +411,14 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
""");
req.setContent(content.getBytes(StandardCharsets.US_ASCII));
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
MyFileUploadAction<File> action = container.inject(MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext()
.withServletRequest(createMultipartRequestMaxFileSize(req));
.withServletRequest(createMultipartRequestMaxFileSize(req));
interceptor.intercept(mai);
@@ -435,8 +429,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: 11!",
msg);
}
public void testMultipartRequestMaxStringLength() throws Exception {
@@ -464,14 +458,14 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
""");
req.setContent(content.getBytes(StandardCharsets.US_ASCII));
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
MyFileUploadAction<File> action = container.inject(MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext()
.withServletRequest(createMultipartRequestMaxStringLength(req));
.withServletRequest(createMultipartRequestMaxStringLength(req));
interceptor.intercept(mai);
@@ -481,8 +475,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 {
@@ -502,15 +496,15 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
""");
req.setContent(content.getBytes(StandardCharsets.US_ASCII));
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
MyFileUploadAction<File> action = container.inject(MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
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);
@@ -524,20 +518,15 @@ 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;
return endline +
"--" + boundary +
endline +
"Content-Disposition: form-data; name=\"" + "file" + "\"; filename=\"" + filename +
endline +
"Content-Type: " + contentType +
endline +
endline +
content;
}
private MultiPartRequestWrapper createMultipartRequestMaxFileSize(HttpServletRequest req) {
@@ -557,12 +546,12 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
}
private MultiPartRequestWrapper createMultipartRequest(HttpServletRequest req, int maxsize, int maxfilesize, int maxfiles, int maxStringLength) {
JakartaMultiPartRequest jak = new JakartaMultiPartRequest();
jak.setMaxSize(String.valueOf(maxsize));
jak.setMaxFileSize(String.valueOf(maxfilesize));
jak.setMaxFiles(String.valueOf(maxfiles));
jak.setMaxStringLength(String.valueOf(maxStringLength));
jak.setDefaultEncoding(StandardCharsets.UTF_8.name());
return new MultiPartRequestWrapper(jak, req, tempDir.getAbsolutePath(), new DefaultLocaleProvider());
}
@@ -582,15 +571,15 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
super.tearDown();
}
public static class MyFileUploadAction extends ActionSupport implements UploadedFilesAware {
private List<UploadedFile> uploadedFiles;
public static class MyFileUploadAction<T> extends ActionSupport implements UploadedFilesAware {
private List<UploadedFile<File>> uploadedFiles;
@Override
public void withUploadedFiles(List<UploadedFile> uploadedFiles) {
public void withUploadedFiles(List<UploadedFile<File>> uploadedFiles) {
this.uploadedFiles = uploadedFiles;
}
public List<UploadedFile> getUploadFiles() {
public List<UploadedFile<File>> getUploadFiles() {
return this.uploadedFiles;
}
}
@@ -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,35 +45,14 @@ 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;
/**
* Test case for FileUploadInterceptor.
*/
public class FileUploadInterceptorTest extends StrutsInternalTestCase {
public static final UploadedFile EMPTY_FILE = new UploadedFile() {
private static final UploadedFile<String> EMPTY_FILE = new UploadedFile<>() {
@Override
public Long length() {
return 0L;
@@ -84,8 +79,8 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
}
@Override
public byte[] getContent() {
return new byte[0];
public String getContent() {
return "";
}
@Override
@@ -210,7 +205,11 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
URL url = ClassLoaderUtil.getResource("log4j2.xml", FileUploadInterceptorTest.class);
File file = new File(new URI(url.toString()));
assertTrue("log4j2.xml should be in src/test folder", file.exists());
UploadedFile uploadedFile = StrutsUploadedFile.Builder.create(file).withContentType("text/html").withOriginalName("filename").build();
UploadedFile<File> uploadedFile = StrutsUploadedFile.Builder.create(file)
.withContentType("text/html")
.withOriginalName("filename")
.build();
boolean notOk = interceptor.acceptFile(validation, uploadedFile, "filename", "text/html", "inputName");
assertFalse(notOk);
@@ -221,10 +220,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"
);
}
@@ -253,7 +252,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext().withParameters(HttpParameters.create().build());
ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000));
ActionContext.getContext().withServletRequest(createMultipartRequestMaxSize(req, 2000));
interceptor.intercept(mai);
@@ -275,7 +274,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext().withParameters(HttpParameters.create().build());
ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000));
ActionContext.getContext().withServletRequest(createMultipartRequestMaxSize(req, 2000));
interceptor.intercept(mai);
@@ -306,7 +305,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
mai.setInvocationContext(ActionContext.getContext());
Map<String, Object> param = new HashMap<>();
ActionContext.getContext().withParameters(HttpParameters.create(param).build());
ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000));
ActionContext.getContext().withServletRequest(createMultipartRequestMaxSize(req, 2000));
interceptor.intercept(mai);
@@ -314,7 +313,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
HttpParameters parameters = mai.getInvocationContext().getParameters();
assertEquals(3, parameters.keySet().size());
UploadedFile[] files = (UploadedFile[]) parameters.get("file").getObject();
UploadedFile<File>[] files = (UploadedFile<File>[]) parameters.get("file").getObject();
String[] fileContentTypes = parameters.get("fileContentType").getMultipleValues();
String[] fileRealFilenames = parameters.get("fileFileName").getMultipleValues();
@@ -343,15 +342,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));
@@ -364,14 +363,14 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
mai.setInvocationContext(ActionContext.getContext());
Map<String, Object> param = new HashMap<>();
ActionContext.getContext().withParameters(HttpParameters.create(param).build());
ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000));
ActionContext.getContext().withServletRequest(createMultipartRequestMaxSize(req, 2000));
interceptor.setAllowedTypes("text/html");
interceptor.intercept(mai);
HttpParameters parameters = mai.getInvocationContext().getParameters();
assertEquals(3, parameters.keySet().size());
UploadedFile[] files = (UploadedFile[]) parameters.get("file").getObject();
UploadedFile<File>[] files = (UploadedFile<File>[]) parameters.get("file").getObject();
String[] fileContentTypes = parameters.get("fileContentType").getMultipleValues();
String[] fileRealFilenames = parameters.get("fileFileName").getMultipleValues();
@@ -396,14 +395,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));
@@ -416,7 +415,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
mai.setInvocationContext(ActionContext.getContext());
Map<String, Object> param = new HashMap<>();
ActionContext.getContext().withParameters(HttpParameters.create(param).build());
ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxFiles(req));
ActionContext.getContext().withServletRequest(createMultipartRequestMaxFiles(req));
interceptor.setAllowedTypes("text/html");
interceptor.intercept(mai);
@@ -466,7 +465,7 @@ public class FileUploadInterceptorTest 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!",
"File deleteme.txt assigned to file exceeded allowed size limit! Max size allowed is: 10 but file was: 11!",
msg);
}
@@ -543,9 +542,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 +559,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) {
@@ -592,12 +591,12 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
}
private MultiPartRequestWrapper createMultipartRequest(HttpServletRequest req, int maxsize, int maxfilesize, int maxfiles, int maxStringLength) {
JakartaMultiPartRequest jak = new JakartaMultiPartRequest();
jak.setMaxSize(String.valueOf(maxsize));
jak.setMaxFileSize(String.valueOf(maxfilesize));
jak.setMaxFiles(String.valueOf(maxfiles));
jak.setMaxStringLength(String.valueOf(maxStringLength));
jak.setDefaultEncoding(StandardCharsets.UTF_8.name());
return new MultiPartRequestWrapper(jak, req, tempDir.getAbsolutePath(), new DefaultLocaleProvider());
}
@@ -618,11 +617,6 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
}
public static class MyFileupAction extends ActionSupport {
private static final long serialVersionUID = 6255238895447968889L;
// no methods
}
}
+1
View File
@@ -29,5 +29,6 @@
<Root level="info">
<AppenderRef ref="STDOUT"/>
</Root>
<Logger name="org.apache.struts2.dispatcher.multipart" level="debug"/>
</Loggers>
</Configuration>
+2 -2
View File
@@ -818,8 +818,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>