1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.apache.commons.fileupload;
17
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.OutputStream;
21 import java.io.UnsupportedEncodingException;
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import javax.servlet.http.HttpServletRequest;
27
28 import org.apache.commons.fileupload.servlet.ServletRequestContext;
29
30 /***
31 * <p>High level API for processing file uploads.</p>
32 *
33 * <p>This class handles multiple files per single HTML widget, sent using
34 * <code>multipart/mixed</code> encoding type, as specified by
35 * <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>. Use {@link
36 * #parseRequest(HttpServletRequest)} to acquire a list of {@link
37 * org.apache.commons.fileupload.FileItem}s associated with a given HTML
38 * widget.</p>
39 *
40 * <p>How the data for individual parts is stored is determined by the factory
41 * used to create them; a given part may be in memory, on disk, or somewhere
42 * else.</p>
43 *
44 * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>
45 * @author <a href="mailto:dlr@collab.net">Daniel Rall</a>
46 * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
47 * @author <a href="mailto:jmcnally@collab.net">John McNally</a>
48 * @author <a href="mailto:martinc@apache.org">Martin Cooper</a>
49 * @author Sean C. Sullivan
50 *
51 * @version $Id: FileUploadBase.java 367087 2006-01-08 20:19:37Z martinc $
52 */
53 public abstract class FileUploadBase {
54
55
56
57
58 /***
59 * <p>Utility method that determines whether the request contains multipart
60 * content.</p>
61 *
62 * <p><strong>NOTE:</strong>This method will be moved to the
63 * <code>ServletFileUpload</code> class after the FileUpload 1.1 release.
64 * Unfortunately, since this method is static, it is not possible to
65 * provide its replacement until this method is removed.</p>
66 *
67 * @param ctx The request context to be evaluated. Must be non-null.
68 *
69 * @return <code>true</code> if the request is multipart;
70 * <code>false</code> otherwise.
71 */
72 public static final boolean isMultipartContent(RequestContext ctx) {
73 String contentType = ctx.getContentType();
74 if (contentType == null) {
75 return false;
76 }
77 if (contentType.toLowerCase().startsWith(MULTIPART)) {
78 return true;
79 }
80 return false;
81 }
82
83
84 /***
85 * Utility method that determines whether the request contains multipart
86 * content.
87 *
88 * @param req The servlet request to be evaluated. Must be non-null.
89 *
90 * @return <code>true</code> if the request is multipart;
91 * <code>false</code> otherwise.
92 *
93 * @deprecated Use the method on <code>ServletFileUpload</code> instead.
94 */
95 public static final boolean isMultipartContent(HttpServletRequest req) {
96 if (!"post".equals(req.getMethod().toLowerCase())) {
97 return false;
98 }
99 String contentType = req.getContentType();
100 if (contentType == null) {
101 return false;
102 }
103 if (contentType.toLowerCase().startsWith(MULTIPART)) {
104 return true;
105 }
106 return false;
107 }
108
109
110
111
112
113 /***
114 * HTTP content type header name.
115 */
116 public static final String CONTENT_TYPE = "Content-type";
117
118
119 /***
120 * HTTP content disposition header name.
121 */
122 public static final String CONTENT_DISPOSITION = "Content-disposition";
123
124
125 /***
126 * Content-disposition value for form data.
127 */
128 public static final String FORM_DATA = "form-data";
129
130
131 /***
132 * Content-disposition value for file attachment.
133 */
134 public static final String ATTACHMENT = "attachment";
135
136
137 /***
138 * Part of HTTP content type header.
139 */
140 public static final String MULTIPART = "multipart/";
141
142
143 /***
144 * HTTP content type header for multipart forms.
145 */
146 public static final String MULTIPART_FORM_DATA = "multipart/form-data";
147
148
149 /***
150 * HTTP content type header for multiple uploads.
151 */
152 public static final String MULTIPART_MIXED = "multipart/mixed";
153
154
155 /***
156 * The maximum length of a single header line that will be parsed
157 * (1024 bytes).
158 */
159 public static final int MAX_HEADER_SIZE = 1024;
160
161
162
163
164
165 /***
166 * The maximum size permitted for an uploaded file. A value of -1 indicates
167 * no maximum.
168 */
169 private long sizeMax = -1;
170
171
172 /***
173 * The content encoding to use when reading part headers.
174 */
175 private String headerEncoding;
176
177
178
179
180
181 /***
182 * Returns the factory class used when creating file items.
183 *
184 * @return The factory class for new file items.
185 */
186 public abstract FileItemFactory getFileItemFactory();
187
188
189 /***
190 * Sets the factory class to use when creating file items.
191 *
192 * @param factory The factory class for new file items.
193 */
194 public abstract void setFileItemFactory(FileItemFactory factory);
195
196
197 /***
198 * Returns the maximum allowed upload size.
199 *
200 * @return The maximum allowed size, in bytes.
201 *
202 * @see #setSizeMax(long)
203 *
204 */
205 public long getSizeMax() {
206 return sizeMax;
207 }
208
209
210 /***
211 * Sets the maximum allowed upload size. If negative, there is no maximum.
212 *
213 * @param sizeMax The maximum allowed size, in bytes, or -1 for no maximum.
214 *
215 * @see #getSizeMax()
216 *
217 */
218 public void setSizeMax(long sizeMax) {
219 this.sizeMax = sizeMax;
220 }
221
222
223 /***
224 * Retrieves the character encoding used when reading the headers of an
225 * individual part. When not specified, or <code>null</code>, the request
226 * encoding is used. If that is also not specified, or <code>null</code>,
227 * the platform default encoding is used.
228 *
229 * @return The encoding used to read part headers.
230 */
231 public String getHeaderEncoding() {
232 return headerEncoding;
233 }
234
235
236 /***
237 * Specifies the character encoding to be used when reading the headers of
238 * individual part. When not specified, or <code>null</code>, the request
239 * encoding is used. If that is also not specified, or <code>null</code>,
240 * the platform default encoding is used.
241 *
242 * @param encoding The encoding used to read part headers.
243 */
244 public void setHeaderEncoding(String encoding) {
245 headerEncoding = encoding;
246 }
247
248
249
250
251
252 /***
253 * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
254 * compliant <code>multipart/form-data</code> stream.
255 *
256 * @param req The servlet request to be parsed.
257 *
258 * @return A list of <code>FileItem</code> instances parsed from the
259 * request, in the order that they were transmitted.
260 *
261 * @throws FileUploadException if there are problems reading/parsing
262 * the request or storing files.
263 *
264 * @deprecated Use the method in <code>ServletFileUpload</code> instead.
265 */
266 public List
267 throws FileUploadException {
268 return parseRequest(new ServletRequestContext(req));
269 }
270
271 /***
272 * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
273 * compliant <code>multipart/form-data</code> stream.
274 *
275 * @param ctx The context for the request to be parsed.
276 *
277 * @return A list of <code>FileItem</code> instances parsed from the
278 * request, in the order that they were transmitted.
279 *
280 * @throws FileUploadException if there are problems reading/parsing
281 * the request or storing files.
282 */
283 public List
284 throws FileUploadException {
285 if (ctx == null) {
286 throw new NullPointerException("ctx parameter");
287 }
288
289 ArrayList items = new ArrayList();
290 String contentType = ctx.getContentType();
291
292 if ((null == contentType)
293 || (!contentType.toLowerCase().startsWith(MULTIPART))) {
294 throw new InvalidContentTypeException(
295 "the request doesn't contain a "
296 + MULTIPART_FORM_DATA
297 + " or "
298 + MULTIPART_MIXED
299 + " stream, content type header is "
300 + contentType);
301 }
302 int requestSize = ctx.getContentLength();
303
304 if (requestSize == -1) {
305 throw new UnknownSizeException(
306 "the request was rejected because its size is unknown");
307 }
308
309 if (sizeMax >= 0 && requestSize > sizeMax) {
310 throw new SizeLimitExceededException(
311 "the request was rejected because its size (" + requestSize
312 + ") exceeds the configured maximum (" + sizeMax + ")",
313 requestSize, sizeMax);
314 }
315
316 String charEncoding = headerEncoding;
317 if (charEncoding == null) {
318 charEncoding = ctx.getCharacterEncoding();
319 }
320
321 try {
322 byte[] boundary = getBoundary(contentType);
323 if (boundary == null) {
324 throw new FileUploadException(
325 "the request was rejected because "
326 + "no multipart boundary was found");
327 }
328
329 InputStream input = ctx.getInputStream();
330
331 MultipartStream multi = new MultipartStream(input, boundary);
332 multi.setHeaderEncoding(charEncoding);
333
334 boolean nextPart = multi.skipPreamble();
335 while (nextPart) {
336 Map headers = parseHeaders(multi.readHeaders());
337 String fieldName = getFieldName(headers);
338 if (fieldName != null) {
339 String subContentType = getHeader(headers, CONTENT_TYPE);
340 if (subContentType != null && subContentType
341 .toLowerCase().startsWith(MULTIPART_MIXED)) {
342
343 byte[] subBoundary = getBoundary(subContentType);
344 multi.setBoundary(subBoundary);
345 boolean nextSubPart = multi.skipPreamble();
346 while (nextSubPart) {
347 headers = parseHeaders(multi.readHeaders());
348 if (getFileName(headers) != null) {
349 FileItem item =
350 createItem(headers, false);
351 OutputStream os = item.getOutputStream();
352 try {
353 multi.readBodyData(os);
354 } finally {
355 os.close();
356 }
357 items.add(item);
358 } else {
359
360
361 multi.discardBodyData();
362 }
363 nextSubPart = multi.readBoundary();
364 }
365 multi.setBoundary(boundary);
366 } else {
367 FileItem item = createItem(headers,
368 getFileName(headers) == null);
369 OutputStream os = item.getOutputStream();
370 try {
371 multi.readBodyData(os);
372 } finally {
373 os.close();
374 }
375 items.add(item);
376 }
377 } else {
378
379 multi.discardBodyData();
380 }
381 nextPart = multi.readBoundary();
382 }
383 } catch (IOException e) {
384 throw new FileUploadException(
385 "Processing of " + MULTIPART_FORM_DATA
386 + " request failed. " + e.getMessage());
387 }
388
389 return items;
390 }
391
392
393
394
395
396 /***
397 * Retrieves the boundary from the <code>Content-type</code> header.
398 *
399 * @param contentType The value of the content type header from which to
400 * extract the boundary value.
401 *
402 * @return The boundary, as a byte array.
403 */
404 protected byte[] getBoundary(String contentType) {
405 ParameterParser parser = new ParameterParser();
406 parser.setLowerCaseNames(true);
407
408 Map params = parser.parse(contentType, ';');
409 String boundaryStr = (String) params.get("boundary");
410
411 if (boundaryStr == null) {
412 return null;
413 }
414 byte[] boundary;
415 try {
416 boundary = boundaryStr.getBytes("ISO-8859-1");
417 } catch (UnsupportedEncodingException e) {
418 boundary = boundaryStr.getBytes();
419 }
420 return boundary;
421 }
422
423
424 /***
425 * Retrieves the file name from the <code>Content-disposition</code>
426 * header.
427 *
428 * @param headers A <code>Map</code> containing the HTTP request headers.
429 *
430 * @return The file name for the current <code>encapsulation</code>.
431 */
432 protected String getFileName(Map
433 String fileName = null;
434 String cd = getHeader(headers, CONTENT_DISPOSITION);
435 if (cd != null) {
436 String cdl = cd.toLowerCase();
437 if (cdl.startsWith(FORM_DATA) || cdl.startsWith(ATTACHMENT)) {
438 ParameterParser parser = new ParameterParser();
439 parser.setLowerCaseNames(true);
440
441 Map params = parser.parse(cd, ';');
442 if (params.containsKey("filename")) {
443 fileName = (String) params.get("filename");
444 if (fileName != null) {
445 fileName = fileName.trim();
446 } else {
447
448
449
450 fileName = "";
451 }
452 }
453 }
454 }
455 return fileName;
456 }
457
458
459 /***
460 * Retrieves the field name from the <code>Content-disposition</code>
461 * header.
462 *
463 * @param headers A <code>Map</code> containing the HTTP request headers.
464 *
465 * @return The field name for the current <code>encapsulation</code>.
466 */
467 protected String getFieldName(Map
468 String fieldName = null;
469 String cd = getHeader(headers, CONTENT_DISPOSITION);
470 if (cd != null && cd.toLowerCase().startsWith(FORM_DATA)) {
471
472 ParameterParser parser = new ParameterParser();
473 parser.setLowerCaseNames(true);
474
475 Map params = parser.parse(cd, ';');
476 fieldName = (String) params.get("name");
477 if (fieldName != null) {
478 fieldName = fieldName.trim();
479 }
480 }
481 return fieldName;
482 }
483
484
485 /***
486 * Creates a new {@link FileItem} instance.
487 *
488 * @param headers A <code>Map</code> containing the HTTP request
489 * headers.
490 * @param isFormField Whether or not this item is a form field, as
491 * opposed to a file.
492 *
493 * @return A newly created <code>FileItem</code> instance.
494 *
495 * @throws FileUploadException if an error occurs.
496 */
497 protected FileItem createItem(Map
498 boolean isFormField)
499 throws FileUploadException {
500 return getFileItemFactory().createItem(getFieldName(headers),
501 getHeader(headers, CONTENT_TYPE),
502 isFormField,
503 getFileName(headers));
504 }
505
506
507 /***
508 * <p> Parses the <code>header-part</code> and returns as key/value
509 * pairs.
510 *
511 * <p> If there are multiple headers of the same names, the name
512 * will map to a comma-separated list containing the values.
513 *
514 * @param headerPart The <code>header-part</code> of the current
515 * <code>encapsulation</code>.
516 *
517 * @return A <code>Map</code> containing the parsed HTTP request headers.
518 */
519 protected Map
520 Map headers = new HashMap();
521 char[] buffer = new char[MAX_HEADER_SIZE];
522 boolean done = false;
523 int j = 0;
524 int i;
525 String header, headerName, headerValue;
526 try {
527 while (!done) {
528 i = 0;
529
530
531 while (i < 2
532 || buffer[i - 2] != '\r' || buffer[i - 1] != '\n') {
533 buffer[i++] = headerPart.charAt(j++);
534 }
535 header = new String(buffer, 0, i - 2);
536 if (header.equals("")) {
537 done = true;
538 } else {
539 if (header.indexOf(':') == -1) {
540
541 continue;
542 }
543 headerName = header.substring(0, header.indexOf(':'))
544 .trim().toLowerCase();
545 headerValue =
546 header.substring(header.indexOf(':') + 1).trim();
547 if (getHeader(headers, headerName) != null) {
548
549
550 headers.put(headerName,
551 getHeader(headers, headerName) + ','
552 + headerValue);
553 } else {
554 headers.put(headerName, headerValue);
555 }
556 }
557 }
558 } catch (IndexOutOfBoundsException e) {
559
560
561 }
562 return headers;
563 }
564
565
566 /***
567 * Returns the header with the specified name from the supplied map. The
568 * header lookup is case-insensitive.
569 *
570 * @param headers A <code>Map</code> containing the HTTP request headers.
571 * @param name The name of the header to return.
572 *
573 * @return The value of specified header, or a comma-separated list if
574 * there were multiple headers of that name.
575 */
576 protected final String getHeader(Map
577 String name) {
578 return (String) headers.get(name.toLowerCase());
579 }
580
581
582 /***
583 * Thrown to indicate that the request is not a multipart request.
584 */
585 public static class InvalidContentTypeException
586 extends FileUploadException {
587 /***
588 * Constructs a <code>InvalidContentTypeException</code> with no
589 * detail message.
590 */
591 public InvalidContentTypeException() {
592 super();
593 }
594
595 /***
596 * Constructs an <code>InvalidContentTypeException</code> with
597 * the specified detail message.
598 *
599 * @param message The detail message.
600 */
601 public InvalidContentTypeException(String message) {
602 super(message);
603 }
604 }
605
606
607 /***
608 * Thrown to indicate that the request size is not specified.
609 */
610 public static class UnknownSizeException
611 extends FileUploadException {
612 /***
613 * Constructs a <code>UnknownSizeException</code> with no
614 * detail message.
615 */
616 public UnknownSizeException() {
617 super();
618 }
619
620 /***
621 * Constructs an <code>UnknownSizeException</code> with
622 * the specified detail message.
623 *
624 * @param message The detail message.
625 */
626 public UnknownSizeException(String message) {
627 super(message);
628 }
629 }
630
631
632 /***
633 * Thrown to indicate that the request size exceeds the configured maximum.
634 */
635 public static class SizeLimitExceededException
636 extends FileUploadException {
637 /***
638 * The actual size of the request.
639 */
640 private long actual;
641
642 /***
643 * The maximum permitted size of the request.
644 */
645 private long permitted;
646
647 /***
648 * Constructs a <code>SizeExceededException</code> with no
649 * detail message.
650 */
651 public SizeLimitExceededException() {
652 super();
653 }
654
655 /***
656 * Constructs a <code>SizeExceededException</code> with
657 * the specified detail message.
658 *
659 * @param message The detail message.
660 */
661 public SizeLimitExceededException(String message) {
662 super(message);
663 }
664
665 /***
666 * Constructs a <code>SizeExceededException</code> with
667 * the specified detail message, and actual and permitted sizes.
668 *
669 * @param message The detail message.
670 * @param actual The actual request size.
671 * @param permitted The maximum permitted request size.
672 */
673 public SizeLimitExceededException(String message, long actual,
674 long permitted) {
675 super(message);
676 this.actual = actual;
677 this.permitted = permitted;
678 }
679
680 /***
681 * Retrieves the actual size of the request.
682 *
683 * @return The actual size of the request.
684 */
685 public long getActualSize() {
686 return actual;
687 }
688
689 /***
690 * Retrieves the permitted size of the request.
691 *
692 * @return The permitted size of the request.
693 */
694 public long getPermittedSize() {
695 return permitted;
696 }
697 }
698
699 }