This is an automated email from the ASF dual-hosted git repository. kusal pushed a commit to branch issue-85/CONFSRVDEV-37138-merge-upstream in repository https://gitbox.apache.org/repos/asf/struts.git
commit 98348790e54c3855190879a303677d248a974878 Author: Kusal Kithul-Godage <g...@kusal.io> AuthorDate: Mon Feb 3 11:10:13 2025 +1100 CONFSRVDEV-37099 Backport of CVE-2024-53677 fix from 7.0 --- .../struts2/interceptor/FileUploadInterceptor.java | 263 ------- core/src/main/resources/struts-default.xml | 9 - .../interceptor/FileUploadInterceptorTest.java | 806 --------------------- 3 files changed, 1078 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java deleted file mode 100644 index 86f8a64be..000000000 --- a/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * 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 com.opensymphony.xwork2.ActionContext; -import com.opensymphony.xwork2.ActionInvocation; -import com.opensymphony.xwork2.ActionProxy; -import com.opensymphony.xwork2.interceptor.ValidationAware; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.struts2.dispatcher.Parameter; -import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper; -import org.apache.struts2.dispatcher.multipart.UploadedFile; - -import javax.servlet.http.HttpServletRequest; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * <!-- START SNIPPET: description --> - * <p> - * Interceptor that is based off of {@link MultiPartRequestWrapper}, which is automatically applied for any request that - * includes a file. It adds the following parameters, where [File Name] is the name given to the file uploaded by the - * HTML form: - * </p> - * <ul> - * - * <li>[File Name] : File - the actual File</li> - * - * <li>[File Name]ContentType : String - the content type of the file</li> - * - * <li>[File Name]FileName : String - the actual name of the file uploaded (not the HTML name)</li> - * - * </ul> - * - * <p>You can get access to these files by merely providing setters in your action that correspond to any of the three - * patterns above, such as setDocument(File document), setDocumentContentType(String contentType), etc. - * <br>See the example code section. - * </p> - * - * <p> This interceptor will add several field errors, assuming that the action implements {@link ValidationAware}. - * These error messages are based on several i18n values stored in struts-messages.properties, a default i18n file - * processed for all i18n requests. You can override the text of these messages by providing text for the following - * keys: - * </p> - * - * <ul> - * - * <li>struts.messages.error.uploading - a general error that occurs when the file could not be uploaded</li> - * - * <li>struts.messages.error.file.too.large - occurs when the uploaded file is too large</li> - * - * <li>struts.messages.error.content.type.not.allowed - occurs when the uploaded file does not match the expected - * content types specified</li> - * - * <li>struts.messages.error.file.extension.not.allowed - occurs when the uploaded file does not match the expected - * file extensions specified</li> - * - * </ul> - * <p> - * <!-- END SNIPPET: description --> - * - * <p><u>Interceptor parameters:</u></p> - * <p> - * <!-- START SNIPPET: parameters --> - * - * <ul> - * - * <li>maximumSize (optional) - the maximum size (in bytes) that the interceptor will allow a file reference to be set - * on the action. Note, this is <b>not</b> related to the various properties found in struts.properties. - * Default to approximately 2MB.</li> - * - * <li>allowedTypes (optional) - a comma separated list of content types (ie: text/html) that the interceptor will allow - * a file reference to be set on the action. If none is specified allow all types to be uploaded.</li> - * - * <li>allowedExtensions (optional) - a comma separated list of file extensions (ie: .html) that the interceptor will allow - * a file reference to be set on the action. If none is specified allow all extensions to be uploaded.</li> - * </ul> - * <p> - * <p> - * <!-- END SNIPPET: parameters --> - * - * <p><u>Extending the interceptor:</u></p> - * <p> - * <p> - * <p> - * <!-- START SNIPPET: extending --> - * <p> - * You can extend this interceptor and override the acceptFile method to provide more control over which files - * are supported and which are not. - * </p> - * <!-- END SNIPPET: extending --> - * - * <p><u>Example code:</u></p> - * - * <pre> - * <!-- START SNIPPET: example-configuration --> - * <action name="doUpload" class="com.example.UploadAction"> - * <interceptor-ref name="fileUpload"/> - * <interceptor-ref name="basicStack"/> - * <result name="success">good_result.jsp</result> - * </action> - * <!-- END SNIPPET: example-configuration --> - * </pre> - * <p> - * <!-- START SNIPPET: multipart-note --> - * <p> - * You must set the encoding to <code>multipart/form-data</code> in the form where the user selects the file to upload. - * </p> - * <!-- END SNIPPET: multipart-note --> - * - * <pre> - * <!-- START SNIPPET: example-form --> - * <s:form action="doUpload" method="post" enctype="multipart/form-data"> - * <s:file name="upload" label="File"/> - * <s:submit/> - * </s:form> - * <!-- END SNIPPET: example-form --> - * </pre> - * <p> - * And then in your action code you'll have access to the File object if you provide setters according to the - * naming convention documented in the start. - * </p> - * - * <pre> - * <!-- START SNIPPET: example-action --> - * package com.example; - * - * import java.io.File; - * import com.opensymphony.xwork2.ActionSupport; - * - * public UploadAction extends ActionSupport { - * private File file; - * private String contentType; - * private String filename; - * - * public void setUpload(File file) { - * this.file = file; - * } - * - * public void setUploadContentType(String contentType) { - * this.contentType = contentType; - * } - * - * public void setUploadFileName(String filename) { - * this.filename = filename; - * } - * - * public String execute() { - * //... - * return SUCCESS; - * } - * } - * <!-- END SNIPPET: example-action --> - * </pre> - * - * @deprecated since Struts 6.4.0, use {@link ActionFileUploadInterceptor} instead - */ -@Deprecated -public class FileUploadInterceptor extends AbstractFileUploadInterceptor { - - private static final long serialVersionUID = -4764627478894962478L; - - protected static final Logger LOG = LogManager.getLogger(FileUploadInterceptor.class); - - /* (non-Javadoc) - * @see com.opensymphony.xwork2.interceptor.Interceptor#intercept(com.opensymphony.xwork2.ActionInvocation) - */ - - public String intercept(ActionInvocation invocation) throws Exception { - ActionContext ac = invocation.getInvocationContext(); - - HttpServletRequest request = ac.getServletRequest(); - - if (!(request instanceof MultiPartRequestWrapper)) { - if (LOG.isDebugEnabled()) { - ActionProxy proxy = invocation.getProxy(); - LOG.debug(getTextMessage(STRUTS_MESSAGES_BYPASS_REQUEST_KEY, new String[]{proxy.getNamespace(), proxy.getActionName()})); - } - - return invocation.invoke(); - } - - Object action = invocation.getAction(); - MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request; - - applyValidation(action, multiWrapper); - - // bind allowed Files - Enumeration<String> fileParameterNames = multiWrapper.getFileParameterNames(); - while (fileParameterNames != null && fileParameterNames.hasMoreElements()) { - // get the value of this input tag - String inputName = fileParameterNames.nextElement(); - - // get the content type - String[] contentType = multiWrapper.getContentTypes(inputName); - - if (isNonEmpty(contentType)) { - // get the name of the file from the input tag - String[] fileName = multiWrapper.getFileNames(inputName); - - if (isNonEmpty(fileName)) { - // get a File object for the uploaded File - UploadedFile[] files = multiWrapper.getFiles(inputName); - if (files != null && files.length > 0) { - List<UploadedFile> acceptedFiles = new ArrayList<>(files.length); - List<String> acceptedContentTypes = new ArrayList<>(files.length); - List<String> acceptedFileNames = new ArrayList<>(files.length); - String contentTypeName = inputName + "ContentType"; - String fileNameName = inputName + "FileName"; - - for (int index = 0; index < files.length; index++) { - if (acceptFile(action, files[index], fileName[index], contentType[index], inputName)) { - acceptedFiles.add(files[index]); - acceptedContentTypes.add(contentType[index]); - acceptedFileNames.add(fileName[index]); - } - } - - if (!acceptedFiles.isEmpty()) { - Map<String, Parameter> newParams = new HashMap<>(); - newParams.put(inputName, new Parameter.File(inputName, acceptedFiles.toArray(new UploadedFile[0]))); - newParams.put(contentTypeName, new Parameter.File(contentTypeName, acceptedContentTypes.toArray(new String[0]))); - newParams.put(fileNameName, new Parameter.File(fileNameName, acceptedFileNames.toArray(new String[0]))); - ac.getParameters().appendAll(newParams); - } - } - } else { - if (LOG.isWarnEnabled()) { - LOG.warn(getTextMessage(action, STRUTS_MESSAGES_INVALID_FILE_KEY, new String[]{inputName})); - } - } - } else { - if (LOG.isWarnEnabled()) { - LOG.warn(getTextMessage(action, STRUTS_MESSAGES_INVALID_CONTENT_TYPE_KEY, new String[]{inputName})); - } - } - } - - // invoke action - return invocation.invoke(); - } - -} diff --git a/core/src/main/resources/struts-default.xml b/core/src/main/resources/struts-default.xml index 326477bc4..19fda0237 100644 --- a/core/src/main/resources/struts-default.xml +++ b/core/src/main/resources/struts-default.xml @@ -57,7 +57,6 @@ <interceptor name="debugging" class="org.apache.struts2.interceptor.debugging.DebuggingInterceptor"/> <interceptor name="execAndWait" class="org.apache.struts2.interceptor.ExecuteAndWaitInterceptor"/> <interceptor name="exception" class="com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor"/> - <interceptor name="fileUpload" class="org.apache.struts2.interceptor.FileUploadInterceptor"/> <interceptor name="actionFileUpload" class="org.apache.struts2.interceptor.ActionFileUploadInterceptor"/> <interceptor name="i18n" class="org.apache.struts2.interceptor.I18nInterceptor"/> <interceptor name="logger" class="com.opensymphony.xwork2.interceptor.LoggingInterceptor"/> @@ -116,12 +115,6 @@ <interceptor-ref name="workflow"/> </interceptor-stack> - <!-- Sample file upload stack --> - <interceptor-stack name="fileUploadStack"> - <interceptor-ref name="fileUpload"/> - <interceptor-ref name="basicStack"/> - </interceptor-stack> - <!-- Action based file upload stack --> <interceptor-stack name="actionFileUploadStack"> <interceptor-ref name="actionFileUpload"/> @@ -171,7 +164,6 @@ <interceptor-ref name="prepare"/> <interceptor-ref name="chain"/> <interceptor-ref name="modelDriven"/> - <interceptor-ref name="fileUpload"/> <interceptor-ref name="actionFileUpload"/> <interceptor-ref name="staticParams"/> <interceptor-ref name="actionMappingParams"/> @@ -210,7 +202,6 @@ <interceptor-ref name="chain"/> <interceptor-ref name="scopedModelDriven"/> <interceptor-ref name="modelDriven"/> - <interceptor-ref name="fileUpload"/> <interceptor-ref name="actionFileUpload"/> <interceptor-ref name="checkbox"/> <interceptor-ref name="datetime"/> diff --git a/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java deleted file mode 100644 index 36fad5847..000000000 --- a/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java +++ /dev/null @@ -1,806 +0,0 @@ -/* - * 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 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 org.apache.commons.fileupload.servlet.ServletFileUpload; -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 javax.servlet.http.HttpServletRequest; -import java.io.File; -import java.net.URI; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -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() { - @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 byte[] getContent() { - return new byte[0]; - } - - @Override - public String getOriginalName() { - return null; - } - - @Override - public String getContentType() { - return null; - } - - @Override - public String getInputName() { - return null; - } - }; - - private FileUploadInterceptor interceptor; - private File tempDir; - - public void testAcceptFileWithEmptyAllowedTypesAndExtensions() { - // when allowed type is empty - ValidationAwareSupport validation = new ValidationAwareSupport(); - boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename", "text/plain", "inputName"); - - assertTrue(ok); - assertTrue(validation.getFieldErrors().isEmpty()); - assertFalse(validation.hasErrors()); - } - - public void testAcceptFileWithoutEmptyTypes() { - interceptor.setAllowedTypes("text/plain"); - - // when file is of allowed types - ValidationAwareSupport validation = new ValidationAwareSupport(); - boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName"); - - assertTrue(ok); - assertTrue(validation.getFieldErrors().isEmpty()); - assertFalse(validation.hasErrors()); - - // when file is not of allowed types - validation = new ValidationAwareSupport(); - boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/html", "inputName"); - - assertFalse(notOk); - assertFalse(validation.getFieldErrors().isEmpty()); - assertTrue(validation.hasErrors()); - } - - - public void testAcceptFileWithWildcardContent() { - interceptor.setAllowedTypes("text/*"); - - ValidationAwareSupport validation = new ValidationAwareSupport(); - boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName"); - - assertTrue(ok); - assertTrue(validation.getFieldErrors().isEmpty()); - assertFalse(validation.hasErrors()); - - interceptor.setAllowedTypes("text/h*"); - validation = new ValidationAwareSupport(); - boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/plain", "inputName"); - - assertFalse(notOk); - assertFalse(validation.getFieldErrors().isEmpty()); - assertTrue(validation.hasErrors()); - } - - public void testAcceptFileWithoutEmptyExtensions() { - interceptor.setAllowedExtensions(".txt"); - - // when file is of allowed extensions - ValidationAwareSupport validation = new ValidationAwareSupport(); - boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName"); - - assertTrue(ok); - assertTrue(validation.getFieldErrors().isEmpty()); - assertFalse(validation.hasErrors()); - - // when file is not of allowed extensions - validation = new ValidationAwareSupport(); - boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/html", "inputName"); - - assertFalse(notOk); - assertFalse(validation.getFieldErrors().isEmpty()); - assertTrue(validation.hasErrors()); - - //test with multiple extensions - interceptor.setAllowedExtensions(".txt,.lol"); - validation = new ValidationAwareSupport(); - ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.lol", "text/plain", "inputName"); - - assertTrue(ok); - assertTrue(validation.getFieldErrors().isEmpty()); - assertFalse(validation.hasErrors()); - } - - public void testAcceptFileWithNoFile() { - FileUploadInterceptor interceptor = new FileUploadInterceptor(); - interceptor.setContainer(container); - - 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"); - - assertFalse(notOk); - assertFalse(validation.getFieldErrors().isEmpty()); - assertTrue(validation.hasErrors()); - List<String> errors = validation.getFieldErrors().get("inputName"); - assertEquals(1, errors.size()); - String msg = errors.get(0); - assertTrue(msg.startsWith("Error uploading:")); - assertTrue(msg.indexOf("inputName") > 0); - } - - 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", 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(); - boolean notOk = interceptor.acceptFile(validation, uploadedFile, "filename", "text/html", "inputName"); - - assertFalse(notOk); - assertFalse(validation.getFieldErrors().isEmpty()); - assertTrue(validation.hasErrors()); - List<String> errors = validation.getFieldErrors().get("inputName"); - assertEquals(1, errors.size()); - 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" - ); - } - - public void testNoMultipartRequest() throws Exception { - MyFileupAction action = new MyFileupAction(); - - MockActionInvocation mai = new MockActionInvocation(); - mai.setAction(action); - mai.setResultCode("NoMultipart"); - mai.setInvocationContext(ActionContext.getContext()); - - // if no multipart request it will bypass and execute it - assertEquals("NoMultipart", interceptor.intercept(mai)); - } - - public void testInvalidContentTypeMultipartRequest() throws Exception { - MockHttpServletRequest req = new MockHttpServletRequest(); - - req.setContentType("multipart/form-data"); // not a multipart contentype - req.setMethod("post"); - - MyFileupAction action = container.inject(MyFileupAction.class); - MockActionInvocation mai = new MockActionInvocation(); - mai.setAction(action); - mai.setResultCode("success"); - mai.setInvocationContext(ActionContext.getContext()); - - ActionContext.getContext().withParameters(HttpParameters.create().build()); - ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000)); - - interceptor.intercept(mai); - - assertTrue(action.hasErrors()); - } - - 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 - - MyFileupAction action = container.inject(MyFileupAction.class); - MockActionInvocation mai = new MockActionInvocation(); - mai.setAction(action); - mai.setResultCode("success"); - mai.setInvocationContext(ActionContext.getContext()); - - ActionContext.getContext().withParameters(HttpParameters.create().build()); - ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000)); - - interceptor.intercept(mai); - - assertTrue(action.hasErrors()); - } - - public void testSuccessUploadOfATextFileMultipartRequest() throws Exception { - MockHttpServletRequest req = new MockHttpServletRequest(); - req.setCharacterEncoding(StandardCharsets.UTF_8.name()); - req.setMethod("post"); - req.addHeader("Content-type", "multipart/form-data; boundary=---1234"); - - // inspired by the unit tests for jakarta commons fileupload - String content = ("-----1234\r\n" + - "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + - "Content-Type: text/html\r\n" + - "\r\n" + - "Unit test of FileUploadInterceptor" + - "\r\n" + - "-----1234--\r\n"); - req.setContent(content.getBytes(StandardCharsets.US_ASCII)); - - MyFileupAction action = new MyFileupAction(); - - MockActionInvocation mai = new MockActionInvocation(); - mai.setAction(action); - mai.setResultCode("success"); - 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)); - - interceptor.intercept(mai); - - assertFalse(action.hasErrors()); - - HttpParameters parameters = mai.getInvocationContext().getParameters(); - assertEquals(3, parameters.keySet().size()); - UploadedFile[] files = (UploadedFile[]) parameters.get("file").getObject(); - String[] fileContentTypes = parameters.get("fileContentType").getMultipleValues(); - String[] fileRealFilenames = parameters.get("fileFileName").getMultipleValues(); - - assertNotNull(files); - assertNotNull(fileContentTypes); - assertNotNull(fileRealFilenames); - assertEquals(1, files.length); - assertEquals(1, fileContentTypes.length); - assertEquals(1, fileRealFilenames.length); - assertEquals("text/html", fileContentTypes[0]); - assertNotNull("deleteme.txt", fileRealFilenames[0]); - } - - public void testSuccessUploadOfATextFileMultipartRequestNoMaxParamsSet() throws Exception { - MockHttpServletRequest req = new MockHttpServletRequest(); - req.setCharacterEncoding(StandardCharsets.UTF_8.name()); - req.setMethod("post"); - req.addHeader("Content-type", "multipart/form-data; boundary=---1234"); - - // inspired by the unit tests for jakarta commons fileupload - String content = ("-----1234\r\n" + - "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + - "Content-Type: text/html\r\n" + - "\r\n" + - "Unit test of FileUploadInterceptor" + - "\r\n" + - "-----1234--\r\n"); - req.setContent(content.getBytes(StandardCharsets.US_ASCII)); - - MyFileupAction action = new MyFileupAction(); - - MockActionInvocation mai = new MockActionInvocation(); - mai.setAction(action); - mai.setResultCode("success"); - mai.setInvocationContext(ActionContext.getContext()); - Map<String, Object> param = new HashMap<>(); - ActionContext.getContext().withParameters(HttpParameters.create(param).build()); - ActionContext.getContext().withServletRequest(createMultipartRequestNoMaxParamsSet(req)); - - interceptor.intercept(mai); - - assertFalse(action.hasErrors()); - - HttpParameters parameters = mai.getInvocationContext().getParameters(); - assertEquals(3, parameters.keySet().size()); - UploadedFile[] files = (UploadedFile[]) parameters.get("file").getObject(); - String[] fileContentTypes = parameters.get("fileContentType").getMultipleValues(); - String[] fileRealFilenames = parameters.get("fileFileName").getMultipleValues(); - - assertNotNull(files); - assertNotNull(fileContentTypes); - assertNotNull(fileRealFilenames); - assertEquals(1, files.length); - assertEquals(1, fileContentTypes.length); - assertEquals(1, fileRealFilenames.length); - assertEquals("text/html", fileContentTypes[0]); - assertNotNull("deleteme.txt", fileRealFilenames[0]); - } - - public void testSuccessUploadOfATextFileMultipartRequestWithNormalFieldsMaxParamsSet() throws Exception { - MockHttpServletRequest req = new MockHttpServletRequest(); - req.setCharacterEncoding(StandardCharsets.UTF_8.name()); - req.setMethod("post"); - req.addHeader("Content-type", "multipart/form-data; boundary=---1234"); - - // inspired by the unit tests for jakarta commons fileupload - String content = ("-----1234\r\n" + - "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + - "Content-Type: text/html\r\n" + - "\r\n" + - "Unit test of FileUploadInterceptor" + - "\r\n" + - "-----1234\r\n" + - "Content-Disposition: form-data; name=\"normalFormField1\"\r\n" + - "\r\n" + - "normal field 1" + - "\r\n" + - "-----1234\r\n" + - "Content-Disposition: form-data; name=\"normalFormField2\"\r\n" + - "\r\n" + - "normal field 2" + - "\r\n" + - "-----1234--\r\n"); - req.setContent(content.getBytes(StandardCharsets.US_ASCII)); - - MyFileupAction action = new MyFileupAction(); - - MockActionInvocation mai = new MockActionInvocation(); - mai.setAction(action); - mai.setResultCode("success"); - mai.setInvocationContext(ActionContext.getContext()); - Map<String, Object> param = new HashMap<>(); - ActionContext.getContext().withParameters(HttpParameters.create(param).build()); - ActionContext.getContext().withServletRequest(createMultipartRequest(req, 2000, 2000, 5, 100)); - - interceptor.intercept(mai); - - assertFalse(action.hasErrors()); - - HttpParameters parameters = mai.getInvocationContext().getParameters(); - assertEquals(3, parameters.keySet().size()); - UploadedFile[] files = (UploadedFile[]) parameters.get("file").getObject(); - String[] fileContentTypes = parameters.get("fileContentType").getMultipleValues(); - String[] fileRealFilenames = parameters.get("fileFileName").getMultipleValues(); - - assertNotNull(files); - assertNotNull(fileContentTypes); - assertNotNull(fileRealFilenames); - assertEquals(1, files.length); - assertEquals(1, fileContentTypes.length); - assertEquals(1, fileRealFilenames.length); - assertEquals("text/html", fileContentTypes[0]); - assertNotNull("deleteme.txt", fileRealFilenames[0]); - - // Confirm normalFormField1, normalFormField2 were processed by the MultiPartRequestWrapper. - HttpServletRequest invocationServletRequest = mai.getInvocationContext().getServletRequest(); - assertTrue("invocation servelt request is not a MultiPartRequestWrapper ?", invocationServletRequest instanceof MultiPartRequestWrapper); - MultiPartRequestWrapper multipartRequestWrapper = (MultiPartRequestWrapper) invocationServletRequest; - assertNotNull("normalFormField1 missing from MultiPartRequestWrapper parameters ?", multipartRequestWrapper.getParameter("normalFormField1")); - assertNotNull("normalFormField2 missing from MultiPartRequestWrapper parameters ?", multipartRequestWrapper.getParameter("normalFormField2")); - } - - public void testSuccessUploadOfATextFileMultipartRequestWithNormalFieldsNoMaxParamsSet() throws Exception { - MockHttpServletRequest req = new MockHttpServletRequest(); - req.setCharacterEncoding(StandardCharsets.UTF_8.name()); - req.setMethod("post"); - req.addHeader("Content-type", "multipart/form-data; boundary=---1234"); - - // inspired by the unit tests for jakarta commons fileupload - String content = ("-----1234\r\n" + - "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + - "Content-Type: text/html\r\n" + - "\r\n" + - "Unit test of FileUploadInterceptor" + - "\r\n" + - "-----1234\r\n" + - "Content-Disposition: form-data; name=\"normalFormField1\"\r\n" + - "\r\n" + - "normal field 1 with no max parameters set" + - "\r\n" + - "-----1234\r\n" + - "Content-Disposition: form-data; name=\"normalFormField2\"\r\n" + - "\r\n" + - "normal field 2 with no max parameters set" + - "\r\n" + - "-----1234--\r\n"); - req.setContent(content.getBytes(StandardCharsets.US_ASCII)); - - MyFileupAction action = new MyFileupAction(); - - MockActionInvocation mai = new MockActionInvocation(); - mai.setAction(action); - mai.setResultCode("success"); - mai.setInvocationContext(ActionContext.getContext()); - Map<String, Object> param = new HashMap<>(); - ActionContext.getContext().withParameters(HttpParameters.create(param).build()); - ActionContext.getContext().withServletRequest(createMultipartRequestNoMaxParamsSet(req)); - - interceptor.intercept(mai); - - assertFalse(action.hasErrors()); - - HttpParameters parameters = mai.getInvocationContext().getParameters(); - assertEquals(3, parameters.keySet().size()); - UploadedFile[] files = (UploadedFile[]) parameters.get("file").getObject(); - String[] fileContentTypes = parameters.get("fileContentType").getMultipleValues(); - String[] fileRealFilenames = parameters.get("fileFileName").getMultipleValues(); - - assertNotNull(files); - assertNotNull(fileContentTypes); - assertNotNull(fileRealFilenames); - assertEquals(1, files.length); - assertEquals(1, fileContentTypes.length); - assertEquals(1, fileRealFilenames.length); - assertEquals("text/html", fileContentTypes[0]); - assertNotNull("deleteme.txt", fileRealFilenames[0]); - - // Confirm normalFormField1, normalFormField2 were processed by the MultiPartRequestWrapper. - HttpServletRequest invocationServletRequest = mai.getInvocationContext().getServletRequest(); - assertTrue("invocation servelt request is not a MultiPartRequestWrapper ?", invocationServletRequest instanceof MultiPartRequestWrapper); - MultiPartRequestWrapper multipartRequestWrapper = (MultiPartRequestWrapper) invocationServletRequest; - assertNotNull("normalFormField1 missing from MultiPartRequestWrapper parameters ?", multipartRequestWrapper.getParameter("normalFormField1")); - assertNotNull("normalFormField2 missing from MultiPartRequestWrapper parameters ?", multipartRequestWrapper.getParameter("normalFormField2")); - } - - /** - * tests whether with multiple files sent with the same name, the ones with forbiddenTypes (see - * FileUploadInterceptor.setAllowedTypes(...) ) are sorted out. - */ - public void testMultipleAccept() throws Exception { - final String htmlContent = "<html><head></head><body>html content</body></html>"; - final String plainContent = "plain content"; - final String bondary = "simple boundary"; - final String endline = "\r\n"; - - MockHttpServletRequest req = new MockHttpServletRequest(); - req.setCharacterEncoding(StandardCharsets.UTF_8.name()); - 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; - req.setContent(content.getBytes()); - - assertTrue(ServletFileUpload.isMultipartContent(req)); - - MyFileupAction action = new MyFileupAction(); - container.inject(action); - MockActionInvocation mai = new MockActionInvocation(); - mai.setAction(action); - mai.setResultCode("success"); - 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)); - - interceptor.setAllowedTypes("text/html"); - interceptor.intercept(mai); - - HttpParameters parameters = mai.getInvocationContext().getParameters(); - assertEquals(3, parameters.keySet().size()); - UploadedFile[] files = (UploadedFile[]) parameters.get("file").getObject(); - String[] fileContentTypes = parameters.get("fileContentType").getMultipleValues(); - String[] fileRealFilenames = parameters.get("fileFileName").getMultipleValues(); - - assertNotNull(files); - assertNotNull(fileContentTypes); - assertNotNull(fileRealFilenames); - assertEquals("files accepted ", 2, files.length); - assertEquals(2, fileContentTypes.length); - assertEquals(2, fileRealFilenames.length); - assertEquals("text/html", fileContentTypes[0]); - assertNotNull("test1.html", fileRealFilenames[0]); - } - - public void testUnacceptedNumberOfFiles() throws Exception { - final String htmlContent = "<html><head></head><body>html content</body></html>"; - final String plainContent = "plain content"; - final String boundary = "simple boundary"; - final String endline = "\r\n"; - - MockHttpServletRequest req = new MockHttpServletRequest(); - req.setCharacterEncoding(StandardCharsets.UTF_8.name()); - 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; - req.setContent(content.getBytes()); - - assertTrue(ServletFileUpload.isMultipartContent(req)); - - MyFileupAction action = new MyFileupAction(); - container.inject(action); - MockActionInvocation mai = new MockActionInvocation(); - mai.setAction(action); - mai.setResultCode("success"); - 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)); - - interceptor.setAllowedTypes("text/html"); - interceptor.intercept(mai); - - HttpParameters parameters = mai.getInvocationContext().getParameters(); - assertEquals(0, parameters.keySet().size()); - assertEquals(1, action.getActionErrors().size()); - assertEquals("Request exceeded allowed number of files! Max allowed files number is: 3!", action.getActionErrors().iterator().next()); - } - - public void testMultipartRequestMaxFileSize() throws Exception { - MockHttpServletRequest req = new MockHttpServletRequest(); - req.setCharacterEncoding(StandardCharsets.UTF_8.name()); - req.setMethod("post"); - req.addHeader("Content-type", "multipart/form-data; boundary=---1234"); - - // inspired by the unit tests for jakarta commons fileupload - String content = ("-----1234\r\n" + - "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + - "Content-Type: text/html\r\n" + - "\r\n" + - "Unit test of FileUploadInterceptor" + - "\r\n" + - "-----1234--\r\n"); - req.setContent(content.getBytes(StandardCharsets.US_ASCII)); - - MyFileupAction action = container.inject(MyFileupAction.class); - - MockActionInvocation mai = new MockActionInvocation(); - mai.setAction(action); - mai.setResultCode("success"); - mai.setInvocationContext(ActionContext.getContext()); - Map<String, Object> param = new HashMap<>(); - ActionContext.getContext() - .withParameters(HttpParameters.create(param).build()) - .withServletRequest(createMultipartRequestMaxFileSize(req)); - - interceptor.intercept(mai); - - assertTrue(action.hasActionErrors()); - - Collection<String> errors = action.getActionErrors(); - assertEquals(1, errors.size()); - String msg = errors.iterator().next(); - assertEquals( - "File in request exceeded allowed file size limit! Max file size allowed is: 10 but file deleteme.txt was: 34!", - msg); - } - - public void testMultipartRequestMaxStringLength() throws Exception { - MockHttpServletRequest req = new MockHttpServletRequest(); - req.setCharacterEncoding(StandardCharsets.UTF_8.name()); - req.setMethod("post"); - req.addHeader("Content-type", "multipart/form-data; boundary=---1234"); - - // inspired by the unit tests for jakarta commons fileupload - String content = ("-----1234\r\n" + - "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + - "Content-Type: text/html\r\n" + - "\r\n" + - "Unit test of FileUploadInterceptor" + - "\r\n" + - "-----1234\r\n" + - "Content-Disposition: form-data; name=\"normalFormField1\"\r\n" + - "\r\n" + - "it works" + - "\r\n" + - "-----1234\r\n" + - "Content-Disposition: form-data; name=\"normalFormField2\"\r\n" + - "\r\n" + - "long string should not work" + - "\r\n" + - "-----1234--\r\n"); - req.setContent(content.getBytes(StandardCharsets.US_ASCII)); - - MyFileupAction action = container.inject(MyFileupAction.class); - - MockActionInvocation mai = new MockActionInvocation(); - mai.setAction(action); - mai.setResultCode("success"); - mai.setInvocationContext(ActionContext.getContext()); - Map<String, Object> param = new HashMap<>(); - ActionContext.getContext() - .withParameters(HttpParameters.create(param).build()) - .withServletRequest(createMultipartRequestMaxStringLength(req)); - - interceptor.intercept(mai); - - assertTrue(action.hasActionErrors()); - - Collection<String> errors = action.getActionErrors(); - 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); - } - - public void testMultipartRequestLocalizedError() throws Exception { - MockHttpServletRequest req = new MockHttpServletRequest(); - req.setCharacterEncoding(StandardCharsets.UTF_8.name()); - req.setMethod("post"); - req.addHeader("Content-type", "multipart/form-data; boundary=---1234"); - - // inspired by the unit tests for jakarta commons fileupload - String content = ("-----1234\r\n" + - "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + - "Content-Type: text/html\r\n" + - "\r\n" + - "Unit test of FileUploadInterceptor" + - "\r\n" + - "-----1234--\r\n"); - req.setContent(content.getBytes(StandardCharsets.US_ASCII)); - - MyFileupAction action = container.inject(MyFileupAction.class); - - MockActionInvocation mai = new MockActionInvocation(); - mai.setAction(action); - mai.setResultCode("success"); - mai.setInvocationContext(ActionContext.getContext()); - Map<String, Object> param = new HashMap<>(); - ActionContext.getContext() - .withParameters(HttpParameters.create(param).build()) - .withLocale(Locale.GERMAN) - .withServletRequest(createMultipartRequestMaxSize(req, 10)); - - interceptor.intercept(mai); - - assertTrue(action.hasActionErrors()); - - Collection<String> errors = action.getActionErrors(); - assertEquals(1, errors.size()); - String msg = errors.iterator().next(); - // the error message should contain at least this test - assertTrue(msg.startsWith("Der Request übertraf die maximal erlaubte Größe")); - } - - 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; - } - - private MultiPartRequestWrapper createMultipartRequestMaxFileSize(HttpServletRequest req) { - return createMultipartRequest(req, -1, 10, -1, -1); - } - - private MultiPartRequestWrapper createMultipartRequestMaxFiles(HttpServletRequest req) { - return createMultipartRequest(req, -1, -1, 3, -1); - } - - private MultiPartRequestWrapper createMultipartRequestMaxSize(HttpServletRequest req, int maxsize) { - return createMultipartRequest(req, maxsize, -1, -1, -1); - } - - private MultiPartRequestWrapper createMultipartRequestMaxStringLength(HttpServletRequest req) { - return createMultipartRequest(req, -1, -1, -1, 20); - } - - 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)); - return new MultiPartRequestWrapper(jak, req, tempDir.getAbsolutePath(), new DefaultLocaleProvider()); - } - - private MultiPartRequestWrapper createMultipartRequestNoMaxParamsSet(HttpServletRequest req) { - - JakartaMultiPartRequest jak = new JakartaMultiPartRequest(); - return new MultiPartRequestWrapper(jak, req, tempDir.getAbsolutePath(), new DefaultLocaleProvider()); - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - - interceptor = new FileUploadInterceptor(); - container.inject(interceptor); - tempDir = File.createTempFile("struts", "fileupload"); - tempDir.delete(); - tempDir.mkdirs(); - } - - @Override - protected void tearDown() throws Exception { - tempDir.delete(); - interceptor.destroy(); - super.tearDown(); - } - - public static class MyFileupAction extends ActionSupport { - - private static final long serialVersionUID = 6255238895447968889L; - - // no methods - // Note: We do not currently need fields/getters/setters for normalFormField1, normalFormField2 since - // the upload interceptor only prepares the normal field parameters. - } - -}