This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch WW-5273-servlet-upload in repository https://gitbox.apache.org/repos/asf/struts.git
commit a75934a9ff3e589ee382a624a5432981f772cb51 Author: Lukasz Lenart <lukaszlen...@apache.org> AuthorDate: Thu Dec 29 10:36:45 2022 +0100 WW-5273 Supports file upload using Servlet API 3.1 --- apps/showcase/{README.txt => README.md} | 11 +- apps/showcase/pom.xml | 4 - apps/showcase/src/main/resources/struts.xml | 2 + apps/showcase/src/main/webapp/WEB-INF/web.xml | 12 + .../org/apache/struts2/dispatcher/Dispatcher.java | 33 ++- .../dispatcher/filter/FileUploadSupport.java | 65 ++++++ .../dispatcher/filter/StrutsExecuteFilter.java | 17 +- .../filter/StrutsPrepareAndExecuteFilter.java | 7 +- .../dispatcher/filter/StrutsPrepareFilter.java | 2 +- .../multipart/AbstractMultiPartRequest.java | 6 +- .../multipart/JakartaMultiPartRequest.java | 2 + .../multipart/JakartaStreamMultiPartRequest.java | 2 + .../dispatcher/multipart/MultiPartRequest.java | 8 +- .../multipart/ServletMultiPartRequest.java | 256 +++++++++++++++++++++ .../dispatcher/servlet/FileUploadServlet.java | 81 +++++++ .../struts2/interceptor/FileUploadInterceptor.java | 15 +- .../org/apache/struts2/default.properties | 7 +- core/src/main/resources/struts-beans.xml | 2 + ...rutsPrepareAndExecuteFilterIntegrationTest.java | 31 ++- .../dispatcher/TwoFilterIntegrationTest.java | 55 +++-- .../multipart/ServletMultiPartRequestTest.java | 78 +++++++ .../interceptor/FileUploadInterceptorTest.java | 20 +- 22 files changed, 631 insertions(+), 85 deletions(-) diff --git a/apps/showcase/README.txt b/apps/showcase/README.md similarity index 74% rename from apps/showcase/README.txt rename to apps/showcase/README.md index 8e6cbb03c..a1b828042 100644 --- a/apps/showcase/README.txt +++ b/apps/showcase/README.md @@ -1,14 +1,11 @@ -README.txt - showcase +# Showcase App -Showcase is a collection of examples with code that you might be adopt and -adapt in your own applications. +Showcase is a collection of examples with code that you might be adopted and adapt in your own applications. -For more on getting started with Struts, see +For more on getting started with Struts, see https://struts.apache.org/getting-started/ -* http://cwiki.apache.org/WW/home.html +## I18N -I18N: -===== Please note that this project was created with the assumption that it will be run in an environment where the default locale is set to English. This means that the default messages defined in package.properties are in English. If the default diff --git a/apps/showcase/pom.xml b/apps/showcase/pom.xml index cc6d51d5e..9329b7968 100644 --- a/apps/showcase/pom.xml +++ b/apps/showcase/pom.xml @@ -140,10 +140,6 @@ <groupId>org.directwebremoting</groupId> <artifactId>dwr</artifactId> </dependency> - <dependency> - <groupId>commons-fileupload</groupId> - <artifactId>commons-fileupload</artifactId> - </dependency> <dependency> <groupId>junit</groupId> diff --git a/apps/showcase/src/main/resources/struts.xml b/apps/showcase/src/main/resources/struts.xml index cb47af8bf..ad71e9fdb 100644 --- a/apps/showcase/src/main/resources/struts.xml +++ b/apps/showcase/src/main/resources/struts.xml @@ -44,6 +44,8 @@ <constant name="struts.serve.static" value="true" /> <constant name="struts.serve.static.browserCache" value="false" /> + <constant name="struts.multipart.parser" value="servlet" /> + <constant name="struts.action.excludePattern" value=".*/images/.*\.gif,.*/img/.*\.gif,.*/styles/.*\.css,.*/js/.*\.js"/> <include file="struts-interactive.xml" /> diff --git a/apps/showcase/src/main/webapp/WEB-INF/web.xml b/apps/showcase/src/main/webapp/WEB-INF/web.xml index 1bcfa4184..63059a08b 100644 --- a/apps/showcase/src/main/webapp/WEB-INF/web.xml +++ b/apps/showcase/src/main/webapp/WEB-INF/web.xml @@ -114,6 +114,12 @@ <load-on-startup>1</load-on-startup> </servlet> + <servlet> + <servlet-name>fileUploadServlet</servlet-name> + <servlet-class>org.apache.struts2.dispatcher.servlet.FileUploadServlet</servlet-class> + <load-on-startup>2</load-on-startup> + </servlet> + <servlet> <servlet-name>strutsServlet</servlet-name> <servlet-class>org.apache.struts2.dispatcher.servlet.StrutsServlet</servlet-class> @@ -142,6 +148,12 @@ <load-on-startup>4</load-on-startup> </servlet> + <servlet-mapping> + <servlet-name>fileUploadServlet</servlet-name> + <url-pattern>/fileupload/*</url-pattern> + <url-pattern>/tags/ui/*</url-pattern> + </servlet-mapping> + <servlet-mapping> <servlet-name>dwr</servlet-name> <url-pattern>/dwr/*</url-pattern> diff --git a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java index 43794c1c5..cd2aa9572 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java @@ -112,12 +112,12 @@ public class Dispatcher { /** * Provide a thread local instance. */ - private static ThreadLocal<Dispatcher> instance = new ThreadLocal<>(); + private static final ThreadLocal<Dispatcher> instance = new ThreadLocal<>(); /** * Store list of DispatcherListeners. */ - private static List<DispatcherListener> dispatcherListeners = new CopyOnWriteArrayList<>(); + private static final List<DispatcherListener> dispatcherListeners = new CopyOnWriteArrayList<>(); /** * Store state of StrutsConstants.STRUTS_DEVMODE setting. @@ -140,7 +140,7 @@ public class Dispatcher { private String defaultLocale; /** - * Store state of StrutsConstants.STRUTS_MULTIPART_SAVEDIR setting. + * Store state of {@link StrutsConstants#STRUTS_MULTIPART_SAVEDIR} setting. */ private String multipartSaveDir; @@ -399,6 +399,7 @@ public class Dispatcher { private void init_FileManager() throws ClassNotFoundException { if (initParams.containsKey(StrutsConstants.STRUTS_FILE_MANAGER)) { final String fileManagerClassName = initParams.get(StrutsConstants.STRUTS_FILE_MANAGER); + @SuppressWarnings("unchecked") final Class<FileManager> fileManagerClass = (Class<FileManager>) Class.forName(fileManagerClassName); LOG.info("Custom FileManager specified: {}", fileManagerClassName); configurationManager.addContainerProvider(new FileManagerProvider(fileManagerClass, fileManagerClass.getSimpleName())); @@ -408,6 +409,7 @@ public class Dispatcher { } if (initParams.containsKey(StrutsConstants.STRUTS_FILE_MANAGER_FACTORY)) { final String fileManagerFactoryClassName = initParams.get(StrutsConstants.STRUTS_FILE_MANAGER_FACTORY); + @SuppressWarnings("unchecked") final Class<FileManagerFactory> fileManagerFactoryClass = (Class<FileManagerFactory>) Class.forName(fileManagerFactoryClassName); LOG.info("Custom FileManagerFactory specified: {}", fileManagerFactoryClassName); configurationManager.addContainerProvider(new FileManagerFactoryProvider(fileManagerFactoryClass)); @@ -471,7 +473,7 @@ public class Dispatcher { String[] classes = configProvs.split("\\s*[,]\\s*"); for (String cname : classes) { try { - Class cls = ClassLoaderUtil.loadClass(cname, this.getClass()); + Class<?> cls = ClassLoaderUtil.loadClass(cname, this.getClass()); ConfigurationProvider prov = (ConfigurationProvider) cls.newInstance(); if (prov instanceof ServletContextAwareConfigurationProvider) { ((ServletContextAwareConfigurationProvider) prov).initWithContext(servletContext); @@ -807,33 +809,26 @@ public class Dispatcher { if (saveDir.equals("")) { File tempdir = (File) servletContext.getAttribute("javax.servlet.context.tempdir"); - LOG.info("Unable to find 'struts.multipart.saveDir' property setting. Defaulting to javax.servlet.context.tempdir"); + LOG.info("The 'struts.multipart.saveDir' constant is not defined, defaulting to 'javax.servlet.context.tempdir'"); if (tempdir != null) { saveDir = tempdir.toString(); setMultipartSaveDir(saveDir); + } else { + LOG.warn("Cannot figure out the save directory, please either define: '{}' or configure your servlet container", + StrutsConstants.STRUTS_MULTIPART_SAVEDIR); } } else { File multipartSaveDir = new File(saveDir); if (!multipartSaveDir.exists()) { if (!multipartSaveDir.mkdirs()) { - String logMessage; - try { - logMessage = "Could not find create multipart save directory '" + multipartSaveDir.getCanonicalPath() + "'."; - } catch (IOException e) { - logMessage = "Could not find create multipart save directory '" + multipartSaveDir.toString() + "'."; - } - if (devMode) { - LOG.error(logMessage); - } else { - LOG.warn(logMessage); - } + LOG.warn("Could not create multipart save directory '{}'", multipartSaveDir); } } } - LOG.debug("saveDir={}", saveDir); + LOG.debug("The save directory is defined as: {}", saveDir); return saveDir; } @@ -941,7 +936,7 @@ public class Dispatcher { * @return false if disabled * @since 2.5.11 */ - protected boolean isMultipartSupportEnabled(HttpServletRequest request) { + public boolean isMultipartSupportEnabled(HttpServletRequest request) { return multipartSupportEnabled; } @@ -952,7 +947,7 @@ public class Dispatcher { * @return true if it is a multipart request * @since 2.5.11 */ - protected boolean isMultipartRequest(HttpServletRequest request) { + public boolean isMultipartRequest(HttpServletRequest request) { String httpMethod = request.getMethod(); String contentType = request.getContentType(); diff --git a/core/src/main/java/org/apache/struts2/dispatcher/filter/FileUploadSupport.java b/core/src/main/java/org/apache/struts2/dispatcher/filter/FileUploadSupport.java new file mode 100644 index 000000000..cdc09e385 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/dispatcher/filter/FileUploadSupport.java @@ -0,0 +1,65 @@ +/* + * 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.filter; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.RequestUtils; +import org.apache.struts2.dispatcher.Dispatcher; +import org.apache.struts2.dispatcher.multipart.MultiPartRequest; +import org.apache.struts2.dispatcher.multipart.ServletMultiPartRequest; +import sun.print.resources.serviceui; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +/** + * Detects if request is a multipart request with file upload, it works + * only when combined with {@link StrutsExecuteFilter} or {@link StrutsPrepareAndExecuteFilter} + * and {@link org.apache.struts2.dispatcher.servlet.FileUploadServlet} + */ +interface FileUploadSupport { + + Logger LOG = LogManager.getLogger(FileUploadSupport.class); + + default boolean shouldSkipProcessingFileUploadRequest(HttpServletRequest request) throws ServletException { + Dispatcher dispatcher = Dispatcher.getInstance(request.getServletContext()); + + if (dispatcher == null) { + throw new ServletException("Dispatcher is not initialised!"); + } + + if (dispatcher.isMultipartRequest(request)) { + MultiPartRequest multiPartRequest = dispatcher.getContainer().getInstance(MultiPartRequest.class); + if (multiPartRequest instanceof ServletMultiPartRequest) { + LOG.debug("Using the new Servlet API 3.1 based file upload support"); + if (dispatcher.isMultipartSupportEnabled(request)) { + LOG.debug("The file upload request is going to be handled by servlet"); + return true; + } + } else if (LOG.isDebugEnabled()){ + String servletPath = RequestUtils.getServletPath(request); + LOG.debug("Continue processing request: {} as other implementation of: {} is used: {}", + servletPath, MultiPartRequest.class, multiPartRequest.getClass()); + } + } + return false; + } + +} diff --git a/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsExecuteFilter.java b/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsExecuteFilter.java index 0e479c449..acdca655d 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsExecuteFilter.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsExecuteFilter.java @@ -20,12 +20,17 @@ package org.apache.struts2.dispatcher.filter; import org.apache.struts2.StrutsStatics; import org.apache.struts2.dispatcher.Dispatcher; -import org.apache.struts2.dispatcher.mapper.ActionMapping; import org.apache.struts2.dispatcher.ExecuteOperations; import org.apache.struts2.dispatcher.InitOperations; import org.apache.struts2.dispatcher.PrepareOperations; +import org.apache.struts2.dispatcher.mapper.ActionMapping; -import javax.servlet.*; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @@ -34,7 +39,7 @@ import java.io.IOException; * Executes the discovered request information. This filter requires the {@link StrutsPrepareFilter} to have already * been executed in the current chain. */ -public class StrutsExecuteFilter implements StrutsStatics, Filter { +public class StrutsExecuteFilter implements FileUploadSupport, StrutsStatics, Filter { protected PrepareOperations prepare; protected ExecuteOperations execute; @@ -53,7 +58,6 @@ public class StrutsExecuteFilter implements StrutsStatics, Filter { prepare = new PrepareOperations(dispatcher); execute = new ExecuteOperations(dispatcher); } - } public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { @@ -61,6 +65,11 @@ public class StrutsExecuteFilter implements StrutsStatics, Filter { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; + if (shouldSkipProcessingFileUploadRequest(request)) { + chain.doFilter(request, response); + return; + } + if (excludeUrl(request)) { chain.doFilter(request, response); return; diff --git a/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareAndExecuteFilter.java b/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareAndExecuteFilter.java index 54ee6883d..e9e85d23d 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareAndExecuteFilter.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareAndExecuteFilter.java @@ -44,7 +44,7 @@ import java.util.regex.Pattern; * Handles both the preparation and execution phases of the Struts dispatching process. This filter is better to use * when you don't have another filter that needs access to action context information, such as Sitemesh. */ -public class StrutsPrepareAndExecuteFilter implements StrutsStatics, Filter { +public class StrutsPrepareAndExecuteFilter implements FileUploadSupport, StrutsStatics, Filter { private static final Logger LOG = LogManager.getLogger(StrutsPrepareAndExecuteFilter.class); @@ -117,6 +117,11 @@ public class StrutsPrepareAndExecuteFilter implements StrutsStatics, Filter { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; + if (shouldSkipProcessingFileUploadRequest(request)) { + chain.doFilter(request, response); + return; + } + try { String uri = RequestUtils.getUri(request); if (excludedPatterns != null && prepare.isUrlExcluded(request, excludedPatterns)) { diff --git a/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareFilter.java b/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareFilter.java index b35d6cb10..a43b713a6 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareFilter.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/filter/StrutsPrepareFilter.java @@ -67,7 +67,7 @@ public class StrutsPrepareFilter implements StrutsStatics, Filter { /** * Callback for post initialization * - * @param dispatcher the dispatcher + * @param dispatcher the dispatcher * @param filterConfig the filter config */ protected void postInit(Dispatcher dispatcher, FilterConfig filterConfig) { diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java index 700364047..9984d2dc9 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java @@ -44,7 +44,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest { public static final int BUFFER_SIZE = 10240; /** - * Internal list of raised errors to be passed to the the Struts2 framework. + * Internal list of raised errors to be passed to the Struts2 framework. */ protected List<LocalizedMessage> errors = new ArrayList<>(); @@ -134,9 +134,9 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest { int forwardSlash = fileName.lastIndexOf('/'); int backwardSlash = fileName.lastIndexOf('\\'); if (forwardSlash != -1 && forwardSlash > backwardSlash) { - fileName = fileName.substring(forwardSlash + 1, fileName.length()); + fileName = fileName.substring(forwardSlash + 1); } else { - fileName = fileName.substring(backwardSlash + 1, fileName.length()); + fileName = fileName.substring(backwardSlash + 1); } return fileName; } diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java index c629ec043..1a3a37e22 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java @@ -39,7 +39,9 @@ import java.util.*; /** * Multipart form data request adapter for Jakarta Commons Fileupload package. + * @deprecated since Struts 6.2.0 - please use {@link ServletMultiPartRequest} instead */ +@Deprecated public class JakartaMultiPartRequest extends AbstractMultiPartRequest { static final Logger LOG = LogManager.getLogger(JakartaMultiPartRequest.class); diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java index 2d016f56b..2b069a6a9 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java @@ -40,7 +40,9 @@ import java.util.*; * * @author Chris Cranford * @since 2.3.18 + * @deprecated since Struts 6.2.0, please use {@link ServletMultiPartRequest} instead */ +@Deprecated public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { static final Logger LOG = LogManager.getLogger(JakartaStreamMultiPartRequest.class); diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequest.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequest.java index 445dcd9a2..f21a01e51 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequest.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequest.java @@ -18,11 +18,9 @@ */ package org.apache.struts2.dispatcher.multipart; -import javax.servlet.http.HttpServletRequest; - import org.apache.struts2.dispatcher.LocalizedMessage; -import java.io.File; +import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Enumeration; import java.util.List; @@ -33,7 +31,7 @@ import java.util.List; public interface MultiPartRequest { void parse(HttpServletRequest request, String saveDir) throws IOException; - + /** * Returns an enumeration of the parameter names for uploaded files * @@ -48,7 +46,7 @@ public interface MultiPartRequest { * * @param fieldName input field name * @return an array of content encoding for the specified input field name or <tt>null</tt> if - * no content type was specified. + * no content type was specified. */ String[] getContentType(String fieldName); diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/ServletMultiPartRequest.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/ServletMultiPartRequest.java new file mode 100644 index 000000000..756210820 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/ServletMultiPartRequest.java @@ -0,0 +1,256 @@ +/* + * 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.fileupload.FileItem; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.dispatcher.LocalizedMessage; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.Part; +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Pure Servlet API 3.1 based implementation + */ +public class ServletMultiPartRequest extends AbstractMultiPartRequest { + + private static final Logger LOG = LogManager.getLogger(ServletMultiPartRequest.class); + + private Map<String, List<FileData>> uploadedFiles = new HashMap<>(); + private Map<String, List<String>> parameters = new HashMap<>(); + + @Override + public void parse(HttpServletRequest request, String saveDir) throws IOException { + try { + if (isSizeLimitExceeded(request)) { + applySizeLimitExceededError(request); + return; + } + parseParts(request, saveDir); + } catch (ServletException e) { + LOG.warn("Error occurred during parsing of multi part request", e); + LocalizedMessage errorMessage = buildErrorMessage(e, new Object[]{e.getMessage()}); + if (!errors.contains(errorMessage)) { + errors.add(errorMessage); + } + } + } + + private void parseParts(HttpServletRequest request, String saveDir) throws IOException, ServletException { + Collection<Part> parts = request.getParts(); + if (parts.isEmpty()) { + LocalizedMessage error = buildErrorMessage(new IOException(), new Object[]{"No boundary defined!"}); + if (!errors.contains(error)) { + errors.add(error); + } + return; + } + for (Part part : parts) { + if (part.getSubmittedFileName() == null) { // normal field + LOG.debug("Ignoring a normal form field: {}", part.getName()); + } else { // file upload + LOG.debug("Storing file: {} in save dir: {}", part.getSubmittedFileName(), saveDir); + parseFile(part, saveDir); + } + } + } + + private boolean isSizeLimitExceeded(HttpServletRequest request) { + if (request.getContentLength() > -1) { + return maxSizeProvided && request.getContentLength() > maxSize; + } else { + LOG.debug("Request Content Length is: {} which means the size overflows 2 GB!", request.getContentLength()); + return true; + } + } + + private void applySizeLimitExceededError(HttpServletRequest request) { + String exceptionMessage = "Request size: " + request.getContentLength() + " exceeded maximum size limit: " + maxSize; + SizeLimitExceededException exception = new SizeLimitExceededException(exceptionMessage); + LocalizedMessage message = buildErrorMessage(exception, new Object[]{request.getContentLength(), maxSize}); + if (!errors.contains(message)) { + errors.add(message); + } + } + + private void parseFile(Part part, String saveDir) throws IOException { + File file = extractFile(part, saveDir); + List<FileData> data = uploadedFiles.get(part.getName()); + if (data == null) { + data = new ArrayList<>(); + } + data.add(new FileData(file, part.getContentType(), part.getSubmittedFileName())); + uploadedFiles.put(part.getName(), data); + } + + private File extractFile(Part part, String saveDir) throws IOException { + String name = part.getSubmittedFileName() + .substring(part.getSubmittedFileName().lastIndexOf('/') + 1) + .substring(part.getSubmittedFileName().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 tempFile = File.createTempFile(prefix + "_", suffix, new File(saveDir)); + LOG.debug("Stored file: {} as temporary file: {}", part.getSubmittedFileName(), tempFile.getName()); + return tempFile; + } + + @Override + public Enumeration<String> getFileParameterNames() { + return Collections.enumeration(uploadedFiles.keySet()); + } + + @Override + public String[] getContentType(String fieldName) { + List<FileData> fileData = uploadedFiles.get(fieldName); + if (fileData == null) { + LOG.debug("No file data for: {}", fieldName); + return null; + } + return fileData.stream().map(FileData::getContentType).toArray(String[]::new); + } + + @Override + public UploadedFile[] getFile(String fieldName) { + List<FileData> fileData = uploadedFiles.get(fieldName); + if (fileData == null) { + LOG.debug("No file data for: {}", fieldName); + return null; + } + + return fileData.stream().map(data -> new StrutsUploadedFile(data.getFile())).toArray(StrutsUploadedFile[]::new); + } + + @Override + public String[] getFileNames(String fieldName) { + List<FileData> fileData = uploadedFiles.get(fieldName); + if (fileData == null) { + LOG.debug("No file data for: {}", fieldName); + return null; + } + + return fileData.stream().map(FileData::getOriginalName).toArray(String[]::new); + } + + @Override + public String[] getFilesystemName(String fieldName) { + List<FileData> fileData = uploadedFiles.get(fieldName); + if (fileData == null) { + LOG.debug("No file data for: {}", fieldName); + return null; + } + + return fileData.stream().map(data -> data.getFile().getName()).toArray(String[]::new); + } + + @Override + public String getParameter(String name) { + List<String> params = parameters.get(name); + if (params != null && params.size() > 0) { + return params.get(0); + } + LOG.debug("Ignoring parameter: {}", name); + return null; + } + + @Override + public Enumeration<String> getParameterNames() { + return Collections.enumeration(parameters.keySet()); + } + + @Override + public String[] getParameterValues(String name) { + List<String> v = parameters.get(name); + if (v != null && v.size() > 0) { + return v.toArray(new String[0]); + } + + LOG.debug("Ignoring values for parameter: {}", name); + return null; + } + + @Override + public void cleanUp() { + for (List<FileData> fileData : uploadedFiles.values()) { + for (FileData data : fileData) { + LOG.debug("Removing file: {} {}", data.getOriginalName(), data.getFile().getAbsolutePath()); + if (!data.getFile().delete()) { + LOG.warn("Couldn't delete file: {}", data.getFile().getAbsolutePath()); + } + } + } + uploadedFiles = new HashMap<>(); + parameters = new HashMap<>(); + } + + public static class FileData implements Serializable { + + private final File file; + private final String contentType; + private final String originalName; + + public FileData(File file, String contentType, String originalName) { + this.file = file; + this.contentType = contentType; + this.originalName = originalName; + } + + public File getFile() { + return file; + } + + public String getContentType() { + return contentType; + } + + public String getOriginalName() { + return originalName; + } + } + + public static class SizeLimitExceededException extends Exception { + public SizeLimitExceededException(String message) { + super(message); + } + } +} diff --git a/core/src/main/java/org/apache/struts2/dispatcher/servlet/FileUploadServlet.java b/core/src/main/java/org/apache/struts2/dispatcher/servlet/FileUploadServlet.java new file mode 100644 index 000000000..880eb21d3 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/dispatcher/servlet/FileUploadServlet.java @@ -0,0 +1,81 @@ +/* + * 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.servlet; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.RequestUtils; +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.dispatcher.Dispatcher; +import org.apache.struts2.dispatcher.ExecuteOperations; +import org.apache.struts2.dispatcher.PrepareOperations; +import org.apache.struts2.dispatcher.mapper.ActionMapping; + +import javax.servlet.ServletException; +import javax.servlet.annotation.MultipartConfig; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * A dedicated servlet, which is used to support multipart requests using the Servlet API 3.1. + * It works in connection with @{link {@link org.apache.struts2.dispatcher.multipart.ServletMultiPartRequest}} + */ +@MultipartConfig +public class FileUploadServlet extends HttpServlet { + + private static final Logger LOG = LogManager.getLogger(FileUploadServlet.class); + + @Override + public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + Dispatcher dispatcher = Dispatcher.getInstance(request.getServletContext()); + if (dispatcher == null) { + throw new ServletException("No Dispatchers instance, servlet used out of the Struts filters!"); + } + + if (!dispatcher.isMultipartSupportEnabled(request)) { + LOG.warn("Support for file upload is disabled! Either please remove this servlet from web.xml or enable support for multipart requests using: {}!", + StrutsConstants.STRUTS_MULTIPART_ENABLED); + return; + } + + PrepareOperations prepare = new PrepareOperations(dispatcher); + ExecuteOperations execute = new ExecuteOperations(dispatcher); + + String uri = RequestUtils.getUri(request); + + if (dispatcher.isMultipartRequest(request)) { + prepare.setEncodingAndLocale(request, response); + prepare.createActionContext(request, response); + prepare.assignDispatcherToThread(); + HttpServletRequest wrappedRequest = prepare.wrapRequest(request); + ActionMapping mapping = prepare.findActionMapping(wrappedRequest, response, true); + if (mapping == null) { + throw new ServletException(String.format("Cannot find mapping for %s, passing to other filters", uri)); + } else { + LOG.trace("Found mapping {} for {}", mapping, uri); + execute.executeAction(wrappedRequest, response, mapping); + } + } else { + LOG.debug("Not a file upload request, ignoring uri: {}", uri); + } + } + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java index bb77ea093..482ab15b3 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java @@ -266,10 +266,10 @@ public class FileUploadInterceptor extends AbstractInterceptor { } // bind allowed Files - Enumeration fileParameterNames = multiWrapper.getFileParameterNames(); + Enumeration<String> fileParameterNames = multiWrapper.getFileParameterNames(); while (fileParameterNames != null && fileParameterNames.hasMoreElements()) { // get the value of this input tag - String inputName = (String) fileParameterNames.nextElement(); + String inputName = fileParameterNames.nextElement(); // get the content type String[] contentType = multiWrapper.getContentTypes(inputName); @@ -298,9 +298,9 @@ public class FileUploadInterceptor extends AbstractInterceptor { if (!acceptedFiles.isEmpty()) { Map<String, Parameter> newParams = new HashMap<>(); - newParams.put(inputName, new Parameter.File(inputName, acceptedFiles.toArray(new UploadedFile[acceptedFiles.size()]))); - newParams.put(contentTypeName, new Parameter.File(contentTypeName, acceptedContentTypes.toArray(new String[acceptedContentTypes.size()]))); - newParams.put(fileNameName, new Parameter.File(fileNameName, acceptedFileNames.toArray(new String[acceptedFileNames.size()]))); + 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); } } @@ -430,9 +430,10 @@ public class FileUploadInterceptor extends AbstractInterceptor { private boolean isNonEmpty(Object[] objArray) { boolean result = false; - for (int index = 0; index < objArray.length && !result; index++) { - if (objArray[index] != null) { + for (Object o : objArray) { + if (o != null) { result = true; + break; } } return result; diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties index 5847db3be..2bfd81083 100644 --- a/core/src/main/resources/org/apache/struts2/default.properties +++ b/core/src/main/resources/org/apache/struts2/default.properties @@ -61,9 +61,10 @@ struts.objectFactory.spring.enableAopSupport = false # struts.objectTypeDeterminer = notiger ### Parser to handle HTTP POST requests, encoded using the MIME-type multipart/form-data -# struts.multipart.parser=cos -# struts.multipart.parser=pell -# struts.multipart.parser=jakarta-stream +# struts.multipart.parser=servlet - using Servlet API 3.1 +# struts.multipart.parser=jakarta - using Commons Fileupload +# struts.multipart.parser=pell - deprecated +# struts.multipart.parser=jakarta-stream - using Commons Fileupload with stream support struts.multipart.parser=jakarta ### Uses javax.servlet.context.tempdir by default struts.multipart.saveDir= diff --git a/core/src/main/resources/struts-beans.xml b/core/src/main/resources/struts-beans.xml index eac1ff8be..87a517667 100644 --- a/core/src/main/resources/struts-beans.xml +++ b/core/src/main/resources/struts-beans.xml @@ -88,6 +88,8 @@ class="org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest" scope="prototype"/> <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="jakarta-stream" class="org.apache.struts2.dispatcher.multipart.JakartaStreamMultiPartRequest" scope="prototype"/> + <bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="servlet" + class="org.apache.struts2.dispatcher.multipart.ServletMultiPartRequest" scope="prototype"/> <bean type="org.apache.struts2.views.TagLibraryModelProvider" name="s" class="org.apache.struts2.views.DefaultTagLibrary"/> diff --git a/core/src/test/java/org/apache/struts2/dispatcher/StrutsPrepareAndExecuteFilterIntegrationTest.java b/core/src/test/java/org/apache/struts2/dispatcher/StrutsPrepareAndExecuteFilterIntegrationTest.java index a5419cd8b..f7212f22d 100644 --- a/core/src/test/java/org/apache/struts2/dispatcher/StrutsPrepareAndExecuteFilterIntegrationTest.java +++ b/core/src/test/java/org/apache/struts2/dispatcher/StrutsPrepareAndExecuteFilterIntegrationTest.java @@ -26,6 +26,7 @@ import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockFilterConfig; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; @@ -42,9 +43,10 @@ import java.util.ArrayList; public class StrutsPrepareAndExecuteFilterIntegrationTest extends TestCase { public void test404() throws ServletException, IOException { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockServletContext context = new MockServletContext(); + MockHttpServletRequest request = new MockHttpServletRequest(context); MockHttpServletResponse response = new MockHttpServletResponse(); - MockFilterConfig filterConfig = new MockFilterConfig(); + MockFilterConfig filterConfig = new MockFilterConfig(context); MockFilterChain filterChain = new MockFilterChain() { @Override public void doFilter(ServletRequest req, ServletResponse res) { @@ -62,9 +64,10 @@ public class StrutsPrepareAndExecuteFilterIntegrationTest extends TestCase { } public void test200() throws ServletException, IOException { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockServletContext context = new MockServletContext(); + MockHttpServletRequest request = new MockHttpServletRequest(context); MockHttpServletResponse response = new MockHttpServletResponse(); - MockFilterConfig filterConfig = new MockFilterConfig(); + MockFilterConfig filterConfig = new MockFilterConfig(context); MockFilterChain filterChain = new MockFilterChain() { @Override public void doFilter(ServletRequest req, ServletResponse res) { @@ -82,9 +85,10 @@ public class StrutsPrepareAndExecuteFilterIntegrationTest extends TestCase { } public void testActionMappingLookup() throws ServletException, IOException { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockServletContext context = new MockServletContext(); + MockHttpServletRequest request = new MockHttpServletRequest(context); MockHttpServletResponse response = new MockHttpServletResponse(); - MockFilterConfig filterConfig = new MockFilterConfig(); + MockFilterConfig filterConfig = new MockFilterConfig(context); MockFilterChain filterChain = new MockFilterChain() { @Override public void doFilter(ServletRequest req, ServletResponse res) { @@ -116,9 +120,10 @@ public class StrutsPrepareAndExecuteFilterIntegrationTest extends TestCase { } public void testUriPatternExclusion() throws ServletException, IOException { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockServletContext context = new MockServletContext(); + MockHttpServletRequest request = new MockHttpServletRequest(context); MockHttpServletResponse response = new MockHttpServletResponse(); - MockFilterConfig filterConfig = new MockFilterConfig(); + MockFilterConfig filterConfig = new MockFilterConfig(context); MockFilterChain filterChain = new MockFilterChain() { @Override public void doFilter(ServletRequest req, ServletResponse res) { @@ -142,9 +147,10 @@ public class StrutsPrepareAndExecuteFilterIntegrationTest extends TestCase { } public void testStaticFallthrough() throws ServletException, IOException { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockServletContext context = new MockServletContext(); + MockHttpServletRequest request = new MockHttpServletRequest(context); MockHttpServletResponse response = new MockHttpServletResponse(); - MockFilterConfig filterConfig = new MockFilterConfig(); + MockFilterConfig filterConfig = new MockFilterConfig(context); MockFilterChain filterChain = new MockFilterChain() { @Override public void doFilter(ServletRequest req, ServletResponse res) { @@ -169,9 +175,10 @@ public class StrutsPrepareAndExecuteFilterIntegrationTest extends TestCase { } public void testStaticExecute() throws ServletException, IOException { - MockHttpServletRequest request = new MockHttpServletRequest(); + MockServletContext context = new MockServletContext(); + MockHttpServletRequest request = new MockHttpServletRequest(context); MockHttpServletResponse response = new MockHttpServletResponse(); - MockFilterConfig filterConfig = new MockFilterConfig(); + MockFilterConfig filterConfig = new MockFilterConfig(context); MockFilterChain filterChain = new MockFilterChain() { @Override public void doFilter(ServletRequest req, ServletResponse res) { diff --git a/core/src/test/java/org/apache/struts2/dispatcher/TwoFilterIntegrationTest.java b/core/src/test/java/org/apache/struts2/dispatcher/TwoFilterIntegrationTest.java index edc88ba17..7075c7b68 100644 --- a/core/src/test/java/org/apache/struts2/dispatcher/TwoFilterIntegrationTest.java +++ b/core/src/test/java/org/apache/struts2/dispatcher/TwoFilterIntegrationTest.java @@ -20,44 +20,60 @@ package org.apache.struts2.dispatcher; import com.opensymphony.xwork2.ActionContext; import junit.framework.TestCase; -import org.apache.struts2.dispatcher.Dispatcher; -import org.apache.struts2.dispatcher.PrepareOperations; import org.apache.struts2.dispatcher.filter.StrutsExecuteFilter; import org.apache.struts2.dispatcher.filter.StrutsPrepareFilter; -import org.springframework.mock.web.*; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockFilterConfig; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; -import javax.servlet.*; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; import java.io.IOException; -import java.util.LinkedList; import java.util.Arrays; +import java.util.LinkedList; /** * Integration tests for the filter */ public class TwoFilterIntegrationTest extends TestCase { - StrutsExecuteFilter filterExecute; - StrutsPrepareFilter filterPrepare; - Filter failFilter; + + private StrutsExecuteFilter filterExecute; + private StrutsPrepareFilter filterPrepare; + private Filter failFilter; private Filter stringFilter; public void setUp() { filterPrepare = new StrutsPrepareFilter(); filterExecute = new StrutsExecuteFilter(); failFilter = new Filter() { - public void init(FilterConfig filterConfig) throws ServletException {} + public void init(FilterConfig filterConfig) throws ServletException { + } + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { fail("Should never get here"); } - public void destroy() {} + + public void destroy() { + } }; stringFilter = new Filter() { - public void init(FilterConfig filterConfig) throws ServletException {} + public void init(FilterConfig filterConfig) throws ServletException { + } + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { response.getWriter().write("content"); assertNotNull(ActionContext.getContext()); assertNotNull(Dispatcher.getInstance()); } - public void destroy() {} + + public void destroy() { + } }; } @@ -86,7 +102,9 @@ public class TwoFilterIntegrationTest extends TestCase { public void testFilterInMiddle() throws ServletException, IOException { Filter middle = new Filter() { - public void init(FilterConfig filterConfig) throws ServletException {} + public void init(FilterConfig filterConfig) throws ServletException { + } + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { assertNotNull(ActionContext.getContext()); assertNotNull(Dispatcher.getInstance()); @@ -94,7 +112,9 @@ public class TwoFilterIntegrationTest extends TestCase { chain.doFilter(request, response); assertEquals("hello", ActionContext.getContext().getActionInvocation().getProxy().getActionName()); } - public void destroy() {} + + public void destroy() { + } }; MockHttpServletResponse response = run("/hello.action", filterPrepare, middle, filterExecute, failFilter); assertEquals(200, response.getStatus()); @@ -102,9 +122,12 @@ public class TwoFilterIntegrationTest extends TestCase { private MockHttpServletResponse run(String uri, final Filter... filters) throws ServletException, IOException { final LinkedList<Filter> filterList = new LinkedList<>(Arrays.asList(filters)); - MockHttpServletRequest request = new MockHttpServletRequest(); + + MockServletContext context = new MockServletContext(); + MockHttpServletRequest request = new MockHttpServletRequest(context); MockHttpServletResponse response = new MockHttpServletResponse(); - MockFilterConfig filterConfig = new MockFilterConfig(); + MockFilterConfig filterConfig = new MockFilterConfig(context); + MockFilterChain filterChain = new MockFilterChain() { @Override public void doFilter(ServletRequest req, ServletResponse res) { diff --git a/core/src/test/java/org/apache/struts2/dispatcher/multipart/ServletMultiPartRequestTest.java b/core/src/test/java/org/apache/struts2/dispatcher/multipart/ServletMultiPartRequestTest.java new file mode 100644 index 000000000..b036477b2 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/dispatcher/multipart/ServletMultiPartRequestTest.java @@ -0,0 +1,78 @@ +/* + * 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.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 javax.servlet.http.HttpServletRequest; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ServletMultiPartRequestTest { + + private ServletMultiPartRequest multiPart; + private Path tempDir; + + @Before + public void initialize() { + multiPart = new ServletMultiPartRequest(); + tempDir = Paths.get("target", "multi-part-test"); + } + + /** + * Number of bytes in files greater than 2GB overflow the {@code int} primitive. + * 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 = mock(HttpServletRequest.class); + when(request.getContentType()).thenReturn("multipart/form-data; charset=utf-8; boundary=__X_BOUNDARY__"); + when(request.getMethod()).thenReturn("POST"); + 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"; + 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(); + + assertEquals(next.getTextKey(), "struts.messages.upload.error.SizeLimitExceededException"); + } +} diff --git a/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java index b6a41010e..366db033e 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java @@ -29,11 +29,13 @@ 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.ServletMultiPartRequest; import org.apache.struts2.dispatcher.multipart.StrutsUploadedFile; import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper; import org.apache.struts2.dispatcher.multipart.UploadedFile; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockPart; import javax.servlet.http.HttpServletRequest; import java.io.File; @@ -225,7 +227,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { public void testInvalidContentTypeMultipartRequest() throws Exception { MockHttpServletRequest req = new MockHttpServletRequest(); - req.setContentType("multipart/form-data"); // not a multipart contentype + req.setContentType("multipart/form-data"); // not a multipart content type req.setMethod("post"); MyFileupAction action = container.inject(MyFileupAction.class); @@ -279,6 +281,9 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { "\r\n" + "-----1234--\r\n"); req.setContent(content.getBytes("US-ASCII")); + MockPart part = new MockPart("file", "deleteme.txt", "Unit test of FileUploadInterceptor".getBytes()); + part.getHeaders().setContentType(MediaType.TEXT_HTML); + req.addPart(part); MyFileupAction action = new MyFileupAction(); @@ -338,6 +343,15 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { content.append("--"); content.append(endline); req.setContent(content.toString().getBytes()); + MockPart part1 = new MockPart("file", "test.html", plainContent.getBytes()); + part1.getHeaders().setContentType(MediaType.TEXT_PLAIN); + req.addPart(part1); + MockPart part2 = new MockPart("file", "test1.html", htmlContent.getBytes()); + part2.getHeaders().setContentType(MediaType.TEXT_HTML); + req.addPart(part2); + MockPart part3 = new MockPart("file", "test2.html", htmlContent.getBytes()); + part3.getHeaders().setContentType(MediaType.TEXT_HTML); + req.addPart(part3); assertTrue(ServletFileUpload.isMultipartContent(req)); @@ -430,7 +444,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { } private MultiPartRequestWrapper createMultipartRequest(HttpServletRequest req, int maxsize) throws IOException { - JakartaMultiPartRequest jak = new JakartaMultiPartRequest(); + ServletMultiPartRequest jak = new ServletMultiPartRequest(); jak.setMaxSize(String.valueOf(maxsize)); return new MultiPartRequestWrapper(jak, req, tempDir.getAbsolutePath(), new DefaultLocaleProvider()); }