package com.psddev.cms.tool.page;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import com.psddev.dari.db.ObjectMethod;
import com.psddev.dari.util.UuidUtils;
import org.apache.commons.fileupload.FileItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.psddev.cms.db.BulkUploadDraft;
import com.psddev.cms.db.ImageTag;
import com.psddev.cms.db.Site;
import com.psddev.cms.db.ToolUi;
import com.psddev.cms.db.Variation;
import com.psddev.cms.tool.PageServlet;
import com.psddev.cms.tool.Search;
import com.psddev.cms.tool.SearchResultSelection;
import com.psddev.cms.tool.ToolPageContext;
import com.psddev.cms.tool.search.MixedSearchResultView;
import com.psddev.dari.db.Database;
import com.psddev.dari.db.DatabaseEnvironment;
import com.psddev.dari.db.ObjectField;
import com.psddev.dari.db.ObjectFieldComparator;
import com.psddev.dari.db.ObjectType;
import com.psddev.dari.db.State;
import com.psddev.dari.util.ErrorUtils;
import com.psddev.dari.util.MultipartRequest;
import com.psddev.dari.util.MultipartRequestFilter;
import com.psddev.dari.util.ObjectUtils;
import com.psddev.dari.util.RoutingFilter;
import com.psddev.dari.util.Settings;
import com.psddev.dari.util.SparseSet;
import com.psddev.dari.util.StorageItem;
import com.psddev.dari.util.StringUtils;
@RoutingFilter.Path(application = "cms", value = "/content/uploadFiles")
@SuppressWarnings("serial")
public class UploadFiles extends PageServlet {
private static final String CONTAINER_ID_PARAMETER = "containerId";
private static final Logger LOGGER = LoggerFactory.getLogger(UploadFiles.class);
@Override
protected String getPermissionId() {
return "area/dashboard";
}
@Override
protected void doService(ToolPageContext page) throws IOException, ServletException {
if (page.requireUser()) {
return;
}
if (page.paramOrDefault(Boolean.class, "writeInputsOnly", false)) {
writeFileInput(page);
} else {
reallyDoService(page);
}
}
private static void reallyDoService(ToolPageContext page) throws IOException, ServletException {
Database database = Database.Static.getDefault();
DatabaseEnvironment environment = database.getEnvironment();
Exception postError = null;
ObjectType selectedType = environment.getTypeById(page.param(UUID.class, "type"));
UUID uploadId = UuidUtils.createSequentialUuid();
String containerId = page.param(String.class, "containerId");
if (page.isFormPost()) {
database.beginWrites();
try {
MultipartRequest request = MultipartRequestFilter.Static.getInstance(page.getRequest());
if (request == null) {
throw new IllegalStateException("Not multipart!");
}
ErrorUtils.errorIfNull(selectedType, "type");
ObjectField previewField = getPreviewField(selectedType);
if (previewField == null) {
throw new IllegalStateException("No file field!");
}
String inputName = ObjectUtils.firstNonBlank(page.param(String.class, "inputName"), (String) page.getRequest().getAttribute("inputName"), "file");
String pathName = inputName + ".path";
List<String> paths = page.params(String.class, pathName);
List<StorageItem> newStorageItems = new ArrayList<>();
FileItem[] files = request.getFileItems("file");
StringBuilder js = new StringBuilder();
Object common = selectedType.createObject(page.param(UUID.class, "typeForm-" + selectedType.getId()));
page.updateUsingParameters(common);
if (!ObjectUtils.isBlank(paths)) {
//get existing storage item
for (String path : paths) {
String defaultStorageSetting = Settings.get(String.class, StorageItem.DEFAULT_STORAGE_SETTING);
String fieldStorageSetting = previewField.as(ToolUi.class).getStorageSetting();
StorageItem newStorageItem = StorageItem.Static.createIn(defaultStorageSetting);
newStorageItem.setPath(path);
if (!StringUtils.isBlank(fieldStorageSetting) && !fieldStorageSetting.equals(defaultStorageSetting)) {
newStorageItem = StorageItem.Static.copy(newStorageItem, fieldStorageSetting);
}
newStorageItems.add(newStorageItem);
}
} else {
if (files != null && files.length > 0) {
for (FileItem file : files) {
// Checks to make sure the file's content type is valid
String groupsPattern = Settings.get(String.class, "cms/tool/fileContentTypeGroups");
Set<String> contentTypeGroups = new SparseSet(ObjectUtils.isBlank(groupsPattern) ? "+/" : groupsPattern);
if (!contentTypeGroups.contains(file.getContentType())) {
page.getErrors().add(new IllegalArgumentException(String.format(
"Invalid content type [%s]. Must match the pattern [%s].",
file.getContentType(), contentTypeGroups)));
continue;
}
// Disallow HTML disguising as other content types per:
// http://www.adambarth.com/papers/2009/barth-caballero-song.pdf
if (!contentTypeGroups.contains("text/html")) {
InputStream input = file.getInputStream();
try {
byte[] buffer = new byte[1024];
String data = new String(buffer, 0, input.read(buffer)).toLowerCase(Locale.ENGLISH);
String ptr = data.trim();
if (ptr.startsWith("<!")
|| ptr.startsWith("<?")
|| data.startsWith("<html")
|| data.startsWith("<script")
|| data.startsWith("<title")
|| data.startsWith("<body")
|| data.startsWith("<head")
|| data.startsWith("<plaintext")
|| data.startsWith("<table")
|| data.startsWith("<img")
|| data.startsWith("<pre")
|| data.startsWith("text/html")
|| data.startsWith("<a")
|| ptr.startsWith("<frameset")
|| ptr.startsWith("<iframe")
|| ptr.startsWith("<link")
|| ptr.startsWith("<base")
|| ptr.startsWith("<style")
|| ptr.startsWith("<div")
|| ptr.startsWith("<p")
|| ptr.startsWith("<font")
|| ptr.startsWith("<applet")
|| ptr.startsWith("<meta")
|| ptr.startsWith("<center")
|| ptr.startsWith("<form")
|| ptr.startsWith("<isindex")
|| ptr.startsWith("<h1")
|| ptr.startsWith("<h2")
|| ptr.startsWith("<h3")
|| ptr.startsWith("<h4")
|| ptr.startsWith("<h5")
|| ptr.startsWith("<h6")
|| ptr.startsWith("<b")
|| ptr.startsWith("<br")) {
page.getErrors().add(new IllegalArgumentException(String.format(
"Can't upload [%s] file disguising as HTML!",
file.getContentType())));
continue;
}
} finally {
input.close();
}
}
if (file.getSize() == 0) {
continue;
}
String fileName = StringUtils.getFileName(file.getName());
String path = StorageItemField.createStorageItemPath(null, fileName);
Map<String, List<String>> httpHeaders = new LinkedHashMap<String, List<String>>();
httpHeaders.put("Cache-Control", Collections.singletonList("public, max-age=31536000"));
httpHeaders.put("Content-Length", Collections.singletonList(String.valueOf(file.getSize())));
httpHeaders.put("Content-Type", Collections.singletonList(file.getContentType()));
String storageSetting = previewField.as(ToolUi.class).getStorageSetting();
StorageItem item = StorageItem.Static.createIn(storageSetting != null ? Settings.getOrDefault(String.class, storageSetting, null) : null);
String contentType = file.getContentType();
item.setPath(path);
item.setContentType(contentType);
item.getMetadata().put("http.headers", httpHeaders);
item.getMetadata().put("originalFilename", fileName);
item.setData(file.getInputStream());
newStorageItems.add(item);
}
}
}
List<UUID> newObjectIds = new ArrayList<>();
if (!ObjectUtils.isBlank(newStorageItems)) {
for (StorageItem item : newStorageItems) {
if (item == null) {
continue;
}
item.save();
StorageItemField.tryExtractMetadata(item, item.getMetadata(), Optional.empty());
Object object = selectedType.createObject(null);
State state = State.getInstance(object);
state.setValues(State.getInstance(common));
Site site = page.getSite();
if (site != null
&& site.getDefaultVariation() != null) {
state.as(Variation.Data.class).setInitialVariation(site.getDefaultVariation());
}
state.put(previewField.getInternalName(), item);
state.as(BulkUploadDraft.class).setUploadId(uploadId);
state.as(BulkUploadDraft.class).setContainerId(containerId);
page.publish(state);
newObjectIds.add(state.getId());
js.append("$addButton.repeatable('add', function() {");
js.append("var $added = $(this);");
js.append("$input = $added.find(':input.objectId').eq(0);");
js.append("$input.attr('data-label', '").append(StringUtils.escapeJavaScript(state.getLabel())).append("');");
js.append("$input.attr('data-label-html', '").append(StringUtils.escapeJavaScript(page.createObjectLabelHtml(state))).append("');");
js.append("$input.attr('data-preview', '").append(StringUtils.escapeJavaScript(page.getPreviewThumbnailUrl(object))).append("');");
js.append("$input.val('").append(StringUtils.escapeJavaScript(state.getId().toString())).append("');");
js.append("$input.change();");
js.append("});");
}
database.commitWrites();
}
if (page.getErrors().isEmpty()) {
if (Context.FIELD.equals(page.param(Context.class, "context"))) {
page.writeStart("div", "id", page.createId()).writeEnd();
page.writeStart("script", "type", "text/javascript");
page.write("if (typeof jQuery !== 'undefined') (function($, win, undef) {");
page.write("var $page = $('#" + page.getId() + "'),");
page.write("$init = $page.popup('source').repeatable('closestInit'),");
page.write("$addButton = $init.find('.addButton').eq(0),");
page.write("$input;");
page.write("if ($addButton.length > 0) {");
page.write(js.toString());
page.write("$page.popup('close');");
page.write("}");
page.write("})(jQuery, window);");
page.writeEnd();
} else {
SearchResultSelection selection = page.getUser().resetCurrentSelection();
newObjectIds.forEach(selection::addItem);
database.commitWrites();
Search search = new Search();
search.setAdditionalPredicate(selection.createItemsQuery().getPredicate().toString());
search.setLimit(10);
page.writeStart("script", "type", "text/javascript");
page.write("if (typeof jQuery !== 'undefined') (function($, win, undef) {");
page.write("window.location = '");
page.write(page.cmsUrl("/searchAdvancedFull",
"search", ObjectUtils.toJson(search.getState().getSimpleValues()),
"view", MixedSearchResultView.class.getCanonicalName()));
page.write("';");
page.write("})(jQuery, window);");
page.writeEnd();
}
return;
}
} catch (Exception error) {
postError = error;
} finally {
database.endWrites();
}
}
Set<ObjectType> typesSet = new HashSet<ObjectType>();
for (UUID typeId : page.params(UUID.class, "typeId")) {
ObjectType type = environment.getTypeById(typeId);
if (type != null) {
for (ObjectType t : type.as(ToolUi.class).findDisplayTypes()) {
for (ObjectField field : t.getFields()) {
if (ObjectField.FILE_TYPE.equals(field.getInternalItemType())) {
typesSet.add(t);
break;
}
}
}
}
}
List<ObjectType> types = new ArrayList<ObjectType>(typesSet);
Collections.sort(types, new ObjectFieldComparator("name", false));
page.writeStart("h1");
page.writeHtml(page.localize(UploadFiles.class, "title"));
page.writeEnd();
page.writeStart("form",
"method", "post",
"enctype", "multipart/form-data",
"action", page.url(null));
page.writeElement("input",
"type", "hidden",
"name", CONTAINER_ID_PARAMETER,
"value", containerId);
for (ObjectType type : types) {
page.writeElement("input", "type", "hidden", "name", "typeId", "value", type.getId());
}
if (postError != null) {
page.writeStart("div", "class", "message message-error");
page.writeObject(postError);
page.writeEnd();
} else if (!page.getErrors().isEmpty()) {
page.writeStart("div", "class", "message message-error");
for (Throwable error : page.getErrors()) {
page.writeHtml(error.getMessage());
}
page.writeEnd();
}
page.writeStart("div", "class", "inputContainer bulk-upload-files");
page.writeStart("div", "class", "inputLabel");
page.writeStart("label", "for", page.createId());
page.writeHtml(page.localize(UploadFiles.class, "label.files"));
page.writeEnd();
page.writeEnd();
page.writeStart("div", "class", "inputSmall");
page.writeElement("input",
"id", page.getId(),
"type", "file",
"name", "file",
"multiple", "multiple");
page.writeEnd();
page.writeEnd();
page.writeStart("div", "class", "inputContainer");
page.writeStart("div", "class", "inputLabel");
page.writeStart("label", "for", page.createId());
page.writeHtml(page.localize(UploadFiles.class, "label.type"));
page.writeEnd();
page.writeEnd();
page.writeStart("div", "class", "inputSmall");
page.writeStart("select",
"class", "toggleable",
"data-root", "form",
"id", page.getId(),
"name", "type");
for (ObjectType type : types) {
page.writeStart("option",
"data-hide", ".typeForm",
"data-show", ".typeForm-" + type.getId(),
"selected", type.equals(selectedType) ? "selected" : null,
"value", type.getId());
page.writeHtml(type.getDisplayName());
page.writeEnd();
}
page.writeEnd();
page.writeEnd();
page.writeStart("div", "class", "inputLarge");
for (ObjectType type : types) {
String name = "typeForm-" + type.getId();
Object common = type.createObject(null);
page.writeStart("div", "class", "typeForm " + name);
page.writeElement("input",
"type", "hidden",
"name", name,
"value", State.getInstance(common).getId());
ObjectField previewField = getPreviewField(type);
List<String> excludedFields = null;
if (previewField != null) {
excludedFields = Arrays.asList(previewField.getInternalName());
}
page.writeSomeFormFields(common, false, null, excludedFields);
page.writeEnd();
}
page.writeEnd();
page.writeEnd();
page.writeStart("input", "type", "hidden", "name", "context", "value", page.param(Context.class, "context"));
page.writeStart("div", "class", "buttons");
page.writeStart("button", "name", "action-upload");
page.writeHtml(page.localize(UploadFiles.class, "action.upload"));
page.writeEnd();
page.writeEnd();
page.writeEnd();
}
public static void writeFileInput(ToolPageContext page) throws IOException, ServletException {
String inputName = ObjectUtils.firstNonBlank(page.param(String.class, "inputName"), (String) page.getRequest().getAttribute("inputName"), "file");
String pathName = inputName + ".path";
String path = page.param(String.class, pathName);
if (ObjectUtils.isBlank(path)) {
return;
}
HttpServletResponse response = page.getResponse();
StorageItem newStorageItem = StorageItem.Static.createIn(Settings.get(String.class, StorageItem.DEFAULT_STORAGE_SETTING));
newStorageItem.setPath(path);
ImageTag.Builder imageTagBuilder = new ImageTag.Builder(newStorageItem);
imageTagBuilder.setWidth(170);
response.setContentType("text/html");
page.writeStart("div");
page.write(imageTagBuilder.toHtml());
page.writeTag("input", "type", "hidden", "name", pathName, "value", page.h(path));
page.writeEnd();
}
private static ObjectField getPreviewField(ObjectType type) {
ObjectField previewField = type.getField(type.getPreviewField());
if (previewField instanceof ObjectMethod) {
previewField = null;
}
if (previewField == null) {
for (ObjectField field : type.getFields()) {
if (ObjectField.FILE_TYPE.equals(field.getInternalItemType())) {
previewField = field;
break;
}
}
}
return previewField;
}
public enum Context {
FIELD,
GLOBAL
}
}