View Javadoc

1   /*
2    * $Id: FileUploadInterceptor.java 781032 2009-06-02 13:50:54Z wesw $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  package org.apache.struts2.interceptor;
23  
24  import java.io.File;
25  import java.util.*;
26  
27  import javax.servlet.http.HttpServletRequest;
28  
29  import org.apache.struts2.ServletActionContext;
30  import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
31  
32  import com.opensymphony.xwork2.ActionContext;
33  import com.opensymphony.xwork2.ActionInvocation;
34  import com.opensymphony.xwork2.ActionProxy;
35  import com.opensymphony.xwork2.ValidationAware;
36  import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
37  import com.opensymphony.xwork2.util.LocalizedTextUtil;
38  import com.opensymphony.xwork2.util.TextParseUtil;
39  import com.opensymphony.xwork2.util.logging.Logger;
40  import com.opensymphony.xwork2.util.logging.LoggerFactory;
41  
42  /***
43   * <!-- START SNIPPET: description -->
44   * <p/>
45   * Interceptor that is based off of {@link MultiPartRequestWrapper}, which is automatically applied for any request that
46   * includes a file. It adds the following parameters, where [File Name] is the name given to the file uploaded by the
47   * HTML form:
48   * <p/>
49   * <ul>
50   * <p/>
51   * <li>[File Name] : File - the actual File</li>
52   * <p/>
53   * <li>[File Name]ContentType : String - the content type of the file</li>
54   * <p/>
55   * <li>[File Name]FileName : String - the actual name of the file uploaded (not the HTML name)</li>
56   * <p/>
57   * </ul>
58   * <p/>
59   * <p/> You can get access to these files by merely providing setters in your action that correspond to any of the three
60   * patterns above, such as setDocument(File document), setDocumentContentType(String contentType), etc.
61   * <br/>See the example code section.
62   * <p/>
63   * <p/> This interceptor will add several field errors, assuming that the action implements {@link ValidationAware}.
64   * These error messages are based on several i18n values stored in struts-messages.properties, a default i18n file
65   * processed for all i18n requests. You can override the text of these messages by providing text for the following
66   * keys:
67   * <p/>
68   * <ul>
69   * <p/>
70   * <li>struts.messages.error.uploading - a general error that occurs when the file could not be uploaded</li>
71   * <p/>
72   * <li>struts.messages.error.file.too.large - occurs when the uploaded file is too large</li>
73   * <p/>
74   * <li>struts.messages.error.content.type.not.allowed - occurs when the uploaded file does not match the expected
75   * content types specified</li>
76   * <p/>
77   * <li>struts.messages.error.file.extension.not.allowed - occurs when the uploaded file does not match the expected
78   * file extensions specified</li>
79   * <p/>
80   * </ul>
81   * <p/>
82   * <!-- END SNIPPET: description -->
83   * <p/>
84   * <p/> <u>Interceptor parameters:</u>
85   * <p/>
86   * <!-- START SNIPPET: parameters -->
87   * <p/>
88   * <ul>
89   * <p/>
90   * <li>maximumSize (optional) - the maximum size (in bytes) that the interceptor will allow a file reference to be set
91   * on the action. Note, this is <b>not</b> related to the various properties found in struts.properties.
92   * Default to approximately 2MB.</li>
93   * <p/>
94   * <li>allowedTypes (optional) - a comma separated list of content types (ie: text/html) that the interceptor will allow
95   * a file reference to be set on the action. If none is specified allow all types to be uploaded.</li>
96   * <p/>
97   * <li>allowedExtensions (optional) - a comma separated list of file extensions (ie: .html) that the interceptor will allow
98   * a file reference to be set on the action. If none is specified allow all extensions to be uploaded.</li>
99   * </ul>
100  * <p/>
101  * <p/>
102  * <!-- END SNIPPET: parameters -->
103  * <p/>
104  * <p/> <u>Extending the interceptor:</u>
105  * <p/>
106  * <p/>
107  * <p/>
108  * <!-- START SNIPPET: extending -->
109  * <p/>
110  * You can extend this interceptor and override the acceptFile method to provide more control over which files
111  * are supported and which are not.
112  * <p/>
113  * <!-- END SNIPPET: extending -->
114  * <p/>
115  * <p/> <u>Example code:</u>
116  * <p/>
117  * <pre>
118  * <!-- START SNIPPET: example-configuration -->
119  * &lt;action name="doUpload" class="com.example.UploadAction"&gt;
120  *     &lt;interceptor-ref name="fileUpload"/&gt;
121  *     &lt;interceptor-ref name="basicStack"/&gt;
122  *     &lt;result name="success"&gt;good_result.jsp&lt;/result&gt;
123  * &lt;/action&gt;
124  * <!-- END SNIPPET: example-configuration -->
125  * </pre>
126  * <p/>
127  * <!-- START SNIPPET: multipart-note -->
128  * <p/>
129  * You must set the encoding to <code>multipart/form-data</code> in the form where the user selects the file to upload.
130  * <p/>
131  * <!-- END SNIPPET: multipart-note -->
132  * <p/>
133  * <pre>
134  * <!-- START SNIPPET: example-form -->
135  *   &lt;s:form action="doUpload" method="post" enctype="multipart/form-data"&gt;
136  *       &lt;s:file name="upload" label="File"/&gt;
137  *       &lt;s:submit/&gt;
138  *   &lt;/s:form&gt;
139  * <!-- END SNIPPET: example-form -->
140  * </pre>
141  * <p/>
142  * And then in your action code you'll have access to the File object if you provide setters according to the
143  * naming convention documented in the start.
144  * <p/>
145  * <pre>
146  * <!-- START SNIPPET: example-action -->
147  *    package com.example;
148  * <p/>
149  *    import java.io.File;
150  *    import com.opensymphony.xwork2.ActionSupport;
151  * <p/>
152  *    public UploadAction extends ActionSupport {
153  *       private File file;
154  *       private String contentType;
155  *       private String filename;
156  * <p/>
157  *       public void setUpload(File file) {
158  *          this.file = file;
159  *       }
160  * <p/>
161  *       public void setUploadContentType(String contentType) {
162  *          this.contentType = contentType;
163  *       }
164  * <p/>
165  *       public void setUploadFileName(String filename) {
166  *          this.filename = filename;
167  *       }
168  * <p/>
169  *       public String execute() {
170  *          //...
171  *          return SUCCESS;
172  *       }
173  *  }
174  * <!-- END SNIPPET: example-action -->
175  * </pre>
176  */
177 public class FileUploadInterceptor extends AbstractInterceptor {
178 
179     private static final long serialVersionUID = -4764627478894962478L;
180 
181     protected static final Logger LOG = LoggerFactory.getLogger(FileUploadInterceptor.class);
182     private static final String DEFAULT_MESSAGE = "no.message.found";
183 
184     protected boolean useActionMessageBundle;
185 
186     protected Long maximumSize;
187     protected Set<String> allowedTypesSet = Collections.emptySet();
188     protected Set<String> allowedExtensionsSet = Collections.emptySet();
189 
190     public void setUseActionMessageBundle(String value) {
191         this.useActionMessageBundle = Boolean.valueOf(value);
192     }
193 
194     /***
195      * Sets the allowed extensions
196      *
197      * @param allowedExtensions A comma-delimited list of extensions
198      */
199     public void setAllowedExtensions(String allowedExtensions) {
200         allowedExtensionsSet = TextParseUtil.commaDelimitedStringToSet(allowedExtensions);
201     }
202 
203     /***
204      * Sets the allowed mimetypes
205      *
206      * @param allowedTypes A comma-delimited list of types
207      */
208     public void setAllowedTypes(String allowedTypes) {
209         allowedTypesSet = TextParseUtil.commaDelimitedStringToSet(allowedTypes);
210     }
211 
212     /***
213      * Sets the maximum size of an uploaded file
214      *
215      * @param maximumSize The maximum size in bytes
216      */
217     public void setMaximumSize(Long maximumSize) {
218         this.maximumSize = maximumSize;
219     }
220 
221     /* (non-Javadoc)
222      * @see com.opensymphony.xwork2.interceptor.Interceptor#intercept(com.opensymphony.xwork2.ActionInvocation)
223      */
224     public String intercept(ActionInvocation invocation) throws Exception {
225         ActionContext ac = invocation.getInvocationContext();
226 
227         HttpServletRequest request = (HttpServletRequest) ac.get(ServletActionContext.HTTP_REQUEST);
228 
229         if (!(request instanceof MultiPartRequestWrapper)) {
230             if (LOG.isDebugEnabled()) {
231                 ActionProxy proxy = invocation.getProxy();
232                 LOG.debug(getTextMessage("struts.messages.bypass.request", new Object[]{proxy.getNamespace(), proxy.getActionName()}, ac.getLocale()));
233             }
234 
235             return invocation.invoke();
236         }
237 
238         ValidationAware validation = null;
239 
240         Object action = invocation.getAction();
241         
242         if (action instanceof ValidationAware) {
243             validation = (ValidationAware) action;
244         }
245 
246         MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;
247 
248         if (multiWrapper.hasErrors()) {
249             for (String error : multiWrapper.getErrors()) {
250                 if (validation != null) {
251                     validation.addActionError(error);
252                 }
253 
254                 LOG.warn(error);
255             }
256         }
257 
258         // bind allowed Files
259         Enumeration fileParameterNames = multiWrapper.getFileParameterNames();
260         while (fileParameterNames != null && fileParameterNames.hasMoreElements()) {
261             // get the value of this input tag
262             String inputName = (String) fileParameterNames.nextElement();
263 
264             // get the content type
265             String[] contentType = multiWrapper.getContentTypes(inputName);
266 
267             if (isNonEmpty(contentType)) {
268                 // get the name of the file from the input tag
269                 String[] fileName = multiWrapper.getFileNames(inputName);
270 
271                 if (isNonEmpty(fileName)) {
272                     // get a File object for the uploaded File
273                     File[] files = multiWrapper.getFiles(inputName);
274                     if (files != null && files.length > 0) {
275                         List<File> acceptedFiles = new ArrayList<File>(files.length);
276                         List<String> acceptedContentTypes = new ArrayList<String>(files.length);
277                         List<String> acceptedFileNames = new ArrayList<String>(files.length);
278                         String contentTypeName = inputName + "ContentType";
279                         String fileNameName = inputName + "FileName";
280 
281                         for (int index = 0; index < files.length; index++) {
282                             if (acceptFile(action, files[index], fileName[index], contentType[index], inputName, validation, ac.getLocale())) {
283                                 acceptedFiles.add(files[index]);
284                                 acceptedContentTypes.add(contentType[index]);
285                                 acceptedFileNames.add(fileName[index]);
286                             }
287                         }
288 
289                         if (!acceptedFiles.isEmpty()) {
290                             Map<String, Object> params = ac.getParameters();
291 
292                             params.put(inputName, acceptedFiles.toArray(new File[acceptedFiles.size()]));
293                             params.put(contentTypeName, acceptedContentTypes.toArray(new String[acceptedContentTypes.size()]));
294                             params.put(fileNameName, acceptedFileNames.toArray(new String[acceptedFileNames.size()]));
295                         }
296                     }
297                 } else {
298                     LOG.warn(getTextMessage(action, "struts.messages.invalid.file", new Object[]{inputName}, ac.getLocale()));
299                 }
300             } else {
301                 LOG.warn(getTextMessage(action, "struts.messages.invalid.content.type", new Object[]{inputName}, ac.getLocale()));
302             }
303         }
304 
305         // invoke action
306         String result = invocation.invoke();
307 
308         // cleanup
309         fileParameterNames = multiWrapper.getFileParameterNames();
310         while (fileParameterNames != null && fileParameterNames.hasMoreElements()) {
311             String inputValue = (String) fileParameterNames.nextElement();
312             File[] files = multiWrapper.getFiles(inputValue);
313 
314             for (File currentFile : files) {
315                 if (LOG.isInfoEnabled()) {
316                     LOG.info(getTextMessage(action, "struts.messages.removing.file", new Object[]{inputValue, currentFile}, ac.getLocale()));
317                 }
318 
319                 if ((currentFile != null) && currentFile.isFile()) {
320                     if (currentFile.delete() == false) {
321                         LOG.warn("Resource Leaking:  Could not remove uploaded file '"+currentFile.getCanonicalPath()+"'.");
322                     }
323                 }
324             }
325         }
326 
327         return result;
328     }
329 
330     /***
331      * Override for added functionality. Checks if the proposed file is acceptable based on contentType and size.
332      *
333      * @param action      - uploading action for message retrieval.
334      * @param file        - proposed upload file.
335      * @param contentType - contentType of the file.
336      * @param inputName   - inputName of the file.
337      * @param validation  - Non-null ValidationAware if the action implements ValidationAware, allowing for better
338      *                    logging.
339      * @param locale
340      * @return true if the proposed file is acceptable by contentType and size.
341      */
342     protected boolean acceptFile(Object action, File file, String filename, String contentType, String inputName, ValidationAware validation, Locale locale) {
343         boolean fileIsAcceptable = false;
344 
345         // If it's null the upload failed
346         if (file == null) {
347             String errMsg = getTextMessage(action, "struts.messages.error.uploading", new Object[]{inputName}, locale);
348             if (validation != null) {
349                 validation.addFieldError(inputName, errMsg);
350             }
351 
352             LOG.warn(errMsg);
353         } else if (maximumSize != null && maximumSize < file.length()) {
354             String errMsg = getTextMessage(action, "struts.messages.error.file.too.large", new Object[]{inputName, filename, file.getName(), "" + file.length()}, locale);
355             if (validation != null) {
356                 validation.addFieldError(inputName, errMsg);
357             }
358 
359             LOG.warn(errMsg);
360         } else if ((!allowedTypesSet.isEmpty()) && (!containsItem(allowedTypesSet, contentType))) {
361             String errMsg = getTextMessage(action, "struts.messages.error.content.type.not.allowed", new Object[]{inputName, filename, file.getName(), contentType}, locale);
362             if (validation != null) {
363                 validation.addFieldError(inputName, errMsg);
364             }
365 
366             LOG.warn(errMsg);
367         } else if ((! allowedExtensionsSet.isEmpty()) && (!hasAllowedExtension(allowedExtensionsSet, filename))) {
368             String errMsg = getTextMessage(action, "struts.messages.error.file.extension.not.allowed", new Object[]{inputName, filename, file.getName(), contentType}, locale);
369             if (validation != null) {
370                 validation.addFieldError(inputName, errMsg);
371             }
372 
373             LOG.warn(errMsg);
374         } else {
375             fileIsAcceptable = true;
376         }
377 
378         return fileIsAcceptable;
379     }
380 
381     /***
382      * @param extensionCollection - Collection of extensions (all lowercase).
383      * @param filename            - filename to check.
384      * @return true if the filename has an allowed extension, false otherwise.
385      */
386     private static boolean hasAllowedExtension(Collection<String> extensionCollection, String filename) {
387         if (filename == null) {
388             return false;
389         }
390 
391         String lowercaseFilename = filename.toLowerCase();
392         for (String extension : extensionCollection) {
393             if (lowercaseFilename.endsWith(extension)) {
394                 return true;
395             }
396         }
397 
398         return false;
399     }
400 
401     /***
402      * @param itemCollection - Collection of string items (all lowercase).
403      * @param item           - Item to search for.
404      * @return true if itemCollection contains the item, false otherwise.
405      */
406     private static boolean containsItem(Collection<String> itemCollection, String item) {
407         return itemCollection.contains(item.toLowerCase());
408     }
409 
410     private static boolean isNonEmpty(Object[] objArray) {
411         boolean result = false;
412         for (int index = 0; index < objArray.length && !result; index++) {
413             if (objArray[index] != null) {
414                 result = true;
415             }
416         }
417         return result;
418     }
419 
420     private String getTextMessage(String messageKey, Object[] args, Locale locale) {
421         return getTextMessage(null, messageKey, args, locale);
422     }
423     
424     private String getTextMessage(Object action, String messageKey, Object[] args, Locale locale) {
425         if (args == null || args.length == 0) {
426             if ( action != null && useActionMessageBundle) {
427                 return LocalizedTextUtil.findText(action.getClass(), messageKey, locale);
428             }
429             return LocalizedTextUtil.findText(this.getClass(), messageKey, locale);                        
430         } else {
431             if ( action != null && useActionMessageBundle) {
432                 return LocalizedTextUtil.findText(action.getClass(), messageKey, locale, DEFAULT_MESSAGE, args);
433             }
434             return LocalizedTextUtil.findText(this.getClass(), messageKey, locale, DEFAULT_MESSAGE, args);
435         }
436     }
437 }