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 */
017package org.apache.wicket.protocol.http.servlet;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.UnsupportedEncodingException;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import javax.servlet.ServletException;
029import javax.servlet.http.HttpServletRequest;
030import javax.servlet.http.Part;
031
032import org.apache.commons.fileupload.FileItem;
033import org.apache.commons.fileupload.FileItemFactory;
034import org.apache.commons.fileupload.FileUploadBase;
035import org.apache.commons.fileupload.FileUploadException;
036import org.apache.commons.fileupload.disk.DiskFileItemFactory;
037import org.apache.commons.fileupload.servlet.ServletFileUpload;
038import org.apache.commons.fileupload.servlet.ServletRequestContext;
039import org.apache.commons.io.FileCleaningTracker;
040import org.apache.wicket.Application;
041import org.apache.wicket.WicketRuntimeException;
042import org.apache.wicket.util.file.FileCleanerTrackerAdapter;
043import org.apache.wicket.util.file.IFileCleaner;
044import org.apache.wicket.util.lang.Args;
045import org.apache.wicket.util.lang.Bytes;
046import org.apache.wicket.util.string.StringValue;
047import org.apache.wicket.util.value.ValueMap;
048
049/**
050 * Servlet specific WebRequest subclass for multipart content uploads.
051 *
052 * @author Jonathan Locke
053 * @author Eelco Hillenius
054 * @author Cameron Braid
055 * @author Ate Douma
056 * @author Igor Vaynberg (ivaynberg)
057 */
058public class MultipartServletWebRequestImpl extends MultipartServletWebRequest
059{
060        /** Map of file items. */
061        private final Map<String, List<FileItem>> files;
062
063        /** Map of parameters. */
064        private final ValueMap parameters;
065
066        private final String upload;
067        private final FileItemFactory fileItemFactory;
068
069        /**
070         * total bytes uploaded (downloaded from server's pov) so far. used for upload notifications
071         */
072        private int bytesUploaded;
073
074        /** content length cache, used for upload notifications */
075        private int totalBytes;
076
077        /**
078         * Constructor.
079         *
080         * This constructor will use {@link DiskFileItemFactory} to store uploads.
081         *
082         * @param request
083         *            the servlet request
084         * @param filterPrefix
085         *            prefix to wicket filter mapping
086         * @param maxSize
087         *            the maximum size allowed for this request
088         * @param upload
089         *            upload identifier for {@link UploadInfo}
090         * @throws FileUploadException
091         *             Thrown if something goes wrong with upload
092         */
093        public MultipartServletWebRequestImpl(HttpServletRequest request, String filterPrefix,
094                Bytes maxSize, String upload) throws FileUploadException
095        {
096                this(request, filterPrefix, maxSize, upload, new DiskFileItemFactory()
097                {
098                        @Override
099                        public FileCleaningTracker getFileCleaningTracker()
100                        {
101                                IFileCleaner fileCleaner = Application.get()
102                                                .getResourceSettings()
103                                                .getFileCleaner();
104                                return new FileCleanerTrackerAdapter(fileCleaner);
105                        }
106                });
107        }
108
109        /**
110         * Constructor
111         *
112         * @param request
113         *            the servlet request
114         * @param filterPrefix
115         *            prefix to wicket filter mapping
116         * @param maxSize
117         *            the maximum size allowed for this request
118         * @param upload
119         *            upload identifier for {@link UploadInfo}
120         * @param factory
121         *            {@link DiskFileItemFactory} to use when creating file items used to represent
122         *            uploaded files
123         * @throws FileUploadException
124         *             Thrown if something goes wrong with upload
125         */
126        public MultipartServletWebRequestImpl(HttpServletRequest request, String filterPrefix,
127                Bytes maxSize, String upload, FileItemFactory factory) throws FileUploadException
128        {
129                super(request, filterPrefix);
130
131                Args.notNull(upload, "upload");
132                this.upload = upload;
133                this.fileItemFactory = factory;
134                parameters = new ValueMap();
135                files = new HashMap<>();
136
137                // Check that request is multipart
138                final boolean isMultipart = ServletFileUpload.isMultipartContent(request);
139                if (!isMultipart)
140                {
141                        throw new IllegalStateException(
142                                "ServletRequest does not contain multipart content. One possible solution is to explicitly call Form.setMultipart(true), Wicket tries its best to auto-detect multipart forms but there are certain situation where it cannot.");
143                }
144
145                setMaxSize(maxSize);
146        }
147
148        @Override
149        public void parseFileParts() throws FileUploadException
150        {
151                HttpServletRequest request = getContainerRequest();
152
153                // The encoding that will be used to decode the string parameters
154                // It should NOT be null at this point, but it may be
155                // especially if the older Servlet API 2.2 is used
156                String encoding = request.getCharacterEncoding();
157
158                // The encoding can also be null when using multipart/form-data encoded forms.
159                // In that case we use the [application-encoding] which we always demand using
160                // the attribute 'accept-encoding' in wicket forms.
161                if (encoding == null)
162                {
163                        encoding = Application.get().getRequestCycleSettings().getResponseRequestEncoding();
164                }
165
166                FileUploadBase fileUpload = newFileUpload(encoding);
167
168                List<FileItem> items;
169
170                if (wantUploadProgressUpdates())
171                {
172                        ServletRequestContext ctx = new ServletRequestContext(request)
173                        {
174                                @Override
175                                public InputStream getInputStream() throws IOException
176                                {
177                                        return new CountingInputStream(super.getInputStream());
178                                }
179                        };
180                        totalBytes = request.getContentLength();
181
182                        onUploadStarted(totalBytes);
183                        try
184                        {
185                                items = fileUpload.parseRequest(ctx);
186                        }
187                        finally
188                        {
189                                onUploadCompleted();
190                        }
191                }
192                else
193                {
194                        // try to parse the file uploads by using Apache Commons FileUpload APIs
195                        // because they are feature richer (e.g. progress updates, cleaner)
196                        items = fileUpload.parseRequest(new ServletRequestContext(request));
197                        if (items.isEmpty())
198                        {
199                                // fallback to Servlet 3.0 APIs
200                                items = readServlet3Parts(request);
201                        }
202                }
203
204                // Loop through items
205                for (final FileItem item : items)
206                {
207                        // Get next item
208                        // If item is a form field
209                        if (item.isFormField())
210                        {
211                                // Set parameter value
212                                final String value;
213                                if (encoding != null)
214                                {
215                                        try
216                                        {
217                                                value = item.getString(encoding);
218                                        }
219                                        catch (UnsupportedEncodingException e)
220                                        {
221                                                throw new WicketRuntimeException(e);
222                                        }
223                                }
224                                else
225                                {
226                                        value = item.getString();
227                                }
228
229                                addParameter(item.getFieldName(), value);
230                        }
231                        else
232                        {
233                                List<FileItem> fileItems = files.get(item.getFieldName());
234                                if (fileItems == null)
235                                {
236                                        fileItems = new ArrayList<>();
237                                        files.put(item.getFieldName(), fileItems);
238                                }
239                                // Add to file list
240                                fileItems.add(item);
241                        }
242                }
243        }
244
245        /**
246         * Reads the uploads' parts by using Servlet 3.0 APIs.
247         *
248         * <strong>Note</strong>: By using Servlet 3.0 APIs the application won't be able to use
249         * upload progress updates.
250         *
251         * @param request
252         *              The http request with the upload data
253         * @return A list of {@link FileItem}s
254         * @throws FileUploadException
255         */
256        private List<FileItem> readServlet3Parts(HttpServletRequest request) throws FileUploadException
257        {
258                List<FileItem> itemsFromParts = new ArrayList<>();
259                try
260                {
261                        Collection<Part> parts = request.getParts();
262                        if (parts != null)
263                        {
264                                for (Part part : parts)
265                                {
266                                        FileItem fileItem = new ServletPartFileItem(part);
267                                        itemsFromParts.add(fileItem);
268                                }
269                        }
270                } catch (IOException | ServletException e)
271                {
272                        throw new FileUploadException("An error occurred while reading the upload parts", e);
273                }
274                return itemsFromParts;
275        }
276
277        /**
278         * Factory method for creating new instances of FileUploadBase
279         *
280         * @param encoding
281         *            The encoding to use while reading the data
282         * @return A new instance of FileUploadBase
283         */
284        protected FileUploadBase newFileUpload(String encoding) {
285                // Configure the factory here, if desired.
286                ServletFileUpload fileUpload = new ServletFileUpload(fileItemFactory);
287
288                // set encoding specifically when we found it
289                if (encoding != null)
290                {
291                        fileUpload.setHeaderEncoding(encoding);
292                }
293
294                fileUpload.setSizeMax(getMaxSize().bytes());
295
296                Bytes fileMaxSize = getFileMaxSize();
297                if (fileMaxSize != null) {
298                        fileUpload.setFileSizeMax(fileMaxSize.bytes());
299                }
300
301                fileUpload.setFileCountMax(getFileCountMax());
302
303                return fileUpload;
304        }
305
306    /**
307         * Adds a parameter to the parameters value map
308         *
309         * @param name
310         *            parameter name
311         * @param value
312         *            parameter value
313         */
314        private void addParameter(final String name, final String value)
315        {
316                final String[] currVal = (String[])parameters.get(name);
317
318                String[] newVal;
319
320                if (currVal != null)
321                {
322                        newVal = new String[currVal.length + 1];
323                        System.arraycopy(currVal, 0, newVal, 0, currVal.length);
324                        newVal[currVal.length] = value;
325                }
326                else
327                {
328                        newVal = new String[] { value };
329
330                }
331
332                parameters.put(name, newVal);
333        }
334
335        /**
336         * @return Returns the files.
337         */
338        @Override
339        public Map<String, List<FileItem>> getFiles()
340        {
341                return files;
342        }
343
344        /**
345         * Gets the file that was uploaded using the given field name.
346         *
347         * @param fieldName
348         *            the field name that was used for the upload
349         * @return the upload with the given field name
350         */
351        @Override
352        public List<FileItem> getFile(final String fieldName)
353        {
354                return files.get(fieldName);
355        }
356
357        @Override
358        protected Map<String, List<StringValue>> generatePostParameters()
359        {
360                Map<String, List<StringValue>> res = new HashMap<>();
361                for (Map.Entry<String, Object> entry : parameters.entrySet())
362                {
363                        String key = entry.getKey();
364                        String[] val = (String[])entry.getValue();
365                        if (val != null && val.length > 0)
366                        {
367                                List<StringValue> items = new ArrayList<>();
368                                for (String s : val)
369                                {
370                                        items.add(StringValue.valueOf(s));
371                                }
372                                res.put(key, items);
373                        }
374                }
375                return res;
376        }
377
378        /**
379         * Subclasses that want to receive upload notifications should return true. By default it takes
380         * the value from {@link org.apache.wicket.settings.ApplicationSettings#isUploadProgressUpdatesEnabled()}.
381         *
382         * @return true if upload status update event should be invoked
383         */
384        protected boolean wantUploadProgressUpdates()
385        {
386                return Application.get().getApplicationSettings().isUploadProgressUpdatesEnabled();
387        }
388
389        /**
390         * Upload start callback
391         *
392         * @param totalBytes
393         */
394        protected void onUploadStarted(int totalBytes)
395        {
396                UploadInfo info = new UploadInfo(totalBytes);
397
398                setUploadInfo(getContainerRequest(), upload, info);
399        }
400
401        /**
402         * Upload status update callback
403         *
404         * @param bytesUploaded
405         * @param total
406         */
407        protected void onUploadUpdate(int bytesUploaded, int total)
408        {
409                HttpServletRequest request = getContainerRequest();
410                UploadInfo info = getUploadInfo(request, upload);
411                if (info == null)
412                {
413                        throw new IllegalStateException(
414                                "could not find UploadInfo object in session which should have been set when uploaded started");
415                }
416                info.setBytesUploaded(bytesUploaded);
417
418                setUploadInfo(request, upload, info);
419        }
420
421        /**
422         * Upload completed callback
423         */
424        protected void onUploadCompleted()
425        {
426                clearUploadInfo(getContainerRequest(), upload);
427        }
428
429        /**
430         * An {@link InputStream} that updates total number of bytes read
431         *
432         * @author Igor Vaynberg (ivaynberg)
433         */
434        private class CountingInputStream extends InputStream
435        {
436
437                private final InputStream in;
438
439                /**
440                 * Constructs a new CountingInputStream.
441                 *
442                 * @param in
443                 *            InputStream to delegate to
444                 */
445                public CountingInputStream(InputStream in)
446                {
447                        this.in = in;
448                }
449
450                /**
451                 * @see java.io.InputStream#read()
452                 */
453                @Override
454                public int read() throws IOException
455                {
456                        int read = in.read();
457                        bytesUploaded += (read < 0) ? 0 : 1;
458                        onUploadUpdate(bytesUploaded, totalBytes);
459                        return read;
460                }
461
462                /**
463                 * @see java.io.InputStream#read(byte[])
464                 */
465                @Override
466                public int read(byte[] b) throws IOException
467                {
468                        int read = in.read(b);
469                        bytesUploaded += (read < 0) ? 0 : read;
470                        onUploadUpdate(bytesUploaded, totalBytes);
471                        return read;
472                }
473
474                /**
475                 * @see java.io.InputStream#read(byte[], int, int)
476                 */
477                @Override
478                public int read(byte[] b, int off, int len) throws IOException
479                {
480                        int read = in.read(b, off, len);
481                        bytesUploaded += (read < 0) ? 0 : read;
482                        onUploadUpdate(bytesUploaded, totalBytes);
483                        return read;
484                }
485
486        }
487
488        @Override
489        public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload)
490                throws FileUploadException
491        {
492                // FIXME mgrigorov: Why these checks are made here ?!
493                // Why they are not done also at org.apache.wicket.protocol.http.servlet.MultipartServletWebRequestImpl.newMultipartWebRequest(org.apache.wicket.util.lang.Bytes, java.lang.String, org.apache.wicket.util.upload.FileItemFactory)() ?
494                // Why there is no check that the summary of all files' sizes is less than the set maxSize ?
495                // Setting a breakpoint here never breaks with the standard upload examples.
496
497                Bytes fileMaxSize = getFileMaxSize();
498                for (Map.Entry<String, List<FileItem>> entry : files.entrySet())
499                {
500                        List<FileItem> fileItems = entry.getValue();
501                        for (FileItem fileItem : fileItems)
502                        {
503                                if (fileMaxSize != null && fileItem.getSize() > fileMaxSize.bytes())
504                                {
505                                        String fieldName = entry.getKey();
506                                        FileUploadException fslex = new FileUploadBase.FileSizeLimitExceededException("The field '" +
507                                                        fieldName + "' exceeds its maximum permitted size of '" +
508                                                        maxSize + "' characters.", fileItem.getSize(), fileMaxSize.bytes());
509                                        throw fslex;
510                                }
511                        }
512                }
513                return this;
514        }
515
516        @Override
517        public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload, FileItemFactory factory)
518                        throws FileUploadException
519        {
520                return this;
521        }
522
523        private static final String SESSION_KEY = MultipartServletWebRequestImpl.class.getName();
524
525        private static String getSessionKey(String upload)
526        {
527                return SESSION_KEY + ":" + upload;
528        }
529
530        /**
531         * Retrieves {@link UploadInfo} from session, null if not found.
532         *
533         * @param req
534         *            http servlet request, not null
535         * @param upload
536         *            upload identifier
537         * @return {@link UploadInfo} object from session, or null if not found
538         */
539        public static UploadInfo getUploadInfo(final HttpServletRequest req, String upload)
540        {
541                Args.notNull(req, "req");
542                return (UploadInfo)req.getSession().getAttribute(getSessionKey(upload));
543        }
544
545        /**
546         * Sets the {@link UploadInfo} object into session.
547         *
548         * @param req
549         *            http servlet request, not null
550         * @param upload
551         *            upload identifier
552         * @param uploadInfo
553         *            {@link UploadInfo} object to be put into session, not null
554         */
555        public static void setUploadInfo(final HttpServletRequest req, String upload,
556                final UploadInfo uploadInfo)
557        {
558                Args.notNull(req, "req");
559                Args.notNull(upload, "upload");
560                Args.notNull(uploadInfo, "uploadInfo");
561                req.getSession().setAttribute(getSessionKey(upload), uploadInfo);
562        }
563
564        /**
565         * Clears the {@link UploadInfo} object from session if one exists.
566         *
567         * @param req
568         *            http servlet request, not null
569         * @param upload
570         *            upload identifier
571         */
572        public static void clearUploadInfo(final HttpServletRequest req, String upload)
573        {
574                Args.notNull(req, "req");
575                Args.notNull(upload, "upload");
576                req.getSession().removeAttribute(getSessionKey(upload));
577        }
578
579}