1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 * <action name="doUpload" class="com.example.UploadAction">
120 * <interceptor-ref name="fileUpload"/>
121 * <interceptor-ref name="basicStack"/>
122 * <result name="success">good_result.jsp</result>
123 * </action>
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 * <s:form action="doUpload" method="post" enctype="multipart/form-data">
136 * <s:file name="upload" label="File"/>
137 * <s:submit/>
138 * </s:form>
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
222
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
259 Enumeration fileParameterNames = multiWrapper.getFileParameterNames();
260 while (fileParameterNames != null && fileParameterNames.hasMoreElements()) {
261
262 String inputName = (String) fileParameterNames.nextElement();
263
264
265 String[] contentType = multiWrapper.getContentTypes(inputName);
266
267 if (isNonEmpty(contentType)) {
268
269 String[] fileName = multiWrapper.getFileNames(inputName);
270
271 if (isNonEmpty(fileName)) {
272
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
306 String result = invocation.invoke();
307
308
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
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 }