blob: cc27c4e2480812b5072afe8d22ca227569c3fb10 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.wicket.protocol.http.servlet;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jakarta.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.Part;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.servlet.ServletRequestContext;
import org.apache.commons.io.FileCleaningTracker;
import org.apache.wicket.Application;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.util.file.FileCleanerTrackerAdapter;
import org.apache.wicket.util.file.IFileCleaner;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Bytes;
import org.apache.wicket.util.string.StringValue;
import org.apache.wicket.util.value.ValueMap;
/**
* Servlet specific WebRequest subclass for multipart content uploads.
*
* @author Jonathan Locke
* @author Eelco Hillenius
* @author Cameron Braid
* @author Ate Douma
* @author Igor Vaynberg (ivaynberg)
*/
public class MultipartServletWebRequestImpl extends MultipartServletWebRequest
{
/** Map of file items. */
private final Map<String, List<FileItem>> files;
/** Map of parameters. */
private final ValueMap parameters;
private final String upload;
private final FileItemFactory fileItemFactory;
/**
* total bytes uploaded (downloaded from server's pov) so far. used for upload notifications
*/
private int bytesUploaded;
/** content length cache, used for upload notifications */
private int totalBytes;
/**
* Constructor.
*
* This constructor will use {@link DiskFileItemFactory} to store uploads.
*
* @param request
* the servlet request
* @param filterPrefix
* prefix to wicket filter mapping
* @param maxSize
* the maximum size allowed for this request
* @param upload
* upload identifier for {@link UploadInfo}
* @throws FileUploadException
* Thrown if something goes wrong with upload
*/
public MultipartServletWebRequestImpl(HttpServletRequest request, String filterPrefix,
Bytes maxSize, String upload) throws FileUploadException
{
this(request, filterPrefix, maxSize, upload, new DiskFileItemFactory()
{
@Override
public FileCleaningTracker getFileCleaningTracker()
{
IFileCleaner fileCleaner = Application.get()
.getResourceSettings()
.getFileCleaner();
return new FileCleanerTrackerAdapter(fileCleaner);
}
});
}
/**
* Constructor
*
* @param request
* the servlet request
* @param filterPrefix
* prefix to wicket filter mapping
* @param maxSize
* the maximum size allowed for this request
* @param upload
* upload identifier for {@link UploadInfo}
* @param factory
* {@link DiskFileItemFactory} to use when creating file items used to represent
* uploaded files
* @throws FileUploadException
* Thrown if something goes wrong with upload
*/
public MultipartServletWebRequestImpl(HttpServletRequest request, String filterPrefix,
Bytes maxSize, String upload, FileItemFactory factory) throws FileUploadException
{
super(request, filterPrefix);
Args.notNull(upload, "upload");
this.upload = upload;
this.fileItemFactory = factory;
parameters = new ValueMap();
files = new HashMap<>();
// Check that request is multipart
// FIXME Wicket 10 This needs a new release of Commons FileUpload (https://issues.apache.org/jira/browse/FILEUPLOAD-309)
final boolean isMultipart = ServletFileUpload.isMultipartContent(request);
if (!isMultipart)
{
throw new IllegalStateException(
"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.");
}
setMaxSize(maxSize);
}
@Override
public void parseFileParts() throws FileUploadException
{
HttpServletRequest request = new javax.servlet.http.HttpServletRequest.Impl(getContainerRequest());
// The encoding that will be used to decode the string parameters
// It should NOT be null at this point, but it may be
// especially if the older Servlet API 2.2 is used
String encoding = request.getCharacterEncoding();
// The encoding can also be null when using multipart/form-data encoded forms.
// In that case we use the [application-encoding] which we always demand using
// the attribute 'accept-encoding' in wicket forms.
if (encoding == null)
{
encoding = Application.get().getRequestCycleSettings().getResponseRequestEncoding();
}
FileUploadBase fileUpload = newFileUpload(encoding);
List<FileItem> items;
if (wantUploadProgressUpdates())
{
ServletRequestContext ctx = new ServletRequestContext(request)
{
@Override
public InputStream getInputStream() throws IOException
{
return new CountingInputStream(super.getInputStream());
}
};
totalBytes = request.getContentLength();
onUploadStarted(totalBytes);
try
{
items = fileUpload.parseRequest(ctx);
}
finally
{
onUploadCompleted();
}
}
else
{
// try to parse the file uploads by using Apache Commons FileUpload APIs
// because they are feature richer (e.g. progress updates, cleaner)
items = fileUpload.parseRequest(new ServletRequestContext(request));
if (items.isEmpty())
{
// fallback to Servlet 3.0 APIs
items = readServlet3Parts(request);
}
}
// Loop through items
for (final FileItem item : items)
{
// Get next item
// If item is a form field
if (item.isFormField())
{
// Set parameter value
final String value;
if (encoding != null)
{
try
{
value = item.getString(encoding);
}
catch (UnsupportedEncodingException e)
{
throw new WicketRuntimeException(e);
}
}
else
{
value = item.getString();
}
addParameter(item.getFieldName(), value);
}
else
{
List<FileItem> fileItems = files.get(item.getFieldName());
if (fileItems == null)
{
fileItems = new ArrayList<>();
files.put(item.getFieldName(), fileItems);
}
// Add to file list
fileItems.add(item);
}
}
}
/**
* Reads the uploads' parts by using Servlet 3.0 APIs.
*
* <strong>Note</strong>: By using Servlet 3.0 APIs the application won't be able to use
* upload progress updates.
*
* @param request
* The http request with the upload data
* @return A list of {@link FileItem}s
* @throws FileUploadException
*/
private List<FileItem> readServlet3Parts(HttpServletRequest request) throws FileUploadException
{
List<FileItem> itemsFromParts = new ArrayList<>();
try
{
Collection<Part> parts = request.getParts();
if (parts != null)
{
for (Part part : parts)
{
FileItem fileItem = new ServletPartFileItem(part);
itemsFromParts.add(fileItem);
}
}
} catch (IOException | ServletException e)
{
throw new FileUploadException("An error occurred while reading the upload parts", e);
}
return itemsFromParts;
}
/**
* Factory method for creating new instances of FileUploadBase
*
* @param encoding
* The encoding to use while reading the data
* @return A new instance of FileUploadBase
*/
protected FileUploadBase newFileUpload(String encoding) {
// Configure the factory here, if desired.
ServletFileUpload fileUpload = new ServletFileUpload(fileItemFactory);
// set encoding specifically when we found it
if (encoding != null)
{
fileUpload.setHeaderEncoding(encoding);
}
fileUpload.setSizeMax(getMaxSize().bytes());
Bytes fileMaxSize = getFileMaxSize();
if (fileMaxSize != null) {
fileUpload.setFileSizeMax(fileMaxSize.bytes());
}
return fileUpload;
}
/**
* Adds a parameter to the parameters value map
*
* @param name
* parameter name
* @param value
* parameter value
*/
private void addParameter(final String name, final String value)
{
final String[] currVal = (String[])parameters.get(name);
String[] newVal;
if (currVal != null)
{
newVal = new String[currVal.length + 1];
System.arraycopy(currVal, 0, newVal, 0, currVal.length);
newVal[currVal.length] = value;
}
else
{
newVal = new String[] { value };
}
parameters.put(name, newVal);
}
/**
* @return Returns the files.
*/
@Override
public Map<String, List<FileItem>> getFiles()
{
return files;
}
/**
* Gets the file that was uploaded using the given field name.
*
* @param fieldName
* the field name that was used for the upload
* @return the upload with the given field name
*/
@Override
public List<FileItem> getFile(final String fieldName)
{
return files.get(fieldName);
}
@Override
protected Map<String, List<StringValue>> generatePostParameters()
{
Map<String, List<StringValue>> res = new HashMap<>();
for (Map.Entry<String, Object> entry : parameters.entrySet())
{
String key = entry.getKey();
String[] val = (String[])entry.getValue();
if (val != null && val.length > 0)
{
List<StringValue> items = new ArrayList<>();
for (String s : val)
{
items.add(StringValue.valueOf(s));
}
res.put(key, items);
}
}
return res;
}
/**
* Subclasses that want to receive upload notifications should return true. By default it takes
* the value from {@link org.apache.wicket.settings.ApplicationSettings#isUploadProgressUpdatesEnabled()}.
*
* @return true if upload status update event should be invoked
*/
protected boolean wantUploadProgressUpdates()
{
return Application.get().getApplicationSettings().isUploadProgressUpdatesEnabled();
}
/**
* Upload start callback
*
* @param totalBytes
*/
protected void onUploadStarted(int totalBytes)
{
UploadInfo info = new UploadInfo(totalBytes);
setUploadInfo(new javax.servlet.http.HttpServletRequest.Impl(getContainerRequest()), upload, info);
}
/**
* Upload status update callback
*
* @param bytesUploaded
* @param total
*/
protected void onUploadUpdate(int bytesUploaded, int total)
{
HttpServletRequest request = new javax.servlet.http.HttpServletRequest.Impl(getContainerRequest());
UploadInfo info = getUploadInfo(request, upload);
if (info == null)
{
throw new IllegalStateException(
"could not find UploadInfo object in session which should have been set when uploaded started");
}
info.setBytesUploaded(bytesUploaded);
setUploadInfo(request, upload, info);
}
/**
* Upload completed callback
*/
protected void onUploadCompleted()
{
clearUploadInfo(new javax.servlet.http.HttpServletRequest.Impl(getContainerRequest()), upload);
}
/**
* An {@link InputStream} that updates total number of bytes read
*
* @author Igor Vaynberg (ivaynberg)
*/
private class CountingInputStream extends InputStream
{
private final InputStream in;
/**
* Constructs a new CountingInputStream.
*
* @param in
* InputStream to delegate to
*/
public CountingInputStream(InputStream in)
{
this.in = in;
}
/**
* @see java.io.InputStream#read()
*/
@Override
public int read() throws IOException
{
int read = in.read();
bytesUploaded += (read < 0) ? 0 : 1;
onUploadUpdate(bytesUploaded, totalBytes);
return read;
}
/**
* @see java.io.InputStream#read(byte[])
*/
@Override
public int read(byte[] b) throws IOException
{
int read = in.read(b);
bytesUploaded += (read < 0) ? 0 : read;
onUploadUpdate(bytesUploaded, totalBytes);
return read;
}
/**
* @see java.io.InputStream#read(byte[], int, int)
*/
@Override
public int read(byte[] b, int off, int len) throws IOException
{
int read = in.read(b, off, len);
bytesUploaded += (read < 0) ? 0 : read;
onUploadUpdate(bytesUploaded, totalBytes);
return read;
}
}
@Override
public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload)
throws FileUploadException
{
// FIXME mgrigorov: Why these checks are made here ?!
// 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)() ?
// Why there is no check that the summary of all files' sizes is less than the set maxSize ?
// Setting a breakpoint here never breaks with the standard upload examples.
Bytes fileMaxSize = getFileMaxSize();
for (Map.Entry<String, List<FileItem>> entry : files.entrySet())
{
List<FileItem> fileItems = entry.getValue();
for (FileItem fileItem : fileItems)
{
if (fileMaxSize != null && fileItem.getSize() > fileMaxSize.bytes())
{
String fieldName = entry.getKey();
FileUploadException fslex = new FileUploadBase.FileSizeLimitExceededException("The field '" +
fieldName + "' exceeds its maximum permitted size of '" +
maxSize + "' characters.", fileItem.getSize(), fileMaxSize.bytes());
throw fslex;
}
}
}
return this;
}
@Override
public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload, FileItemFactory factory)
throws FileUploadException
{
return this;
}
private static final String SESSION_KEY = MultipartServletWebRequestImpl.class.getName();
private static String getSessionKey(String upload)
{
return SESSION_KEY + ":" + upload;
}
/**
* Retrieves {@link UploadInfo} from session, null if not found.
*
* @param req
* http servlet request, not null
* @param upload
* upload identifier
* @return {@link UploadInfo} object from session, or null if not found
*/
public static UploadInfo getUploadInfo(final HttpServletRequest req, String upload)
{
Args.notNull(req, "req");
return (UploadInfo)req.getSession().getAttribute(getSessionKey(upload));
}
/**
* Sets the {@link UploadInfo} object into session.
*
* @param req
* http servlet request, not null
* @param upload
* upload identifier
* @param uploadInfo
* {@link UploadInfo} object to be put into session, not null
*/
public static void setUploadInfo(final HttpServletRequest req, String upload,
final UploadInfo uploadInfo)
{
Args.notNull(req, "req");
Args.notNull(upload, "upload");
Args.notNull(uploadInfo, "uploadInfo");
req.getSession().setAttribute(getSessionKey(upload), uploadInfo);
}
/**
* Clears the {@link UploadInfo} object from session if one exists.
*
* @param req
* http servlet request, not null
* @param upload
* upload identifier
*/
public static void clearUploadInfo(final HttpServletRequest req, String upload)
{
Args.notNull(req, "req");
Args.notNull(upload, "upload");
req.getSession().removeAttribute(getSessionKey(upload));
}
}