/**
* Copyright (C) 2010-2017 Structr GmbH
*
* This file is part of Structr <http://structr.org>.
*
* Structr is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Structr 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Structr. If not, see <http://www.gnu.org/licenses/>.
*/
package org.structr.web.servlet;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.api.RetryException;
import org.structr.api.config.Settings;
import org.structr.common.AccessMode;
import org.structr.common.PathHelper;
import org.structr.common.Permission;
import org.structr.common.SecurityContext;
import org.structr.common.ThreadLocalMatcher;
import org.structr.common.error.FrameworkException;
import org.structr.core.GraphObject;
import org.structr.core.app.StructrApp;
import org.structr.core.auth.exception.AuthenticationException;
import org.structr.core.entity.AbstractNode;
import org.structr.core.graph.NodeInterface;
import org.structr.core.graph.Tx;
import org.structr.core.property.PropertyMap;
import org.structr.rest.service.HttpServiceServlet;
import org.structr.rest.service.StructrHttpServiceConfig;
import org.structr.schema.SchemaHelper;
import org.structr.web.auth.UiAuthenticator;
import org.structr.web.common.FileHelper;
import org.structr.web.entity.FileBase;
import org.structr.web.entity.Folder;
//~--- classes ----------------------------------------------------------------
/**
* Simple upload servlet.
*
*
*/
public class UploadServlet extends HttpServlet implements HttpServiceServlet {
private static final Logger logger = LoggerFactory.getLogger(UploadServlet.class.getName());
private static final ThreadLocalMatcher threadLocalUUIDMatcher = new ThreadLocalMatcher("[a-fA-F0-9]{32}");
private static final String REDIRECT_AFTER_UPLOAD_PARAMETER = "redirectOnSuccess";
private static final String APPEND_UUID_ON_REDIRECT = "appendUuidOnRedirect";
private static final int MEGABYTE = 1024 * 1024;
private static final int MEMORY_THRESHOLD = 10 * MEGABYTE; // above 10 MB, files are stored on disk
private static final String MAX_FILE_SIZE = "1000"; // unit is MB
private static final String MAX_REQUEST_SIZE = "1000"; // unit is MB
// non-static fields
private ServletFileUpload uploader = null;
private File filesDir = null;
private final StructrHttpServiceConfig config = new StructrHttpServiceConfig();
public UploadServlet() {
}
//~--- methods --------------------------------------------------------
@Override
public StructrHttpServiceConfig getConfig() {
return config;
}
@Override
public void init() {
try (final Tx tx = StructrApp.getInstance().tx()) {
DiskFileItemFactory fileFactory = new DiskFileItemFactory();
fileFactory.setSizeThreshold(MEMORY_THRESHOLD);
filesDir = new File(Settings.TmpPath.getValue()); // new File(Services.getInstance().getTmpPath());
if (!filesDir.exists()) {
filesDir.mkdir();
}
fileFactory.setRepository(filesDir);
uploader = new ServletFileUpload(fileFactory);
tx.success();
} catch (FrameworkException t) {
logger.warn("", t);
}
}
@Override
public void destroy() {
}
@Override
protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException {
try {
if (!ServletFileUpload.isMultipartContent(request)) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getOutputStream().write("ERROR (400): Request does not contain multipart content.\n".getBytes("UTF-8"));
return;
}
} catch (IOException ioex) {
logger.warn("Unable to send response", ioex);
}
SecurityContext securityContext = null;
String redirectUrl = null;
boolean appendUuidOnRedirect = false;
// isolate request authentication in a transaction
try (final Tx tx = StructrApp.getInstance().tx()) {
try {
securityContext = getConfig().getAuthenticator().initializeAndExamineRequest(request, response);
} catch (AuthenticationException ae) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().write("ERROR (401): Invalid user or password.\n".getBytes("UTF-8"));
return;
}
tx.success();
} catch (FrameworkException fex) {
logger.warn("Unable to examine request", fex);
} catch (IOException ioex) {
logger.warn("Unable to send response", ioex);
}
// something went wrong, but we don't know what...
if (securityContext == null) {
logger.warn("No SecurityContext, aborting.");
return;
}
try {
if (securityContext.getUser(false) == null && !Settings.UploadAllowAnonymous.getValue()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().write("ERROR (401): Anonymous uploads forbidden.\n".getBytes("UTF-8"));
return;
}
// Ensure access mode is frontend
securityContext.setAccessMode(AccessMode.Frontend);
request.setCharacterEncoding("UTF-8");
// Important: Set character encoding before calling response.getWriter() !!, see Servlet Spec 5.4
response.setCharacterEncoding("UTF-8");
// don't continue on redirects
if (response.getStatus() == 302) {
return;
}
final String pathInfo = request.getPathInfo();
String type = null;
if (StringUtils.isNotBlank(pathInfo)) {
type = SchemaHelper.normalizeEntityName(StringUtils.stripStart(pathInfo.trim(), "/"));
}
uploader.setFileSizeMax(MEGABYTE * Settings.UploadMaxFileSize.getValue());
uploader.setSizeMax(MEGABYTE * Settings.UploadMaxRequestSize.getValue());
response.setContentType("text/html");
List<FileItem> fileItemsList = uploader.parseRequest(request);
Iterator<FileItem> fileItemsIterator = fileItemsList.iterator();
final Map<String, Object> params = new HashMap<>();
while (fileItemsIterator.hasNext()) {
final FileItem item = fileItemsIterator.next();
if (item.isFormField()) {
final String fieldName = item.getFieldName();
if (REDIRECT_AFTER_UPLOAD_PARAMETER.equals(fieldName)) {
redirectUrl = item.getString();
} else if (APPEND_UUID_ON_REDIRECT.equals(fieldName)) {
appendUuidOnRedirect = "true".equalsIgnoreCase(item.getString());
} else {
params.put(item.getFieldName(), item.getString());
}
} else {
try {
final String contentType = item.getContentType();
boolean isImage = (contentType != null && contentType.startsWith("image"));
boolean isVideo = (contentType != null && contentType.startsWith("video"));
// Override type from path info
if (params.containsKey(NodeInterface.type.jsonName())) {
type = (String) params.get(NodeInterface.type.jsonName());
}
Class cls = null;
if (type != null) {
cls = SchemaHelper.getEntityClassForRawType(type);
} else {
if (isImage) {
cls = org.structr.dynamic.Image.class;
} else if (isVideo) {
cls = SchemaHelper.getEntityClassForRawType("VideoFile");
if (cls == null) {
logger.warn("Unable to create entity of type VideoFile, class is not defined.");
}
} else {
cls = org.structr.dynamic.File.class;
}
}
final String name = item.getName().replaceAll("\\\\", "/");
FileBase newFile = null;
String uuid = null;
boolean retry = true;
while (retry) {
retry = false;
try (final Tx tx = StructrApp.getInstance().tx()) {
newFile = FileHelper.createFile(securityContext, IOUtils.toByteArray(item.getInputStream()), contentType, cls);
final PropertyMap changedProperties = new PropertyMap();
changedProperties.put(AbstractNode.name, PathHelper.getName(name));
changedProperties.putAll(PropertyMap.inputTypeToJavaType(securityContext, cls, params));
final String defaultUploadFolderConfigValue = Settings.DefaultUploadFolder.getValue();
if (StringUtils.isNotBlank(defaultUploadFolderConfigValue)) {
final Folder defaultUploadFolder = FileHelper.createFolderPath(SecurityContext.getSuperUserInstance(), defaultUploadFolderConfigValue);
// can only happen if the configuration value is invalid or maps to the root folder
if (defaultUploadFolder != null) {
changedProperties.put(FileBase.hasParent, true);
changedProperties.put(FileBase.parent, defaultUploadFolder);
}
}
newFile.setProperties(securityContext, changedProperties);
uuid = newFile.getUuid();
tx.success();
} catch (RetryException rex) {
retry = true;
}
}
// since the transaction can be repeated, we need to make sure that
// only the actual existing file creates a UUID output
if (newFile != null) {
// upload trigger
newFile.notifyUploadCompletion();
// send redirect to allow form-based file upload without JavaScript..
if (StringUtils.isNotBlank(redirectUrl)) {
if (appendUuidOnRedirect) {
response.sendRedirect(redirectUrl + uuid);
} else {
response.sendRedirect(redirectUrl);
}
} else {
// Just write out the uuids of the new files
response.getWriter().write(uuid);
}
}
} catch (IOException ex) {
logger.warn("Could not upload file", ex);
}
}
}
} catch (Throwable t) {
final String content;
if (t instanceof FrameworkException) {
final FrameworkException fex = (FrameworkException) t;
logger.error(fex.toString());
content = errorPage(fex);
} else {
logger.error("Exception while processing upload request", t);
content = errorPage(t);
}
try {
final ServletOutputStream out = response.getOutputStream();
IOUtils.write(content, out);
} catch (IOException ex) {
logger.error("Could not write to response", ex);
}
}
}
@Override
protected void doPut(final HttpServletRequest request, final HttpServletResponse response) throws ServletException {
try (final Tx tx = StructrApp.getInstance().tx(true, false, false)) {
final String uuid = PathHelper.getName(request.getPathInfo());
if (uuid == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getOutputStream().write("URL path doesn't end with UUID.\n".getBytes("UTF-8"));
return;
}
Matcher matcher = threadLocalUUIDMatcher.get();
matcher.reset(uuid);
if (!matcher.matches()) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getOutputStream().write("ERROR (400): URL path doesn't end with UUID.\n".getBytes("UTF-8"));
return;
}
final SecurityContext securityContext = getConfig().getAuthenticator().initializeAndExamineRequest(request, response);
// Ensure access mode is frontend
securityContext.setAccessMode(AccessMode.Frontend);
request.setCharacterEncoding("UTF-8");
// Important: Set character encoding before calling response.getWriter() !!, see Servlet Spec 5.4
response.setCharacterEncoding("UTF-8");
// don't continue on redirects
if (response.getStatus() == 302) {
return;
}
uploader.setFileSizeMax(MEGABYTE * Settings.UploadMaxFileSize.getValue());
uploader.setSizeMax(MEGABYTE * Settings.UploadMaxRequestSize.getValue());
List<FileItem> fileItemsList = uploader.parseRequest(request);
Iterator<FileItem> fileItemsIterator = fileItemsList.iterator();
while (fileItemsIterator.hasNext()) {
final FileItem fileItem = fileItemsIterator.next();
try {
final GraphObject node = StructrApp.getInstance().getNodeById(uuid);
if (node == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.getOutputStream().write("ERROR (404): File not found.\n".getBytes("UTF-8"));
}
if (node instanceof org.structr.web.entity.AbstractFile) {
final org.structr.dynamic.File file = (org.structr.dynamic.File) node;
if (file.isGranted(Permission.write, securityContext)) {
FileHelper.writeToFile(file, fileItem.getInputStream());
file.increaseVersion();
// upload trigger
file.notifyUploadCompletion();
} else {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getOutputStream().write("ERROR (403): Write access forbidden.\n".getBytes("UTF-8"));
}
}
} catch (IOException ex) {
logger.warn("Could not write to file", ex);
}
}
tx.success();
} catch (FrameworkException | IOException | FileUploadException t) {
logger.error("Exception while processing request", t);
UiAuthenticator.writeInternalServerError(response);
}
}
private String errorPage(final Throwable t) {
return "<html><head><title>Error in Upload</title></head><body><h1>Error in Upload</h1><p>" + t.toString() + "</p>\n<!--" + ExceptionUtils.getStackTrace(t) + "--></body></html>";
}
}