This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch feature/WW-5371-modern-upload in repository https://gitbox.apache.org/repos/asf/struts.git
commit 0bc0217b9de49545b8307a98d6ca2cce3a85390f Author: Lukasz Lenart <lukaszlen...@apache.org> AuthorDate: Mon Dec 11 07:37:38 2023 +0100 WW-5371 Implements action based file upload --- .../UploadedFilesAware.java} | 32 +- .../multipart/JakartaMultiPartRequest.java | 6 +- .../multipart/JakartaStreamMultiPartRequest.java | 36 ++- .../dispatcher/multipart/StrutsUploadedFile.java | 51 ++- .../struts2/dispatcher/multipart/UploadedFile.java | 4 + .../interceptor/AbstractFileUploadInterceptor.java | 247 ++++++++++++++ .../interceptor/ActionFileUploadInterceptor.java | 248 ++++++++++++++ .../struts2/interceptor/FileUploadInterceptor.java | 256 ++------------- core/src/main/resources/struts-default.xml | 9 + .../conversion/UploadedFileConverterTest.java | 19 +- ...t.java => ActionFileUploadInterceptorTest.java} | 359 ++++++++++----------- .../interceptor/FileUploadInterceptorTest.java | 170 +++++----- .../dispatcher/multipart/PellMultiPartRequest.java | 13 +- 13 files changed, 897 insertions(+), 553 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/UploadedFile.java b/core/src/main/java/org/apache/struts2/action/UploadedFilesAware.java similarity index 54% copy from core/src/main/java/org/apache/struts2/dispatcher/multipart/UploadedFile.java copy to core/src/main/java/org/apache/struts2/action/UploadedFilesAware.java index d4edb5221..635e0b2d4 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/UploadedFile.java +++ b/core/src/main/java/org/apache/struts2/action/UploadedFilesAware.java @@ -16,23 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.struts2.dispatcher.multipart; +package org.apache.struts2.action; -/** - * Virtual representation of a uploaded file used by {@link MultiPartRequest} - */ -public interface UploadedFile { - - Long length(); - - String getName(); +import org.apache.struts2.dispatcher.multipart.UploadedFile; - boolean isFile(); +import java.util.List; - boolean delete(); - - String getAbsolutePath(); - - Object getContent(); +/** + * Actions that want to be aware of all the uploaded file should implement this interface. + * The {@link org.apache.struts2.interceptor.ActionFileUploadInterceptor} will use the interface + * to notify action about the multiple uploaded files. + */ +public interface UploadedFilesAware { + + /** + * Notifies action about the multiple uploaded files, when a single file is uploaded + * the list will have just one element + * + * @param uploadedFiles a list of {@link UploadedFile}. + */ + void withUploadedFiles(List<UploadedFile> uploadedFiles); } 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 20b948fd3..1c0f458a0 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 @@ -243,7 +243,11 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest { LOG.error("Cannot write uploaded empty file to disk: {}", storeLocation.getAbsolutePath(), e); } } - fileList.add(new StrutsUploadedFile(storeLocation)); + UploadedFile uploadedFile = StrutsUploadedFile.Builder.create(storeLocation) + .withContentType(fileItem.getContentType()) + .withOriginalName(fileItem.getName()) + .build(); + fileList.add(uploadedFile); } return fileList.toArray(new UploadedFile[0]); 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 dec0b6841..2618d6ace 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 @@ -22,6 +22,7 @@ import org.apache.commons.fileupload.FileItemIterator; import org.apache.commons.fileupload.FileItemStream; import org.apache.commons.fileupload.FileUploadBase; import org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededException; +import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.fileupload.util.Streams; import org.apache.logging.log4j.LogManager; @@ -110,7 +111,11 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { List<UploadedFile> files = new ArrayList<>(infos.size()); for (FileInfo fileInfo : infos) { - files.add(new StrutsUploadedFile(fileInfo.getFile())); + UploadedFile file = StrutsUploadedFile.Builder.create(fileInfo.getFile()) + .withContentType(fileInfo.contentType) + .withOriginalName(fileInfo.originalName) + .build(); + files.add(file); } return files.toArray(new UploadedFile[0]); @@ -162,7 +167,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { */ public String getParameter(String name) { List<String> values = parameters.get(name); - if (values != null && values.size() > 0) { + if (values != null && !values.isEmpty()) { return values.get(0); } return null; @@ -180,7 +185,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { */ public String[] getParameterValues(String name) { List<String> values = parameters.get(name); - if (values != null && values.size() > 0) { + if (values != null && !values.isEmpty()) { return values.toArray(new String[0]); } return null; @@ -207,9 +212,8 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { * * @param request the servlet request * @param saveDir location of the save dir - * @throws Exception */ - protected void processUpload(HttpServletRequest request, String saveDir) throws Exception { + protected void processUpload(HttpServletRequest request, String saveDir) throws IOException, FileUploadException { // Sanity check that the request is a multi-part/form-data request. if (ServletFileUpload.isMultipartContent(request)) { @@ -292,7 +296,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { * Add a file skipped message notification for action messages. * * @param fileName file name - * @param request the servlet request + * @param request the servlet request */ protected void addFileSkippedError(String fileName, HttpServletRequest request) { String exceptionMessage = "Skipped file " + fileName + "; request size limit exceeded."; @@ -330,11 +334,11 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { * Processes the FileItemStream as a file field. * * @param itemStream file item stream - * @param location location + * @param location location */ protected void processFileItemStreamAsFileField(FileItemStream itemStream, String location) { // Skip file uploads that don't have a file name - meaning that no file was selected. - if (itemStream.getName() == null || itemStream.getName().trim().length() < 1) { + if (itemStream.getName() == null || itemStream.getName().trim().isEmpty()) { LOG.debug("No file has been uploaded for the field: {}", itemStream.getFieldName()); return; } @@ -368,8 +372,8 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { */ protected File createTemporaryFile(String fileName, String location) throws IOException { String name = fileName - .substring(fileName.lastIndexOf('/') + 1) - .substring(fileName.lastIndexOf('\\') + 1); + .substring(fileName.lastIndexOf('/') + 1) + .substring(fileName.lastIndexOf('\\') + 1); String prefix = name; String suffix = ""; @@ -392,14 +396,14 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { * Streams the file upload stream to the specified file. * * @param itemStream file item stream - * @param file the file + * @param file the file * @return true if stream was successfully * @throws IOException in case of IO errors */ protected boolean streamFileToDisk(FileItemStream itemStream, File file) throws IOException { boolean result; try (InputStream input = itemStream.openStream(); - OutputStream output = new BufferedOutputStream(Files.newOutputStream(file.toPath()), bufferSize)) { + OutputStream output = new BufferedOutputStream(Files.newOutputStream(file.toPath()), bufferSize)) { byte[] buffer = new byte[bufferSize]; LOG.debug("Streaming file using buffer size {}.", bufferSize); for (int length; ((length = input.read(buffer)) > 0); ) { @@ -416,7 +420,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { * invocation process. * * @param itemStream file item stream - * @param file the file + * @param file the file */ protected void createFileInfoFromItemStream(FileItemStream itemStream, File file) { // gather attributes from file upload stream. @@ -440,7 +444,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { * * @since 7.0.0 */ - public static class FileInfo implements Serializable { + public static class FileInfo implements Serializable { private static final long serialVersionUID = 1083158552766906037L; @@ -451,8 +455,8 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { /** * Default constructor. * - * @param file the file - * @param contentType content type + * @param file the file + * @param contentType content type * @param originalName original file name */ public FileInfo(File file, String contentType, String originalName) { diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/StrutsUploadedFile.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/StrutsUploadedFile.java index e09d79aff..97cb9cefd 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/StrutsUploadedFile.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/StrutsUploadedFile.java @@ -22,10 +22,21 @@ import java.io.File; public class StrutsUploadedFile implements UploadedFile { - private File file; + private final File file; + private final String contentType; + private final String originalName; + @Deprecated public StrutsUploadedFile(File file) { this.file = file; + this.contentType = null; + this.originalName = null; + } + + private StrutsUploadedFile(File file, String contentType, String originalName) { + this.file = file; + this.contentType = contentType; + this.originalName = originalName; } @Override @@ -57,4 +68,42 @@ public class StrutsUploadedFile implements UploadedFile { public File getContent() { return file; } + + @Override + public String getContentType() { + return this.contentType; + } + + @Override + public String getOriginalName() { + return originalName; + } + + public static class Builder { + private final File file; + private String contentType; + private String originalName; + + private Builder(File file) { + this.file = file; + } + + public static Builder create(File file) { + return new Builder(file); + } + + public Builder withContentType(String contentType) { + this.contentType = contentType; + return this; + } + + public Builder withOriginalName(String originalName) { + this.originalName = originalName; + return this; + } + + public UploadedFile build() { + return new StrutsUploadedFile(this.file, this.contentType, this.originalName); + } + } } diff --git a/core/src/main/java/org/apache/struts2/dispatcher/multipart/UploadedFile.java b/core/src/main/java/org/apache/struts2/dispatcher/multipart/UploadedFile.java index d4edb5221..896c681be 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/multipart/UploadedFile.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/multipart/UploadedFile.java @@ -27,6 +27,8 @@ public interface UploadedFile { String getName(); + String getOriginalName(); + boolean isFile(); boolean delete(); @@ -35,4 +37,6 @@ public interface UploadedFile { Object getContent(); + String getContentType(); + } diff --git a/core/src/main/java/org/apache/struts2/interceptor/AbstractFileUploadInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/AbstractFileUploadInterceptor.java new file mode 100644 index 000000000..3857a7b4e --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/AbstractFileUploadInterceptor.java @@ -0,0 +1,247 @@ +/* + * 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.LocaleProvider; +import com.opensymphony.xwork2.LocaleProviderFactory; +import com.opensymphony.xwork2.TextProvider; +import com.opensymphony.xwork2.TextProviderFactory; +import com.opensymphony.xwork2.inject.Container; +import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.interceptor.AbstractInterceptor; +import com.opensymphony.xwork2.interceptor.ValidationAware; +import com.opensymphony.xwork2.util.TextParseUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.dispatcher.LocalizedMessage; +import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper; +import org.apache.struts2.dispatcher.multipart.UploadedFile; +import org.apache.struts2.util.ContentTypeMatcher; + +import java.text.NumberFormat; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Set; + +public abstract class AbstractFileUploadInterceptor extends AbstractInterceptor { + + private static final Logger LOG = LogManager.getLogger(AbstractFileUploadInterceptor.class); + + protected Long maximumSize; + protected Set<String> allowedTypesSet = Collections.emptySet(); + protected Set<String> allowedExtensionsSet = Collections.emptySet(); + + private ContentTypeMatcher<Object> matcher; + private Container container; + + @Inject + public void setMatcher(ContentTypeMatcher<Object> matcher) { + this.matcher = matcher; + } + + @Inject + public void setContainer(Container container) { + this.container = container; + } + + /** + * Sets the allowed extensions + * + * @param allowedExtensions A comma-delimited list of extensions + */ + public void setAllowedExtensions(String allowedExtensions) { + allowedExtensionsSet = TextParseUtil.commaDelimitedStringToSet(allowedExtensions); + } + + /** + * Sets the allowed mimetypes + * + * @param allowedTypes A comma-delimited list of types + */ + public void setAllowedTypes(String allowedTypes) { + allowedTypesSet = TextParseUtil.commaDelimitedStringToSet(allowedTypes); + } + + /** + * Sets the maximum size of an uploaded file + * + * @param maximumSize The maximum size in bytes + */ + public void setMaximumSize(Long maximumSize) { + this.maximumSize = maximumSize; + } + + /** + * Override for added functionality. Checks if the proposed file is acceptable based on contentType and size. + * + * @param action - uploading action for message retrieval. + * @param file - proposed upload file. + * @param filename - name of the file. + * @param contentType - contentType of the file. + * @param inputName - inputName of the file. + * @return true if the proposed file is acceptable by contentType and size. + */ + protected boolean acceptFile(Object action, UploadedFile file, String filename, String contentType, String inputName) { + boolean fileIsAcceptable = false; + + ValidationAware validation = null; + if (action instanceof ValidationAware) { + validation = (ValidationAware) action; + } + + // If it's null the upload failed + if (file == null) { + String errMsg = getTextMessage(action, "struts.messages.error.uploading", new String[]{inputName}); + if (validation != null) { + validation.addFieldError(inputName, errMsg); + } + LOG.warn(errMsg); + } else if (file.getContent() == null) { + String errMsg = getTextMessage(action, "struts.messages.error.uploading", new String[]{filename}); + if (validation != null) { + validation.addFieldError(inputName, errMsg); + } + LOG.warn(errMsg); + } else if (maximumSize != null && maximumSize < file.length()) { + String errMsg = getTextMessage(action, "struts.messages.error.file.too.large", new String[]{inputName, filename, file.getName(), "" + file.length(), getMaximumSizeStr(action)}); + if (validation != null) { + validation.addFieldError(inputName, errMsg); + } + LOG.warn(errMsg); + } else if ((!allowedTypesSet.isEmpty()) && (!containsItem(allowedTypesSet, contentType))) { + String errMsg = getTextMessage(action, "struts.messages.error.content.type.not.allowed", new String[]{inputName, filename, file.getName(), contentType}); + if (validation != null) { + validation.addFieldError(inputName, errMsg); + } + LOG.warn(errMsg); + } else if ((!allowedExtensionsSet.isEmpty()) && (!hasAllowedExtension(allowedExtensionsSet, filename))) { + String errMsg = getTextMessage(action, "struts.messages.error.file.extension.not.allowed", new String[]{inputName, filename, file.getName(), contentType}); + if (validation != null) { + validation.addFieldError(inputName, errMsg); + } + LOG.warn(errMsg); + } else { + fileIsAcceptable = true; + } + + return fileIsAcceptable; + } + + private String getMaximumSizeStr(Object action) { + return NumberFormat.getNumberInstance(getLocaleProvider(action).getLocale()).format(maximumSize); + } + + /** + * @param extensionCollection - Collection of extensions (all lowercase). + * @param filename - filename to check. + * @return true if the filename has an allowed extension, false otherwise. + */ + private boolean hasAllowedExtension(Collection<String> extensionCollection, String filename) { + if (filename == null) { + return false; + } + + String lowercaseFilename = filename.toLowerCase(); + for (String extension : extensionCollection) { + if (lowercaseFilename.endsWith(extension)) { + return true; + } + } + + return false; + } + + /** + * @param itemCollection - Collection of string items (all lowercase). + * @param item - Item to search for. + * @return true if itemCollection contains the item, false otherwise. + */ + private boolean containsItem(Collection<String> itemCollection, String item) { + for (String pattern : itemCollection) + if (matchesWildcard(pattern, item)) + return true; + return false; + } + + private boolean matchesWildcard(String pattern, String text) { + Object o = matcher.compilePattern(pattern); + return matcher.match(new HashMap<>(), text, o); + } + + protected boolean isNonEmpty(Object[] objArray) { + boolean result = false; + for (Object o : objArray) { + if (o != null) { + result = true; + break; + } + } + return result; + } + + protected String getTextMessage(String messageKey, String[] args) { + return getTextMessage(this, messageKey, args); + } + + protected String getTextMessage(Object action, String messageKey, String[] args) { + if (action instanceof TextProvider) { + return ((TextProvider) action).getText(messageKey, args); + } + return getTextProvider(action).getText(messageKey, args); + } + + protected TextProvider getTextProvider(Object action) { + TextProviderFactory tpf = container.getInstance(TextProviderFactory.class); + return tpf.createInstance(action.getClass()); + } + + private LocaleProvider getLocaleProvider(Object action) { + LocaleProvider localeProvider; + if (action instanceof LocaleProvider) { + localeProvider = (LocaleProvider) action; + } else { + LocaleProviderFactory localeProviderFactory = container.getInstance(LocaleProviderFactory.class); + localeProvider = localeProviderFactory.createLocaleProvider(); + } + return localeProvider; + } + + protected void applyValidation(Object action, MultiPartRequestWrapper multiWrapper) { + ValidationAware validation = null; + if (action instanceof ValidationAware) { + validation = (ValidationAware) action; + } + + if (multiWrapper.hasErrors() && validation != null) { + TextProvider textProvider = getTextProvider(action); + for (LocalizedMessage error : multiWrapper.getErrors()) { + String errorMessage; + if (textProvider.hasKey(error.getTextKey())) { + errorMessage = textProvider.getText(error.getTextKey(), Arrays.asList(error.getArgs())); + } else { + errorMessage = textProvider.getText("struts.messages.error.uploading", error.getDefaultMessage()); + } + validation.addActionError(errorMessage); + } + } + } + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/ActionFileUploadInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ActionFileUploadInterceptor.java new file mode 100644 index 000000000..4cff8617e --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/ActionFileUploadInterceptor.java @@ -0,0 +1,248 @@ +/* + * 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.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.action.UploadedFilesAware; +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.List; + +/** + * <!-- 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> + */ +public class ActionFileUploadInterceptor extends AbstractFileUploadInterceptor { + + protected static final Logger LOG = LogManager.getLogger(ActionFileUploadInterceptor.class); + + /* (non-Javadoc) + * @see com.opensymphony.xwork2.interceptor.Interceptor#intercept(com.opensymphony.xwork2.ActionInvocation) + */ + public String intercept(ActionInvocation invocation) throws Exception { + HttpServletRequest request = invocation.getInvocationContext().getServletRequest(); + if (!(request instanceof MultiPartRequestWrapper)) { + ActionProxy proxy = invocation.getProxy(); + LOG.debug(getTextMessage("struts.messages.bypass.request", new String[]{proxy.getNamespace(), proxy.getActionName()})); + return invocation.invoke(); + } + + MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request; + + if (!(invocation.getAction() instanceof UploadedFilesAware)) { + LOG.debug("Action: {} doesn't implement: {}, ignoring file upload", + invocation.getProxy().getActionName(), + UploadedFilesAware.class.getSimpleName()); + return invocation.invoke(); + } + UploadedFilesAware action = (UploadedFilesAware) invocation.getAction(); + + 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); + + for (int index = 0; index < files.length; index++) { + if (acceptFile(action, files[index], fileName[index], contentType[index], inputName)) { + acceptedFiles.add(files[index]); + } + } + + if (acceptedFiles.isEmpty()) { + LOG.debug("No files have been uploaded/accepted"); + } else { + LOG.debug("Passing: {} uploaded file(s) to action", acceptedFiles.size()); + action.withUploadedFiles(acceptedFiles); + } + } + } else { + if (LOG.isWarnEnabled()) { + LOG.warn(getTextMessage(action, "struts.messages.invalid.file", new String[]{inputName})); + } + } + } else { + if (LOG.isWarnEnabled()) { + LOG.warn(getTextMessage(action, "struts.messages.invalid.content.type", new String[]{inputName})); + } + } + } + + // invoke action + return invocation.invoke(); + } + +} 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..4bcdec07b 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java @@ -18,23 +18,22 @@ */ package org.apache.struts2.interceptor; -import com.opensymphony.xwork2.*; -import com.opensymphony.xwork2.inject.Container; -import com.opensymphony.xwork2.inject.Inject; -import com.opensymphony.xwork2.interceptor.AbstractInterceptor; +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.ActionProxy; import com.opensymphony.xwork2.interceptor.ValidationAware; -import com.opensymphony.xwork2.util.TextParseUtil; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.struts2.dispatcher.LocalizedMessage; import org.apache.struts2.dispatcher.Parameter; import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper; import org.apache.struts2.dispatcher.multipart.UploadedFile; -import org.apache.struts2.util.ContentTypeMatcher; import javax.servlet.http.HttpServletRequest; -import java.text.NumberFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * <!-- START SNIPPET: description --> @@ -77,11 +76,11 @@ import java.util.*; * file extensions specified</li> * * </ul> - * + * <p> * <!-- END SNIPPET: description --> * * <p><u>Interceptor parameters:</u></p> - * + * <p> * <!-- START SNIPPET: parameters --> * * <ul> @@ -96,14 +95,14 @@ import java.util.*; * <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 @@ -122,7 +121,7 @@ import java.util.*; * </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. @@ -174,56 +173,12 @@ import java.util.*; * <!-- END SNIPPET: example-action --> * </pre> */ -public class FileUploadInterceptor extends AbstractInterceptor { +public class FileUploadInterceptor extends AbstractFileUploadInterceptor { private static final long serialVersionUID = -4764627478894962478L; protected static final Logger LOG = LogManager.getLogger(FileUploadInterceptor.class); - protected Long maximumSize; - protected Set<String> allowedTypesSet = Collections.emptySet(); - protected Set<String> allowedExtensionsSet = Collections.emptySet(); - - private ContentTypeMatcher matcher; - private Container container; - - @Inject - public void setMatcher(ContentTypeMatcher matcher) { - this.matcher = matcher; - } - - @Inject - public void setContainer(Container container) { - this.container = container; - } - - /** - * Sets the allowed extensions - * - * @param allowedExtensions A comma-delimited list of extensions - */ - public void setAllowedExtensions(String allowedExtensions) { - allowedExtensionsSet = TextParseUtil.commaDelimitedStringToSet(allowedExtensions); - } - - /** - * Sets the allowed mimetypes - * - * @param allowedTypes A comma-delimited list of types - */ - public void setAllowedTypes(String allowedTypes) { - allowedTypesSet = TextParseUtil.commaDelimitedStringToSet(allowedTypes); - } - - /** - * Sets the maximum size of an uploaded file - * - * @param maximumSize The maximum size in bytes - */ - public void setMaximumSize(Long maximumSize) { - this.maximumSize = maximumSize; - } - /* (non-Javadoc) * @see com.opensymphony.xwork2.interceptor.Interceptor#intercept(com.opensymphony.xwork2.ActionInvocation) */ @@ -242,34 +197,16 @@ public class FileUploadInterceptor extends AbstractInterceptor { return invocation.invoke(); } - ValidationAware validation = null; - Object action = invocation.getAction(); - - if (action instanceof ValidationAware) { - validation = (ValidationAware) action; - } - MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request; - if (multiWrapper.hasErrors() && validation != null) { - TextProvider textProvider = getTextProvider(action); - for (LocalizedMessage error : multiWrapper.getErrors()) { - String errorMessage; - if (textProvider.hasKey(error.getTextKey())) { - errorMessage = textProvider.getText(error.getTextKey(), Arrays.asList(error.getArgs())); - } else { - errorMessage = textProvider.getText("struts.messages.error.uploading", error.getDefaultMessage()); - } - validation.addActionError(errorMessage); - } - } + applyValidation(action, multiWrapper); // 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); @@ -289,7 +226,7 @@ public class FileUploadInterceptor extends AbstractInterceptor { String fileNameName = inputName + "FileName"; for (int index = 0; index < files.length; index++) { - if (acceptFile(action, files[index], fileName[index], contentType[index], inputName, validation)) { + if (acceptFile(action, files[index], fileName[index], contentType[index], inputName)) { acceptedFiles.add(files[index]); acceptedContentTypes.add(contentType[index]); acceptedFileNames.add(fileName[index]); @@ -298,9 +235,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); } } @@ -320,149 +257,4 @@ public class FileUploadInterceptor extends AbstractInterceptor { return invocation.invoke(); } - /** - * Override for added functionality. Checks if the proposed file is acceptable based on contentType and size. - * - * @param action - uploading action for message retrieval. - * @param file - proposed upload file. - * @param filename - name of the file. - * @param contentType - contentType of the file. - * @param inputName - inputName of the file. - * @param validation - Non-null ValidationAware if the action implements ValidationAware, allowing for better - * logging. - * @return true if the proposed file is acceptable by contentType and size. - */ - protected boolean acceptFile(Object action, UploadedFile file, String filename, String contentType, String inputName, ValidationAware validation) { - boolean fileIsAcceptable = false; - - // If it's null the upload failed - if (file == null) { - String errMsg = getTextMessage(action, "struts.messages.error.uploading", new String[]{inputName}); - if (validation != null) { - validation.addFieldError(inputName, errMsg); - } - - if (LOG.isWarnEnabled()) { - LOG.warn(errMsg); - } - } else if (file.getContent() == null) { - String errMsg = getTextMessage(action, "struts.messages.error.uploading", new String[]{filename}); - if (validation != null) { - validation.addFieldError(inputName, errMsg); - } - if (LOG.isWarnEnabled()) { - LOG.warn(errMsg); - } - } else if (maximumSize != null && maximumSize < file.length()) { - String errMsg = getTextMessage(action, "struts.messages.error.file.too.large", new String[]{inputName, filename, file.getName(), "" + file.length(), getMaximumSizeStr(action)}); - if (validation != null) { - validation.addFieldError(inputName, errMsg); - } - - if (LOG.isWarnEnabled()) { - LOG.warn(errMsg); - } - } else if ((!allowedTypesSet.isEmpty()) && (!containsItem(allowedTypesSet, contentType))) { - String errMsg = getTextMessage(action, "struts.messages.error.content.type.not.allowed", new String[]{inputName, filename, file.getName(), contentType}); - if (validation != null) { - validation.addFieldError(inputName, errMsg); - } - - if (LOG.isWarnEnabled()) { - LOG.warn(errMsg); - } - } else if ((!allowedExtensionsSet.isEmpty()) && (!hasAllowedExtension(allowedExtensionsSet, filename))) { - String errMsg = getTextMessage(action, "struts.messages.error.file.extension.not.allowed", new String[]{inputName, filename, file.getName(), contentType}); - if (validation != null) { - validation.addFieldError(inputName, errMsg); - } - - if (LOG.isWarnEnabled()) { - LOG.warn(errMsg); - } - } else { - fileIsAcceptable = true; - } - - return fileIsAcceptable; - } - - private String getMaximumSizeStr(Object action) { - return NumberFormat.getNumberInstance(getLocaleProvider(action).getLocale()).format(maximumSize); - } - - /** - * @param extensionCollection - Collection of extensions (all lowercase). - * @param filename - filename to check. - * @return true if the filename has an allowed extension, false otherwise. - */ - private boolean hasAllowedExtension(Collection<String> extensionCollection, String filename) { - if (filename == null) { - return false; - } - - String lowercaseFilename = filename.toLowerCase(); - for (String extension : extensionCollection) { - if (lowercaseFilename.endsWith(extension)) { - return true; - } - } - - return false; - } - - /** - * @param itemCollection - Collection of string items (all lowercase). - * @param item - Item to search for. - * @return true if itemCollection contains the item, false otherwise. - */ - private boolean containsItem(Collection<String> itemCollection, String item) { - for (String pattern : itemCollection) - if (matchesWildcard(pattern, item)) - return true; - return false; - } - - private boolean matchesWildcard(String pattern, String text) { - Object o = matcher.compilePattern(pattern); - return matcher.match(new HashMap<String, String>(), text, o); - } - - private boolean isNonEmpty(Object[] objArray) { - boolean result = false; - for (int index = 0; index < objArray.length && !result; index++) { - if (objArray[index] != null) { - result = true; - } - } - return result; - } - - protected String getTextMessage(String messageKey, String[] args) { - return getTextMessage(this, messageKey, args); - } - - protected String getTextMessage(Object action, String messageKey, String[] args) { - if (action instanceof TextProvider) { - return ((TextProvider) action).getText(messageKey, args); - } - return getTextProvider(action).getText(messageKey, args); - } - - private TextProvider getTextProvider(Object action) { - TextProviderFactory tpf = container.getInstance(TextProviderFactory.class); - return tpf.createInstance(action.getClass()); - } - - private LocaleProvider getLocaleProvider(Object action) { - LocaleProvider localeProvider; - if (action instanceof LocaleProvider) { - localeProvider = (LocaleProvider) action; - } else { - LocaleProviderFactory localeProviderFactory = container.getInstance(LocaleProviderFactory.class); - localeProvider = localeProviderFactory.createLocaleProvider(); - } - return localeProvider; - } - } diff --git a/core/src/main/resources/struts-default.xml b/core/src/main/resources/struts-default.xml index 0fdcc2b37..326477bc4 100644 --- a/core/src/main/resources/struts-default.xml +++ b/core/src/main/resources/struts-default.xml @@ -58,6 +58,7 @@ <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"/> <interceptor name="modelDriven" class="com.opensymphony.xwork2.interceptor.ModelDrivenInterceptor"/> @@ -121,6 +122,12 @@ <interceptor-ref name="basicStack"/> </interceptor-stack> + <!-- Action based file upload stack --> + <interceptor-stack name="actionFileUploadStack"> + <interceptor-ref name="actionFileUpload"/> + <interceptor-ref name="basicStack"/> + </interceptor-stack> + <!-- Sample model-driven stack --> <interceptor-stack name="modelDrivenStack"> <interceptor-ref name="modelDriven"/> @@ -165,6 +172,7 @@ <interceptor-ref name="chain"/> <interceptor-ref name="modelDriven"/> <interceptor-ref name="fileUpload"/> + <interceptor-ref name="actionFileUpload"/> <interceptor-ref name="staticParams"/> <interceptor-ref name="actionMappingParams"/> <interceptor-ref name="params"/> @@ -203,6 +211,7 @@ <interceptor-ref name="scopedModelDriven"/> <interceptor-ref name="modelDriven"/> <interceptor-ref name="fileUpload"/> + <interceptor-ref name="actionFileUpload"/> <interceptor-ref name="checkbox"/> <interceptor-ref name="datetime"/> <interceptor-ref name="multiselect"/> diff --git a/core/src/test/java/org/apache/struts2/conversion/UploadedFileConverterTest.java b/core/src/test/java/org/apache/struts2/conversion/UploadedFileConverterTest.java index b88d2b36b..3832a514f 100644 --- a/core/src/test/java/org/apache/struts2/conversion/UploadedFileConverterTest.java +++ b/core/src/test/java/org/apache/struts2/conversion/UploadedFileConverterTest.java @@ -34,10 +34,12 @@ import static org.assertj.core.api.Assertions.assertThat; public class UploadedFileConverterTest { private Map<String, Object> context; - private Class target; + private Class<?> target; private Member member; private String propertyName; private File tempFile; + private String contentType; + private String originalName; @Before public void setUp() throws Exception { @@ -46,6 +48,8 @@ public class UploadedFileConverterTest { member = File.class.getMethod("length"); propertyName = "ignore"; tempFile = File.createTempFile("struts", "test"); + contentType = "text/plain"; + originalName = tempFile.getName(); } @After @@ -54,10 +58,10 @@ public class UploadedFileConverterTest { } @Test - public void convertUploadedFileToFile() throws Exception { + public void convertUploadedFileToFile() { // given UploadedFileConverter ufc = new UploadedFileConverter(); - UploadedFile uploadedFile = new StrutsUploadedFile(tempFile); + UploadedFile uploadedFile = StrutsUploadedFile.Builder.create(tempFile).withContentType(this.contentType).withOriginalName(this.originalName).build(); // when Object result = ufc.convertValue(context, target, member, propertyName, uploadedFile, File.class); @@ -70,10 +74,15 @@ public class UploadedFileConverterTest { } @Test - public void convertUploadedFileArrayToFile() throws Exception { + public void convertUploadedFileArrayToFile() { // given UploadedFileConverter ufc = new UploadedFileConverter(); - UploadedFile[] uploadedFile = new UploadedFile[] { new StrutsUploadedFile(tempFile) }; + UploadedFile[] uploadedFile = new UploadedFile[]{ + StrutsUploadedFile.Builder.create(tempFile) + .withContentType(this.contentType) + .withOriginalName(this.originalName) + .build() + }; // when Object result = ufc.convertValue(context, target, member, propertyName, uploadedFile, File.class); diff --git a/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java similarity index 57% copy from core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java copy to core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java index b9bab88d4..415d4ca67 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java @@ -23,12 +23,12 @@ 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.mock.MockActionProxy; 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.TestAction; -import org.apache.struts2.dispatcher.HttpParameters; +import org.apache.struts2.action.UploadedFilesAware; import org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest; import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper; import org.apache.struts2.dispatcher.multipart.StrutsUploadedFile; @@ -37,21 +37,17 @@ import org.springframework.mock.web.MockHttpServletRequest; import javax.servlet.http.HttpServletRequest; import java.io.File; -import java.io.IOException; 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; - /** - * Test case for FileUploadInterceptor. + * Test case for {@link ActionFileUploadInterceptor}. */ -public class FileUploadInterceptorTest extends StrutsInternalTestCase { +public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase { public static final UploadedFile EMPTY_FILE = new UploadedFile() { @Override @@ -83,28 +79,42 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { public byte[] getContent() { return new byte[0]; } + + @Override + public String getOriginalName() { + return null; + } + + @Override + public String getContentType() { + return null; + } }; - private FileUploadInterceptor interceptor; + private ActionFileUploadInterceptor interceptor; private File tempDir; - private TestAction action; - public void testAcceptFileWithEmptyAllowedTypesAndExtensions() throws Exception { + private final String htmlContent = "<html><head></head><body>html content</body></html>"; + private final String plainContent = "plain content"; + private final String boundary = "simple boundary"; + private final String endline = "\r\n"; + + public void testAcceptFileWithEmptyAllowedTypesAndExtensions() { // when allowed type is empty ValidationAwareSupport validation = new ValidationAwareSupport(); - boolean ok = interceptor.acceptFile(action, EMPTY_FILE, "filename", "text/plain", "inputName", validation); + boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename", "text/plain", "inputName"); assertTrue(ok); assertTrue(validation.getFieldErrors().isEmpty()); assertFalse(validation.hasErrors()); } - public void testAcceptFileWithoutEmptyTypes() throws Exception { + public void testAcceptFileWithoutEmptyTypes() { interceptor.setAllowedTypes("text/plain"); // when file is of allowed types ValidationAwareSupport validation = new ValidationAwareSupport(); - boolean ok = interceptor.acceptFile(action, EMPTY_FILE, "filename.txt", "text/plain", "inputName", validation); + boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName"); assertTrue(ok); assertTrue(validation.getFieldErrors().isEmpty()); @@ -112,7 +122,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { // when file is not of allowed types validation = new ValidationAwareSupport(); - boolean notOk = interceptor.acceptFile(action, EMPTY_FILE, "filename.html", "text/html", "inputName", validation); + boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/html", "inputName"); assertFalse(notOk); assertFalse(validation.getFieldErrors().isEmpty()); @@ -120,11 +130,11 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { } - public void testAcceptFileWithWildcardContent() throws Exception { + public void testAcceptFileWithWildcardContent() { interceptor.setAllowedTypes("text/*"); ValidationAwareSupport validation = new ValidationAwareSupport(); - boolean ok = interceptor.acceptFile(action, EMPTY_FILE, "filename.txt", "text/plain", "inputName", validation); + boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName"); assertTrue(ok); assertTrue(validation.getFieldErrors().isEmpty()); @@ -132,19 +142,19 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { interceptor.setAllowedTypes("text/h*"); validation = new ValidationAwareSupport(); - boolean notOk = interceptor.acceptFile(action, EMPTY_FILE, "filename.html", "text/plain", "inputName", validation); + boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/plain", "inputName"); assertFalse(notOk); assertFalse(validation.getFieldErrors().isEmpty()); assertTrue(validation.hasErrors()); } - public void testAcceptFileWithoutEmptyExtensions() throws Exception { + public void testAcceptFileWithoutEmptyExtensions() { interceptor.setAllowedExtensions(".txt"); // when file is of allowed extensions ValidationAwareSupport validation = new ValidationAwareSupport(); - boolean ok = interceptor.acceptFile(action, EMPTY_FILE, "filename.txt", "text/plain", "inputName", validation); + boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName"); assertTrue(ok); assertTrue(validation.getFieldErrors().isEmpty()); @@ -152,7 +162,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { // when file is not of allowed extensions validation = new ValidationAwareSupport(); - boolean notOk = interceptor.acceptFile(action, EMPTY_FILE, "filename.html", "text/html", "inputName", validation); + boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/html", "inputName"); assertFalse(notOk); assertFalse(validation.getFieldErrors().isEmpty()); @@ -161,49 +171,52 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { //test with multiple extensions interceptor.setAllowedExtensions(".txt,.lol"); validation = new ValidationAwareSupport(); - ok = interceptor.acceptFile(action, EMPTY_FILE, "filename.lol", "text/plain", "inputName", validation); + ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.lol", "text/plain", "inputName"); assertTrue(ok); assertTrue(validation.getFieldErrors().isEmpty()); assertFalse(validation.hasErrors()); } - public void testAcceptFileWithNoFile() throws Exception { - FileUploadInterceptor interceptor = new FileUploadInterceptor(); + public void testAcceptFileWithNoFile() { + ActionFileUploadInterceptor interceptor = new ActionFileUploadInterceptor(); + interceptor.setContainer(container); + interceptor.setAllowedTypes("text/plain"); // when file is not of allowed types ValidationAwareSupport validation = new ValidationAwareSupport(); - boolean notOk = interceptor.acceptFile(action, null, "filename.html", "text/html", "inputName", validation); + boolean notOk = interceptor.acceptFile(validation, null, "filename.html", "text/html", "inputName"); assertFalse(notOk); assertFalse(validation.getFieldErrors().isEmpty()); assertTrue(validation.hasErrors()); - List errors = (List) validation.getFieldErrors().get("inputName"); + List<String> errors = validation.getFieldErrors().get("inputName"); assertEquals(1, errors.size()); - String msg = (String) errors.get(0); + String msg = errors.get(0); assertTrue(msg.startsWith("Error uploading:")); assertTrue(msg.indexOf("inputName") > 0); } public void testAcceptFileWithMaxSize() throws Exception { interceptor.setAllowedTypes("text/plain"); - interceptor.setMaximumSize(new Long(10)); + interceptor.setMaximumSize(10L); // when file is not of allowed types ValidationAwareSupport validation = new ValidationAwareSupport(); - URL url = ClassLoaderUtil.getResource("log4j2.xml", FileUploadInterceptorTest.class); + URL url = ClassLoaderUtil.getResource("log4j2.xml", ActionFileUploadInterceptorTest.class); File file = new File(new URI(url.toString())); assertTrue("log4j2.xml should be in src/test folder", file.exists()); - boolean notOk = interceptor.acceptFile(action, new StrutsUploadedFile(file), "filename", "text/html", "inputName", validation); + 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 = (String) errors.get(0); + String msg = errors.get(0); // the error message should contain at least this test assertTrue(msg.startsWith("The file is too large to be uploaded")); assertTrue(msg.indexOf("inputName") > 0); @@ -211,11 +224,15 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { } public void testNoMultipartRequest() throws Exception { - MyFileupAction action = new MyFileupAction(); + MyFileUploadAction action = new MyFileUploadAction(); MockActionInvocation mai = new MockActionInvocation(); mai.setAction(action); mai.setResultCode("NoMultipart"); + MockActionProxy proxy = new MockActionProxy(); + proxy.setNamespace("/test"); + proxy.setActionName("myFileUpload"); + mai.setProxy(proxy); mai.setInvocationContext(ActionContext.getContext()); // if no multipart request it will bypass and execute it @@ -228,13 +245,12 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { req.setContentType("multipart/form-data"); // not a multipart contentype req.setMethod("post"); - MyFileupAction action = container.inject(MyFileupAction.class); + MyFileUploadAction action = container.inject(MyFileUploadAction.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); @@ -250,13 +266,12 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { req.addHeader("Content-type", "multipart/form-data"); req.setContent(null); // there is no content - MyFileupAction action = container.inject(MyFileupAction.class); + MyFileUploadAction action = container.inject(MyFileUploadAction.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); @@ -272,143 +287,106 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { // 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("US-ASCII")); + "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + + "Content-Type: text/html\r\n" + + "\r\n" + + "Unit test of ActionFileUploadInterceptor" + + "\r\n" + + "-----1234--\r\n"); + req.setContent(content.getBytes(StandardCharsets.US_ASCII)); - MyFileupAction action = new MyFileupAction(); + MyFileUploadAction action = new MyFileUploadAction(); 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); - assertTrue(!action.hasErrors()); + assertFalse(action.hasErrors()); - HttpParameters parameters = mai.getInvocationContext().getParameters(); - assertTrue(parameters.keySet().size() == 3); - UploadedFile[] files = (UploadedFile[]) parameters.get("file").getObject(); - String[] fileContentTypes = parameters.get("fileContentType").getMultipleValues(); - String[] fileRealFilenames = parameters.get("fileFileName").getMultipleValues(); + List<UploadedFile> files = action.getUploadFiles(); assertNotNull(files); - assertNotNull(fileContentTypes); - assertNotNull(fileRealFilenames); - assertTrue(files.length == 1); - assertTrue(fileContentTypes.length == 1); - assertTrue(fileRealFilenames.length == 1); - assertEquals("text/html", fileContentTypes[0]); - assertNotNull("deleteme.txt", fileRealFilenames[0]); + assertEquals(1, files.size()); + assertEquals("text/html", files.get(0).getContentType()); + assertNotNull("deleteme.txt", files.get(0).getOriginalName()); } /** * tests whether with multiple files sent with the same name, the ones with forbiddenTypes (see - * FileUploadInterceptor.setAllowedTypes(...) ) are sorted out. - * - * @throws Exception + * ActionFileUploadInterceptor.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); - StringBuilder content = new StringBuilder(128); - content.append(encodeTextFile(bondary, endline, "file", "test.html", "text/plain", plainContent)); - content.append(encodeTextFile(bondary, endline, "file", "test1.html", "text/html", htmlContent)); - content.append(encodeTextFile(bondary, endline, "file", "test2.html", "text/html", htmlContent)); - content.append(endline); - content.append(endline); - content.append(endline); - content.append("--"); - content.append(bondary); - content.append("--"); - content.append(endline); - req.setContent(content.toString().getBytes()); + req.addHeader("Content-type", "multipart/form-data; boundary=" + boundary); + String content = encodeTextFile("test.html", "text/plain", plainContent) + + encodeTextFile("test1.html", "text/html", htmlContent) + + encodeTextFile("test2.html", "text/html", htmlContent) + + endline + + endline + + endline + + "--" + + boundary + + "--" + + endline; + req.setContent(content.getBytes()); assertTrue(ServletFileUpload.isMultipartContent(req)); - MyFileupAction action = new MyFileupAction(); + MyFileUploadAction action = new MyFileUploadAction(); container.inject(action); MockActionInvocation mai = new MockActionInvocation(); mai.setAction(action); mai.setResultCode("success"); mai.setInvocationContext(ActionContext.getContext()); - Map<String, Object> param = new HashMap<String, Object>(); - 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(); + List<UploadedFile> files = action.getUploadFiles(); 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]); + assertEquals("files accepted ", 2, files.size()); + assertEquals("text/html", files.get(0).getContentType()); + assertNotNull("test1.html", files.get(0).getOriginalName()); } 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); - StringBuilder content = new StringBuilder(128); - content.append(encodeTextFile(boundary, endline, "file", "test.html", "text/plain", plainContent)); - content.append(encodeTextFile(boundary, endline, "file", "test1.html", "text/html", htmlContent)); - content.append(encodeTextFile(boundary, endline, "file", "test2.html", "text/html", htmlContent)); - content.append(encodeTextFile(boundary, endline, "file", "test3.html", "text/html", htmlContent)); - content.append(endline); - content.append("--"); - content.append(boundary); - content.append("--"); - content.append(endline); - req.setContent(content.toString().getBytes()); + 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(); + MyFileUploadAction action = new MyFileUploadAction(); 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, 3)); + ActionContext.getContext().withServletRequest(createMultipartRequestMaxFiles(req)); interceptor.setAllowedTypes("text/html"); interceptor.intercept(mai); - HttpParameters parameters = mai.getInvocationContext().getParameters(); - assertEquals(0, parameters.keySet().size()); + assertNull(action.getUploadFiles()); assertEquals(1, action.getActionErrors().size()); assertEquals("Request exceeded allowed number of files! Max allowed files number is: 3!", action.getActionErrors().iterator().next()); } @@ -421,24 +399,22 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { // 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=\"file\"; filename=\"deleteme.txt\"\r\n" + + "Content-Type: text/html\r\n" + + "\r\n" + + "Unit test of ActionFileUploadInterceptor" + + "\r\n" + + "-----1234--\r\n"); req.setContent(content.getBytes(StandardCharsets.US_ASCII)); - MyFileupAction action = container.inject(MyFileupAction.class); + MyFileUploadAction action = container.inject(MyFileUploadAction.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, 10)); + .withServletRequest(createMultipartRequestMaxFileSize(req)); interceptor.intercept(mai); @@ -448,8 +424,8 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { 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); + "File in request exceeded allowed file size limit! Max file size allowed is: 10 but file deleteme.txt was: 40!", + msg); } public void testMultipartRequestMaxStringLength() throws Exception { @@ -460,34 +436,32 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { // 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"); + "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + + "Content-Type: text/html\r\n" + + "\r\n" + + "Unit test of ActionFileUploadInterceptor" + + "\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); + MyFileUploadAction action = container.inject(MyFileUploadAction.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, 20)); + .withServletRequest(createMultipartRequestMaxStringLength(req)); interceptor.intercept(mai); @@ -497,8 +471,8 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { assertEquals(1, errors.size()); String msg = errors.iterator().next(); assertEquals( - "The request parameter \"normalFormField2\" was too long. Max length allowed is 20, but found 27!", - msg); + "The request parameter \"normalFormField2\" was too long. Max length allowed is 20, but found 27!", + msg); } public void testMultipartRequestLocalizedError() throws Exception { @@ -509,23 +483,21 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { // 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=\"file\"; filename=\"deleteme.txt\"\r\n" + + "Content-Type: text/html\r\n" + + "\r\n" + + "Unit test of ActionFileUploadInterceptor" + + "\r\n" + + "-----1234--\r\n"); req.setContent(content.getBytes(StandardCharsets.US_ASCII)); - MyFileupAction action = container.inject(MyFileupAction.class); + MyFileUploadAction action = container.inject(MyFileUploadAction.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)); @@ -540,44 +512,40 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { assertTrue(msg.startsWith("Der Request übertraf die maximal erlaubte Größe")); } - private String encodeTextFile(String bondary, String endline, String name, String filename, String contentType, String content) { - final StringBuilder sb = new StringBuilder(64); - sb.append(endline); - sb.append("--"); - sb.append(bondary); - sb.append(endline); - sb.append("Content-Disposition: form-data; name=\""); - sb.append(name); - sb.append("\"; filename=\""); - sb.append(filename); - sb.append(endline); - sb.append("Content-Type: "); - sb.append(contentType); - sb.append(endline); - sb.append(endline); - sb.append(content); - - return sb.toString(); + 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, int maxfilesize) throws IOException { - return createMultipartRequest(req, -1, maxfilesize, -1, -1); + private MultiPartRequestWrapper createMultipartRequestMaxFileSize(HttpServletRequest req) { + return createMultipartRequest(req, -1, 10, -1, -1); } - private MultiPartRequestWrapper createMultipartRequestMaxFiles(HttpServletRequest req, int maxfiles) throws IOException { - return createMultipartRequest(req, -1, -1, maxfiles, -1); + private MultiPartRequestWrapper createMultipartRequestMaxFiles(HttpServletRequest req) { + return createMultipartRequest(req, -1, -1, 3, -1); } - private MultiPartRequestWrapper createMultipartRequestMaxSize(HttpServletRequest req, int maxsize) throws IOException { + private MultiPartRequestWrapper createMultipartRequestMaxSize(HttpServletRequest req, int maxsize) { return createMultipartRequest(req, maxsize, -1, -1, -1); } - private MultiPartRequestWrapper createMultipartRequestMaxStringLength(HttpServletRequest req, int maxStringLength) throws IOException { - return createMultipartRequest(req, -1, -1, -1, maxStringLength); + 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) throws IOException { + private MultiPartRequestWrapper createMultipartRequest(HttpServletRequest req, int maxsize, int maxfilesize, int maxfiles, int maxStringLength) { JakartaMultiPartRequest jak = new JakartaMultiPartRequest(); jak.setMaxSize(String.valueOf(maxsize)); @@ -589,10 +557,8 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { protected void setUp() throws Exception { super.setUp(); - action = new TestAction(); - container.inject(action); - interceptor = new FileUploadInterceptor(); + interceptor = new ActionFileUploadInterceptor(); container.inject(interceptor); tempDir = File.createTempFile("struts", "fileupload"); tempDir.delete(); @@ -605,12 +571,17 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { super.tearDown(); } - public static class MyFileupAction extends ActionSupport { + public static class MyFileUploadAction extends ActionSupport implements UploadedFilesAware { + private List<UploadedFile> uploadedFiles; - private static final long serialVersionUID = 6255238895447968889L; + @Override + public void withUploadedFiles(List<UploadedFile> uploadedFiles) { + this.uploadedFiles = uploadedFiles; + } - // no methods + public List<UploadedFile> getUploadFiles() { + return this.uploadedFiles; + } } - } 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 b9bab88d4..18758c422 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java @@ -83,28 +83,37 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { public byte[] getContent() { return new byte[0]; } + + @Override + public String getOriginalName() { + return null; + } + + @Override + public String getContentType() { + return null; + } }; private FileUploadInterceptor interceptor; private File tempDir; - private TestAction action; - public void testAcceptFileWithEmptyAllowedTypesAndExtensions() throws Exception { + public void testAcceptFileWithEmptyAllowedTypesAndExtensions() { // when allowed type is empty ValidationAwareSupport validation = new ValidationAwareSupport(); - boolean ok = interceptor.acceptFile(action, EMPTY_FILE, "filename", "text/plain", "inputName", validation); + boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename", "text/plain", "inputName"); assertTrue(ok); assertTrue(validation.getFieldErrors().isEmpty()); assertFalse(validation.hasErrors()); } - public void testAcceptFileWithoutEmptyTypes() throws Exception { + public void testAcceptFileWithoutEmptyTypes() { interceptor.setAllowedTypes("text/plain"); // when file is of allowed types ValidationAwareSupport validation = new ValidationAwareSupport(); - boolean ok = interceptor.acceptFile(action, EMPTY_FILE, "filename.txt", "text/plain", "inputName", validation); + boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName"); assertTrue(ok); assertTrue(validation.getFieldErrors().isEmpty()); @@ -112,7 +121,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { // when file is not of allowed types validation = new ValidationAwareSupport(); - boolean notOk = interceptor.acceptFile(action, EMPTY_FILE, "filename.html", "text/html", "inputName", validation); + boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/html", "inputName"); assertFalse(notOk); assertFalse(validation.getFieldErrors().isEmpty()); @@ -120,11 +129,11 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { } - public void testAcceptFileWithWildcardContent() throws Exception { + public void testAcceptFileWithWildcardContent() { interceptor.setAllowedTypes("text/*"); ValidationAwareSupport validation = new ValidationAwareSupport(); - boolean ok = interceptor.acceptFile(action, EMPTY_FILE, "filename.txt", "text/plain", "inputName", validation); + boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName"); assertTrue(ok); assertTrue(validation.getFieldErrors().isEmpty()); @@ -132,19 +141,19 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { interceptor.setAllowedTypes("text/h*"); validation = new ValidationAwareSupport(); - boolean notOk = interceptor.acceptFile(action, EMPTY_FILE, "filename.html", "text/plain", "inputName", validation); + boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/plain", "inputName"); assertFalse(notOk); assertFalse(validation.getFieldErrors().isEmpty()); assertTrue(validation.hasErrors()); } - public void testAcceptFileWithoutEmptyExtensions() throws Exception { + public void testAcceptFileWithoutEmptyExtensions() { interceptor.setAllowedExtensions(".txt"); // when file is of allowed extensions ValidationAwareSupport validation = new ValidationAwareSupport(); - boolean ok = interceptor.acceptFile(action, EMPTY_FILE, "filename.txt", "text/plain", "inputName", validation); + boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName"); assertTrue(ok); assertTrue(validation.getFieldErrors().isEmpty()); @@ -152,7 +161,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { // when file is not of allowed extensions validation = new ValidationAwareSupport(); - boolean notOk = interceptor.acceptFile(action, EMPTY_FILE, "filename.html", "text/html", "inputName", validation); + boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/html", "inputName"); assertFalse(notOk); assertFalse(validation.getFieldErrors().isEmpty()); @@ -161,34 +170,36 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { //test with multiple extensions interceptor.setAllowedExtensions(".txt,.lol"); validation = new ValidationAwareSupport(); - ok = interceptor.acceptFile(action, EMPTY_FILE, "filename.lol", "text/plain", "inputName", validation); + ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.lol", "text/plain", "inputName"); assertTrue(ok); assertTrue(validation.getFieldErrors().isEmpty()); assertFalse(validation.hasErrors()); } - public void testAcceptFileWithNoFile() throws Exception { + 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(action, null, "filename.html", "text/html", "inputName", validation); + boolean notOk = interceptor.acceptFile(validation, null, "filename.html", "text/html", "inputName"); assertFalse(notOk); assertFalse(validation.getFieldErrors().isEmpty()); assertTrue(validation.hasErrors()); - List errors = (List) validation.getFieldErrors().get("inputName"); + List<String> errors = validation.getFieldErrors().get("inputName"); assertEquals(1, errors.size()); - String msg = (String) errors.get(0); + String msg = errors.get(0); assertTrue(msg.startsWith("Error uploading:")); assertTrue(msg.indexOf("inputName") > 0); } public void testAcceptFileWithMaxSize() throws Exception { interceptor.setAllowedTypes("text/plain"); - interceptor.setMaximumSize(new Long(10)); + interceptor.setMaximumSize(10L); // when file is not of allowed types ValidationAwareSupport validation = new ValidationAwareSupport(); @@ -196,14 +207,15 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { URL url = ClassLoaderUtil.getResource("log4j2.xml", FileUploadInterceptorTest.class); File file = new File(new URI(url.toString())); assertTrue("log4j2.xml should be in src/test folder", file.exists()); - boolean notOk = interceptor.acceptFile(action, new StrutsUploadedFile(file), "filename", "text/html", "inputName", validation); + 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 = (String) errors.get(0); + String msg = errors.get(0); // the error message should contain at least this test assertTrue(msg.startsWith("The file is too large to be uploaded")); assertTrue(msg.indexOf("inputName") > 0); @@ -278,7 +290,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { "Unit test of FileUploadInterceptor" + "\r\n" + "-----1234--\r\n"); - req.setContent(content.getBytes("US-ASCII")); + req.setContent(content.getBytes(StandardCharsets.US_ASCII)); MyFileupAction action = new MyFileupAction(); @@ -292,10 +304,10 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { interceptor.intercept(mai); - assertTrue(!action.hasErrors()); + assertFalse(action.hasErrors()); HttpParameters parameters = mai.getInvocationContext().getParameters(); - assertTrue(parameters.keySet().size() == 3); + assertEquals(3, parameters.keySet().size()); UploadedFile[] files = (UploadedFile[]) parameters.get("file").getObject(); String[] fileContentTypes = parameters.get("fileContentType").getMultipleValues(); String[] fileRealFilenames = parameters.get("fileFileName").getMultipleValues(); @@ -303,9 +315,9 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { assertNotNull(files); assertNotNull(fileContentTypes); assertNotNull(fileRealFilenames); - assertTrue(files.length == 1); - assertTrue(fileContentTypes.length == 1); - assertTrue(fileRealFilenames.length == 1); + assertEquals(1, files.length); + assertEquals(1, fileContentTypes.length); + assertEquals(1, fileRealFilenames.length); assertEquals("text/html", fileContentTypes[0]); assertNotNull("deleteme.txt", fileRealFilenames[0]); } @@ -313,8 +325,6 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { /** * tests whether with multiple files sent with the same name, the ones with forbiddenTypes (see * FileUploadInterceptor.setAllowedTypes(...) ) are sorted out. - * - * @throws Exception */ public void testMultipleAccept() throws Exception { final String htmlContent = "<html><head></head><body>html content</body></html>"; @@ -326,18 +336,17 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { req.setCharacterEncoding(StandardCharsets.UTF_8.name()); req.setMethod("POST"); req.addHeader("Content-type", "multipart/form-data; boundary=" + bondary); - StringBuilder content = new StringBuilder(128); - content.append(encodeTextFile(bondary, endline, "file", "test.html", "text/plain", plainContent)); - content.append(encodeTextFile(bondary, endline, "file", "test1.html", "text/html", htmlContent)); - content.append(encodeTextFile(bondary, endline, "file", "test2.html", "text/html", htmlContent)); - content.append(endline); - content.append(endline); - content.append(endline); - content.append("--"); - content.append(bondary); - content.append("--"); - content.append(endline); - req.setContent(content.toString().getBytes()); + 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)); @@ -347,7 +356,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { mai.setAction(action); mai.setResultCode("success"); mai.setInvocationContext(ActionContext.getContext()); - Map<String, Object> param = new HashMap<String, Object>(); + Map<String, Object> param = new HashMap<>(); ActionContext.getContext().withParameters(HttpParameters.create(param).build()); ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000)); @@ -380,17 +389,16 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { req.setCharacterEncoding(StandardCharsets.UTF_8.name()); req.setMethod("POST"); req.addHeader("Content-type", "multipart/form-data; boundary=" + boundary); - StringBuilder content = new StringBuilder(128); - content.append(encodeTextFile(boundary, endline, "file", "test.html", "text/plain", plainContent)); - content.append(encodeTextFile(boundary, endline, "file", "test1.html", "text/html", htmlContent)); - content.append(encodeTextFile(boundary, endline, "file", "test2.html", "text/html", htmlContent)); - content.append(encodeTextFile(boundary, endline, "file", "test3.html", "text/html", htmlContent)); - content.append(endline); - content.append("--"); - content.append(boundary); - content.append("--"); - content.append(endline); - req.setContent(content.toString().getBytes()); + 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)); @@ -402,7 +410,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { mai.setInvocationContext(ActionContext.getContext()); Map<String, Object> param = new HashMap<>(); ActionContext.getContext().withParameters(HttpParameters.create(param).build()); - ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxFiles(req, 3)); + ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxFiles(req)); interceptor.setAllowedTypes("text/html"); interceptor.intercept(mai); @@ -438,7 +446,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { Map<String, Object> param = new HashMap<>(); ActionContext.getContext() .withParameters(HttpParameters.create(param).build()) - .withServletRequest(createMultipartRequestMaxFileSize(req, 10)); + .withServletRequest(createMultipartRequestMaxFileSize(req)); interceptor.intercept(mai); @@ -487,7 +495,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { Map<String, Object> param = new HashMap<>(); ActionContext.getContext() .withParameters(HttpParameters.create(param).build()) - .withServletRequest(createMultipartRequestMaxStringLength(req, 20)); + .withServletRequest(createMultipartRequestMaxStringLength(req)); interceptor.intercept(mai); @@ -540,44 +548,40 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { assertTrue(msg.startsWith("Der Request übertraf die maximal erlaubte Größe")); } - private String encodeTextFile(String bondary, String endline, String name, String filename, String contentType, String content) { - final StringBuilder sb = new StringBuilder(64); - sb.append(endline); - sb.append("--"); - sb.append(bondary); - sb.append(endline); - sb.append("Content-Disposition: form-data; name=\""); - sb.append(name); - sb.append("\"; filename=\""); - sb.append(filename); - sb.append(endline); - sb.append("Content-Type: "); - sb.append(contentType); - sb.append(endline); - sb.append(endline); - sb.append(content); - - return sb.toString(); + 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, int maxfilesize) throws IOException { - return createMultipartRequest(req, -1, maxfilesize, -1, -1); + private MultiPartRequestWrapper createMultipartRequestMaxFileSize(HttpServletRequest req) { + return createMultipartRequest(req, -1, 10, -1, -1); } - private MultiPartRequestWrapper createMultipartRequestMaxFiles(HttpServletRequest req, int maxfiles) throws IOException { - return createMultipartRequest(req, -1, -1, maxfiles, -1); + private MultiPartRequestWrapper createMultipartRequestMaxFiles(HttpServletRequest req) { + return createMultipartRequest(req, -1, -1, 3, -1); } - private MultiPartRequestWrapper createMultipartRequestMaxSize(HttpServletRequest req, int maxsize) throws IOException { + private MultiPartRequestWrapper createMultipartRequestMaxSize(HttpServletRequest req, int maxsize) { return createMultipartRequest(req, maxsize, -1, -1, -1); } - private MultiPartRequestWrapper createMultipartRequestMaxStringLength(HttpServletRequest req, int maxStringLength) throws IOException { - return createMultipartRequest(req, -1, -1, -1, maxStringLength); + 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) throws IOException { + private MultiPartRequestWrapper createMultipartRequest(HttpServletRequest req, int maxsize, int maxfilesize, int maxfiles, int maxStringLength) { JakartaMultiPartRequest jak = new JakartaMultiPartRequest(); jak.setMaxSize(String.valueOf(maxsize)); @@ -589,8 +593,6 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { protected void setUp() throws Exception { super.setUp(); - action = new TestAction(); - container.inject(action); interceptor = new FileUploadInterceptor(); container.inject(interceptor); diff --git a/plugins/pell-multipart/src/main/java/org/apache/struts2/dispatcher/multipart/PellMultiPartRequest.java b/plugins/pell-multipart/src/main/java/org/apache/struts2/dispatcher/multipart/PellMultiPartRequest.java index 9688be65e..d2db975d4 100644 --- a/plugins/pell-multipart/src/main/java/org/apache/struts2/dispatcher/multipart/PellMultiPartRequest.java +++ b/plugins/pell-multipart/src/main/java/org/apache/struts2/dispatcher/multipart/PellMultiPartRequest.java @@ -31,7 +31,6 @@ import java.util.List; /** * Multipart form data request adapter for Jason Pell's multipart utils package. - * */ public class PellMultiPartRequest extends AbstractMultiPartRequest { @@ -69,7 +68,11 @@ public class PellMultiPartRequest extends AbstractMultiPartRequest { } public UploadedFile[] getFile(String fieldName) { - return new UploadedFile[]{ new StrutsUploadedFile(multi.getFile(fieldName)) }; + return new UploadedFile[]{StrutsUploadedFile.Builder.create(multi.getFile(fieldName)) + .withContentType(multi.getContentType(fieldName)) + .withOriginalName(multi.getFileSystemName(fieldName)) + .build() + }; } public String[] getFileNames(String fieldName) { @@ -132,7 +135,7 @@ public class PellMultiPartRequest extends AbstractMultiPartRequest { } } catch (IllegalArgumentException e) { if (LOG.isInfoEnabled()) { - LOG.info("Could not get encoding property 'struts.i18n.encoding' for file upload. Using system default"); + LOG.info("Could not get encoding property 'struts.i18n.encoding' for file upload. Using system default"); } } catch (UnsupportedEncodingException e) { LOG.error("Encoding " + encoding + " is not a valid encoding. Please check your struts.properties file."); @@ -140,8 +143,8 @@ public class PellMultiPartRequest extends AbstractMultiPartRequest { } /* (non-Javadoc) - * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp() - */ + * @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp() + */ public void cleanUp() { Enumeration fileParameterNames = multi.getFileParameterNames(); while (fileParameterNames != null && fileParameterNames.hasMoreElements()) {