View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  package org.apache.commons.fileupload2.core;
    18
    19  import java.io.IOException;
    20  import java.io.InputStream;
    21  import java.util.Iterator;
    22  import java.util.Locale;
    23  import java.util.NoSuchElementException;
    24  import java.util.Objects;
    25
    26  import org.apache.commons.io.Charsets;
    27  import org.apache.commons.io.IOUtils;
    28  import org.apache.commons.io.input.BoundedInputStream;
    29
    30  /**
    31   * The iterator returned by {@link AbstractFileUpload#getItemIterator(RequestContext)}.
    32   */
    33  class FileItemInputIteratorImpl implements FileItemInputIterator {
    34
    35      /**
    36       * The file uploads processing utility.
    37       *
    38       * @see AbstractFileUpload
    39       */
    40      private final AbstractFileUpload<?, ?, ?> fileUpload;
    41
    42      /**
    43       * The request context.
    44       *
    45       * @see RequestContext
    46       */
    47      private final RequestContext requestContext;
    48
    49      /**
    50       * The maximum allowed size of a complete request.
    51       */
    52      private long sizeMax;
    53
    54      /**
    55       * The maximum allowed size of a single uploaded file.
    56       */
    57      private long fileSizeMax;
    58
    59      /**
    60       * The multi part stream to process.
    61       */
    62      private MultipartInput multiPartInput;
    63
    64      /**
    65       * The notifier, which used for triggering the {@link ProgressListener}.
    66       */
    67      private MultipartInput.ProgressNotifier progressNotifier;
    68
    69      /**
    70       * The boundary, which separates the various parts.
    71       */
    72      private byte[] multiPartBoundary;
    73
    74      /**
    75       * The item, which we currently process.
    76       */
    77      private FileItemInputImpl currentItem;
    78
    79      /**
    80       * The current items field name.
    81       */
    82      private String currentFieldName;
    83
    84      /**
    85       * Whether we are currently skipping the preamble.
    86       */
    87      private boolean skipPreamble;
    88
    89      /**
    90       * Whether the current item may still be read.
    91       */
    92      private boolean itemValid;
    93
    94      /**
    95       * Whether we have seen the end of the file.
    96       */
    97      private boolean eof;
    98
    99      /**
    100      * Constructs a new instance.
    101      *
    102      * @param fileUploadBase Main processor.
    103      * @param requestContext The request context.
    104      * @throws FileUploadException An error occurred while parsing the request.
    105      * @throws IOException         An I/O error occurred.
    106      */
    107     FileItemInputIteratorImpl(final AbstractFileUpload<?, ?, ?> fileUploadBase, final RequestContext requestContext) throws FileUploadException, IOException {
    108         this.fileUpload = fileUploadBase;
    109         this.sizeMax = fileUploadBase.getSizeMax();
    110         this.fileSizeMax = fileUploadBase.getFileSizeMax();
    111         this.requestContext = Objects.requireNonNull(requestContext, "requestContext");
    112         this.skipPreamble = true;
    113         findNextItem();
    114     }
    115
    116     /**
    117      * Finds the next item, if any.
    118      *
    119      * @return True, if an next item was found, otherwise false.
    120      * @throws IOException An I/O error occurred.
    121      */
    122     private boolean findNextItem() throws FileUploadException, IOException {
    123         if (eof) {
    124             return false;
    125         }
    126         if (currentItem != null) {
    127             currentItem.close();
    128             currentItem = null;
    129         }
    130         final var multi = getMultiPartInput();
    131         for (;;) {
    132             final boolean nextPart;
    133             if (skipPreamble) {
    134                 nextPart = multi.skipPreamble();
    135             } else {
    136                 nextPart = multi.readBoundary();
    137             }
    138             if (!nextPart) {
    139                 if (currentFieldName == null) {
    140                     // Outer multipart terminated -> No more data
    141                     eof = true;
    142                     return false;
    143                 }
    144                 // Inner multipart terminated -> Return to parsing the outer
    145                 multi.setBoundary(multiPartBoundary);
    146                 currentFieldName = null;
    147                 continue;
    148             }
    149             final var headers = fileUpload.getParsedHeaders(multi.readHeaders());
    150             if (currentFieldName == null) {
    151                 // We're parsing the outer multipart
    152                 final var fieldName = fileUpload.getFieldName(headers);
    153                 if (fieldName != null) {
    154                     final var subContentType = headers.getHeader(AbstractFileUpload.CONTENT_TYPE);
    155                     if (subContentType != null && subContentType.toLowerCase(Locale.ENGLISH).startsWith(AbstractFileUpload.MULTIPART_MIXED)) {
    156                         currentFieldName = fieldName;
    157                         // Multiple files associated with this field name
    158                         final var subBoundary = fileUpload.getBoundary(subContentType);
    159                         multi.setBoundary(subBoundary);
    160                         skipPreamble = true;
    161                         continue;
    162                     }
    163                     final var fileName = fileUpload.getFileName(headers);
    164                     currentItem = new FileItemInputImpl(this, fileName, fieldName, headers.getHeader(AbstractFileUpload.CONTENT_TYPE), fileName == null,
    165                             getContentLength(headers));
    166                     currentItem.setHeaders(headers);
    167                     progressNotifier.noteItem();
    168                     itemValid = true;
    169                     return true;
    170                 }
    171             } else {
    172                 final var fileName = fileUpload.getFileName(headers);
    173                 if (fileName != null) {
    174                     currentItem = new FileItemInputImpl(this, fileName, currentFieldName, headers.getHeader(AbstractFileUpload.CONTENT_TYPE), false,
    175                             getContentLength(headers));
    176                     currentItem.setHeaders(headers);
    177                     progressNotifier.noteItem();
    178                     itemValid = true;
    179                     return true;
    180                 }
    181             }
    182             multi.discardBodyData();
    183         }
    184     }
    185
    186     private long getContentLength(final FileItemHeaders headers) {
    187         try {
    188             return Long.parseLong(headers.getHeader(AbstractFileUpload.CONTENT_LENGTH));
    189         } catch (final Exception e) {
    190             return -1;
    191         }
    192     }
    193
    194     @Override
    195     public long getFileSizeMax() {
    196         return fileSizeMax;
    197     }
    198
    199     public MultipartInput getMultiPartInput() throws FileUploadException, IOException {
    200         if (multiPartInput == null) {
    201             init(fileUpload, requestContext);
    202         }
    203         return multiPartInput;
    204     }
    205
    206     @Override
    207     public long getSizeMax() {
    208         return sizeMax;
    209     }
    210
    211     /**
    212      * Tests whether another instance of {@link FileItemInput} is available.
    213      *
    214      * @throws FileUploadException Parsing or processing the file item failed.
    215      * @throws IOException         Reading the file item failed.
    216      * @return True, if one or more additional file items are available, otherwise false.
    217      */
    218     @Override
    219     public boolean hasNext() throws IOException {
    220         if (eof) {
    221             return false;
    222         }
    223         if (itemValid) {
    224             return true;
    225         }
    226         return findNextItem();
    227     }
    228
    229     protected void init(final AbstractFileUpload<?, ?, ?> fileUploadBase, final RequestContext initContext) throws FileUploadException, IOException {
    230         final var contentType = requestContext.getContentType();
    231         if (null == contentType || !contentType.toLowerCase(Locale.ENGLISH).startsWith(AbstractFileUpload.MULTIPART)) {
    232             throw new FileUploadContentTypeException(String.format("the request doesn't contain a %s or %s stream, content type header is %s",
    233                     AbstractFileUpload.MULTIPART_FORM_DATA, AbstractFileUpload.MULTIPART_MIXED, contentType), contentType);
    234         }
    235         final var contentLengthInt = requestContext.getContentLength();
    236         // @formatter:off
    237         final var requestSize = RequestContext.class.isAssignableFrom(requestContext.getClass())
    238                                  // Inline conditional is OK here CHECKSTYLE:OFF
    239                                  ? requestContext.getContentLength()
    240                                  : contentLengthInt;
    241                                  // CHECKSTYLE:ON
    242         // @formatter:on
    243         final InputStream inputStream; // N.B. this is eventually closed in MultipartInput processing
    244         if (sizeMax >= 0) {
    245             if (requestSize != -1 && requestSize > sizeMax) {
    246                 throw new FileUploadSizeException(
    247                         String.format("the request was rejected because its size (%s) exceeds the configured maximum (%s)", requestSize, sizeMax), sizeMax,
    248                         requestSize);
    249             }
    250             // N.B. this is eventually closed in MultipartInput processing
    251             inputStream = new BoundedInputStream(requestContext.getInputStream(), sizeMax) {
    252                 @Override
    253                 protected void onMaxLength(final long maxLen, final long count) throws IOException {
    254                     throw new FileUploadSizeException(
    255                             String.format("The request was rejected because its size (%s) exceeds the configured maximum (%s)", count, maxLen), maxLen, count);
    256                 }
    257             };
    258         } else {
    259             inputStream = requestContext.getInputStream();
    260         }
    261
    262         final var charset = Charsets.toCharset(fileUploadBase.getHeaderCharset(), requestContext.getCharset());
    263         multiPartBoundary = fileUploadBase.getBoundary(contentType);
    264         if (multiPartBoundary == null) {
    265             IOUtils.closeQuietly(inputStream); // avoid possible resource leak
    266             throw new FileUploadException("the request was rejected because no multipart boundary was found");
    267         }
    268
    269         progressNotifier = new MultipartInput.ProgressNotifier(fileUploadBase.getProgressListener(), requestSize);
    270         try {
    271             multiPartInput = MultipartInput.builder().setInputStream(inputStream).setBoundary(multiPartBoundary).setProgressNotifier(progressNotifier).get();
    272         } catch (final IllegalArgumentException e) {
    273             IOUtils.closeQuietly(inputStream); // avoid possible resource leak
    274             throw new FileUploadContentTypeException(String.format("The boundary specified in the %s header is too long", AbstractFileUpload.CONTENT_TYPE), e);
    275         }
    276         multiPartInput.setHeaderCharset(charset);
    277     }
    278
    279     /**
    280      * Returns the next available {@link FileItemInput}.
    281      *
    282      * @throws java.util.NoSuchElementException No more items are available. Use {@link #hasNext()} to prevent this exception.
    283      * @throws FileUploadException              Parsing or processing the file item failed.
    284      * @throws IOException                      Reading the file item failed.
    285      * @return FileItemInput instance, which provides access to the next file item.
    286      */
    287     @Override
    288     public FileItemInput next() throws IOException {
    289         if (eof || !itemValid && !hasNext()) {
    290             throw new NoSuchElementException();
    291         }
    292         itemValid = false;
    293         return currentItem;
    294     }
    295
    296     @Override
    297     public void setFileSizeMax(final long fileSizeMax) {
    298         this.fileSizeMax = fileSizeMax;
    299     }
    300
    301     @Override
    302     public void setSizeMax(final long sizeMax) {
    303         this.sizeMax = sizeMax;
    304     }
    305
    306     @Override
    307     public Iterator<FileItemInput> unwrap() {
    308         // TODO Something better?
    309         return (Iterator<FileItemInput>) this;
    310     }
    311
    312 }