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 }