/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 1997-2013 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] [Payara Foundation and/or its affiliates] package org.apache.catalina.fileupload; import org.apache.catalina.Globals; import org.apache.catalina.core.StandardServer; import org.apache.catalina.util.RequestUtil; import org.glassfish.logging.annotation.LogMessageInfo; import javax.servlet.http.Part; import java.io.*; import java.nio.charset.Charset; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; /** * This class implements java.servlet.http.Part. * * Original authors: * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a> * @author <a href="mailto:sean@informage.net">Sean Legassick</a> * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a> * @author <a href="mailto:jmcnally@apache.org">John McNally</a> * @author <a href="mailto:martinc@apache.org">Martin Cooper</a> * @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 = StandardServer.log; private final ResourceBundle rb = log.getResourceBundle(); @LogMessageInfo( message = "file data is empty.", level = "INFO" ) public static final String FILE_DATA_IS_EMPTY_INFO = "AS-WEB-CORE-00285"; // ----------------------------------------------------------- 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 <code>null</code> 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 <code>DiskFileItem</code> 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 * <code>null</code> 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 * <code>null</code> 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(); } // ------------------------------- 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 <code>null</code> if * not defined. * * @return The content type passed by the agent or <code>null</code> if * not defined. */ public String getContentType() { return contentType; } /** * Returns the content charset passed by the agent or <code>null</code> if * not defined. * * @return The content charset passed by the agent or <code>null</code> if * not defined. */ public String getCharSet() { ParameterParser parser = new ParameterParser(); parser.setLowerCaseNames(true); // Parameter parser can handle null input Map<String, String> 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 <code>true</code> if the file contents will be read * from memory; <code>false</code> 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, 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. * <p> * 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. * <p> * This method is only guaranteed to work <em>once</em>, 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 <code>FileItem</code> 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 <code>FileItem</code> instance represents * a simple form field. * * @return <code>true</code> if the instance represents a simple form * field; <code>false</code> if it represents an uploaded file. * * @see #setFormField(boolean) * */ public boolean isFormField() { return isFormField; } /** * Specifies whether or not a <code>FileItem</code> instance represents * a simple form field. * * @param state <code>true</code> if the instance represents a simple form * field; <code>false</code> 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 <code>FileItem</code>'s * data's temporary location on the disk. Note that for * <code>FileItem</code>s that have their data stored in memory, * this method will return <code>null</code>. 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 <code>null</code> 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. */ protected void finalize() { 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 <code>FileItem</code> 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 OutputStream output = getOutputStream(); if (cachedContent != null) { output.write(cachedContent); } else { FileInputStream input = new FileInputStream(dfosFile); Streams.copy(input, output, false); deleteFile(dfosFile); dfosFile = null; } output.close(); 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<String> getHeaders(String name) { List<String> 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<String> getHeaderNames() { return headers.getHeaderNames(); } private void deleteFile(File file) { if (!file.delete() && log.isLoggable(Level.FINE)) { log.log(Level.FINE, "Cannot delete file: " + file); } } }