ActionFileUploadInterceptorTest.java
/*
* 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.interceptor;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload;
import org.apache.struts2.ActionContext;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.ValidationAwareSupport;
import org.apache.struts2.action.UploadedFilesAware;
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.apache.struts2.locale.DefaultLocaleProvider;
import org.apache.struts2.mock.MockActionInvocation;
import org.apache.struts2.mock.MockActionProxy;
import org.apache.struts2.util.ClassLoaderUtil;
import org.assertj.core.util.Files;
import org.springframework.mock.web.MockHttpServletRequest;
import java.io.File;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test case for {@link ActionFileUploadInterceptor}.
*/
public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
private MockHttpServletRequest request;
private ActionFileUploadInterceptor interceptor;
private File tempDir;
private final String htmlContent = "<html><head></head><body>html content</body></html>";
private final String plainContent = "plain content";
private final String boundary = "simple boundary";
private final String endLine = "\r\n";
public void testAcceptFileWithEmptyAllowedTypesAndExtensions() {
// when allowed type is empty
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename", "text/plain", "inputName");
assertThat(ok).isTrue();
assertThat(validation.getFieldErrors()).isEmpty();
assertThat(validation.hasErrors()).isFalse();
}
public void testAcceptFileWithoutEmptyTypes() {
interceptor.setAllowedTypes("text/plain");
// when file is of allowed types
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.txt", "text/plain", "inputName");
assertThat(ok).isTrue();
assertThat(validation.getFieldErrors()).isEmpty();
assertThat(validation.hasErrors()).isFalse();
// when file is not of allowed types
validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.html", "text/html", "inputName");
assertThat(notOk).isFalse();
assertThat(validation.getFieldErrors()).isNotEmpty();
assertThat(validation.hasErrors()).isTrue();
}
public void testAcceptFileWithWildcardContent() {
interceptor.setAllowedTypes("text/*");
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.txt", "text/plain", "inputName");
assertThat(ok).isTrue();
assertThat(validation.getFieldErrors()).isEmpty();
assertThat(validation.hasErrors()).isFalse();
interceptor.setAllowedTypes("text/h*");
validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.html", "text/plain", "inputName");
assertThat(notOk).isFalse();
assertThat(validation.getFieldErrors()).isNotEmpty();
assertThat(validation.hasErrors()).isTrue();
}
public void testAcceptFileWithoutEmptyExtensions() {
interceptor.setAllowedExtensions(".txt");
// when file is of allowed extensions
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.txt", "text/plain", "inputName");
assertThat(ok).isTrue();
assertThat(validation.getFieldErrors()).isEmpty();
assertThat(validation.hasErrors()).isFalse();
// when file is not of allowed extensions
validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.html", "text/html", "inputName");
assertThat(notOk).isFalse();
assertThat(validation.getFieldErrors()).isNotEmpty();
assertThat(validation.hasErrors()).isTrue();
interceptor.setAllowedExtensions(".txt,.lol");
validation = new ValidationAwareSupport();
ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.lol", "text/plain", "inputName");
assertThat(ok).isTrue();
assertThat(validation.getFieldErrors()).isEmpty();
assertThat(validation.hasErrors()).isFalse();
}
public void testAcceptFileWithNoFile() {
interceptor.setAllowedTypes("text/plain");
// when file is not of allowed types
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, null, "filename.html", "text/html", "inputName");
assertThat(notOk).isFalse();
assertThat(validation.getFieldErrors()).isNotEmpty();
assertThat(validation.hasErrors()).isTrue();
assertThat(validation.getFieldErrors().get("inputName"))
.hasSize(1)
.first()
.asString()
.startsWith("Error uploading:")
.contains("inputName");
}
public void testAcceptFileWithNoContent() {
interceptor.setAllowedTypes("text/plain");
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, createTestFile(null), "filename.html", "text/plain", "inputName");
assertThat(notOk).isFalse();
assertThat(validation.getFieldErrors()).isNotEmpty();
assertThat(validation.hasErrors()).isTrue();
assertThat(validation.getFieldErrors().get("inputName"))
.hasSize(1)
.first()
.asString()
.startsWith("Error uploading:")
.contains("inputName");
}
public void testAcceptFileWithMaxSize() throws Exception {
interceptor.setMaximumSize(10L);
// when file is not of allowed types
ValidationAwareSupport validation = new ValidationAwareSupport();
URL url = ClassLoaderUtil.getResource("log4j2.xml", ActionFileUploadInterceptorTest.class);
File file = new File(new URI(url.toString()));
assertThat(file).exists();
UploadedFile uploadedFile = StrutsUploadedFile.Builder.create(file).withContentType("text/html").withOriginalName("filename").build();
boolean notOk = interceptor.acceptFile(validation, uploadedFile, "filename", "text/html", "inputName");
assertThat(notOk).isFalse();
assertThat(validation.getFieldErrors()).isNotEmpty();
assertThat(validation.hasErrors()).isTrue();
assertThat(validation.getFieldErrors().get("inputName"))
.hasSize(1)
.first()
.asString()
.contains("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();
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("NoMultipart");
MockActionProxy proxy = new MockActionProxy();
proxy.setNamespace("/test");
proxy.setActionName("myFileUpload");
mai.setProxy(proxy);
mai.setInvocationContext(ActionContext.getContext());
// if no multipart request it will bypass and execute it
assertThat(interceptor.intercept(mai)).isEqualTo("NoMultipart");
}
public void testInvalidContentTypeMultipartRequest() throws Exception {
request.setContentType("multipart/form-data"); // not a multipart Content-Type
request.setMethod("post");
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext().withServletRequest(createMultipartRequestMaxSize(2000));
interceptor.intercept(mai);
assertThat(action.hasErrors()).isTrue();
}
public void testNoContentMultipartRequest() throws Exception {
MockHttpServletRequest req = new MockHttpServletRequest();
req.setCharacterEncoding(StandardCharsets.UTF_8.name());
req.setMethod("post");
req.addHeader("Content-type", "multipart/form-data");
req.setContent(null); // there is no content
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext().withServletRequest(createMultipartRequestMaxSize(2000));
interceptor.intercept(mai);
assertThat(action.hasErrors()).isTrue();
}
public void testSuccessUploadOfATextFileMultipartRequest() throws Exception {
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
request.setMethod("post");
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
// inspired by the unit tests for Jakarta Commons FileUpload
String content = ("""
-----1234\r
Content-Disposition: form-data; name="file"; filename="deleteme.txt"\r
Content-Type: text/html\r
\r
Unit test of ActionFileUploadInterceptor\r
-----1234--\r
""");
request.setContent(content.getBytes(StandardCharsets.US_ASCII));
MyFileUploadAction action = new MyFileUploadAction();
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext().withServletRequest(createMultipartRequestMaxSize(2000));
interceptor.intercept(mai);
assertThat(action.hasErrors()).isFalse();
List<UploadedFile> files = action.getUploadFiles();
assertThat(files).isNotNull().hasSize(1);
assertThat(files.get(0).getContentType()).isEqualTo("text/html");
assertThat(files.get(0).getOriginalName()).isEqualTo("deleteme.txt");
}
/**
* Tests whether with multiple files sent with the same name, the ones with forbiddenTypes (see
* ActionFileUploadInterceptor.setAllowedTypes(...) ) are sorted out.
*/
public void testMultipleAccept() throws Exception {
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
request.setMethod("POST");
request.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 + "--" + boundary + "--";
request.setContent(content.getBytes());
assertThat(JakartaServletDiskFileUpload.isMultipartContent(request)).isTrue();
MyFileUploadAction action = new MyFileUploadAction();
container.inject(action);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext().withServletRequest(createMultipartRequestMaxFiles());
interceptor.setAllowedTypes("text/html");
interceptor.intercept(mai);
List<UploadedFile> files = action.getUploadFiles();
assertThat(files).isNotNull().hasSize(2);
assertThat(files.get(0).getContentType()).isEqualTo("text/html");
assertThat(files.get(0).getOriginalName()).isEqualTo("test1.html");
assertThat(files.get(1).getContentType()).isEqualTo("text/html");
assertThat(files.get(1).getOriginalName()).isEqualTo("test2.html");
}
public void testUnacceptedNumberOfFiles() throws Exception {
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
request.setMethod("POST");
request.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;
request.setContent(content.getBytes());
assertThat(JakartaServletFileUpload.isMultipartContent(request)).isTrue();
MyFileUploadAction action = new MyFileUploadAction();
container.inject(action);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext().withServletRequest(createMultipartRequestMaxFiles());
interceptor.setAllowedTypes("text/html");
interceptor.intercept(mai);
assertThat(action.getUploadFiles()).isNull();
assertThat(action.getActionErrors())
.hasSize(1)
.first()
.isEqualTo("Request exceeded allowed number of files! Permitted number of files is: 3!");
}
public void testMultipartRequestMaxFileSize() throws Exception {
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
request.setMethod("post");
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
// inspired by the unit tests for Jakarta Commons FileUpload
String content = ("""
-----1234\r
Content-Disposition: form-data; name="file"; filename="deleteme.txt"\r
Content-Type: text/html\r
\r
Unit test of ActionFileUploadInterceptor\r
-----1234--\r
""");
request.setContent(content.getBytes(StandardCharsets.US_ASCII));
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext()
.withServletRequest(createMultipartRequestMaxFileSize());
interceptor.intercept(mai);
assertThat(action.hasActionErrors()).isTrue();
assertThat(action.getActionErrors())
.hasSize(1)
.first()
.isEqualTo("File deleteme.txt assigned to file exceeded allowed size limit! Max size allowed is: 10 but file was: 11!");
}
public void testMultipartRequestMaxStringLength() throws Exception {
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
request.setMethod("post");
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
// inspired by the unit tests for Jakarta Commons FileUpload
String content = ("""
-----1234\r
Content-Disposition: form-data; name="file"; filename="deleteme.txt"\r
Content-Type: text/html\r
\r
Unit test of ActionFileUploadInterceptor\r
-----1234\r
Content-Disposition: form-data; name="normalFormField1"\r
\r
it works\r
-----1234\r
Content-Disposition: form-data; name="normalFormField2"\r
\r
long string should not work\r
-----1234--\r
""");
request.setContent(content.getBytes(StandardCharsets.US_ASCII));
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext()
.withServletRequest(createMultipartRequestMaxStringLength());
interceptor.intercept(mai);
assertThat(action.hasActionErrors()).isTrue();
assertThat(action.getActionErrors())
.hasSize(1)
.first()
.isEqualTo("The request parameter \"normalFormField2\" was too long. Max length allowed is 20, but found 27!");
}
public void testMultipartRequestLocalizedError() throws Exception {
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
request.setMethod("post");
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
String content = ("""
-----1234\r
Content-Disposition: form-data; name="file"; filename="deleteme.txt"\r
Content-Type: text/html\r
\r
Unit test of ActionFileUploadInterceptor\r
-----1234--\r
""");
request.setContent(content.getBytes(StandardCharsets.US_ASCII));
MyFileUploadAction 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(10));
interceptor.intercept(mai);
assertThat(action.hasActionErrors()).isTrue();
assertThat(action.getActionErrors())
.hasSize(1)
.first()
.asString()
.startsWith("Der Request ��bertraf die maximal erlaubte Gr����e");
}
private String encodeTextFile(String filename, String contentType, String content) {
return endLine +
"--" + boundary +
endLine +
"Content-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"" +
endLine +
"Content-Type: " + contentType +
endLine +
endLine +
content;
}
private MultiPartRequestWrapper createMultipartRequestMaxFileSize() {
return createMultipartRequest(-1, 10, -1, -1);
}
private MultiPartRequestWrapper createMultipartRequestMaxFiles() {
return createMultipartRequest(-1, -1, 3, -1);
}
private MultiPartRequestWrapper createMultipartRequestMaxSize(int maxsize) {
return createMultipartRequest(maxsize, -1, -1, -1);
}
private MultiPartRequestWrapper createMultipartRequestMaxStringLength() {
return createMultipartRequest(-1, -1, -1, 20);
}
private MultiPartRequestWrapper createMultipartRequest(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, request, tempDir.getAbsolutePath(), new DefaultLocaleProvider());
}
protected void setUp() throws Exception {
super.setUp();
request = new MockHttpServletRequest();
interceptor = new ActionFileUploadInterceptor();
container.inject(interceptor);
tempDir = File.createTempFile("struts", "fileupload");
assertThat(tempDir.delete()).isTrue();
assertThat(tempDir.mkdirs()).isTrue();
}
protected void tearDown() throws Exception {
interceptor.destroy();
super.tearDown();
}
private UploadedFile createTestFile(File content) {
return new UploadedFile() {
@Override
public Long length() {
return 0L;
}
@Override
public String getName() {
return "";
}
@Override
public boolean isFile() {
return false;
}
@Override
public boolean delete() {
return false;
}
@Override
public String getAbsolutePath() {
return null;
}
@Override
public File getContent() {
return content;
}
@Override
public String getOriginalName() {
return null;
}
@Override
public String getContentType() {
return null;
}
@Override
public String getInputName() {
return null;
}
};
}
public static class MyFileUploadAction extends ActionSupport implements UploadedFilesAware {
private List<UploadedFile> uploadedFiles;
@Override
public void withUploadedFiles(List<UploadedFile> uploadedFiles) {
this.uploadedFiles = uploadedFiles;
}
public List<UploadedFile> getUploadFiles() {
return this.uploadedFiles;
}
}
}