001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.wicket.util.upload;
018:
019: import java.io.IOException;
020: import java.io.InputStream;
021: import java.io.OutputStream;
022: import java.io.UnsupportedEncodingException;
023: import java.util.ArrayList;
024: import java.util.HashMap;
025: import java.util.List;
026: import java.util.Map;
027:
028: /**
029: * <p>
030: * High level API for processing file uploads.
031: * </p>
032: *
033: * <p>
034: * This class handles multiple files per single HTML widget, sent using
035: * <code>multipart/mixed</code> encoding type, as specified by <a
036: * href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>.
037: *
038: * <p>
039: * How the data for individual parts is stored is determined by the factory used
040: * to create them; a given part may be in memory, on disk, or somewhere else.
041: * </p>
042: *
043: * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>
044: * @author <a href="mailto:dlr@collab.net">Daniel Rall</a>
045: * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
046: * @author <a href="mailto:jmcnally@collab.net">John McNally</a>
047: * @author <a href="mailto:martinc@apache.org">Martin Cooper</a>
048: * @author Sean C. Sullivan
049: */
050: public abstract class FileUploadBase {
051:
052: // ---------------------------------------------------------- Class methods
053:
054: /**
055: * <p>
056: * Utility method that determines whether the request contains multipart
057: * content.
058: * </p>
059: *
060: * <p>
061: * <strong>NOTE:</strong>This method will be moved to the
062: * <code>ServletFileUpload</code> class after the FileUpload 1.1 release.
063: * Unfortunately, since this method is static, it is not possible to provide
064: * its replacement until this method is removed.
065: * </p>
066: *
067: * @param ctx
068: * The request context to be evaluated. Must be non-null.
069: *
070: * @return <code>true</code> if the request is multipart;
071: * <code>false</code> otherwise.
072: */
073: public static final boolean isMultipartContent(RequestContext ctx) {
074: String contentType = ctx.getContentType();
075: if (contentType == null) {
076: return false;
077: }
078: if (contentType.toLowerCase().startsWith(MULTIPART)) {
079: return true;
080: }
081: return false;
082: }
083:
084: // ----------------------------------------------------- Manifest constants
085:
086: /**
087: * HTTP content type header name.
088: */
089: public static final String CONTENT_TYPE = "Content-type";
090:
091: /**
092: * HTTP content disposition header name.
093: */
094: public static final String CONTENT_DISPOSITION = "Content-disposition";
095:
096: /**
097: * Content-disposition value for form data.
098: */
099: public static final String FORM_DATA = "form-data";
100:
101: /**
102: * Content-disposition value for file attachment.
103: */
104: public static final String ATTACHMENT = "attachment";
105:
106: /**
107: * Part of HTTP content type header.
108: */
109: public static final String MULTIPART = "multipart/";
110:
111: /**
112: * HTTP content type header for multipart forms.
113: */
114: public static final String MULTIPART_FORM_DATA = "multipart/form-data";
115:
116: /**
117: * HTTP content type header for multiple uploads.
118: */
119: public static final String MULTIPART_MIXED = "multipart/mixed";
120:
121: /**
122: * The maximum length of a single header line that will be parsed (1024
123: * bytes).
124: */
125: public static final int MAX_HEADER_SIZE = 1024;
126:
127: // ----------------------------------------------------------- Data members
128:
129: /**
130: * The maximum size permitted for an uploaded file. A value of -1 indicates
131: * no maximum.
132: */
133: private long sizeMax = -1;
134:
135: /**
136: * The content encoding to use when reading part headers.
137: */
138: private String headerEncoding;
139:
140: // ----------------------------------------------------- Property accessors
141:
142: /**
143: * Returns the factory class used when creating file items.
144: *
145: * @return The factory class for new file items.
146: */
147: public abstract FileItemFactory getFileItemFactory();
148:
149: /**
150: * Sets the factory class to use when creating file items.
151: *
152: * @param factory
153: * The factory class for new file items.
154: */
155: public abstract void setFileItemFactory(FileItemFactory factory);
156:
157: /**
158: * Returns the maximum allowed upload size.
159: *
160: * @return The maximum allowed size, in bytes.
161: *
162: * @see #setSizeMax(long)
163: *
164: */
165: public long getSizeMax() {
166: return sizeMax;
167: }
168:
169: /**
170: * Sets the maximum allowed upload size. If negative, there is no maximum.
171: *
172: * @param sizeMax
173: * The maximum allowed size, in bytes, or -1 for no maximum.
174: *
175: * @see #getSizeMax()
176: *
177: */
178: public void setSizeMax(long sizeMax) {
179: this .sizeMax = sizeMax;
180: }
181:
182: /**
183: * Retrieves the character encoding used when reading the headers of an
184: * individual part. When not specified, or <code>null</code>, the
185: * platform default encoding is used.
186: *
187: * @return The encoding used to read part headers.
188: */
189: public String getHeaderEncoding() {
190: return headerEncoding;
191: }
192:
193: /**
194: * Specifies the character encoding to be used when reading the headers of
195: * individual parts. When not specified, or <code>null</code>, the
196: * platform default encoding is used.
197: *
198: * @param encoding
199: * The encoding used to read part headers.
200: */
201: public void setHeaderEncoding(String encoding) {
202: headerEncoding = encoding;
203: }
204:
205: // --------------------------------------------------------- Public methods
206:
207: /**
208: * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
209: * compliant <code>multipart/form-data</code> stream.
210: *
211: * @param ctx
212: * The context for the request to be parsed.
213: *
214: * @return A list of <code>FileItem</code> instances parsed from the
215: * request, in the order that they were transmitted.
216: *
217: * @exception FileUploadException
218: * if there are problems reading/parsing the request or
219: * storing files.
220: */
221: public List /* FileItem */parseRequest(RequestContext ctx)
222: throws FileUploadException {
223: if (ctx == null) {
224: throw new IllegalArgumentException(
225: "ctx parameter cannot be null");
226: }
227:
228: ArrayList items = new ArrayList();
229: String contentType = ctx.getContentType();
230:
231: if ((null == contentType)
232: || (!contentType.toLowerCase().startsWith(MULTIPART))) {
233: throw new InvalidContentTypeException(
234: "the request doesn't contain a "
235: + MULTIPART_FORM_DATA + " or "
236: + MULTIPART_MIXED
237: + " stream, content type header is "
238: + contentType);
239: }
240: int requestSize = ctx.getContentLength();
241:
242: if (requestSize == -1) {
243: throw new UnknownSizeException(
244: "the request was rejected because its size is unknown");
245: }
246:
247: if (sizeMax >= 0 && requestSize > sizeMax) {
248: throw new SizeLimitExceededException(
249: "the request was rejected because "
250: + "its size exceeds allowed range");
251: }
252:
253: try {
254: byte[] boundary = getBoundary(contentType);
255: if (boundary == null) {
256: throw new FileUploadException(
257: "the request was rejected because "
258: + "no multipart boundary was found");
259: }
260:
261: InputStream input = ctx.getInputStream();
262:
263: MultipartFormInputStream multi = new MultipartFormInputStream(
264: input, boundary);
265: multi.setHeaderEncoding(headerEncoding);
266:
267: boolean nextPart = multi.skipPreamble();
268:
269: // Don't allow a header larger than this size (to prevent DOS
270: // attacks)
271: final int maxHeaderBytes = 65536;
272: while (nextPart) {
273: Map headers = parseHeaders(multi
274: .readHeaders(maxHeaderBytes));
275: String fieldName = getFieldName(headers);
276: if (fieldName != null) {
277: String subContentType = getHeader(headers,
278: CONTENT_TYPE);
279: if (subContentType != null
280: && subContentType.toLowerCase().startsWith(
281: MULTIPART_MIXED)) {
282: // Multiple files.
283: byte[] subBoundary = getBoundary(subContentType);
284: multi.setBoundary(subBoundary);
285: boolean nextSubPart = multi.skipPreamble();
286: while (nextSubPart) {
287: headers = parseHeaders(multi
288: .readHeaders(maxHeaderBytes));
289: if (getFileName(headers) != null) {
290: FileItem item = createItem(headers,
291: false);
292: OutputStream os = item
293: .getOutputStream();
294: try {
295: multi.readBodyData(os);
296: } finally {
297: os.close();
298: }
299: items.add(item);
300: } else {
301: // Ignore anything but files inside
302: // multipart/mixed.
303: multi.discardBodyData();
304: }
305: nextSubPart = multi.readBoundary();
306: }
307: multi.setBoundary(boundary);
308: } else {
309: FileItem item = createItem(headers,
310: getFileName(headers) == null);
311: OutputStream os = item.getOutputStream();
312: try {
313: multi.readBodyData(os);
314: } finally {
315: os.close();
316: }
317: items.add(item);
318: }
319: } else {
320: // Skip this part.
321: multi.discardBodyData();
322: }
323: nextPart = multi.readBoundary();
324: }
325: } catch (IOException e) {
326: throw new FileUploadException("Processing of "
327: + MULTIPART_FORM_DATA + " request failed. "
328: + e.getMessage(), e);
329: }
330:
331: return items;
332: }
333:
334: // ------------------------------------------------------ Protected methods
335:
336: /**
337: * Retrieves the boundary from the <code>Content-type</code> header.
338: *
339: * @param contentType
340: * The value of the content type header from which to extract the
341: * boundary value.
342: *
343: * @return The boundary, as a byte array.
344: */
345: protected byte[] getBoundary(String contentType) {
346: ParameterParser parser = new ParameterParser();
347: parser.setLowerCaseNames(true);
348: // Parameter parser can handle null input
349: Map params = parser.parse(contentType, ';');
350: String boundaryStr = (String) params.get("boundary");
351:
352: if (boundaryStr == null) {
353: return null;
354: }
355: byte[] boundary;
356: try {
357: boundary = boundaryStr.getBytes("ISO-8859-1");
358: } catch (UnsupportedEncodingException e) {
359: boundary = boundaryStr.getBytes();
360: }
361: return boundary;
362: }
363:
364: /**
365: * Retrieves the file name from the <code>Content-disposition</code>
366: * header.
367: *
368: * @param headers
369: * A <code>Map</code> containing the HTTP request headers.
370: *
371: * @return The file name for the current <code>encapsulation</code>.
372: */
373: protected String getFileName(Map /* String, String */headers) {
374: String fileName = null;
375: String cd = getHeader(headers, CONTENT_DISPOSITION);
376: if (cd.startsWith(FORM_DATA) || cd.startsWith(ATTACHMENT)) {
377: ParameterParser parser = new ParameterParser();
378: parser.setLowerCaseNames(true);
379: // Parameter parser can handle null input
380: Map params = parser.parse(cd, ';');
381: if (params.containsKey("filename")) {
382: fileName = (String) params.get("filename");
383: if (fileName != null) {
384: fileName = fileName.trim();
385: int index = fileName.lastIndexOf('\\');
386: if (index == -1) {
387: index = fileName.lastIndexOf('/');
388: }
389: if (index != -1) {
390: fileName = fileName.substring(index + 1);
391: }
392: } else {
393: // Even if there is no value, the parameter is present, so
394: // we return an empty file name rather than no file name.
395: fileName = "";
396: }
397: }
398: }
399: return fileName;
400: }
401:
402: /**
403: * Retrieves the field name from the <code>Content-disposition</code>
404: * header.
405: *
406: * @param headers
407: * A <code>Map</code> containing the HTTP request headers.
408: *
409: * @return The field name for the current <code>encapsulation</code>.
410: */
411: protected String getFieldName(Map /* String, String */headers) {
412: String fieldName = null;
413: String cd = getHeader(headers, CONTENT_DISPOSITION);
414: if (cd != null && cd.startsWith(FORM_DATA)) {
415:
416: ParameterParser parser = new ParameterParser();
417: parser.setLowerCaseNames(true);
418: // Parameter parser can handle null input
419: Map params = parser.parse(cd, ';');
420: fieldName = (String) params.get("name");
421: if (fieldName != null) {
422: fieldName = fieldName.trim();
423: }
424: }
425: return fieldName;
426: }
427:
428: /**
429: * Creates a new {@link FileItem} instance.
430: *
431: * @param headers
432: * A <code>Map</code> containing the HTTP request headers.
433: * @param isFormField
434: * Whether or not this item is a form field, as opposed to a
435: * file.
436: *
437: * @return A newly created <code>FileItem</code> instance.
438: */
439: protected FileItem createItem(Map /* String, String */headers,
440: boolean isFormField) {
441: return getFileItemFactory().createItem(getFieldName(headers),
442: getHeader(headers, CONTENT_TYPE), isFormField,
443: getFileName(headers));
444: }
445:
446: /**
447: * <p>
448: * Parses the <code>header-part</code> and returns as key/value pairs.
449: *
450: * <p>
451: * If there are multiple headers of the same names, the name will map to a
452: * comma-separated list containing the values.
453: *
454: * @param headerPart
455: * The <code>header-part</code> of the current
456: * <code>encapsulation</code>.
457: *
458: * @return A <code>Map</code> containing the parsed HTTP request headers.
459: */
460: protected Map /* String, String */parseHeaders(String headerPart) {
461: Map headers = new HashMap();
462: char[] buffer = new char[MAX_HEADER_SIZE];
463: boolean done = false;
464: int j = 0;
465: int i;
466: String header, headerName, headerValue;
467: try {
468: while (!done) {
469: i = 0;
470: // Copy a single line of characters into the buffer,
471: // omitting trailing CRLF.
472: while (i < 2 || buffer[i - 2] != '\r'
473: || buffer[i - 1] != '\n') {
474: buffer[i++] = headerPart.charAt(j++);
475: }
476: header = new String(buffer, 0, i - 2);
477: if (header.equals("")) {
478: done = true;
479: } else {
480: if (header.indexOf(':') == -1) {
481: // This header line is malformed, skip it.
482: continue;
483: }
484: headerName = header.substring(0,
485: header.indexOf(':')).trim().toLowerCase();
486: headerValue = header.substring(
487: header.indexOf(':') + 1).trim();
488: if (getHeader(headers, headerName) != null) {
489: // More that one heder of that name exists,
490: // append to the list.
491: headers.put(headerName, getHeader(headers,
492: headerName)
493: + ',' + headerValue);
494: } else {
495: headers.put(headerName, headerValue);
496: }
497: }
498: }
499: } catch (IndexOutOfBoundsException e) {
500: // Headers were malformed. continue with all that was
501: // parsed.
502: }
503: return headers;
504: }
505:
506: /**
507: * Returns the header with the specified name from the supplied map. The
508: * header lookup is case-insensitive.
509: *
510: * @param headers
511: * A <code>Map</code> containing the HTTP request headers.
512: * @param name
513: * The name of the header to return.
514: *
515: * @return The value of specified header, or a comma-separated list if there
516: * were multiple headers of that name.
517: */
518: protected final String getHeader(Map /* String, String */headers,
519: String name) {
520: return (String) headers.get(name.toLowerCase());
521: }
522:
523: /**
524: * Thrown to indicate that the request is not a multipart request.
525: */
526: public static class InvalidContentTypeException extends
527: FileUploadException {
528:
529: private static final long serialVersionUID = 1L;
530:
531: /**
532: * Constructs a <code>InvalidContentTypeException</code> with no
533: * detail message.
534: */
535: public InvalidContentTypeException() {
536: super ();
537: }
538:
539: /**
540: * Constructs an <code>InvalidContentTypeException</code> with the
541: * specified detail message.
542: *
543: * @param message
544: * The detail message.
545: */
546: public InvalidContentTypeException(String message) {
547: super (message);
548: }
549: }
550:
551: /**
552: * Thrown to indicate that the request size is not specified.
553: */
554: public static class UnknownSizeException extends
555: FileUploadException {
556:
557: private static final long serialVersionUID = 1L;
558:
559: /**
560: * Constructs a <code>UnknownSizeException</code> with no detail
561: * message.
562: */
563: public UnknownSizeException() {
564: super ();
565: }
566:
567: /**
568: * Constructs an <code>UnknownSizeException</code> with the specified
569: * detail message.
570: *
571: * @param message
572: * The detail message.
573: */
574: public UnknownSizeException(String message) {
575: super (message);
576: }
577: }
578:
579: /**
580: * Thrown to indicate that the request size exceeds the configured maximum.
581: */
582: public static class SizeLimitExceededException extends
583: FileUploadException {
584:
585: private static final long serialVersionUID = 1L;
586:
587: /**
588: * Constructs a <code>SizeExceededException</code> with no detail
589: * message.
590: */
591: public SizeLimitExceededException() {
592: super ();
593: }
594:
595: /**
596: * Constructs an <code>SizeExceededException</code> with the specified
597: * detail message.
598: *
599: * @param message
600: * The detail message.
601: */
602: public SizeLimitExceededException(String message) {
603: super(message);
604: }
605: }
606:
607: }
|