/** * The contents of this file are subject to the license and copyright * detailed in the LICENSE and NOTICE files at the root of the source * tree and available online at * * http://www.dspace.org/license/ */ package org.dspace.app.xmlui.cocoon.servlet.multipart; import org.apache.cocoon.servlet.multipart.*; import org.apache.cocoon.util.NullOutputStream; import org.apache.commons.fileupload.ParameterParser; import org.apache.commons.lang.StringUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.*; import java.util.Enumeration; import java.util.Hashtable; import java.util.Map; import java.util.Vector; /** * This class is used to implement a multipart request wrapper. * It will parse the http post stream and and fill its hashtable with values. * * This class has been adjusted by DSpace to allow the uploading of files with an = sign in it * * The hashtable will contain: * Vector: inline part values * FilePart: file part * * @version $Id: MultipartParser.java 638211 2008-03-18 04:41:23Z joerg $ */ public class DSpaceMultipartParser { public static final String UPLOAD_STATUS_SESSION_ATTR = "org.apache.cocoon.servlet.multipartparser.status"; private final static int FILE_BUFFER_SIZE = 4096; private static final int MAX_BOUNDARY_SIZE = 128; private boolean saveUploadedFilesToDisk; private File uploadDirectory = null; private boolean allowOverwrite; private boolean silentlyRename; private int maxUploadSize; private String characterEncoding; private Hashtable parts; private boolean oversized = false; private int contentLength; private HttpSession session; private boolean hasSession; private Hashtable uploadStatus; /** * Constructor, parses given request * * @param saveUploadedFilesToDisk Write fileparts to the uploadDirectory. If true the corresponding object * in the hashtable will contain a FilePartFile, if false a FilePartArray * @param uploadDirectory The directory to write to if saveUploadedFilesToDisk is true. * @param allowOverwrite Allow existing files to be overwritten. * @param silentlyRename If file exists rename file (using filename+number). * @param maxUploadSize The maximum content length accepted. * @param characterEncoding The character encoding to be used. */ public DSpaceMultipartParser(boolean saveUploadedFilesToDisk, File uploadDirectory, boolean allowOverwrite, boolean silentlyRename, int maxUploadSize, String characterEncoding) { this.saveUploadedFilesToDisk = saveUploadedFilesToDisk; this.uploadDirectory = uploadDirectory; this.allowOverwrite = allowOverwrite; this.silentlyRename = silentlyRename; this.maxUploadSize = maxUploadSize; this.characterEncoding = characterEncoding; } private void parseParts(int contentLength, String contentType, InputStream requestStream) throws IOException, MultipartException { this.contentLength = contentLength; if (contentLength > this.maxUploadSize) { this.oversized = true; } BufferedInputStream bufferedStream = new BufferedInputStream(requestStream); PushbackInputStream pushbackStream = new PushbackInputStream(bufferedStream, MAX_BOUNDARY_SIZE); DSpaceTokenStream stream = new DSpaceTokenStream(pushbackStream); parseMultiPart(stream, getBoundary(contentType)); } public Hashtable getParts(int contentLength, String contentType, InputStream requestStream) throws IOException, MultipartException { this.parts = new Hashtable(); parseParts(contentLength, contentType, requestStream); return this.parts; } public Hashtable getParts(HttpServletRequest request) throws IOException, MultipartException { this.parts = new Hashtable(); // Copy all parameters coming from the request URI to the parts table. // This happens when a form's action attribute has some parameters Enumeration names = request.getParameterNames(); while(names.hasMoreElements()) { String name = (String)names.nextElement(); String[] values = request.getParameterValues(name); Vector v = new Vector(values.length); for (int i = 0; i < values.length; i++) { v.add(values[i]); } this.parts.put(name, v); } // upload progress bar support this.session = request.getSession(); this.hasSession = this.session != null; if (this.hasSession) { this.uploadStatus = new Hashtable(); this.uploadStatus.put("started", Boolean.FALSE); this.uploadStatus.put("finished", Boolean.FALSE); this.uploadStatus.put("sent", new Integer(0)); this.uploadStatus.put("total", new Integer(request.getContentLength())); this.uploadStatus.put("filename", ""); this.uploadStatus.put("error", Boolean.FALSE); this.uploadStatus.put("uploadsdone", new Integer(0)); this.session.setAttribute(UPLOAD_STATUS_SESSION_ATTR, this.uploadStatus); } parseParts(request.getContentLength(), request.getContentType(), request.getInputStream()); if (this.hasSession) { this.uploadStatus.put("finished", Boolean.TRUE); } return this.parts; } /** * Parse a multipart block * * @param ts * @param boundary * * @throws java.io.IOException * @throws org.apache.cocoon.servlet.multipart.MultipartException */ private void parseMultiPart(DSpaceTokenStream ts, String boundary) throws IOException, MultipartException { ts.setBoundary(boundary.getBytes()); ts.read(); // read first boundary away ts.setBoundary(("\r\n" + boundary).getBytes()); while (ts.getState() == DSpaceTokenStream.STATE_NEXTPART) { ts.nextPart(); parsePart(ts); } if (ts.getState() != DSpaceTokenStream.STATE_ENDMULTIPART) { // sanity check throw new MultipartException("Malformed stream"); } } /** * Parse a single part * * @param ts * * @throws java.io.IOException * @throws org.apache.cocoon.servlet.multipart.MultipartException */ private void parsePart(DSpaceTokenStream ts) throws IOException, MultipartException { Hashtable headers = new Hashtable(); headers = readHeaders(ts); try { if (headers.containsKey("filename")) { if (!"".equals(headers.get("filename"))) { parseFilePart(ts, headers); } else { // IE6 sends an empty part with filename="" for // empty upload fields. Just parse away the part byte[] buf = new byte[32]; while(ts.getState() == DSpaceTokenStream.STATE_READING) ts.read(buf); } } else if (((String) headers.get("content-disposition")) .toLowerCase().equals("form-data")) { parseInlinePart(ts, headers); } // FIXME: multipart/mixed parts are untested. else if (((String) headers.get("content-disposition")).toLowerCase() .indexOf("multipart") > -1) { parseMultiPart(new DSpaceTokenStream(ts, MAX_BOUNDARY_SIZE), "--" + (String) headers.get("boundary")); ts.read(); // read past boundary } else { throw new MultipartException("Unknown part type"); } } catch (IOException e) { throw new MultipartException("Malformed stream: " + e.getMessage()); } catch (NullPointerException e) { e.printStackTrace(); throw new MultipartException("Malformed header"); } } /** * Parse a file part * * @param in * @param headers * * @throws java.io.IOException * @throws org.apache.cocoon.servlet.multipart.MultipartException */ private void parseFilePart(DSpaceTokenStream in, Hashtable headers) throws IOException, MultipartException { byte[] buf = new byte[FILE_BUFFER_SIZE]; OutputStream out; File file = null; if (oversized) { out = new NullOutputStream(); } else if (!saveUploadedFilesToDisk) { out = new ByteArrayOutputStream(); } else { String fileName = (String) headers.get("filename"); if(File.separatorChar == '\\') fileName = fileName.replace('/','\\'); else fileName = fileName.replace('\\','/'); String filePath = uploadDirectory.getPath() + File.separator; fileName = new File(fileName).getName(); file = new File(filePath + fileName); if (!allowOverwrite && !file.createNewFile()) { if (silentlyRename) { int c = 0; do { file = new File(filePath + c++ + "_" + fileName); } while (!file.createNewFile()); } else { throw new MultipartException("Duplicate file '" + file.getName() + "' in '" + file.getParent() + "'"); } } out = new FileOutputStream(file); } if (hasSession) { // upload widget support this.uploadStatus.put("finished", Boolean.FALSE); this.uploadStatus.put("started", Boolean.TRUE); this.uploadStatus.put("widget", headers.get("name")); this.uploadStatus.put("filename", headers.get("filename")); } int length = 0; // Track length for OversizedPart try { int read = 0; while (in.getState() == DSpaceTokenStream.STATE_READING) { // read data read = in.read(buf); length += read; out.write(buf, 0, read); if (this.hasSession) { this.uploadStatus.put("sent", new Integer(((Integer)this.uploadStatus.get("sent")).intValue() + read) ); } } if (this.hasSession) { // upload widget support this.uploadStatus.put("uploadsdone", new Integer(((Integer)this.uploadStatus.get("uploadsdone")).intValue() + 1) ); this.uploadStatus.put("error", Boolean.FALSE); } } catch (IOException ioe) { // don't let incomplete file uploads pile up in the upload dir. // this usually happens with aborted form submits containing very large files. out.close(); out = null; if ( file!=null ) file.delete(); if (this.hasSession) { // upload widget support this.uploadStatus.put("error", Boolean.TRUE); } throw ioe; } finally { if ( out!=null ) out.close(); } String name = (String)headers.get("name"); if (oversized) { this.parts.put(name, new RejectedPart(headers, length, this.contentLength, this.maxUploadSize)); } else if (file == null) { byte[] bytes = ((ByteArrayOutputStream) out).toByteArray(); this.parts.put(name, new PartInMemory(headers, bytes)); } else { this.parts.put(name, new PartOnDisk(headers, file)); } } /** * Parse an inline part * * @param in * @param headers * * @throws java.io.IOException */ private void parseInlinePart(DSpaceTokenStream in, Hashtable headers) throws IOException { // Buffer incoming bytes for proper string decoding (there can be multibyte chars) ByteArrayOutputStream bos = new ByteArrayOutputStream(); while (in.getState() == DSpaceTokenStream.STATE_READING) { int c = in.read(); if (c != -1) bos.write(c); } String field = (String) headers.get("name"); Vector v = (Vector) this.parts.get(field); if (v == null) { v = new Vector(); this.parts.put(field, v); } v.add(new String(bos.toByteArray(), this.characterEncoding)); } /** * Read part headers * * @param in * * @throws java.io.IOException */ private Hashtable readHeaders(DSpaceTokenStream in) throws IOException { Hashtable headers = new Hashtable(); String hdrline = readln(in); ParameterParser parser = new ParameterParser(); while (!"".equals(hdrline)) { String name = StringUtils.substringBefore(hdrline, ": ").toLowerCase(); String value; if(hdrline.contains(";")){ value = StringUtils.substringBetween(hdrline, ": ", "; "); }else{ value = StringUtils.substringAfter(hdrline, ": "); } headers.put(name, value); hdrline = StringUtils.substringAfter(hdrline, ";"); if (StringUtils.isNotBlank(hdrline)) { Map parsed = parser.parse(hdrline, ';'); if (parsed.containsKey("filename") && parsed.get("filename") == null) { parsed.put("filename", ""); // apparently, IE6 sometimes submits filename="" } headers.putAll(parsed); // source out parsing of the rest of the header - it gets quite tricky with respecting quotes etc } hdrline = readln(in); } return headers; } /** * Get boundary from contentheader */ private String getBoundary(String hdr) { int start = hdr.toLowerCase().indexOf("boundary="); if (start > -1) { return "--" + hdr.substring(start + 9); } return null; } /** * Read string until newline or end of stream * * @param in * * @throws java.io.IOException */ private String readln(DSpaceTokenStream in) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); int b = in.read(); while ((b != -1) && (b != '\r')) { bos.write(b); b = in.read(); } if (b == '\r') { in.read(); // read '\n' } return new String(bos.toByteArray(), this.characterEncoding); } }