/* * 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 net.formio.upload; import java.io.File; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.formio.EncodingException; import net.formio.internal.FormUtils; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.disk.DiskFileItemFactory; /** * Preprocesses multipart/form-data request to {@link RequestUploadedFile}(s) and * string request parameters. * @author Radek Beran */ public class MultipartRequestPreprocessor { /** Max. size per single file */ public static final int SINGLE_FILE_SIZE_MAX = 5242880; // 5 MB /** Max. for total size of request. */ public static final int TOTAL_SIZE_MAX = 10485760; // 10 MB /** Max. size of file that is stored only in memory. */ public static final int SIZE_THRESHOLD = 10240; // 10 KB public static final String DEFAULT_ENCODING = "utf-8"; public static File getDefaultTempDir() { return FormUtils.getTempDir(); } private final String defaultEncoding; private final RequestProcessingError error; /** * Wrapper which preprocesses multipart request. * @param parser multipart request parser * @param defaultEncoding header and request parameter encoding * @param tempDir temporary directory to store files bigger than specified size threshold * @param sizeThreshold max size of file (in bytes) that is loaded into the memory and not temporarily stored to disk * @param totalSizeMax maximum allowed size of the whole request in bytes * @param singleFileSizeMax maximum allowed size of a single uploaded file */ public MultipartRequestPreprocessor(MultipartRequestParser parser, String defaultEncoding, File tempDir, int sizeThreshold, long totalSizeMax, long singleFileSizeMax) { this.defaultEncoding = defaultEncoding; final DiskFileItemFactory fif = new DiskFileItemFactory(); if (tempDir != null) { fif.setRepository(tempDir); } if (sizeThreshold > 0) { fif.setSizeThreshold(sizeThreshold); } try { List<FileItem> fileItems = parser.parseFileItems(fif, singleFileSizeMax, totalSizeMax, defaultEncoding); convertToMaps(fileItems); } finally { this.error = parser.getError(); } } /** * Return all request parameter names, for both regular form fields and file * upload fields. */ public Enumeration<String> getParameterNames() { Set<String> allNames = new LinkedHashSet<String>(); allNames.addAll(regularParams.keySet()); allNames.addAll(fileParams.keySet()); return Collections.enumeration(allNames); } /** * Return the parameter value. Applies only to regular parameters, not to * file upload parameters. * * <p> * If the parameter is not present in the underlying request, then * <tt>null</tt> is returned. * <p> * If the parameter is present, but has no associated value, then an empty * string is returned. * <p> * If the parameter is multivalued, the first value that appears in the * request is returned. */ public String getParameter(String name) { String result = null; List<String> values = regularParams.get(name); if (values == null) { // you might try the wrappee, to see if it has a value } else if (values.isEmpty()) { // param name known, but no values present result = ""; } else { // return first value in list result = values.get(FIRST_VALUE); } return result; } /** * Return the parameter values. Applies only to regular parameters, not to * file upload parameters. */ public String[] getParameterValues(String name) { String[] result = null; List<String> values = regularParams.get(name); if (values != null) { result = values.toArray(new String[values.size()]); } return result; } /** * Return a {@code Map<String, String[]>} for all regular parameters. Does * not return any file upload parameters at all. */ public Map<String, String[]> getParameterMap() { Map<String, String[]> res = new LinkedHashMap<String, String[]>(); for (Map.Entry<String, List<String>> entry : regularParams.entrySet()) { res.put(entry.getKey(), entry.getValue().toArray(new String[0])); } return Collections.unmodifiableMap(res); } /** * Return the {@link FileItem} of the given name. * <p> * If the name is unknown, then return <tt>null</tt>. */ public RequestUploadedFile[] getUploadedFiles(String paramName) { List<RequestUploadedFile> files = fileParams.get(paramName); if (files == null) return null; return files.toArray(new RequestUploadedFile[files.size()]); } /** * Returns error from processing the request if there was one, or {@code null}. * @return */ public RequestProcessingError getError() { return this.error; } // PRIVATE /** Store regular params only. May be multivalued (hence the List). */ private final Map<String, List<String>> regularParams = new LinkedHashMap<String, List<String>>(); /** Store file params only. */ private final Map<String, List<RequestUploadedFile>> fileParams = new LinkedHashMap<String, List<RequestUploadedFile>>(); private static final int FIRST_VALUE = 0; private static final Pattern EXTRACT_LASTPART_PATTERN = Pattern.compile(".*[\\\\/]([^\\\\/]+)"); private void convertToMaps(List<FileItem> fileItems) { for (FileItem item : fileItems) { String cts = item.getContentType(); if (!item.isFormField()) { // not simple form field String filename = item.getName(); long size = item.getSize(); // size in bytes // some browsers are sending "files" even if none is selected? if (size == 0 && ((cts == null || cts.isEmpty()) || "application/octet-stream".equalsIgnoreCase(cts)) && (filename == null || filename.isEmpty())) continue; String fileNameLastPart = filename; if (filename != null) { Matcher m = EXTRACT_LASTPART_PATTERN.matcher(filename); if (m.matches()) { fileNameLastPart = m.group(1); } } String mime = cts; if (mime == null) { mime = guessContentType(filename); } RequestUploadedFile file = new RequestUploadedFile(fileNameLastPart, mime, size, item); if (fileParams.get(item.getFieldName()) != null) { addMultivaluedFile(file, item); } else { addSingleValueFile(file, item); } } else { if (regularParams.get(item.getFieldName()) != null) { addMultivaluedItem(item); } else { addSingleValueItem(item); } } } } private void addSingleValueItem(FileItem item) { List<String> list = new ArrayList<String>(); addItemValue(list, item); regularParams.put(item.getFieldName(), list); } private void addMultivaluedItem(FileItem item) { List<String> values = regularParams.get(item.getFieldName()); addItemValue(values, item); } private void addSingleValueFile(RequestUploadedFile file, FileItem item) { List<RequestUploadedFile> list = new ArrayList<RequestUploadedFile>(); list.add(file); fileParams.put(item.getFieldName(), list); } private void addMultivaluedFile(RequestUploadedFile file, FileItem item) { List<RequestUploadedFile> values = fileParams.get(item.getFieldName()); values.add(file); } private void addItemValue(List<String> list, FileItem item) { try { list.add(item.getString(defaultEncoding)); } catch (UnsupportedEncodingException ex) { throw new EncodingException(ex.getMessage(), ex); } } private String guessContentType(String filename) { if (filename != null) { int d = filename.lastIndexOf('.'); if (d >= 0) { String ext = filename.substring(d); if (ext.equalsIgnoreCase("txt")) return "text/plain"; if (ext.equalsIgnoreCase("html")) return "text/html"; if (ext.equalsIgnoreCase("htm")) return "text/htm"; if (ext.equalsIgnoreCase("jpg")) return "image/jpeg"; if (ext.equalsIgnoreCase("jpeg")) return "image/jpeg"; if (ext.equalsIgnoreCase("gif")) return "image/gif"; if (ext.equalsIgnoreCase("png")) return "image/png"; if (ext.equalsIgnoreCase("bmp")) return "image/bmp"; } } return "application/octet-stream"; } }