/*
* 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.tomcat.util.http.fileupload;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl;
import org.apache.tomcat.util.http.fileupload.impl.FileItemStreamImpl;
import org.apache.tomcat.util.http.fileupload.impl.FileUploadIOException;
import org.apache.tomcat.util.http.fileupload.impl.IOFileUploadException;
import org.apache.tomcat.util.http.fileupload.util.FileItemHeadersImpl;
import org.apache.tomcat.util.http.fileupload.util.Streams;
High level API for processing file uploads.
This class handles multiple files per single HTML widget, sent using multipart/mixed
encoding type, as specified by RFC 1867. Use parseRequest(RequestContext)
to acquire a list of FileItem
s associated with a given HTML widget.
How the data for individual parts is stored is determined by the factory
used to create them; a given part may be in memory, on disk, or somewhere
else.
/**
* <p>High level API for processing file uploads.</p>
*
* <p>This class handles multiple files per single HTML widget, sent using
* {@code multipart/mixed} encoding type, as specified by
* <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>. Use {@link
* #parseRequest(RequestContext)} to acquire a list of {@link
* org.apache.tomcat.util.http.fileupload.FileItem}s associated with a given HTML
* widget.</p>
*
* <p>How the data for individual parts is stored is determined by the factory
* used to create them; a given part may be in memory, on disk, or somewhere
* else.</p>
*/
public abstract class FileUploadBase {
// ---------------------------------------------------------- Class methods
Utility method that determines whether the request contains multipart
content.
NOTE:This method will be moved to the ServletFileUpload
class after the FileUpload 1.1 release. Unfortunately, since this method is static, it is not possible to provide its replacement until this method is removed.
Params: - ctx – The request context to be evaluated. Must be non-null.
Returns: true
if the request is multipart; false
otherwise.
/**
* <p>Utility method that determines whether the request contains multipart
* content.</p>
*
* <p><strong>NOTE:</strong>This method will be moved to the
* {@code ServletFileUpload} class after the FileUpload 1.1 release.
* Unfortunately, since this method is static, it is not possible to
* provide its replacement until this method is removed.</p>
*
* @param ctx The request context to be evaluated. Must be non-null.
*
* @return {@code true} if the request is multipart;
* {@code false} otherwise.
*/
public static final boolean isMultipartContent(RequestContext ctx) {
String contentType = ctx.getContentType();
if (contentType == null) {
return false;
}
if (contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART)) {
return true;
}
return false;
}
// ----------------------------------------------------- Manifest constants
HTTP content type header name.
/**
* HTTP content type header name.
*/
public static final String CONTENT_TYPE = "Content-type";
HTTP content disposition header name.
/**
* HTTP content disposition header name.
*/
public static final String CONTENT_DISPOSITION = "Content-disposition";
HTTP content length header name.
/**
* HTTP content length header name.
*/
public static final String CONTENT_LENGTH = "Content-length";
Content-disposition value for form data.
/**
* Content-disposition value for form data.
*/
public static final String FORM_DATA = "form-data";
Content-disposition value for file attachment.
/**
* Content-disposition value for file attachment.
*/
public static final String ATTACHMENT = "attachment";
Part of HTTP content type header.
/**
* Part of HTTP content type header.
*/
public static final String MULTIPART = "multipart/";
HTTP content type header for multipart forms.
/**
* HTTP content type header for multipart forms.
*/
public static final String MULTIPART_FORM_DATA = "multipart/form-data";
HTTP content type header for multiple uploads.
/**
* HTTP content type header for multiple uploads.
*/
public static final String MULTIPART_MIXED = "multipart/mixed";
// ----------------------------------------------------------- Data members
The maximum size permitted for the complete request, as opposed to fileSizeMax
. A value of -1 indicates no maximum. /**
* The maximum size permitted for the complete request, as opposed to
* {@link #fileSizeMax}. A value of -1 indicates no maximum.
*/
private long sizeMax = -1;
The maximum size permitted for a single uploaded file, as opposed to sizeMax
. A value of -1 indicates no maximum. /**
* The maximum size permitted for a single uploaded file, as opposed
* to {@link #sizeMax}. A value of -1 indicates no maximum.
*/
private long fileSizeMax = -1;
The content encoding to use when reading part headers.
/**
* The content encoding to use when reading part headers.
*/
private String headerEncoding;
The progress listener.
/**
* The progress listener.
*/
private ProgressListener listener;
// ----------------------------------------------------- Property accessors
Returns the factory class used when creating file items.
Returns: The factory class for new file items.
/**
* Returns the factory class used when creating file items.
*
* @return The factory class for new file items.
*/
public abstract FileItemFactory getFileItemFactory();
Sets the factory class to use when creating file items.
Params: - factory – The factory class for new file items.
/**
* Sets the factory class to use when creating file items.
*
* @param factory The factory class for new file items.
*/
public abstract void setFileItemFactory(FileItemFactory factory);
Returns the maximum allowed size of a complete request, as opposed to getFileSizeMax()
. See Also: Returns: The maximum allowed size, in bytes. The default value of
-1 indicates, that there is no limit.
/**
* Returns the maximum allowed size of a complete request, as opposed
* to {@link #getFileSizeMax()}.
*
* @return The maximum allowed size, in bytes. The default value of
* -1 indicates, that there is no limit.
*
* @see #setSizeMax(long)
*
*/
public long getSizeMax() {
return sizeMax;
}
Sets the maximum allowed size of a complete request, as opposed to setFileSizeMax(long)
. Params: - sizeMax – The maximum allowed size, in bytes. The default value of
-1 indicates, that there is no limit.
See Also:
/**
* Sets the maximum allowed size of a complete request, as opposed
* to {@link #setFileSizeMax(long)}.
*
* @param sizeMax The maximum allowed size, in bytes. The default value of
* -1 indicates, that there is no limit.
*
* @see #getSizeMax()
*
*/
public void setSizeMax(long sizeMax) {
this.sizeMax = sizeMax;
}
Returns the maximum allowed size of a single uploaded file, as opposed to getSizeMax()
. See Also: Returns: Maximum size of a single uploaded file.
/**
* Returns the maximum allowed size of a single uploaded file,
* as opposed to {@link #getSizeMax()}.
*
* @see #setFileSizeMax(long)
* @return Maximum size of a single uploaded file.
*/
public long getFileSizeMax() {
return fileSizeMax;
}
Sets the maximum allowed size of a single uploaded file, as opposed to getSizeMax()
. Params: - fileSizeMax – Maximum size of a single uploaded file.
See Also:
/**
* Sets the maximum allowed size of a single uploaded file,
* as opposed to {@link #getSizeMax()}.
*
* @see #getFileSizeMax()
* @param fileSizeMax Maximum size of a single uploaded file.
*/
public void setFileSizeMax(long fileSizeMax) {
this.fileSizeMax = fileSizeMax;
}
Retrieves the character encoding used when reading the headers of an individual part. When not specified, or null
, the request encoding is used. If that is also not specified, or null
, the platform default encoding is used. Returns: The encoding used to read part headers.
/**
* Retrieves the character encoding used when reading the headers of an
* individual part. When not specified, or {@code null}, the request
* encoding is used. If that is also not specified, or {@code null},
* the platform default encoding is used.
*
* @return The encoding used to read part headers.
*/
public String getHeaderEncoding() {
return headerEncoding;
}
Specifies the character encoding to be used when reading the headers of individual part. When not specified, or null
, the request encoding is used. If that is also not specified, or null
, the platform default encoding is used. Params: - encoding – The encoding used to read part headers.
/**
* Specifies the character encoding to be used when reading the headers of
* individual part. When not specified, or {@code null}, the request
* encoding is used. If that is also not specified, or {@code null},
* the platform default encoding is used.
*
* @param encoding The encoding used to read part headers.
*/
public void setHeaderEncoding(String encoding) {
headerEncoding = encoding;
}
// --------------------------------------------------------- Public methods
Processes an RFC 1867 compliant multipart/form-data
stream. Params: - ctx – The context for the request to be parsed.
Throws: - FileUploadException – if there are problems reading/parsing
the request or storing files.
- IOException – An I/O error occurred. This may be a network
error while communicating with the client or a problem while
storing the uploaded content.
Returns: An iterator to instances of FileItemStream
parsed from the request, in the order that they were transmitted.
/**
* Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
* compliant {@code multipart/form-data} stream.
*
* @param ctx The context for the request to be parsed.
*
* @return An iterator to instances of {@code FileItemStream}
* parsed from the request, in the order that they were
* transmitted.
*
* @throws FileUploadException if there are problems reading/parsing
* the request or storing files.
* @throws IOException An I/O error occurred. This may be a network
* error while communicating with the client or a problem while
* storing the uploaded content.
*/
public FileItemIterator getItemIterator(RequestContext ctx)
throws FileUploadException, IOException {
try {
return new FileItemIteratorImpl(this, ctx);
} catch (FileUploadIOException e) {
// unwrap encapsulated SizeException
throw (FileUploadException) e.getCause();
}
}
Processes an RFC 1867 compliant multipart/form-data
stream. Params: - ctx – The context for the request to be parsed.
Throws: - FileUploadException – if there are problems reading/parsing
the request or storing files.
Returns: A list of FileItem
instances parsed from the request, in the order that they were transmitted.
/**
* Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
* compliant {@code multipart/form-data} stream.
*
* @param ctx The context for the request to be parsed.
*
* @return A list of {@code FileItem} instances parsed from the
* request, in the order that they were transmitted.
*
* @throws FileUploadException if there are problems reading/parsing
* the request or storing files.
*/
public List<FileItem> parseRequest(RequestContext ctx)
throws FileUploadException {
List<FileItem> items = new ArrayList<>();
boolean successful = false;
try {
FileItemIterator iter = getItemIterator(ctx);
FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(), "No FileItemFactory has been set.");
final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
while (iter.hasNext()) {
final FileItemStream item = iter.next();
// Don't use getName() here to prevent an InvalidFileNameException.
final String fileName = ((FileItemStreamImpl) item).getName();
FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
item.isFormField(), fileName);
items.add(fileItem);
try {
Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
} catch (FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (IOException e) {
throw new IOFileUploadException(String.format("Processing of %s request failed. %s",
MULTIPART_FORM_DATA, e.getMessage()), e);
}
final FileItemHeaders fih = item.getHeaders();
fileItem.setHeaders(fih);
}
successful = true;
return items;
} catch (FileUploadException e) {
throw e;
} catch (IOException e) {
throw new FileUploadException(e.getMessage(), e);
} finally {
if (!successful) {
for (FileItem fileItem : items) {
try {
fileItem.delete();
} catch (Exception ignored) {
// ignored TODO perhaps add to tracker delete failure list somehow?
}
}
}
}
}
Processes an RFC 1867 compliant multipart/form-data
stream. Params: - ctx – The context for the request to be parsed.
Throws: - FileUploadException – if there are problems reading/parsing
the request or storing files.
Returns: A map of FileItem
instances parsed from the request. Since: 1.3
/**
* Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
* compliant {@code multipart/form-data} stream.
*
* @param ctx The context for the request to be parsed.
*
* @return A map of {@code FileItem} instances parsed from the request.
*
* @throws FileUploadException if there are problems reading/parsing
* the request or storing files.
*
* @since 1.3
*/
public Map<String, List<FileItem>> parseParameterMap(RequestContext ctx)
throws FileUploadException {
final List<FileItem> items = parseRequest(ctx);
final Map<String, List<FileItem>> itemsMap = new HashMap<>(items.size());
for (FileItem fileItem : items) {
String fieldName = fileItem.getFieldName();
List<FileItem> mappedItems = itemsMap.get(fieldName);
if (mappedItems == null) {
mappedItems = new ArrayList<>();
itemsMap.put(fieldName, mappedItems);
}
mappedItems.add(fileItem);
}
return itemsMap;
}
// ------------------------------------------------------ Protected methods
Retrieves the boundary from the Content-type
header. Params: - contentType – The value of the content type header from which to
extract the boundary value.
Returns: The boundary, as a byte array.
/**
* Retrieves the boundary from the {@code Content-type} header.
*
* @param contentType The value of the content type header from which to
* extract the boundary value.
*
* @return The boundary, as a byte array.
*/
public byte[] getBoundary(String contentType) {
ParameterParser parser = new ParameterParser();
parser.setLowerCaseNames(true);
// Parameter parser can handle null input
Map<String, String> params = parser.parse(contentType, new char[] {';', ','});
String boundaryStr = params.get("boundary");
if (boundaryStr == null) {
return null;
}
byte[] boundary;
boundary = boundaryStr.getBytes(StandardCharsets.ISO_8859_1);
return boundary;
}
Retrieves the file name from the Content-disposition
header. Params: - headers – The HTTP headers object.
Returns: The file name for the current encapsulation
.
/**
* Retrieves the file name from the {@code Content-disposition}
* header.
*
* @param headers The HTTP headers object.
*
* @return The file name for the current {@code encapsulation}.
*/
public String getFileName(FileItemHeaders headers) {
return getFileName(headers.getHeader(CONTENT_DISPOSITION));
}
Returns the given content-disposition headers file name.
Params: - pContentDisposition – The content-disposition headers value.
Returns: The file name
/**
* Returns the given content-disposition headers file name.
* @param pContentDisposition The content-disposition headers value.
* @return The file name
*/
private String getFileName(String pContentDisposition) {
String fileName = null;
if (pContentDisposition != null) {
String cdl = pContentDisposition.toLowerCase(Locale.ENGLISH);
if (cdl.startsWith(FORM_DATA) || cdl.startsWith(ATTACHMENT)) {
ParameterParser parser = new ParameterParser();
parser.setLowerCaseNames(true);
// Parameter parser can handle null input
Map<String, String> params = parser.parse(pContentDisposition, ';');
if (params.containsKey("filename")) {
fileName = params.get("filename");
if (fileName != null) {
fileName = fileName.trim();
} else {
// Even if there is no value, the parameter is present,
// so we return an empty file name rather than no file
// name.
fileName = "";
}
}
}
}
return fileName;
}
Retrieves the field name from the Content-disposition
header. Params: - headers – A
Map
containing the HTTP request headers.
Returns: The field name for the current encapsulation
.
/**
* Retrieves the field name from the {@code Content-disposition}
* header.
*
* @param headers A {@code Map} containing the HTTP request headers.
*
* @return The field name for the current {@code encapsulation}.
*/
public String getFieldName(FileItemHeaders headers) {
return getFieldName(headers.getHeader(CONTENT_DISPOSITION));
}
Returns the field name, which is given by the content-disposition
header.
Params: - pContentDisposition – The content-dispositions header value.
Returns: The field jake
/**
* Returns the field name, which is given by the content-disposition
* header.
* @param pContentDisposition The content-dispositions header value.
* @return The field jake
*/
private String getFieldName(String pContentDisposition) {
String fieldName = null;
if (pContentDisposition != null
&& pContentDisposition.toLowerCase(Locale.ENGLISH).startsWith(FORM_DATA)) {
ParameterParser parser = new ParameterParser();
parser.setLowerCaseNames(true);
// Parameter parser can handle null input
Map<String, String> params = parser.parse(pContentDisposition, ';');
fieldName = params.get("name");
if (fieldName != null) {
fieldName = fieldName.trim();
}
}
return fieldName;
}
Parses the header-part
and returns as key/value pairs.
If there are multiple headers of the same names, the name
will map to a comma-separated list containing the values.
Params: - headerPart – The
header-part
of the current encapsulation
.
Returns: A Map
containing the parsed HTTP request headers.
/**
* <p> Parses the {@code header-part} and returns as key/value
* pairs.
*
* <p> If there are multiple headers of the same names, the name
* will map to a comma-separated list containing the values.
*
* @param headerPart The {@code header-part} of the current
* {@code encapsulation}.
*
* @return A {@code Map} containing the parsed HTTP request headers.
*/
public FileItemHeaders getParsedHeaders(String headerPart) {
final int len = headerPart.length();
FileItemHeadersImpl headers = newFileItemHeaders();
int start = 0;
for (;;) {
int end = parseEndOfLine(headerPart, start);
if (start == end) {
break;
}
StringBuilder header = new StringBuilder(headerPart.substring(start, end));
start = end + 2;
while (start < len) {
int nonWs = start;
while (nonWs < len) {
char c = headerPart.charAt(nonWs);
if (c != ' ' && c != '\t') {
break;
}
++nonWs;
}
if (nonWs == start) {
break;
}
// Continuation line found
end = parseEndOfLine(headerPart, nonWs);
header.append(' ').append(headerPart, nonWs, end);
start = end + 2;
}
parseHeaderLine(headers, header.toString());
}
return headers;
}
Creates a new instance of FileItemHeaders
. Returns: The new instance.
/**
* Creates a new instance of {@link FileItemHeaders}.
* @return The new instance.
*/
protected FileItemHeadersImpl newFileItemHeaders() {
return new FileItemHeadersImpl();
}
Skips bytes until the end of the current line.
Params: - headerPart – The headers, which are being parsed.
- end – Index of the last byte, which has yet been
processed.
Returns: Index of the \r\n sequence, which indicates
end of line.
/**
* Skips bytes until the end of the current line.
* @param headerPart The headers, which are being parsed.
* @param end Index of the last byte, which has yet been
* processed.
* @return Index of the \r\n sequence, which indicates
* end of line.
*/
private int parseEndOfLine(String headerPart, int end) {
int index = end;
for (;;) {
int offset = headerPart.indexOf('\r', index);
if (offset == -1 || offset + 1 >= headerPart.length()) {
throw new IllegalStateException(
"Expected headers to be terminated by an empty line.");
}
if (headerPart.charAt(offset + 1) == '\n') {
return offset;
}
index = offset + 1;
}
}
Reads the next header line.
Params: - headers – String with all headers.
- header – Map where to store the current header.
/**
* Reads the next header line.
* @param headers String with all headers.
* @param header Map where to store the current header.
*/
private void parseHeaderLine(FileItemHeadersImpl headers, String header) {
final int colonOffset = header.indexOf(':');
if (colonOffset == -1) {
// This header line is malformed, skip it.
return;
}
String headerName = header.substring(0, colonOffset).trim();
String headerValue =
header.substring(header.indexOf(':') + 1).trim();
headers.addHeader(headerName, headerValue);
}
Returns the progress listener.
Returns: The progress listener, if any, or null.
/**
* Returns the progress listener.
*
* @return The progress listener, if any, or null.
*/
public ProgressListener getProgressListener() {
return listener;
}
Sets the progress listener.
Params: - pListener – The progress listener, if any. Defaults to null.
/**
* Sets the progress listener.
*
* @param pListener The progress listener, if any. Defaults to null.
*/
public void setProgressListener(ProgressListener pListener) {
listener = pListener;
}
}