org.apache.catalina.fileupload.PartItem Maven / Gradle / Ivy
Show all versions of payara-micro Show documentation
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2016 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* 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.
*/
// Portions Copyright [2016-2021] [Payara Foundation and/or its affiliates]
package org.apache.catalina.fileupload;
import org.apache.catalina.Globals;
import org.apache.catalina.LogFacade;
import org.apache.catalina.util.RequestUtil;
import jakarta.servlet.http.Part;
import java.io.*;
import org.glassfish.hk2.utilities.CleanerFactory;
import java.nio.charset.Charset;
import java.sql.SQLException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This class implements java.servlet.http.Part.
*
* Original authors:
* @author Rafal Krzewski
* @author Sean Legassick
* @author Jason van Zyl
* @author John McNally
* @author Martin Cooper
* @author Sean C. Sullivan
*
* Adopted for Glassfish:
* @author Kin-man Chung
*
* @version $Id: PartItem.java $
*/
class PartItem
implements Serializable, Part {
// ----------------------------------------------------- Manifest constants
/**
* The UID to use when serializing this instance.
*/
private static final long serialVersionUID = 2237570099615271025L;
private static final Logger log = LogFacade.getLogger();
private final ResourceBundle rb = log.getResourceBundle();
// ----------------------------------------------------------- Data members
/**
* UID used in unique file name generation.
*/
private static final String UID =
new java.rmi.server.UID().toString()
.replace(':', '_').replace('-', '_');
/**
* Counter used in unique identifier generation.
*/
private static int counter = 0;
/**
* The name of the form field as provided by the browser.
*/
private String fieldName;
/**
* The content type passed by the browser, or null
if
* not defined.
*/
private String contentType;
/**
* Whether or not this item is a simple form field.
*/
private boolean isFormField;
/**
* The original filename in the user's filesystem.
*/
private String fileName;
/**
* The size of the item, in bytes. This is used to cache the size when a
* file item is moved from its original location.
*/
private long size = -1;
/**
* The threshold above which uploads will be stored on disk.
*/
private int sizeThreshold;
/**
* The directory in which uploaded files will be stored, if stored on disk.
*/
private File repository;
/**
* Cached contents of the file.
*/
private byte[] cachedContent;
/**
* Output stream for this item.
*/
private transient DeferredFileOutputStream dfos;
/**
* The temporary file to use.
*/
private transient File tempFile;
/**
* File to allow for serialization of the content of this item.
*/
private File dfosFile;
/**
* The items headers.
*/
private PartHeaders headers;
/**
* The Multipart base.
*/
private Multipart multipart;
/**
* The request character encoding;
*/
private String requestCharEncoding;
// ----------------------------------------------------------- Constructors
/**
* Constructs a new DiskFileItem
instance.
*
* @param multipart The Multipart instance.
* @param headers The PartHeaders instance.
* @param fieldName The name of the form field.
* @param contentType The content type passed by the browser or
* null
if not specified.
* @param isFormField Whether or not this item is a plain form field, as
* opposed to a file upload.
* @param fileName The original filename in the user's filesystem, or
* null
if not specified.
* @param requestCharEncoding
* The request character encoding.
*/
public PartItem(Multipart multipart, PartHeaders headers,
String fieldName, String contentType,
boolean isFormField, String fileName,
String requestCharEncoding) {
this.multipart = multipart;
this.headers = headers;
this.fieldName = fieldName;
this.contentType = contentType;
this.isFormField = isFormField;
this.fileName = fileName;
this.requestCharEncoding = requestCharEncoding;
this.sizeThreshold = multipart.getFileSizeThreshold();
this.repository = multipart.getRepository();
registerCleanupEvent();
}
// ------------------------------- Methods from javax.activation.DataSource
/**
* Returns an {@link java.io.InputStream InputStream} that can be
* used to retrieve the contents of the file.
*
* @return An {@link java.io.InputStream InputStream} that can be
* used to retrieve the contents of the file.
*
* @throws IOException if an error occurs.
*/
public InputStream getInputStream()
throws IOException {
if (!isInMemory()) {
return new FileInputStream(dfos.getFile());
}
if (cachedContent == null) {
cachedContent = dfos.getData();
}
return new ByteArrayInputStream(cachedContent);
}
/**
* Returns the content type passed by the agent or null
if
* not defined.
*
* @return The content type passed by the agent or null
if
* not defined.
*/
public String getContentType() {
return contentType;
}
/**
* Returns the content charset passed by the agent or null
if
* not defined.
*
* @return The content charset passed by the agent or null
if
* not defined.
*/
public String getCharSet() {
ParameterParser parser = new ParameterParser();
parser.setLowerCaseNames(true);
// Parameter parser can handle null input
Map params = parser.parse(getContentType(), ';');
return params.get("charset");
}
/**
* Returns the original filename in the client's filesystem.
*
* @return The original filename in the client's filesystem.
*/
public String getSubmittedFileName() {
return fileName;
}
// ------------------------------------------------------- FileItem methods
/**
* Provides a hint as to whether or not the file contents will be read
* from memory.
*
* @return true
if the file contents will be read
* from memory; false
otherwise.
*/
public boolean isInMemory() {
if (cachedContent != null) {
return true;
}
return dfos.isInMemory();
}
/**
* Returns the size of the file.
*
* @return The size of the file, in bytes.
*/
public long getSize() {
if (size >= 0) {
return size;
} else if (cachedContent != null) {
return cachedContent.length;
} else if (dfos.isInMemory()) {
return dfos.getData().length;
} else {
return dfos.getFile().length();
}
}
/**
* Returns the contents of the file as an array of bytes. If the
* contents of the file were not yet cached in memory, they will be
* loaded from the disk storage and cached.
*
* @return The contents of the file as an array of bytes.
*/
public byte[] get() {
if (isInMemory()) {
if (cachedContent == null) {
cachedContent = dfos.getData();
}
return cachedContent;
}
byte[] fileData = new byte[(int) getSize()];
FileInputStream fis = null;
try {
fis = new FileInputStream(dfos.getFile());
if (fis.read(fileData) != (int)getSize())
if (log.isLoggable(Level.INFO))
log.log(Level.INFO, LogFacade.FILE_DATA_IS_EMPTY_INFO);
} catch (IOException e) {
fileData = null;
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// ignore
}
}
}
return fileData;
}
/**
* Returns the contents of the file as a String, using the specified
* encoding. This method uses {@link #get()} to retrieve the
* contents of the file.
*
* @param charset The charset to use.
*
* @return The contents of the file, as a string.
*
* @throws UnsupportedEncodingException if the requested character
* encoding is not available.
*/
public String getString(final String charset)
throws UnsupportedEncodingException {
return new String(get(), RequestUtil.lookupCharset(charset));
}
/**
* Returns the contents of the file as a String, using the default
* character encoding. This method uses {@link #get()} to retrieve the
* contents of the file.
*
* @return The contents of the file, as a string.
*
* @todo Consider making this method throw UnsupportedEncodingException.
*/
public String getString() {
byte[] rawdata = get();
String charset = getCharSet();
if (charset == null) {
// If content-type does not specify a charset, use the request
// character encoding if it is non-null;
if (requestCharEncoding != null) {
charset = requestCharEncoding;
} else {
// Media subtypes of type "text" are defined to have a default
// charset value of "ISO-8859-1" when received via HTTP
charset = Globals.ISO_8859_1_ENCODING;
}
}
try {
return new String(rawdata, RequestUtil.lookupCharset(charset));
} catch (UnsupportedEncodingException e) {
return new String(rawdata, Charset.defaultCharset());
}
}
/**
* A convenience method to write an uploaded item to disk. The client code
* is not concerned with whether or not the item is stored in memory, or on
* disk in a temporary location. They just want to write the uploaded item
* to a file.
*
* This implementation first attempts to rename the uploaded item to the
* specified destination file, if the item was originally written to disk.
* Otherwise, the data will be copied to the specified file.
*
* This method is only guaranteed to work once, the first time it
* is invoked for a particular item. This is because, in the event that the
* method renames a temporary file, that file will no longer be available
* to copy or rename again at a later time.
*
* @param file The file into which the uploaded item should be stored.
*
* @throws IOException if an error occurs.
*/
public void write(File file) throws IOException {
if (isInMemory()) {
FileOutputStream fout = null;
try {
fout = new FileOutputStream(file);
fout.write(get());
} finally {
if (fout != null) {
fout.close();
}
}
} else {
File outputFile = getStoreLocation();
if (outputFile != null) {
// Save the length of the file
size = outputFile.length();
/*
* The uploaded file is being stored on disk
* in a temporary location so move it to the
* desired file.
*/
if (!outputFile.renameTo(file)) {
BufferedInputStream in = null;
BufferedOutputStream out = null;
try {
in = new BufferedInputStream(
new FileInputStream(outputFile));
out = new BufferedOutputStream(
new FileOutputStream(file));
Streams.copy(in, out, false);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// ignore
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
// ignore
}
}
}
}
} else {
/*
* For whatever reason we cannot write the
* file to disk.
*/
throw new IOException(
"Cannot write uploaded file to disk!");
}
}
}
public void write (String file) throws IOException {
write(new File(repository, file));
}
/**
* Deletes the underlying storage for a file item, including deleting any
* associated temporary disk file. Although this storage will be deleted
* automatically when the FileItem
instance is garbage
* collected, this method can be used to ensure that this is done at an
* earlier time, thus preserving system resources.
*/
public void delete() {
cachedContent = null;
File outputFile = getStoreLocation();
if (outputFile != null && outputFile.exists()) {
deleteFile(outputFile);
}
}
/**
* Returns the name of the field in the multipart form corresponding to
* this file item.
*
* @return The name of the form field.
*
* @see #setName(java.lang.String)
*
*/
public String getName() {
return fieldName;
}
/**
* Sets the field name used to reference this file item.
*
* @param fieldName The name of the form field.
*
* @see #getName()
*
*/
public void setName(String fieldName) {
this.fieldName = fieldName;
}
/**
* Determines whether or not a FileItem
instance represents
* a simple form field.
*
* @return true
if the instance represents a simple form
* field; false
if it represents an uploaded file.
*
* @see #setFormField(boolean)
*
*/
public boolean isFormField() {
return isFormField;
}
/**
* Specifies whether or not a FileItem
instance represents
* a simple form field.
*
* @param state true
if the instance represents a simple form
* field; false
if it represents an uploaded file.
*
* @see #isFormField()
*
*/
public void setFormField(boolean state) {
isFormField = state;
}
/**
* Returns an {@link java.io.OutputStream OutputStream} that can
* be used for storing the contents of the file.
*
* @return An {@link java.io.OutputStream OutputStream} that can be used
* for storing the contensts of the file.
*
* @throws IOException if an error occurs.
*/
public OutputStream getOutputStream()
throws IOException {
if (dfos == null) {
File outputFile = getTempFile();
dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
}
return dfos;
}
// --------------------------------------------------------- Public methods
/**
* Returns the {@link java.io.File} object for the FileItem
's
* data's temporary location on the disk. Note that for
* FileItem
s that have their data stored in memory,
* this method will return null
. When handling large
* files, you can use {@link java.io.File#renameTo(java.io.File)} to
* move the file to new location without copying the data, if the
* source and destination locations reside within the same logical
* volume.
*
* @return The data file, or null
if the data is stored in
* memory.
*/
public File getStoreLocation() {
return dfos == null ? null : dfos.getFile();
}
// ------------------------------------------------------ Protected methods
/**
* Removes the file contents from the temporary storage.
*/
public final void registerCleanupEvent() {
CleanerFactory.create().register(this, () -> {
File outputFile = dfos.getFile();
if (outputFile != null && outputFile.exists()) {
deleteFile(outputFile);
}
});
}
/**
* Creates and returns a {@link java.io.File File} representing a uniquely
* named temporary file in the configured repository path. The lifetime of
* the file is tied to the lifetime of the FileItem
instance;
* the file will be deleted when the instance is garbage collected.
*
* @return The {@link java.io.File File} to be used for temporary storage.
*/
protected File getTempFile() {
if (tempFile == null) {
File tempDir = repository;
if (tempDir == null) {
tempDir = new File(System.getProperty("java.io.tmpdir"));
}
String tempFileName =
"upload_" + UID + "_" + getUniqueId() + ".tmp";
tempFile = new File(tempDir, tempFileName);
}
return tempFile;
}
// -------------------------------------------------------- Private methods
/**
* Returns an identifier that is unique within the class loader used to
* load this class, but does not have random-like apearance.
*
* @return A String with the non-random looking instance identifier.
*/
private static String getUniqueId() {
final int limit = 100000000;
int current;
synchronized (PartItem.class) {
current = counter++;
}
String id = Integer.toString(current);
// If you manage to get more than 100 million of ids, you'll
// start getting ids longer than 8 characters.
if (current < limit) {
id = ("00000000" + id).substring(id.length());
}
return id;
}
/**
* Returns a string representation of this object.
*
* @return a string representation of this object.
*/
public String toString() {
return "File name=" + this.getSubmittedFileName()
+ ", StoreLocation="
+ String.valueOf(this.getStoreLocation())
+ ", size="
+ this.getSize()
+ "bytes, "
+ "isFormField=" + isFormField()
+ ", FieldName="
+ this.getName();
}
// -------------------------------------------------- Serialization methods
/**
* Writes the state of this object during serialization.
*
* @param out The stream to which the state should be written.
*
* @throws IOException if an error occurs.
*/
private void writeObject(ObjectOutputStream out) throws IOException {
// Read the data
if (dfos.isInMemory()) {
cachedContent = get();
} else {
cachedContent = null;
dfosFile = dfos.getFile();
}
// write out values
out.defaultWriteObject();
}
/**
* Reads the state of this object during deserialization.
*
* @param in The stream from which the state should be read.
*
* @throws IOException if an error occurs.
* @throws ClassNotFoundException if class cannot be found.
*/
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
// read values
in.defaultReadObject();
// PAYARA-953 protect against null byte attacks
if (repository != null) {
if (repository.getPath().contains("\0")) {
throw new IOException("Repository path cannot contain a null byte");
}
if (!repository.isDirectory()) {
throw new IOException("Repository path " + repository.getPath() + " is not a directory ");
}
}
if (dfosFile != null) {
if (dfosFile.getPath().contains("\0")) {
throw new IOException("File path cannot contain a null byte");
}
}
// END PAYARA-953
try (OutputStream output = getOutputStream()) {
if (cachedContent != null) {
output.write(cachedContent);
} else {
FileInputStream input = new FileInputStream(dfosFile);
Streams.copy(input, output, false);
deleteFile(dfosFile);
dfosFile = null;
}
}
cachedContent = null;
}
/**
* Returns the value of the specified mime header as a String.
* @param name a String specifying the header name
* @return a String containing the value of the requested header,
* or null if the part does not have a header of that name
*/
public String getHeader(String name) {
return headers.getHeader(name);
}
/**
* Returns all the values of the specified Part header
* @param name - a String specifying the header name
* @return a Collection of the values of the requested header.
* If the Part does not have any headers of that name return an
* empty Collection. If the container does not allow access to header
* information, return null
*/
public Collection getHeaders(String name) {
List values = headers.getHeaders(name);
if (values != Collections.EMPTY_LIST) {
values = Collections.unmodifiableList(values);
}
return values;
}
/**
* Returns a Collection of all the header names this part contains.
* @return a Collection of all the header names sent with this part;
* if the part has no headers, an empty Collection;
* if the servlet container does not allow servlets to use this
* method, null
*/
public Collection getHeaderNames() {
return headers.getHeaderNames();
}
private void deleteFile(File file) {
if (!file.delete() && log.isLoggable(Level.FINE)) {
log.log(Level.FINE, "Cannot delete file: " + file);
}
}
}