View Javadoc

1   /*
2    * Copyright 2001-2005 The Apache Software Foundation
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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 350197 2005-12-01 07:10:29Z martinc $
52   */
53  public abstract class FileUploadBase {
54  
55      // ---------------------------------------------------------- Class methods
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     // ----------------------------------------------------- Manifest constants
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     // ----------------------------------------------------------- Data members
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     // ----------------------------------------------------- Property accessors
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     // --------------------------------------------------------- Public methods
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 /* FileItem */ parseRequest(HttpServletRequest req)
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 /* FileItem */ parseRequest(RequestContext ctx)
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                         // Multiple files.
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                                 // Ignore anything but files inside
360                                 // multipart/mixed.
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                     // Skip this part.
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     // ------------------------------------------------------ Protected methods
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         // Parameter parser can handle null input
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 /* String, String */ headers) {
433         String fileName = null;
434         String cd = getHeader(headers, CONTENT_DISPOSITION);
435         if (cd != null) {
436             cd = cd.toLowerCase();
437             if (cd.startsWith(FORM_DATA) || cd.startsWith(ATTACHMENT)) {
438                 ParameterParser parser = new ParameterParser();
439                 parser.setLowerCaseNames(true);
440                 // Parameter parser can handle null input
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                         // Even if there is no value, the parameter is present,
448                         // so we return an empty file name rather than no file
449                         // name.
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 /* String, String */ headers) {
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             // Parameter parser can handle null input
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 /* String, String */ headers,
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 /* String, String */ parseHeaders(String headerPart) {
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                 // Copy a single line of characters into the buffer,
530                 // omitting trailing CRLF.
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                         // This header line is malformed, skip it.
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                         // More that one heder of that name exists,
549                         // append to the list.
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             // Headers were malformed. continue with all that was
560             // parsed.
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 /* String, String */ headers,
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 }