diff --git a/apps/showcase/src/main/java/org/apache/struts2/showcase/fileupload/FileUploadAction.java b/apps/showcase/src/main/java/org/apache/struts2/showcase/fileupload/FileUploadAction.java index ed8a11b48..c2ac471f4 100644 --- a/apps/showcase/src/main/java/org/apache/struts2/showcase/fileupload/FileUploadAction.java +++ b/apps/showcase/src/main/java/org/apache/struts2/showcase/fileupload/FileUploadAction.java @@ -21,75 +21,67 @@ package org.apache.struts2.showcase.fileupload; import com.opensymphony.xwork2.ActionSupport; +import org.apache.struts2.action.UploadedFilesAware; +import org.apache.struts2.dispatcher.multipart.UploadedFile; -import java.io.File; +import java.util.List; /** * Show case File Upload example's action. FileUploadAction */ -public class FileUploadAction extends ActionSupport { +public class FileUploadAction extends ActionSupport implements UploadedFilesAware { - private static final long serialVersionUID = 5156288255337069381L; + private String contentType; + private UploadedFile uploadedFile; + private String fileName; + private String caption; + private String originalName; - private String contentType; - private File upload; - private String fileName; - private String caption; + public String input() throws Exception { + return SUCCESS; + } - public String input() throws Exception { - return SUCCESS; - } + public String upload() throws Exception { + return SUCCESS; + } - public String upload() throws Exception { - return SUCCESS; - } + public String getContentType() { + return contentType; + } - // since we are using the file name will be - // obtained through getter/setter of FileName - public String getUploadFileName() { - return fileName; - } + public String getFileName() { + return fileName; + } - public void setUploadFileName(String fileName) { - this.fileName = fileName; - } + public String getOriginalName() { + return originalName; + } + public Object getUploadedFile() { + return uploadedFile.getContent(); + } - // since we are using the content type will be - // obtained through getter/setter of ContentType - public String getUploadContentType() { - return contentType; - } + public String getCaption() { + return caption; + } - public void setUploadContentType(String contentType) { - this.contentType = contentType; - } + public void setCaption(String caption) { + this.caption = caption; + } - - // since we are using the File itself will be - // obtained through getter/setter of - public File getUpload() { - return upload; - } - - public void setUpload(File upload) { - this.upload = upload; - } - - - public String getCaption() { - return caption; - } - - public void setCaption(String caption) { - this.caption = caption; - } - - public long getUploadSize() { - if (upload != null) { - return upload.length(); - } else { - return 0; - } + public long getUploadSize() { + if (uploadedFile != null) { + return uploadedFile.length(); + } else { + return 0; } + } + + @Override + public void withUploadedFiles(List uploadedFiles) { + this.uploadedFile = uploadedFiles.get(0); + this.fileName = uploadedFile.getName(); + this.contentType = uploadedFile.getContentType(); + this.originalName = uploadedFile.getOriginalName(); + } } diff --git a/apps/showcase/src/main/webapp/WEB-INF/fileupload/upload-success.jsp b/apps/showcase/src/main/webapp/WEB-INF/fileupload/upload-success.jsp index d50b61be0..a1b06277a 100644 --- a/apps/showcase/src/main/webapp/WEB-INF/fileupload/upload-success.jsp +++ b/apps/showcase/src/main/webapp/WEB-INF/fileupload/upload-success.jsp @@ -19,7 +19,7 @@ */ --> <%@ page - language="java" + language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="s" uri="/struts-tags" %> @@ -37,9 +37,10 @@
    -
  • ContentType:
  • -
  • FileName:
  • -
  • File:
  • +
  • ContentType:
  • +
  • FileName:
  • +
  • Original FileName:
  • +
  • File:
  • Caption:
diff --git a/core/src/main/java/org/apache/struts2/action/UploadedFilesAware.java b/core/src/main/java/org/apache/struts2/action/UploadedFilesAware.java new file mode 100644 index 000000000..92ec9c98b --- /dev/null +++ b/core/src/main/java/org/apache/struts2/action/UploadedFilesAware.java @@ -0,0 +1,40 @@ +/* + * 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.action; + +import org.apache.struts2.dispatcher.multipart.UploadedFile; + +import java.util.List; + +/** + * 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}, cannot be null. It can be empty. + */ + void withUploadedFiles(List 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..428ae112f 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; @@ -108,12 +109,12 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { return null; } - List files = new ArrayList<>(infos.size()); - for (FileInfo fileInfo : infos) { - files.add(new StrutsUploadedFile(fileInfo.getFile())); - } - - return files.toArray(new UploadedFile[0]); + return infos.stream().map(fileInfo -> + StrutsUploadedFile.Builder.create(fileInfo.getFile()) + .withContentType(fileInfo.contentType) + .withOriginalName(fileInfo.originalName) + .build() + ).toArray(UploadedFile[]::new); } /* (non-Javadoc) @@ -162,7 +163,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { */ public String getParameter(String name) { List values = parameters.get(name); - if (values != null && values.size() > 0) { + if (values != null && !values.isEmpty()) { return values.get(0); } return null; @@ -180,7 +181,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { */ public String[] getParameterValues(String name) { List 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 +208,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 +292,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 +330,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 +368,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 +392,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 +416,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 +440,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 +451,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..5976f578f 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,26 @@ import java.io.File; public class StrutsUploadedFile implements UploadedFile { - private File file; + private final File file; + private final String contentType; + private final String originalName; + /** + * Use builder instead of constructor + * @param file an uploaded file + * @deprecated since Struts 6.4.0 + */ + @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 +73,50 @@ public class StrutsUploadedFile implements UploadedFile { public File getContent() { return file; } + + @Override + public String getContentType() { + return this.contentType; + } + + @Override + public String getOriginalName() { + return originalName; + } + + @Override + public String toString() { + return "StrutsUploadedFile{" + + "contentType='" + contentType + '\'' + + ", originalName='" + 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..ada27ff6c 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 @@ -18,15 +18,19 @@ */ package org.apache.struts2.dispatcher.multipart; +import java.io.Serializable; + /** * Virtual representation of a uploaded file used by {@link MultiPartRequest} */ -public interface UploadedFile { +public interface UploadedFile extends Serializable { Long length(); String getName(); + String getOriginalName(); + boolean isFile(); boolean delete(); @@ -35,4 +39,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..1113f4491 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/AbstractFileUploadInterceptor.java @@ -0,0 +1,263 @@ +/* + * 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.HashSet; +import java.util.Set; + +public abstract class AbstractFileUploadInterceptor extends AbstractInterceptor { + + private static final Logger LOG = LogManager.getLogger(AbstractFileUploadInterceptor.class); + + public static final String STRUTS_MESSAGES_BYPASS_REQUEST_KEY = "struts.messages.bypass.request"; + public static final String STRUTS_MESSAGES_ERROR_UPLOADING_KEY = "struts.messages.error.uploading"; + public static final String STRUTS_MESSAGES_ERROR_FILE_TOO_LARGE_KEY = "struts.messages.error.file.too.large"; + public static final String STRUTS_MESSAGES_INVALID_FILE_KEY = "struts.messages.invalid.file"; + public static final String STRUTS_MESSAGES_INVALID_CONTENT_TYPE_KEY = "struts.messages.invalid.content.type"; + public static final String STRUTS_MESSAGES_ERROR_CONTENT_TYPE_NOT_ALLOWED_KEY = "struts.messages.error.content.type.not.allowed"; + public static final String STRUTS_MESSAGES_ERROR_FILE_EXTENSION_NOT_ALLOWED_KEY = "struts.messages.error.file.extension.not.allowed"; + + private Long maximumSize; + private Set allowedTypesSet = Collections.emptySet(); + private Set 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; + } + + /** + * 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 originalFilename - 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 originalFilename, String contentType, String inputName) { + Set errorMessages = new HashSet<>(); + + 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_KEY, new String[]{inputName}); + if (validation != null) { + validation.addFieldError(inputName, errMsg); + } + LOG.warn(errMsg); + return false; + } + + if (file.getContent() == null) { + String errMsg = getTextMessage(action, STRUTS_MESSAGES_ERROR_UPLOADING_KEY, new String[]{originalFilename}); + errorMessages.add(errMsg); + LOG.warn(errMsg); + } + if (maximumSize != null && maximumSize < file.length()) { + String errMsg = getTextMessage(action, STRUTS_MESSAGES_ERROR_FILE_TOO_LARGE_KEY, new String[]{ + inputName, originalFilename, file.getName(), "" + file.length(), getMaximumSizeStr(action) + }); + errorMessages.add(errMsg); + LOG.warn(errMsg); + } + if ((!allowedTypesSet.isEmpty()) && (!containsItem(allowedTypesSet, contentType))) { + String errMsg = getTextMessage(action, STRUTS_MESSAGES_ERROR_CONTENT_TYPE_NOT_ALLOWED_KEY, new String[]{ + inputName, originalFilename, file.getName(), contentType + }); + errorMessages.add(errMsg); + LOG.warn(errMsg); + } + if ((!allowedExtensionsSet.isEmpty()) && (!hasAllowedExtension(allowedExtensionsSet, originalFilename))) { + String errMsg = getTextMessage(action, STRUTS_MESSAGES_ERROR_FILE_EXTENSION_NOT_ALLOWED_KEY, new String[]{ + inputName, originalFilename, file.getName(), contentType + }); + errorMessages.add(errMsg); + LOG.warn(errMsg); + } + if (validation != null) { + for (String errorMsg : errorMessages) { + validation.addFieldError(inputName, errorMsg); + } + } + + return errorMessages.isEmpty(); + } + + 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 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 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_KEY, 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..c42d125af --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/ActionFileUploadInterceptor.java @@ -0,0 +1,191 @@ +/* + * 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; + +/** + *

+ * Interceptor that is based off of {@link MultiPartRequestWrapper}, which is automatically applied for any request that + * includes a file when the support for multi-part request is enabled, + * see Disabling file upload. + *

+ * + *

+ * You can get access to these files by implementing {@link UploadedFilesAware} interface. The interceptor will then + * call {@link UploadedFilesAware#withUploadedFiles(List)} when there are files which were accepted during the upload process. + *

+ * + *

+ * 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: + *

+ * + *
    + *
  • struts.messages.error.uploading - a general error that occurs when the file could not be uploaded
  • + *
  • struts.messages.error.file.too.large - occurs when the uploaded file is too large
  • + *
  • struts.messages.error.content.type.not.allowed - occurs when the uploaded file does not match the expected + * content types specified
  • + *
  • struts.messages.error.file.extension.not.allowed - occurs when the uploaded file does not match the expected + * file extensions specified
  • + *
+ * + *

Interceptor parameters:

+ *
    + *
  • maximumSize (optional) - the maximum size (in bytes) that the interceptor will allow a file reference to be set + * on the action. Note, this is not related to the various properties found in struts.properties. + * Default to approximately 2MB.
  • + *
  • 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.
  • + *
  • 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.
  • + *
+ * + *

Example code:

+ * + *
+ * <action name="doUpload" class="com.example.UploadAction">
+ *     <interceptor-ref name="actionFileUpload"/>
+ *     <interceptor-ref name="basicStack"/>
+ *     <result name="success">good_result.jsp</result>
+ * </action>
+ * 
+ *

+ *

+ * You must set the encoding to multipart/form-data in the form where the user selects the file to upload. + *

+ *
+ *   <s:form action="doUpload" method="post" enctype="multipart/form-data">
+ *       <s:file name="upload" label="File"/>
+ *       <s:submit/>
+ *   </s:form>
+ * 
+ *

+ * 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. + *

+ * + *
+ *  package com.example;
+ *
+ *  import java.io.File;
+ *  import com.opensymphony.xwork2.ActionSupport;
+ *  import org.apache.struts2.action.UploadedFilesAware;
+ *
+ *  public UploadAction extends ActionSupport implements UploadedFilesAware {
+ *    private UploadedFile uploadedFile;
+ *    private String contentType;
+ *    private String fileName;
+ *    private String originalName;
+ *
+ *    @Override
+ *    public void withUploadedFiles(List uploadedFiles) {
+ *        if (!uploadedFiles.isEmpty() > 0) {
+ *            this.uploadedFile = uploadedFiles.get(0);
+ *            this.fileName = uploadedFile.getName();
+ *            this.contentType = uploadedFile.getContentType();
+ *            this.originalName = uploadedFile.getOriginalName();
+ *        }
+ *    }
+ *
+ *    public String execute() {
+ *       //...
+ *       return SUCCESS;
+ *    }
+ *  }
+ * 
+ */ +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)) { + if (LOG.isDebugEnabled()) { + ActionProxy proxy = invocation.getProxy(); + LOG.debug(getTextMessage(STRUTS_MESSAGES_BYPASS_REQUEST_KEY, 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 fileParameterNames = multiWrapper.getFileParameterNames(); + List acceptedFiles = new ArrayList<>(); + + while (fileParameterNames != null && fileParameterNames.hasMoreElements()) { + // get the value of this input tag + String inputName = fileParameterNames.nextElement(); + UploadedFile[] uploadedFiles = multiWrapper.getFiles(inputName); + + if (uploadedFiles == null || uploadedFiles.length == 0) { + if (LOG.isWarnEnabled()) { + LOG.warn(getTextMessage(action, STRUTS_MESSAGES_INVALID_FILE_KEY, new String[]{inputName})); + } + } else { + for (UploadedFile uploadedFile : uploadedFiles) { + if (acceptFile(action, uploadedFile, uploadedFile.getOriginalName(), uploadedFile.getContentType(), inputName)) { + acceptedFiles.add(uploadedFile); + } + } + } + } + + 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); + } + + // 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..86f8a64be 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; /** * @@ -77,11 +76,11 @@ import java.util.*; * file extensions specified * * - * + *

* * *

Interceptor parameters:

- * + *

* * *

    @@ -96,14 +95,14 @@ import java.util.*; *
  • 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.
  • *
- * - * + *

+ *

* * *

Extending the interceptor:

- * - * - * + *

+ *

+ *

* *

* 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> * * - * + *

* *

* You must set the encoding to multipart/form-data in the form where the user selects the file to upload. @@ -173,57 +172,16 @@ import java.util.*; * } * * + * + * @deprecated since Struts 6.4.0, use {@link ActionFileUploadInterceptor} instead */ -public class FileUploadInterceptor extends AbstractInterceptor { +@Deprecated +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 allowedTypesSet = Collections.emptySet(); - protected Set 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) */ @@ -236,40 +194,22 @@ public class FileUploadInterceptor extends AbstractInterceptor { if (!(request instanceof MultiPartRequestWrapper)) { if (LOG.isDebugEnabled()) { ActionProxy proxy = invocation.getProxy(); - LOG.debug(getTextMessage("struts.messages.bypass.request", new String[]{proxy.getNamespace(), proxy.getActionName()})); + LOG.debug(getTextMessage(STRUTS_MESSAGES_BYPASS_REQUEST_KEY, new String[]{proxy.getNamespace(), proxy.getActionName()})); } 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 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 +229,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,20 +238,20 @@ public class FileUploadInterceptor extends AbstractInterceptor { if (!acceptedFiles.isEmpty()) { Map 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); } } } else { if (LOG.isWarnEnabled()) { - LOG.warn(getTextMessage(action, "struts.messages.invalid.file", new String[]{inputName})); + LOG.warn(getTextMessage(action, STRUTS_MESSAGES_INVALID_FILE_KEY, new String[]{inputName})); } } } else { if (LOG.isWarnEnabled()) { - LOG.warn(getTextMessage(action, "struts.messages.invalid.content.type", new String[]{inputName})); + LOG.warn(getTextMessage(action, STRUTS_MESSAGES_INVALID_CONTENT_TYPE_KEY, new String[]{inputName})); } } } @@ -320,149 +260,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 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 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); - } - - 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/org/apache/struts2/struts-messages_en.properties b/core/src/main/resources/org/apache/struts2/struts-messages_en.properties index 155e13c39..d70fe0119 100644 --- a/core/src/main/resources/org/apache/struts2/struts-messages_en.properties +++ b/core/src/main/resources/org/apache/struts2/struts-messages_en.properties @@ -25,15 +25,40 @@ struts.internal.invalid.token=Form token {0} does not match the session token {1 struts.messages.bypass.request=Bypassing {0}/{1} struts.messages.current.file=File {0} {1} {2} {3} +# 0 - input name struts.messages.invalid.file=Could not find a Filename for {0}. Verify that a valid file was submitted. +# 0 - original filename struts.messages.invalid.content.type=Could not find a Content-Type for {0}. Verify that a valid file was submitted. struts.messages.removing.file=Removing file {0} {1} struts.messages.error.uploading=Error uploading: {0} -struts.messages.error.file.too.large=The file is too large to be uploaded: {0} "{1}" "{2}" {3} +# 0 - input name +# 1 - original filename +# 2 - file name after uploading the file +# 3 - size of the uploaded files +# 4 - maximum allowed size +struts.messages.error.file.too.large=The file is too large to be uploaded: {0} "{1}" "{2}" has size {3} and allowed mx size is {4} +# 0 - input name +# 1 - original filename +# 2 - file name after uploading the file +# 3 - content type of the file struts.messages.error.content.type.not.allowed=Content-Type not allowed: {0} "{1}" "{2}" {3} +# 0 - input name +# 1 - original filename +# 2 - file name after uploading the file +# 3 - content type of the file struts.messages.error.file.extension.not.allowed=File extension not allowed: {0} "{1}" "{2}" {3} # dedicated messages used to handle various problems with file upload - check {@link JakartaMultiPartRequest#parse(HttpServletRequest, String)} +# params depend on exception being handled +# FileUploadBase.SizeLimitExceededException: +# 0 - permitted size +# 1 - actual size +# FileUploadBase.FileSizeLimitExceededException +# 0 - file name +# 1 - permitted size +# 2 - actual size +# FileCountLimitExceededException +# 0 - limit struts.messages.upload.error.SizeLimitExceededException=Request exceeded allowed size limit! Max size allowed is: {0} but request was: {1}! struts.messages.upload.error.IOException=Error uploading: {0}! 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 @@ + @@ -121,6 +122,12 @@ + + + + + + @@ -165,6 +172,7 @@ + @@ -203,6 +211,7 @@ + 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 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/ActionFileUploadInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java new file mode 100644 index 000000000..51747b147 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java @@ -0,0 +1,591 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.interceptor; + +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.ActionSupport; +import com.opensymphony.xwork2.DefaultLocaleProvider; +import com.opensymphony.xwork2.ValidationAwareSupport; +import com.opensymphony.xwork2.mock.MockActionInvocation; +import com.opensymphony.xwork2.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.action.UploadedFilesAware; +import org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest; +import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper; +import org.apache.struts2.dispatcher.multipart.StrutsUploadedFile; +import org.apache.struts2.dispatcher.multipart.UploadedFile; +import org.springframework.mock.web.MockHttpServletRequest; + +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test case for {@link ActionFileUploadInterceptor}. + */ +public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase { + + public static final UploadedFile EMPTY_FILE = new UploadedFile() { + @Override + public Long length() { + return 0L; + } + + @Override + public String getName() { + return ""; + } + + @Override + public boolean isFile() { + return false; + } + + @Override + public boolean delete() { + return false; + } + + @Override + public String getAbsolutePath() { + return null; + } + + @Override + public byte[] getContent() { + return new byte[0]; + } + + @Override + public String getOriginalName() { + return null; + } + + @Override + public String getContentType() { + return null; + } + }; + + private ActionFileUploadInterceptor interceptor; + private File tempDir; + + private final String htmlContent = "html content"; + private final String plainContent = "plain content"; + private final String boundary = "simple boundary"; + private final String endline = "\r\n"; + + public void testAcceptFileWithEmptyAllowedTypesAndExtensions() { + // when allowed type is empty + ValidationAwareSupport validation = new ValidationAwareSupport(); + boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename", "text/plain", "inputName"); + + assertTrue(ok); + assertTrue(validation.getFieldErrors().isEmpty()); + assertFalse(validation.hasErrors()); + } + + public void testAcceptFileWithoutEmptyTypes() { + interceptor.setAllowedTypes("text/plain"); + + // when file is of allowed types + ValidationAwareSupport validation = new ValidationAwareSupport(); + boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName"); + + assertTrue(ok); + assertTrue(validation.getFieldErrors().isEmpty()); + assertFalse(validation.hasErrors()); + + // when file is not of allowed types + validation = new ValidationAwareSupport(); + boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/html", "inputName"); + + assertFalse(notOk); + assertFalse(validation.getFieldErrors().isEmpty()); + assertTrue(validation.hasErrors()); + } + + + public void testAcceptFileWithWildcardContent() { + interceptor.setAllowedTypes("text/*"); + + ValidationAwareSupport validation = new ValidationAwareSupport(); + boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName"); + + assertTrue(ok); + assertTrue(validation.getFieldErrors().isEmpty()); + assertFalse(validation.hasErrors()); + + interceptor.setAllowedTypes("text/h*"); + validation = new ValidationAwareSupport(); + boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/plain", "inputName"); + + assertFalse(notOk); + assertFalse(validation.getFieldErrors().isEmpty()); + assertTrue(validation.hasErrors()); + } + + public void testAcceptFileWithoutEmptyExtensions() { + interceptor.setAllowedExtensions(".txt"); + + // when file is of allowed extensions + ValidationAwareSupport validation = new ValidationAwareSupport(); + boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName"); + + assertTrue(ok); + assertTrue(validation.getFieldErrors().isEmpty()); + assertFalse(validation.hasErrors()); + + // when file is not of allowed extensions + validation = new ValidationAwareSupport(); + boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/html", "inputName"); + + assertFalse(notOk); + assertFalse(validation.getFieldErrors().isEmpty()); + assertTrue(validation.hasErrors()); + + //test with multiple extensions + interceptor.setAllowedExtensions(".txt,.lol"); + validation = new ValidationAwareSupport(); + ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.lol", "text/plain", "inputName"); + + assertTrue(ok); + assertTrue(validation.getFieldErrors().isEmpty()); + assertFalse(validation.hasErrors()); + } + + public void testAcceptFileWithNoFile() { + 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(validation, null, "filename.html", "text/html", "inputName"); + + assertFalse(notOk); + assertFalse(validation.getFieldErrors().isEmpty()); + assertTrue(validation.hasErrors()); + List errors = validation.getFieldErrors().get("inputName"); + assertEquals(1, errors.size()); + String msg = errors.get(0); + assertTrue(msg.startsWith("Error uploading:")); + assertTrue(msg.indexOf("inputName") > 0); + } + + public void testAcceptFileWithMaxSize() throws Exception { + interceptor.setMaximumSize(10L); + + // when file is not of allowed types + ValidationAwareSupport validation = new ValidationAwareSupport(); + + URL url = ClassLoaderUtil.getResource("log4j2.xml", ActionFileUploadInterceptorTest.class); + File file = new File(new URI(url.toString())); + assertTrue("log4j2.xml should be in src/test folder", file.exists()); + UploadedFile uploadedFile = StrutsUploadedFile.Builder.create(file).withContentType("text/html").withOriginalName("filename").build(); + boolean notOk = interceptor.acceptFile(validation, uploadedFile, "filename", "text/html", "inputName"); + + assertFalse(notOk); + assertFalse(validation.getFieldErrors().isEmpty()); + assertTrue(validation.hasErrors()); + List errors = validation.getFieldErrors().get("inputName"); + assertEquals(1, errors.size()); + String msg = errors.get(0); + // the error message should contain at least this test + assertThat(msg).contains( + "The file is too large to be uploaded", + "inputName", + "log4j2.xml", + "allowed mx size is 10" + ); + } + + public void testNoMultipartRequest() throws Exception { + 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 + assertEquals("NoMultipart", interceptor.intercept(mai)); + } + + public void testInvalidContentTypeMultipartRequest() throws Exception { + MockHttpServletRequest req = new MockHttpServletRequest(); + + req.setContentType("multipart/form-data"); // not a multipart contentype + req.setMethod("post"); + + MyFileUploadAction action = container.inject(MyFileUploadAction.class); + MockActionInvocation mai = new MockActionInvocation(); + mai.setAction(action); + mai.setResultCode("success"); + mai.setInvocationContext(ActionContext.getContext()); + + ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000)); + + interceptor.intercept(mai); + + assertTrue(action.hasErrors()); + } + + public void testNoContentMultipartRequest() throws Exception { + MockHttpServletRequest req = new MockHttpServletRequest(); + + req.setCharacterEncoding(StandardCharsets.UTF_8.name()); + req.setMethod("post"); + req.addHeader("Content-type", "multipart/form-data"); + req.setContent(null); // there is no content + + MyFileUploadAction action = container.inject(MyFileUploadAction.class); + MockActionInvocation mai = new MockActionInvocation(); + mai.setAction(action); + mai.setResultCode("success"); + mai.setInvocationContext(ActionContext.getContext()); + + ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000)); + + interceptor.intercept(mai); + + assertTrue(action.hasErrors()); + } + + public void testSuccessUploadOfATextFileMultipartRequest() throws Exception { + MockHttpServletRequest req = new MockHttpServletRequest(); + req.setCharacterEncoding(StandardCharsets.UTF_8.name()); + req.setMethod("post"); + req.addHeader("Content-type", "multipart/form-data; boundary=---1234"); + + // inspired by the unit tests for jakarta commons fileupload + String content = ("-----1234\r\n" + + "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + + "Content-Type: text/html\r\n" + + "\r\n" + + "Unit test of ActionFileUploadInterceptor" + + "\r\n" + + "-----1234--\r\n"); + req.setContent(content.getBytes(StandardCharsets.US_ASCII)); + + MyFileUploadAction action = new MyFileUploadAction(); + + MockActionInvocation mai = new MockActionInvocation(); + mai.setAction(action); + mai.setResultCode("success"); + mai.setInvocationContext(ActionContext.getContext()); + ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000)); + + interceptor.intercept(mai); + + assertFalse(action.hasErrors()); + + List files = action.getUploadFiles(); + + assertNotNull(files); + 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 + * ActionFileUploadInterceptor.setAllowedTypes(...) ) are sorted out. + */ + public void testMultipleAccept() throws Exception { + MockHttpServletRequest req = new MockHttpServletRequest(); + req.setCharacterEncoding(StandardCharsets.UTF_8.name()); + req.setMethod("POST"); + req.addHeader("Content-type", "multipart/form-data; boundary=" + boundary); + String content = encodeTextFile("test.html", "text/plain", plainContent) + + encodeTextFile("test1.html", "text/html", htmlContent) + + encodeTextFile("test2.html", "text/html", htmlContent) + + endline + + endline + + endline + + "--" + + boundary + + "--" + + endline; + req.setContent(content.getBytes()); + + assertTrue(ServletFileUpload.isMultipartContent(req)); + + MyFileUploadAction action = new MyFileUploadAction(); + container.inject(action); + MockActionInvocation mai = new MockActionInvocation(); + mai.setAction(action); + mai.setResultCode("success"); + mai.setInvocationContext(ActionContext.getContext()); + ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000)); + + interceptor.setAllowedTypes("text/html"); + interceptor.intercept(mai); + + List files = action.getUploadFiles(); + + assertNotNull(files); + 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 { + MockHttpServletRequest req = new MockHttpServletRequest(); + req.setCharacterEncoding(StandardCharsets.UTF_8.name()); + req.setMethod("POST"); + req.addHeader("Content-type", "multipart/form-data; boundary=" + boundary); + String content = encodeTextFile("test.html", "text/plain", plainContent) + + encodeTextFile("test1.html", "text/html", htmlContent) + + encodeTextFile("test2.html", "text/html", htmlContent) + + encodeTextFile("test3.html", "text/html", htmlContent) + + endline + + "--" + + boundary + + "--" + + endline; + req.setContent(content.getBytes()); + + assertTrue(ServletFileUpload.isMultipartContent(req)); + + MyFileUploadAction action = new MyFileUploadAction(); + container.inject(action); + MockActionInvocation mai = new MockActionInvocation(); + mai.setAction(action); + mai.setResultCode("success"); + mai.setInvocationContext(ActionContext.getContext()); + ActionContext.getContext().withServletRequest(createMultipartRequestMaxFiles(req)); + + interceptor.setAllowedTypes("text/html"); + interceptor.intercept(mai); + + 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()); + } + + public void testMultipartRequestMaxFileSize() throws Exception { + MockHttpServletRequest req = new MockHttpServletRequest(); + req.setCharacterEncoding(StandardCharsets.UTF_8.name()); + req.setMethod("post"); + req.addHeader("Content-type", "multipart/form-data; boundary=---1234"); + + // inspired by the unit tests for jakarta commons fileupload + String content = ("-----1234\r\n" + + "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + + "Content-Type: text/html\r\n" + + "\r\n" + + "Unit test of ActionFileUploadInterceptor" + + "\r\n" + + "-----1234--\r\n"); + req.setContent(content.getBytes(StandardCharsets.US_ASCII)); + + MyFileUploadAction action = container.inject(MyFileUploadAction.class); + + MockActionInvocation mai = new MockActionInvocation(); + mai.setAction(action); + mai.setResultCode("success"); + mai.setInvocationContext(ActionContext.getContext()); + ActionContext.getContext() + .withServletRequest(createMultipartRequestMaxFileSize(req)); + + interceptor.intercept(mai); + + assertTrue(action.hasActionErrors()); + + Collection errors = action.getActionErrors(); + assertEquals(1, errors.size()); + String msg = errors.iterator().next(); + assertEquals( + "File in request exceeded allowed file size limit! Max file size allowed is: 10 but file deleteme.txt was: 40!", + msg); + } + + public void testMultipartRequestMaxStringLength() throws Exception { + MockHttpServletRequest req = new MockHttpServletRequest(); + req.setCharacterEncoding(StandardCharsets.UTF_8.name()); + req.setMethod("post"); + req.addHeader("Content-type", "multipart/form-data; boundary=---1234"); + + // inspired by the unit tests for jakarta commons fileupload + String content = ("-----1234\r\n" + + "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + + "Content-Type: text/html\r\n" + + "\r\n" + + "Unit test of 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)); + + MyFileUploadAction action = container.inject(MyFileUploadAction.class); + + MockActionInvocation mai = new MockActionInvocation(); + mai.setAction(action); + mai.setResultCode("success"); + mai.setInvocationContext(ActionContext.getContext()); + ActionContext.getContext() + .withServletRequest(createMultipartRequestMaxStringLength(req)); + + interceptor.intercept(mai); + + assertTrue(action.hasActionErrors()); + + Collection errors = action.getActionErrors(); + assertEquals(1, errors.size()); + String msg = errors.iterator().next(); + assertEquals( + "The request parameter \"normalFormField2\" was too long. Max length allowed is 20, but found 27!", + msg); + } + + public void testMultipartRequestLocalizedError() throws Exception { + MockHttpServletRequest req = new MockHttpServletRequest(); + req.setCharacterEncoding(StandardCharsets.UTF_8.name()); + req.setMethod("post"); + req.addHeader("Content-type", "multipart/form-data; boundary=---1234"); + + // inspired by the unit tests for jakarta commons fileupload + String content = ("-----1234\r\n" + + "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + + "Content-Type: text/html\r\n" + + "\r\n" + + "Unit test of ActionFileUploadInterceptor" + + "\r\n" + + "-----1234--\r\n"); + req.setContent(content.getBytes(StandardCharsets.US_ASCII)); + + MyFileUploadAction action = container.inject(MyFileUploadAction.class); + + MockActionInvocation mai = new MockActionInvocation(); + mai.setAction(action); + mai.setResultCode("success"); + mai.setInvocationContext(ActionContext.getContext()); + ActionContext.getContext() + .withLocale(Locale.GERMAN) + .withServletRequest(createMultipartRequestMaxSize(req, 10)); + + interceptor.intercept(mai); + + assertTrue(action.hasActionErrors()); + + Collection errors = action.getActionErrors(); + assertEquals(1, errors.size()); + String msg = errors.iterator().next(); + // the error message should contain at least this test + assertTrue(msg.startsWith("Der Request übertraf die maximal erlaubte Größe")); + } + + private String encodeTextFile(String filename, String contentType, String content) { + return "\r\n" + + "--" + + "simple boundary" + + "\r\n" + + "Content-Disposition: form-data; name=\"" + + "file" + + "\"; filename=\"" + + filename + + "\r\n" + + "Content-Type: " + + contentType + + "\r\n" + + "\r\n" + + content; + } + + private MultiPartRequestWrapper createMultipartRequestMaxFileSize(HttpServletRequest req) { + return createMultipartRequest(req, -1, 10, -1, -1); + } + + private MultiPartRequestWrapper createMultipartRequestMaxFiles(HttpServletRequest req) { + return createMultipartRequest(req, -1, -1, 3, -1); + } + + private MultiPartRequestWrapper createMultipartRequestMaxSize(HttpServletRequest req, int maxsize) { + return createMultipartRequest(req, maxsize, -1, -1, -1); + } + + private MultiPartRequestWrapper createMultipartRequestMaxStringLength(HttpServletRequest req) { + return createMultipartRequest(req, -1, -1, -1, 20); + } + + private MultiPartRequestWrapper createMultipartRequest(HttpServletRequest req, int maxsize, int maxfilesize, int maxfiles, int maxStringLength) { + + JakartaMultiPartRequest jak = new JakartaMultiPartRequest(); + jak.setMaxSize(String.valueOf(maxsize)); + jak.setMaxFileSize(String.valueOf(maxfilesize)); + jak.setMaxFiles(String.valueOf(maxfiles)); + jak.setMaxStringLength(String.valueOf(maxStringLength)); + return new MultiPartRequestWrapper(jak, req, tempDir.getAbsolutePath(), new DefaultLocaleProvider()); + } + + protected void setUp() throws Exception { + super.setUp(); + + interceptor = new ActionFileUploadInterceptor(); + container.inject(interceptor); + tempDir = File.createTempFile("struts", "fileupload"); + tempDir.delete(); + tempDir.mkdirs(); + } + + protected void tearDown() throws Exception { + tempDir.delete(); + interceptor.destroy(); + super.tearDown(); + } + + public static class MyFileUploadAction extends ActionSupport implements UploadedFilesAware { + private List uploadedFiles; + + @Override + public void withUploadedFiles(List uploadedFiles) { + this.uploadedFiles = uploadedFiles; + } + + public List 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..14bb23c36 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java @@ -47,6 +47,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import static org.assertj.core.api.Assertions.assertThat; + /** * Test case for FileUploadInterceptor. @@ -83,28 +85,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 +123,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 +131,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 +143,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 +163,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 +172,35 @@ 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 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,18 +208,22 @@ 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 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); - assertTrue(msg.indexOf("log4j2.xml") > 0); + assertThat(msg).contains( + "The file is too large to be uploaded", + "inputName", + "log4j2.xml", + "allowed mx size is 10" + ); } public void testNoMultipartRequest() throws Exception { @@ -278,7 +294,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 +308,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 +319,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 +329,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 content"; @@ -326,18 +340,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 +360,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { mai.setAction(action); mai.setResultCode("success"); mai.setInvocationContext(ActionContext.getContext()); - Map param = new HashMap(); + Map param = new HashMap<>(); ActionContext.getContext().withParameters(HttpParameters.create(param).build()); ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequestMaxSize(req, 2000)); @@ -380,17 +393,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 +414,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { mai.setInvocationContext(ActionContext.getContext()); Map 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 +450,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { Map param = new HashMap<>(); ActionContext.getContext() .withParameters(HttpParameters.create(param).build()) - .withServletRequest(createMultipartRequestMaxFileSize(req, 10)); + .withServletRequest(createMultipartRequestMaxFileSize(req)); interceptor.intercept(mai); @@ -487,7 +499,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { Map param = new HashMap<>(); ActionContext.getContext() .withParameters(HttpParameters.create(param).build()) - .withServletRequest(createMultipartRequestMaxStringLength(req, 20)); + .withServletRequest(createMultipartRequestMaxStringLength(req)); interceptor.intercept(mai); @@ -540,44 +552,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 +597,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()) {