/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.xpn.xwiki.plugin.fileupload;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUpload;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.RequestContext;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.servlet.ServletRequestContext;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.api.Api;
import com.xpn.xwiki.plugin.XWikiDefaultPlugin;
import com.xpn.xwiki.plugin.XWikiPluginInterface;
/**
* Plugin that offers access to uploaded files. The uploaded files are automatically parsed and preserved as a list of
* {@link FileItem}s.
*
* @version $Id: a75f2176cdfcbd18e9d5703f162869156a680f15 $
*/
public class FileUploadPlugin extends XWikiDefaultPlugin
{
/**
* The name of the plugin; the key that can be used to retrieve this plugin from the context.
*
* @see XWikiPluginInterface#getName()
*/
public static final String PLUGIN_NAME = "fileupload";
/**
* The context name of the uploaded file list. It can be used to retrieve the list of uploaded files from the
* context.
*/
public static final String FILE_LIST_KEY = "fileuploadlist";
/**
* The name of the parameter that can be set in the global XWiki preferences to override the default maximum file
* size.
*/
public static final String UPLOAD_MAXSIZE_PARAMETER = "upload_maxsize";
/**
* The name of the parameter that can be set in the global XWiki preferences to override the default size threshold
* for on-disk storage.
*/
public static final String UPLOAD_SIZETHRESHOLD_PARAMETER = "upload_sizethreshold";
/**
* Log object to log messages in this class.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(FileUploadPlugin.class);
/**
* The default maximum size for uploaded documents. This limit can be changed using the <tt>upload_maxsize</tt>
* XWiki preference.
*/
private static final long UPLOAD_DEFAULT_MAXSIZE = 33554432L;
/**
* The default maximum size for in-memory stored uploaded documents. If a file is larger than this limit, it will be
* stored on disk until the current request finishes. This limit can be changed using the
* <tt>upload_sizethreshold</tt> XWiki preference.
*/
private static final long UPLOAD_DEFAULT_SIZETHRESHOLD = 100000L;
/**
* @param name the plugin name
* @param className the plugin classname (used in logs for example)
* @param context the XWiki Context
* @see XWikiDefaultPlugin#XWikiDefaultPlugin(String,String,com.xpn.xwiki.XWikiContext)
*/
public FileUploadPlugin(String name, String className, XWikiContext context)
{
super(name, className, context);
}
@Override
public String getName()
{
return PLUGIN_NAME;
}
@Override
public void init(XWikiContext context)
{
super.init(context);
}
@Override
public void virtualInit(XWikiContext context)
{
super.virtualInit(context);
}
@Override
public Api getPluginApi(XWikiPluginInterface plugin, XWikiContext context)
{
return new FileUploadPluginApi((FileUploadPlugin) plugin, context);
}
/**
* {@inheritDoc}
* <p>
* Make sure we don't leave files in temp directories and in memory.
* </p>
*/
@Override
public void endRendering(XWikiContext context)
{
// we used to call cleanFileList here but we should not anymore as endRendering is called to
// many times and empties the file upload list. This is handled by XWikiAction and
// XWikiPortlet which clean up lists in a finally block
}
/**
* Deletes all temporary files of the upload.
*
* @param context Context of the request.
* @see FileUploadPluginApi#cleanFileList()
*/
public void cleanFileList(XWikiContext context)
{
LOGGER.debug("Cleaning uploaded files");
List<FileItem> fileuploadlist = getFileItems(context);
if (fileuploadlist != null) {
for (FileItem item : fileuploadlist) {
try {
item.delete();
} catch (Exception ex) {
LOGGER.warn("Exception cleaning uploaded files", ex);
}
}
context.remove(FILE_LIST_KEY);
}
}
/**
* Loads the list of uploaded files in the context if there are any uploaded files.
*
* @param context Context of the request.
* @throws XWikiException An XWikiException is thrown if the request could not be parsed.
* @see FileUploadPluginApi#loadFileList()
*/
public void loadFileList(XWikiContext context) throws XWikiException
{
XWiki xwiki = context.getWiki();
loadFileList(
xwiki.getSpacePreferenceAsLong(UPLOAD_MAXSIZE_PARAMETER, UPLOAD_DEFAULT_MAXSIZE, context),
(int) xwiki.getSpacePreferenceAsLong(UPLOAD_SIZETHRESHOLD_PARAMETER, UPLOAD_DEFAULT_SIZETHRESHOLD, context),
xwiki.Param("xwiki.upload.tempdir"), context);
}
/**
* Loads the list of uploaded files in the context if there are any uploaded files.
*
* @param uploadMaxSize Maximum size of the uploaded files.
* @param uploadSizeThreashold Threashold over which the file data should be stored on disk, and not in memory.
* @param tempdir Temporary directory to store the uploaded files that are not kept in memory.
* @param context Context of the request.
* @throws XWikiException if the request could not be parsed, or the maximum file size was reached.
* @see FileUploadPluginApi#loadFileList(long, int, String)
*/
public void loadFileList(long uploadMaxSize, int uploadSizeThreashold, String tempdir, XWikiContext context)
throws XWikiException
{
LOGGER.debug("Loading uploaded files");
// If we already have a file list then loadFileList was already called
// Continuing would empty the list.. We need to stop.
if (context.get(FILE_LIST_KEY) != null) {
LOGGER.debug("Called loadFileList twice");
return;
}
// Get the FileUpload Data
// Make sure the factory only ever creates file items which will be deleted when the jvm is stopped.
DiskFileItemFactory factory = new DiskFileItemFactory()
{
public FileItem createItem(String fieldName, String contentType, boolean isFormField, String fileName)
{
try {
final DiskFileItem item =
(DiskFileItem) super.createItem(fieldName, contentType, isFormField, fileName);
// Needed to make sure the File object is created.
item.getOutputStream();
item.getStoreLocation().deleteOnExit();
return item;
} catch (IOException e) {
String path = System.getProperty("java.io.tmpdir");
if (super.getRepository() != null) {
path = super.getRepository().getPath();
}
throw new RuntimeException("Unable to create a temporary file for saving the attachment. "
+ "Do you have write access on " + path + "?");
}
}
};
factory.setSizeThreshold(uploadSizeThreashold);
if (tempdir != null) {
File tempdirFile = new File(tempdir);
if (tempdirFile.mkdirs() && tempdirFile.canWrite()) {
factory.setRepository(tempdirFile);
}
}
// TODO: Does this work in portlet mode, or we must use PortletFileUpload?
FileUpload fileupload = new ServletFileUpload(factory);
RequestContext reqContext = new ServletRequestContext(context.getRequest().getHttpServletRequest());
fileupload.setSizeMax(uploadMaxSize);
// context.put("fileupload", fileupload);
try {
@SuppressWarnings("unchecked")
List<FileItem> list = fileupload.parseRequest(reqContext);
if (list.size() > 0) {
LOGGER.info("Loaded " + list.size() + " uploaded files");
}
// We store the file list in the context
context.put(FILE_LIST_KEY, list);
} catch (FileUploadBase.SizeLimitExceededException e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_APP,
XWikiException.ERROR_XWIKI_APP_FILE_EXCEPTION_MAXSIZE, "Exception uploaded file");
} catch (Exception e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_APP,
XWikiException.ERROR_XWIKI_APP_UPLOAD_PARSE_EXCEPTION, "Exception while parsing uploaded file", e);
}
}
/**
* Allows to retrieve the current list of uploaded files, as a list of {@link FileItem}s.
* {@link #loadFileList(XWikiContext)} needs to be called beforehand
*
* @param context Context of the request.
* @return A list of FileItem elements.
* @see FileUploadPluginApi#getFileItems()
*/
public List<FileItem> getFileItems(XWikiContext context)
{
return (List<FileItem>) context.get(FILE_LIST_KEY);
}
/**
* Allows to retrieve the contents of an uploaded file as a sequence of bytes. {@link #loadFileList(XWikiContext)}
* needs to be called beforehand.
*
* @param formfieldName The name of the form field.
* @param context Context of the request.
* @return The contents of the file.
* @throws XWikiException if the data could not be read.
* @see FileUploadPluginApi#getFileItemData(String)
*/
public byte[] getFileItemData(String formfieldName, XWikiContext context) throws XWikiException
{
int size = getFileItemSize(formfieldName, context);
if (size == 0) {
return null;
}
byte[] data = new byte[size];
try {
InputStream fileis = getFileItemInputStream(formfieldName, context);
if (fileis != null) {
fileis.read(data);
fileis.close();
}
} catch (java.lang.OutOfMemoryError e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_APP, XWikiException.ERROR_XWIKI_APP_JAVA_HEAP_SPACE,
"Java Heap Space, Out of memory exception", e);
} catch (IOException ie) {
throw new XWikiException(XWikiException.MODULE_XWIKI_APP,
XWikiException.ERROR_XWIKI_APP_UPLOAD_FILE_EXCEPTION, "Exception while reading uploaded parsed file",
ie);
}
return data;
}
/**
* Allows to retrieve the contents of an uploaded file as a stream. {@link #loadFileList(XWikiContext)} needs to be
* called beforehand.
*
* @param formfieldName The name of the form field.
* @param context Context of the request.
* @return a InputStream on the file content
* @throws IOException if I/O problem occurs
* @since 2.3M2
*/
public InputStream getFileItemInputStream(String formfieldName, XWikiContext context) throws IOException
{
FileItem fileitem = getFile(formfieldName, context);
if (fileitem == null) {
return null;
}
return fileitem.getInputStream();
}
/**
* Retrieve the size of a file content in byte. {@link #loadFileList(XWikiContext)} needs to be called beforehand.
*
* @param formfieldName The name of the form field.
* @param context Context of the request.
* @return the size of the file in byte
* @since 2.3M2
*/
public int getFileItemSize(String formfieldName, XWikiContext context)
{
FileItem fileitem = getFile(formfieldName, context);
if (fileitem == null) {
return 0;
}
return ((int) fileitem.getSize());
}
/**
* Allows to retrieve the contents of an uploaded file as a string. {@link #loadFileList(XWikiContext)} needs to be
* called beforehand.
*
* @param formfieldName The name of the form field.
* @param context Context of the request.
* @return The contents of the file.
* @throws XWikiException if the data could not be read.
* @see FileUploadPluginApi#getFileItemAsString(String)
*/
public String getFileItemAsString(String formfieldName, XWikiContext context) throws XWikiException
{
byte[] data = getFileItemData(formfieldName, context);
if (data == null) {
return null;
}
return new String(data);
}
/**
* Allows to retrieve the contents of an uploaded file as a string. {@link #loadFileList(XWikiContext)} needs to be
* called beforehand.
*
* @deprecated not well named, use {@link #getFileItemAsString(String, com.xpn.xwiki.XWikiContext)}
* @param formfieldName The name of the form field.
* @param context Context of the request.
* @return The contents of the file.
* @throws XWikiException Exception is thrown if the data could not be read.
* @see FileUploadPluginApi#getFileItemAsString(String)
*/
@Deprecated
public String getFileItem(String formfieldName, XWikiContext context) throws XWikiException
{
return getFileItemAsString(formfieldName, context);
}
/**
* Retrieves the list of FileItem names. {@link #loadFileList(XWikiContext)} needs to be called beforehand.
*
* @param context Context of the request
* @return List of strings of the item names
*/
public List<String> getFileItemNames(XWikiContext context)
{
List<String> itemnames = new ArrayList<String>();
List<FileItem> fileuploadlist = getFileItems(context);
if (fileuploadlist == null) {
return itemnames;
}
for (FileItem item : fileuploadlist) {
itemnames.add(item.getFieldName());
}
return itemnames;
}
/**
* Get the name of the file uploaded for a form field.
*
* @param formfieldName The name of the form field.
* @param context Context of the request.
* @return The file name, or <tt>null</tt> if no file was uploaded for that form field.
*/
public String getFileName(String formfieldName, XWikiContext context)
{
FileItem fileitem = getFile(formfieldName, context);
// We need to strip the file path. See http://commons.apache.org/fileupload/faq.html#whole-path-from-IE
return (fileitem == null) ? null : FilenameUtils.getName(fileitem.getName());
}
/**
* Return the FileItem corresponding to the file uploaded for a form field.
*
* @param formfieldName The name of the form field.
* @param context Context of the request.
* @return The corresponding FileItem, or <tt>null</tt> if no file was uploaded for that form field.
*/
public FileItem getFile(String formfieldName, XWikiContext context)
{
LOGGER.debug("Searching file uploaded for field " + formfieldName);
List<FileItem> fileuploadlist = getFileItems(context);
if (fileuploadlist == null) {
return null;
}
FileItem fileitem = null;
for (FileItem item : fileuploadlist) {
if (formfieldName.equals(item.getFieldName())) {
fileitem = item;
LOGGER.debug("Found uploaded file!");
break;
}
}
return fileitem;
}
}