package er.attachment.components; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOActionResults; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WORequest; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSPropertyListSerialization; import er.attachment.model.ERAttachment; import er.attachment.model.ERDatabaseAttachment; import er.attachment.processors.ERAttachmentProcessor; import er.extensions.appserver.ERXHttpStatusCodes; import er.extensions.appserver.ERXResponse; import er.extensions.appserver.ERXWOContext; import er.extensions.components.ERXNonSynchronizingComponent; import er.extensions.eof.ERXQ; import er.extensions.foundation.ERXArrayUtilities; import er.extensions.foundation.ERXProperties; /** * A component designed to allow drag and drop uploads of ERAttachments. * Except where otherwise noted, the javascript functions have a single * argument: the event.<p> * If you want to include a file selector to the drop-area, then either * use includeFileSelector or add one in another location as:<br> * <wo:genericContainer elementName = "input" type = "file" id = "fileSelectorID" multiple = "multiple" /><br> * and bind fileSelectorID to the id.<p> * If you just want a file selector button without the drop area, then * use this component with no content and includeFileSelector set to true.<br> * This version also uses a fix from Michael Kondratov for the completeAllFunction in dndupload.js. completeAllFunction now works as advertised. * * @binding accept an array of accepted mimetypes. If no mimetypes are specified, all are accepted. ex. (image/png, image/jpg, text/*) * @binding action the action to fire when an attachment is uploaded * @binding attachment the uploaded attachment * @binding completeAllFunction a javascript function to execute when all dropped files are processed * @binding completeFunction a javascript function to execute after each uploaded file is processed. This function accepts two args. The first is the event. The second is the file. * @binding configurationName the configuration name. See the package javadocs for more info. * @binding disabled if true, the upload component is disabled and only displays component content * @binding editingContext the editing context where the ERAttachments will be created * @binding enterFunction a javascript function to execute when dragged files enter the drop target * @binding errorFunction a javascript function to execute when a file upload error occurs. This function accepts two args. The first is the event. The second is the file. * @binding exitFunction a javascript function to execute when dragged files exit the drop target * @binding overFunction a javascript function to execute when dragged files are over the drop target * @binding storageType the ERAttachment storage type (db, file, s3, cf, ...) * @binding includeFileSelector a file selector appears at the top of the drag-area. * @binding fileSelectorID include an id to a file selector outside of the component. Can be used if more custom file selector behavior is needed. * * @author Ramsey * @author Fredrik * @author Michael Kondratov */ public class ERDragAndDropUpload extends ERXNonSynchronizingComponent { /** * Do I need to update serialVersionUID? * See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the * <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a> */ private static final long serialVersionUID = 1L; private static final Logger log = LoggerFactory.getLogger(ERDragAndDropUpload.class); private String dropTargetID; private boolean invokeAction = false; private boolean willAccept = true; private NSArray<String> accept; public ERDragAndDropUpload(WOContext context) { super(context); } /** * @return the dropTargetID */ public String dropTargetID() { if(dropTargetID == null) { dropTargetID = ERXWOContext.safeIdentifierName(context(), true); } return dropTargetID; } public String fileSelectorID() { return valueForStringBinding("fileSelectorID", "select_" + dropTargetID()); } @Override protected NSArray<String> additionalJavascriptFiles() { return new NSArray<>("js/dndupload.js"); } @Override protected String _frameworkName() { return "ERAttachment"; } /** * The ajax request URL for this component. * @return the post URL for the ajax post request */ public String postURL() { String key = WOApplication.application().ajaxRequestHandlerKey(); return context().componentActionURL(key); } @Override public WOActionResults invokeAction(WORequest request, WOContext context) { if(invokeAction && willAccept) { invokeAction = false; return (WOActionResults) valueForBinding("action"); } else if (invokeAction) { invokeAction = false; willAccept = true; return new ERXResponse(localizer().localizedStringForKey("UnacceptableMimetype"), ERXHttpStatusCodes.BAD_REQUEST); // CHECKME 406 or 415? } return super.invokeAction(request, context); } @Override public void takeValuesFromRequest(WORequest request, WOContext context) { NSData data = (NSData) request.formValueForKey(dropTargetID()); if(data != null) { String mimetype = (String) request.formValueForKey(dropTargetID() + ".mimetype"); if(!accept().isEmpty() && !acceptMimetype(mimetype)) { // reject mime types that don't match invokeAction = true; willAccept = false; } else { String filename = (String) request.formValueForKey(dropTargetID() + ".filename"); FileOutputStream os = null; try { File uploadedFile = File.createTempFile("DragAndDropUpload-", ".tmp"); os = new FileOutputStream(uploadedFile); data.writeToStream(os); ERAttachment upload = ERAttachmentProcessor.processorForType(storageType()).process(editingContext(), uploadedFile, filename, mimetype, configurationName(), null); setValueForBinding(upload, "attachment"); invokeAction = true; FileUtils.deleteQuietly(uploadedFile); } catch (IOException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } finally { if(os != null) { try { os.close(); } catch (IOException e) { log.error("Error closing file stream", e); } } } } } super.takeValuesFromRequest(request, context); } /** * The ERAttachment storage type for the uploaded files. * * @return the storageType */ public String storageType() { String key = configurationName() == null ?"er.attachment.storageType" :"er.attachment." + configurationName() + ".storageType"; String defaultType = ERXProperties.stringForKeyWithDefault(key, ERDatabaseAttachment.STORAGE_TYPE); return valueForStringBinding("storageType", defaultType); } /** * The ERAttachment configuration name for the uploaded files. * * @return the configurationName */ public String configurationName() { return (String) valueForBinding("configurationName"); } /** * The EOEditingContext where the ERAttachment will be created */ public EOEditingContext editingContext() { return (EOEditingContext) valueForBinding("editingContext"); } public NSArray<String> accept() { if(accept == null) { String acceptString = (String)valueForBinding("accept"); if(acceptString == null) { accept = NSArray.emptyArray(); } else { // plist deserialization chokes on * if(acceptString.indexOf('*') > -1) { acceptString = acceptString.replace("*", ""); } accept = NSPropertyListSerialization.arrayForString(acceptString); } } return accept; } //TODO optimize public boolean acceptMimetype(String mimetype) { NSArray<String> wildcards = ERXQ.endsWith("toString", "/").filtered(accept()); for(String wildcard: wildcards) { if(mimetype.startsWith(wildcard)) { return true; } } NSArray<String> others = ERXArrayUtilities.arrayMinusArray(accept(), wildcards); for(String other: others) { if(mimetype.equals(other)) { return true; } } return false; } }