package com.psddev.cms.tool.page; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; 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.TreeMap; import java.util.UUID; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import org.apache.commons.fileupload.FileItem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.psddev.cms.db.ImageCrop; import com.psddev.cms.db.ImageTag; import com.psddev.cms.db.ImageTextOverlay; import com.psddev.cms.db.StandardImageSize; import com.psddev.cms.db.ToolUi; import com.psddev.cms.tool.FileContentType; import com.psddev.cms.tool.PageServlet; import com.psddev.cms.tool.ToolPageContext; import com.psddev.dari.db.ObjectField; import com.psddev.dari.db.ReferentialText; import com.psddev.dari.db.State; import com.psddev.dari.util.AggregateException; import com.psddev.dari.util.ClassFinder; import com.psddev.dari.util.ImageMetadataMap; import com.psddev.dari.util.IoUtils; import com.psddev.dari.util.JspUtils; 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; import com.psddev.dari.util.TypeReference; /** * @deprecated * * Legacy StorageItemField implementation. Will be replaced * by {@link com.psddev.cms.tool.page.content.field.FileField}. * */ @Deprecated @RoutingFilter.Path(application = "cms", value = "storageItemField") public class StorageItemField extends PageServlet { private static final Logger LOGGER = LoggerFactory.getLogger(StorageItemField.class); public static void processField(ToolPageContext page) throws IOException, ServletException { HttpServletRequest request = page.getRequest(); State state = State.getInstance(request.getAttribute("object")); ObjectField field = (ObjectField) request.getAttribute("field"); String inputName = ObjectUtils.firstNonBlank((String) request.getAttribute("inputName"), page.param(String.class, "inputName")); String actionName = inputName + ".action"; String fileName = inputName + ".file"; String urlName = inputName + ".url"; String dropboxName = inputName + ".dropbox"; String cropsName = inputName + ".crops."; String brightnessName = inputName + ".brightness"; String contrastName = inputName + ".contrast"; String flipHName = inputName + ".flipH"; String flipVName = inputName + ".flipV"; String grayscaleName = inputName + ".grayscale"; String invertName = inputName + ".invert"; String rotateName = inputName + ".rotate"; String sepiaName = inputName + ".sepia"; String sharpenName = inputName + ".sharpen"; String blurName = inputName + ".blur"; String focusXName = inputName + ".focusX"; String focusYName = inputName + ".focusY"; String fieldName = field != null ? field.getInternalName() : page.param(String.class, "fieldName"); StorageItem fieldValue = null; if (state != null) { fieldValue = (StorageItem) state.getValue(fieldName); } String metadataFieldName = fieldName + ".metadata"; String cropsFieldName = fieldName + ".crops"; String action = page.param(actionName); Map<String, Object> fieldValueMetadata = null; boolean isFormPost = request.getAttribute("isFormPost") != null ? (Boolean) request.getAttribute("isFormPost") : false; if (fieldValue != null && (!isFormPost || "keep".equals(action))) { fieldValueMetadata = fieldValue.getMetadata(); } if (fieldValueMetadata == null) { fieldValueMetadata = new LinkedHashMap<String, Object>(); } Map<String, Object> edits = (Map<String, Object>) fieldValueMetadata.get("cms.edits"); if (edits == null) { edits = new HashMap<String, Object>(); fieldValueMetadata.put("cms.edits", edits); } double brightness = ObjectUtils.to(double.class, edits.get("brightness")); double contrast = ObjectUtils.to(double.class, edits.get("contrast")); boolean flipH = ObjectUtils.to(boolean.class, edits.get("flipH")); boolean flipV = ObjectUtils.to(boolean.class, edits.get("flipV")); boolean grayscale = ObjectUtils.to(boolean.class, edits.get("grayscale")); boolean invert = ObjectUtils.to(boolean.class, edits.get("invert")); int rotate = ObjectUtils.to(int.class, edits.get("rotate")); boolean sepia = ObjectUtils.to(boolean.class, edits.get("sepia")); int sharpen = ObjectUtils.to(int.class, edits.get("sharpen")); List<String> blurs = new ArrayList<String>(); if (!ObjectUtils.isBlank(edits.get("blur"))) { Object blur = edits.get("blur"); if (blur instanceof String && ObjectUtils.to(String.class, blur).matches("(\\d+x){3}\\d+")) { blurs.add(ObjectUtils.to(String.class, blur)); } else if (blur instanceof List) { for (Object blurItem : (List) blur) { String blurValue = ObjectUtils.to(String.class, blurItem); if (blurValue.matches("(\\d+x){3}\\d+")) { blurs.add(blurValue); } } } } Map<String, ImageCrop> crops = ImageCrop.createCrops(fieldValueMetadata.get("cms.crops")); if (crops == null) { // for backward compatibility crops = ImageCrop.createCrops(state.getValue(cropsFieldName)); } crops = new TreeMap<String, ImageCrop>(crops); Map<String, StandardImageSize> sizes = new HashMap<String, StandardImageSize>(); for (StandardImageSize size : StandardImageSize.findAll()) { String sizeId = size.getId().toString(); sizes.put(sizeId, size); if (crops.get(sizeId) == null) { crops.put(sizeId, new ImageCrop()); } } Map<String, Double> focusPoint = ObjectUtils.to(new TypeReference<Map<String, Double>>() { }, fieldValueMetadata.get("cms.focus")); if (focusPoint == null) { focusPoint = new HashMap<String, Double>(); } Class hotSpotClass = ObjectUtils.getClassByName(ImageTag.HOTSPOT_CLASS); boolean projectUsingBrightSpotImage = hotSpotClass != null && !ObjectUtils.isBlank(ClassFinder.Static.findClasses(hotSpotClass)); if (isFormPost) { File file = null; try { StorageItem newItem = null; brightness = page.param(double.class, brightnessName); contrast = page.param(double.class, contrastName); flipH = page.param(boolean.class, flipHName); flipV = page.param(boolean.class, flipVName); grayscale = page.param(boolean.class, grayscaleName); invert = page.param(boolean.class, invertName); rotate = page.param(int.class, rotateName); sepia = page.param(boolean.class, sepiaName); sharpen = page.param(int.class, sharpenName); Double focusX = page.paramOrDefault(Double.class, focusXName, null); Double focusY = page.paramOrDefault(Double.class, focusYName, null); edits = new HashMap<String, Object>(); if (brightness != 0.0) { edits.put("brightness", brightness); } if (contrast != 0.0) { edits.put("contrast", contrast); } if (flipH) { edits.put("flipH", flipH); } if (flipV) { edits.put("flipV", flipV); } if (invert) { edits.put("invert", invert); } if (rotate != 0) { edits.put("rotate", rotate); } if (grayscale) { edits.put("grayscale", grayscale); } if (sepia) { edits.put("sepia", sepia); } if (sharpen != 0) { edits.put("sharpen", sharpen); } if (!ObjectUtils.isBlank(page.params(String.class, blurName))) { blurs = new ArrayList<String>(); for (String blur : page.params(String.class, blurName)) { if (!blurs.contains(blur)) { blurs.add(blur); } } if (blurs.size() == 1) { edits.put("blur", blurs.get(0)); } else { edits.put("blur", blurs); } } fieldValueMetadata.put("cms.edits", edits); InputStream newItemData = null; if ("keep".equals(action)) { newItem = fieldValue; } else if ("newUpload".equals(action) || "dropbox".equals(action)) { String name = null; String fileContentType = null; long fileSize = 0; file = File.createTempFile("cms.", ".tmp"); MultipartRequest mpRequest; if ("dropbox".equals(action)) { Map<String, Object> fileData = (Map<String, Object>) ObjectUtils.fromJson(page.param(String.class, dropboxName)); if (fileData != null) { name = ObjectUtils.to(String.class, fileData.get("name")); fileContentType = ObjectUtils.getContentType(name); fileSize = ObjectUtils.to(long.class, fileData.get("bytes")); InputStream fileInput = new URL(ObjectUtils.to(String.class, fileData.get("link"))).openStream(); try { FileOutputStream fileOutput = new FileOutputStream(file); try { IoUtils.copy(fileInput, fileOutput); } finally { fileOutput.close(); } } finally { fileInput.close(); } } } else if ((mpRequest = MultipartRequestFilter.Static.getInstance(request)) != null) { FileItem fileItem = mpRequest.getFileItem(fileName); if (fileItem != null) { name = StringUtils.getFileName(fileItem.getName()); fileContentType = fileItem.getContentType(); fileSize = fileItem.getSize(); try { fileItem.write(file); } catch (Exception e) { state.addError(field, "Unable to write to " + file.getAbsolutePath()); LOGGER.error("Unable to write file", e); } } } if (name != null && fileContentType != null) { // 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(fileContentType)) { state.addError(field, String.format( "Invalid content type [%s]. Must match the pattern [%s].", fileContentType, contentTypeGroups)); return; } // 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 = new FileInputStream(file); 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")) { state.addError(field, String.format( "Can't upload [%s] file disguising as HTML!", fileContentType)); return; } } finally { input.close(); } } if (fileSize > 0) { fieldValueMetadata.put("originalFilename", name); newItem = StorageItem.Static.createIn(getStorageSetting(Optional.of(field))); newItem.setPath(createStorageItemPath(state.getLabel(), name)); newItem.setContentType(fileContentType); 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(fileSize))); httpHeaders.put("Content-Type", Collections.singletonList(fileContentType)); fieldValueMetadata.put("http.headers", httpHeaders); newItem.setData(new FileInputStream(file)); newItemData = new FileInputStream(file); } } } else if ("newUrl".equals(action)) { newItem = StorageItem.Static.createUrl(page.param(urlName)); } if (newItem != null) { tryExtractMetadata(newItem, fieldValueMetadata, Optional.ofNullable(newItemData)); } // Standard sizes. for (Iterator<Map.Entry<String, ImageCrop>> i = crops.entrySet().iterator(); i.hasNext();) { Map.Entry<String, ImageCrop> e = i.next(); String cropId = e.getKey(); double x = page.doubleParam(cropsName + cropId + ".x"); double y = page.doubleParam(cropsName + cropId + ".y"); double width = page.doubleParam(cropsName + cropId + ".width"); double height = page.doubleParam(cropsName + cropId + ".height"); String texts = page.param(cropsName + cropId + ".texts"); String textSizes = page.param(cropsName + cropId + ".textSizes"); String textXs = page.param(cropsName + cropId + ".textXs"); String textYs = page.param(cropsName + cropId + ".textYs"); String textWidths = page.param(cropsName + cropId + ".textWidths"); if (x != 0.0 || y != 0.0 || width != 0.0 || height != 0.0 || !ObjectUtils.isBlank(texts)) { ImageCrop crop = e.getValue(); crop.setX(x); crop.setY(y); crop.setWidth(width); crop.setHeight(height); crop.setTexts(texts); crop.setTextSizes(textSizes); crop.setTextXs(textXs); crop.setTextYs(textYs); crop.setTextWidths(textWidths); for (Iterator<ImageTextOverlay> j = crop.getTextOverlays().iterator(); j.hasNext();) { ImageTextOverlay textOverlay = j.next(); String text = textOverlay.getText(); if (text != null) { StringBuilder cleaned = new StringBuilder(); for (Object item : new ReferentialText(text, true)) { if (item instanceof String) { cleaned.append((String) item); } } text = cleaned.toString(); if (ObjectUtils.isBlank(text.replaceAll("<[^>]*>", ""))) { j.remove(); } else { textOverlay.setText(text); } } } } else { i.remove(); } } fieldValueMetadata.put("cms.crops", crops); // Removes legacy cropping information if (state.getValue(cropsFieldName) != null) { state.remove(cropsFieldName); } // Set focus point if (focusX != null && focusY != null) { // Handle legacy focus points stored as a value 1-100, instead of 0-1 if (focusX > 1 && focusX < 100) { focusX /= 100; } if (focusY > 1 && focusY < 100) { focusY /= 100; } focusPoint.put("x", focusX); focusPoint.put("y", focusY); } fieldValueMetadata.put("cms.focus", focusPoint); // Transfers legacy metadata over to it's new location within the StorageItem object Map<String, Object> legacyMetadata = ObjectUtils.to(new TypeReference<Map<String, Object>>() { }, state.getValue(metadataFieldName)); if (legacyMetadata != null && !legacyMetadata.isEmpty()) { for (Map.Entry<String, Object> entry : legacyMetadata.entrySet()) { if (!fieldValueMetadata.containsKey(entry.getKey())) { fieldValueMetadata.put(entry.getKey(), entry.getValue()); } } state.remove(metadataFieldName); } if (newItem != null) { newItem.setMetadata(fieldValueMetadata); } if (newItem != null && ("newUpload".equals(action) || "dropbox".equals(action))) { newItem.save(); } state.putValue(fieldName, newItem); if (projectUsingBrightSpotImage) { page.include("set/hotSpot.jsp"); } return; } finally { if (file != null && file.exists()) { file.delete(); } } } // --- Presentation --- page.writeStart("div", "class", "inputSmall"); page.writeStart("div", "class", "fileSelector"); page.writeStart("select", "class", "toggleable", "data-root", ".inputContainer", "name", page.h(actionName)); if (fieldValue != null) { page.writeStart("option", "data-hide", ".fileSelectorItem", "data-show", ".fileSelectorExisting", "value", "keep"); page.writeHtml(page.localize(StorageItemField.class, "option.keep")); page.writeEnd(); } if (!field.isRequired()) { page.writeStart("option", "data-hide", ".fileSelectorItem", "value", "none"); page.writeHtml(page.localize(StorageItemField.class, "option.none")); page.writeEnd(); } page.writeStart("option", "data-hide", ".fileSelectorItem", "data-show", ".fileSelectorNewUpload", "value", "newUpload", fieldValue == null && field.isRequired() ? " selected" : ""); page.writeHtml(page.localize(StorageItemField.class, "option.newUpload")); page.writeEnd(); page.writeStart("option", "data-hide", ".fileSelectorItem", "data-show", ".fileSelectorNewUrl", "value", "newUrl"); page.writeHtml(page.localize(StorageItemField.class, "option.newUrl")); page.writeEnd(); if (!ObjectUtils.isBlank(page.getCmsTool().getDropboxApplicationKey())) { page.writeStart("option", "data-hide", ".fileSelectorItem", "data-show", ".fileSelectorDropbox", "value", "dropbox"); page.write("Dropbox"); page.writeEnd(); } page.writeEnd(); page.writeStart("span", "class", "fileSelectorItem fileSelectorNewUpload"); page.writeElement("input", "type", "file", "name", page.h(fileName), "data-input-name", inputName); page.writeEnd(); page.writeTag("input", "class", "fileSelectorItem fileSelectorNewUrl", "type", "text", "name", page.h(urlName)); if (!ObjectUtils.isBlank(page.getCmsTool().getDropboxApplicationKey())) { page.writeStart("span", "class", "fileSelectorItem fileSelectorDropbox"); page.writeElement("input", "class", "DropboxChooserInput", "type", "text", "name", page.h(dropboxName)); page.writeEnd(); } page.writeEnd(); page.writeEnd(); if (fieldValue != null) { String contentType = fieldValue.getContentType(); page.writeStart("div", "class", "inputLarge fileSelectorItem fileSelectorExisting filePreview"); if (field.as(ToolUi.class).getStoragePreviewProcessorApplication() != null) { ToolUi ui = field.as(ToolUi.class); String processorPath = ui.getStoragePreviewProcessorPath(); if (processorPath != null) { JspUtils.include(request, page.getResponse(), page.getWriter(), RoutingFilter.Static.getApplicationPath(ui.getStoragePreviewProcessorApplication()) + StringUtils.ensureStart(processorPath, "/")); } } else { FileContentType.writeFilePreview(page, state, fieldValue); } page.writeEnd(); } if (projectUsingBrightSpotImage) { page.include("set/hotSpot.jsp"); } } public static String createStorageItemPath(String label, String fileName) { String extension = ""; String path = createStoragePathPrefix(); if (!StringUtils.isBlank(fileName)) { int lastDotAt = fileName.indexOf('.'); if (lastDotAt > -1) { extension = fileName.substring(lastDotAt); fileName = fileName.substring(0, lastDotAt); } } if (ObjectUtils.isBlank(label) || ObjectUtils.to(UUID.class, label) != null) { label = fileName; } if (ObjectUtils.isBlank(label)) { label = UUID.randomUUID().toString().replace("-", ""); } path += StringUtils.toNormalized(label); path += extension; return path; } static String createStoragePathPrefix() { String idString = UUID.randomUUID().toString().replace("-", ""); StringBuilder pathBuilder = new StringBuilder(); pathBuilder.append(idString.substring(0, 2)); pathBuilder.append('/'); pathBuilder.append(idString.substring(2, 4)); pathBuilder.append('/'); pathBuilder.append(idString.substring(4)); pathBuilder.append('/'); return pathBuilder.toString(); } /** * Gets storageSetting for current field, * if non exists, get {@code StorageItem.DEFAULT_STORAGE_SETTING} * * @param field to check for storage setting */ static String getStorageSetting(Optional<ObjectField> field) { String storageSetting = null; if (field.isPresent()) { String fieldStorageSetting = field.get().as(ToolUi.class).getStorageSetting(); if (!StringUtils.isBlank(fieldStorageSetting)) { storageSetting = Settings.get(String.class, fieldStorageSetting); } } if (StringUtils.isBlank(storageSetting)) { storageSetting = Settings.get(String.class, StorageItem.DEFAULT_STORAGE_SETTING); } return storageSetting; } static void tryExtractMetadata(StorageItem storageItem, Map<String, Object> fieldValueMetadata, Optional<InputStream> optionalStream) { ImageMetadataMap metadata = null; InputStream inputStream = null; String contentType = storageItem.getContentType(); try { if (!fieldValueMetadata.containsKey("width") && !fieldValueMetadata.containsKey("height") && contentType != null && contentType.startsWith("image/")) { inputStream = optionalStream.isPresent() ? optionalStream.get() : storageItem.getData(); metadata = new ImageMetadataMap(inputStream); List<Throwable> errors = metadata.getErrors(); if (!errors.isEmpty()) { LOGGER.debug("Can't read image metadata", new AggregateException(errors)); } } } catch (IOException e) { LOGGER.debug("Can't read image metadata", e); } finally { IoUtils.closeQuietly(inputStream); } if (metadata != null) { fieldValueMetadata.putAll(metadata); } } @Override protected String getPermissionId() { return null; } @Override protected void doService(ToolPageContext page) throws IOException, ServletException { processField(page); } }