/*
* 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.web;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import javax.script.ScriptContext;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.localization.LocaleUtils;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiAttachment;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.plugin.fileupload.FileUploadPlugin;
/**
* Action that handles uploading document attachments. It saves all the uploaded files whose fieldname start with
* {@code filepath}.
*
* @version $Id: 45e18698b149e5c56db2a4773d1cc0455912366e $
*/
public class UploadAction extends XWikiAction
{
/** Logging helper object. */
private static final Logger LOGGER = LoggerFactory.getLogger(UploadAction.class);
/** The prefix of the accepted file input field name. */
private static final String FILE_FIELD_NAME = "filepath";
/** The prefix of the corresponding filename input field name. */
private static final String FILENAME_FIELD_NAME = "filename";
@Override
public boolean action(XWikiContext context) throws XWikiException
{
XWikiResponse response = context.getResponse();
Object exception = context.get("exception");
boolean ajax = ((Boolean) context.get("ajax")).booleanValue();
// check Exception File upload is large
if (exception != null) {
if (exception instanceof XWikiException) {
XWikiException exp = (XWikiException) exception;
if (exp.getCode() == XWikiException.ERROR_XWIKI_APP_FILE_EXCEPTION_MAXSIZE) {
response.setStatus(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
getCurrentScriptContext().setAttribute("message", "core.action.upload.failure.maxSize",
ScriptContext.ENGINE_SCOPE);
context.put("message", "fileuploadislarge");
return true;
}
}
}
// CSRF prevention
if (!csrfTokenCheck(context)) {
return false;
}
// We need to clone the document before we modify it because the cached storage gives the same instance to other
// requests (until the cache is invalidated).
XWikiDocument doc = context.getDoc().clone();
// It is possible to submit an attachment to a new document (the WYSIWYG content editor does it for instance).
// Let's make sure the new document is created with the right (default) language.
if (doc.isNew()) {
doc.setLocale(Locale.ROOT);
if (doc.getDefaultLocale() == Locale.ROOT) {
doc.setDefaultLocale(
LocaleUtils.toLocale(context.getWiki().getLanguagePreference(context), Locale.ROOT));
}
}
// The document is saved for each attachment in the group.
FileUploadPlugin fileupload = (FileUploadPlugin) context.get("fileuploadplugin");
if (fileupload == null) {
getCurrentScriptContext().setAttribute("message", "core.action.upload.failure.noFiles",
ScriptContext.ENGINE_SCOPE);
return true;
}
Map<String, String> fileNames = new LinkedHashMap<String, String>();
List<String> wrongFileNames = new ArrayList<String>();
Map<String, String> failedFiles = new LinkedHashMap<String, String>();
for (String fieldName : fileupload.getFileItemNames(context)) {
try {
if (fieldName.startsWith(FILE_FIELD_NAME)) {
String fileName = getFileName(fieldName, fileupload, context);
if (fileName != null) {
fileNames.put(fileName, fieldName);
}
}
} catch (Exception ex) {
wrongFileNames.add(fileupload.getFileName(fieldName, context));
}
}
for (Entry<String, String> file : fileNames.entrySet()) {
try {
uploadAttachment(file.getValue(), file.getKey(), fileupload, doc, context);
} catch (Exception ex) {
LOGGER.warn("Saving uploaded file failed", ex);
failedFiles.put(file.getKey(), ExceptionUtils.getRootCauseMessage(ex));
}
}
LOGGER.debug("Found files to upload: " + fileNames);
LOGGER.debug("Failed attachments: " + failedFiles);
LOGGER.debug("Wrong attachment names: " + wrongFileNames);
if (ajax) {
try {
response.getOutputStream().println("ok");
} catch (IOException ex) {
LOGGER.error("Unhandled exception writing output:", ex);
}
return false;
}
// Forward to the attachment page
if (failedFiles.size() > 0 || !wrongFileNames.isEmpty()) {
getCurrentScriptContext().setAttribute("message", "core.action.upload.failure", ScriptContext.ENGINE_SCOPE);
getCurrentScriptContext().setAttribute("failedFiles", failedFiles, ScriptContext.ENGINE_SCOPE);
getCurrentScriptContext().setAttribute("wrongFileNames", wrongFileNames, ScriptContext.ENGINE_SCOPE);
return true;
}
String redirect = fileupload.getFileItemAsString("xredirect", context);
if (StringUtils.isEmpty(redirect)) {
redirect = context.getDoc().getURL("attach", true, context);
}
sendRedirect(response, redirect);
return false;
}
/**
* Attach a file to the current document.
*
* @param fieldName the target file field
* @param filename
* @param fileupload the {@link FileUploadPlugin} holding the form data
* @param doc the target document
* @param context the current request context
* @return {@code true} if the file was successfully attached, {@code false} otherwise.
* @throws XWikiException if the form data cannot be accessed, or if the database operation failed
*/
public boolean uploadAttachment(String fieldName, String filename, FileUploadPlugin fileupload, XWikiDocument doc,
XWikiContext context) throws XWikiException
{
XWikiResponse response = context.getResponse();
String username = context.getUser();
XWikiAttachment attachment;
try {
InputStream contentInputStream = fileupload.getFileItemInputStream(fieldName, context);
attachment = doc.addAttachment(filename, contentInputStream, context);
} catch (IOException e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_APP,
XWikiException.ERROR_XWIKI_APP_UPLOAD_FILE_EXCEPTION, "Exception while reading uploaded parsed file",
e);
}
// Set the document author
doc.setAuthor(username);
if (doc.isNew()) {
doc.setCreator(username);
}
// Calculate and store mime type
attachment.resetMimeType(context);
// Adding a comment with the name and revision of the added attachment.
String comment;
String nextRev = attachment.getNextVersion();
ArrayList<String> params = new ArrayList<String>();
params.add(filename);
params.add(nextRev);
if (attachment.isImage(context)) {
comment = localizePlainOrKey("core.comment.uploadImageComment", params.toArray());
} else {
comment = localizePlainOrKey("core.comment.uploadAttachmentComment", params.toArray());
}
// Save the document.
try {
context.getWiki().saveDocument(doc, comment, context);
} catch (XWikiException e) {
// check Exception is ERROR_XWIKI_APP_JAVA_HEAP_SPACE when saving Attachment
if (e.getCode() == XWikiException.ERROR_XWIKI_APP_JAVA_HEAP_SPACE) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
context.put("message", "javaheapspace");
return true;
}
throw e;
}
return false;
}
/**
* Extract the corresponding attachment name for a given file field. It can either be specified in a separate form
* input field, or it is extracted from the original filename.
*
* @param fieldName the target file field
* @param fileupload the {@link FileUploadPlugin} holding the form data
* @param context the current request context
* @return a valid attachment name
* @throws XWikiException if the form data cannot be accessed, or if the specified filename is invalid
*/
protected String getFileName(String fieldName, FileUploadPlugin fileupload, XWikiContext context)
throws XWikiException
{
String filenameField = FILENAME_FIELD_NAME + fieldName.substring(FILE_FIELD_NAME.length());
String filename = null;
// Try to use the name provided by the user
filename = fileupload.getFileItemAsString(filenameField, context);
if (!StringUtils.isBlank(filename)) {
// TODO These should be supported, the URL should just contain escapes.
if (filename.indexOf("/") != -1 || filename.indexOf("\\") != -1 || filename.indexOf(";") != -1) {
throw new XWikiException(XWikiException.MODULE_XWIKI_APP, XWikiException.ERROR_XWIKI_APP_INVALID_CHARS,
"Invalid filename: " + filename);
}
}
if (StringUtils.isBlank(filename)) {
// Try to get the actual filename on the client
String fname = fileupload.getFileName(fieldName, context);
if (StringUtils.indexOf(fname, "/") >= 0) {
fname = StringUtils.substringAfterLast(fname, "/");
}
if (StringUtils.indexOf(fname, "\\") >= 0) {
fname = StringUtils.substringAfterLast(fname, "\\");
}
filename = fname;
}
// Sometimes spaces are replaced with '+' by the browser.
filename = filename.replaceAll("\\+", " ");
if (StringUtils.isBlank(filename)) {
// The file field was left empty, ignore this
return null;
}
return filename;
}
@Override
public String render(XWikiContext context) throws XWikiException
{
boolean ajax = ((Boolean) context.get("ajax")).booleanValue();
if (ajax) {
try {
context.getResponse().getOutputStream()
.println("error: " + localizePlainOrKey((String) context.get("message")));
} catch (IOException ex) {
LOGGER.error("Unhandled exception writing output:", ex);
}
return null;
}
getCurrentScriptContext().setAttribute("viewer", "uploadfailure", ScriptContext.ENGINE_SCOPE);
return "view";
}
}