package er.ajax; 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.WOResponse; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import er.extensions.appserver.ERXRequest; import er.extensions.components.ERXComponentUtilities; import er.extensions.foundation.ERXValueUtilities; import er.extensions.localization.ERXLocalizer; /** * AjaxFlexibleFileUpload is an enhanced file upload component that uses a call to a hidden iFrame to handle a file * upload. It is based on the code in AjaxFileUpload but extends it by using Andrew Valums' ajaxupload.js (from * http://valums.com/ajax-upload/). This dynamically creates the iFrame at the end of the current page content freeing * this component to be used in or out of a form. The component is fully styleable (including the upload button) and * supports upload progress and canceling. * * @binding accept the attribute specifies the types of files that the server accepts (that can be submitted through a file upload) * @binding cancelLabel the label for for the cancel button (defaults to "Cancel") * @binding startingText the text to display when the progress is starting (defaults "Upload Starting..."); * @binding selectFileLabel the label for the select file button (defaults to "Select File...") * @binding clearLabel the label for the button used to clear a selected file or uploaded file (defaults to "Clear") * @binding uploadLabel the label for the Upload button (defaults to "Upload") * @binding startedFunction the javascript function to execute when the progress is started * @binding canceledFunction the javascript function to execute when the upload is canceled * @binding succeededFunction the javascript function to execute when the upload succeeds * @binding clearedFunction the javascript function to execute when the clear button is clicked * @binding failedFunction the javascript function to execute when the upload fails * @binding finishedFunction the javascript function to execute when the upload finishes (succeeded, failed, or * canceled) * @binding finishedAction the action to fire when the upload finishes (cancel, failed, or succeeded) * @binding canceledAction the action to fire when the upload is canceled * @binding succeededAction the action to fire when the upload succeeded * @binding clearedAction the action to fire when the clear button is clicked * @binding failedAction the action to fire when the upload fails * @binding data the NSData that will be bound with the contents of the upload * @binding inputStream will be bound to an input stream on the contents of the upload * @binding outputStream the output stream to write the contents of the upload to * @binding streamToFilePath the path to write the upload to, can be a directory * @binding finalFilePath the final file path of the upload (when streamToFilePath is set or keepTempFile = true) * @binding filePath the name of the uploaded file * @binding allowCancel if true, the cancel link is visible * @binding refreshTime the number of milliseconds to wait between refreshes (defaults to 2000) * @binding keepTempFile if true, don't delete the temp file that AjaxFileUpload creates * @binding uploadFunctionName the upload button will instead be a function with the given name * @binding autoSubmit should the upload start immediately after a file is selected (defaults to true) * @binding injectDefaultCSS inject the default stylesheet from the Ajax framework (defaults to true); * @binding selectFileButtonClass class for the select file button (defaults to "Button ObjButton SelectFileObjButton"); * @binding uploadButtonClass class for the select file button (defaults to "Button ObjButton UploadFileObjButton") * @binding cancelButtonClass class for the select file button (defaults to "Button ObjButton CancelUploadObjButton") * @binding clearButtonClass class for the select file button (defaults to "Button ObjButton ClearUploadObjButton") * @binding clearUploadProgressOnSuccess if true, displays the select file button instead of the uploaded file name on completion of a successful upload * @binding mimeType set from the content-type of the upload header if available * @binding onClickBefore if the given function returns true, the onClick is executed. This is to support confirm(..) dialogs. * * @author dleber * @author mschrag */ public class AjaxFlexibleFileUpload extends AjaxFileUpload { /** * 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(AjaxFlexibleFileUpload.class); public static interface Keys { public static final String name = "name"; public static final String selectFileLabel = "selectFileLabel"; public static final String cancelLabel = "cancelLabel"; public static final String clearLabel = "clearLabel"; public static final String uploadLabel = "uploadLabel"; public static final String cancelingText = "cancelingText"; public static final String startingText = "startingText"; public static final String failedText = "failedText"; public static final String refreshTime = "refreshTime"; public static final String autoSubmit = "autoSubmit"; public static final String allowCancel = "allowCancel"; public static final String startedFunction = "startedFunction"; public static final String canceledFunction = "canceledFunction"; public static final String finishedFunction = "finishedFunction"; public static final String failedFunction = "failedFunction"; public static final String succeededFunction = "succeededFunction"; public static final String clearedFunction = "clearedFunction"; public static final String selectFileButtonClass = "selectFileButtonClass"; public static final String uploadButtonClass = "uploadButtonClass"; public static final String cancelButtonClass = "cancelButtonClass"; public static final String clearButtonClass = "clearButtonClass"; public static final String injectDefaultCSS = "injectDefaultCSS"; public static final String clearUploadProgressOnSuccess = "clearUploadProgressOnSuccess"; public static final String onClickBefore = "onClickBefore"; } /** * Wrapper class to expose only the methods we need to {@link AjaxProxy}. * * @author paulh * @see <a href="https://github.com/wocommunity/wonder/issues/768">#768</a> */ public final class Proxy { /** * Wrapper for {@link AjaxFlexibleFileUpload#uploadState()}. * * @return see {@link AjaxFlexibleFileUpload#uploadState()} */ public NSDictionary<String, ?> uploadState() { return AjaxFlexibleFileUpload.this.uploadState(); } /** * Wrapper for {@link AjaxFlexibleFileUpload#cancelUpload()}. */ public void cancelUpload() { AjaxFlexibleFileUpload.this.cancelUpload(); return; } /** * Wrapper for {@link AjaxFlexibleFileUpload#uploadState()}. * * @return see {@link AjaxFlexibleFileUpload#uploadState()} */ public WOActionResults clearFileResults() { return AjaxFlexibleFileUpload.this.clearFileResults(); } } /** * Proxy used for method access by {@link AjaxProxy} */ public final Proxy proxy = new Proxy(); private String _refreshTime; private String _clearLabel; private String _cancelLabel; private String _uploadLabel; private String _selectFileLabel; private String _selectFileButtonClass; private String _uploadButtonClass; private String _cancelButtonClass; private String _clearButtonClass; private Boolean _autoSubmit; private Boolean _allowCancel; private Boolean _clearUploadProgressOnSuccess; public boolean testFlag = false; public enum UploadState { DORMANT, STARTED, INPROGRESS, CANCELED, FAILED, SUCCEEDED, FINISHED } public UploadState state = UploadState.DORMANT; public AjaxFlexibleFileUpload(WOContext context) { super(context); } @Override public void appendToResponse(WOResponse response, WOContext context) { super.appendToResponse(response, context); if (ERXComponentUtilities.booleanValueForBinding(this, Keys.injectDefaultCSS, true)) { AjaxUtils.addStylesheetResourceInHead(context, response, "default_ajaxupload.css"); } AjaxUtils.addScriptResourceInHead(context, response, "prototype.js"); AjaxUtils.addScriptResourceInHead(context, response, "effects.js"); AjaxUtils.addScriptResourceInHead(context, response, "wonder.js"); AjaxUtils.addScriptResourceInHead(context, response, "ajaxupload.js"); } // AJAX UPLOAD INIT /** * Generates the script to initialize a new AjaxUpload JS object * * @return script to initialize a new AjaxUpload JS object */ public String ajaxUploadScript() { String result = "AUP.add('" + id() + "', " + ajaxProxyName() +", {" + ajaxUploadLabels() + "}, {" + options() + "}, {" + ajaxUploadOptions() + "});"; log.debug("AFU Create Script: {}", result); return result; } /** * Builds the array of required additional AjaxUpload data items (<i>sessionIdKey</i>, id). * * @return array of required additional AjaxUpload data items (<i>sessionIdKey</i>, id). */ protected NSArray<String> _ajaxUploadData() { NSMutableArray<String> _data = new NSMutableArray<>(WOApplication.application().sessionIdKey() + ":'" + session().sessionID() + "'"); _data.addObject("id:'" + id() + "'"); return _data.immutableClone(); } /** * Returns a comma separated string of AjaxUpload data items. * * @return comma separated string of AjaxUpload data items. */ public String ajaxUploadData() { return _ajaxUploadData().componentsJoinedByString(", "); } /** * Builds the array of AjaxUpload options * * @return array of AjaxUpload options */ protected NSArray<String> _ajaxUploadOptions() { NSMutableArray<String> _options = new NSMutableArray<>("action:'" + uploadUrl() + "'"); // add options if (canGetValueForBinding("accept")) { _options.addObject("accept:'"+ valueForBinding("accept") +"'"); } _options.addObject("data:{" + ajaxUploadData() + "}"); _options.addObject("name:'" + uploadName() + "'"); _options.add("iframeId:'"+ iframeId() +"'"); if ( !autoSubmit().booleanValue() ) { _options.add("onChange:" + onChangeFunction()); _options.add("autoSubmit:false"); } _options.add("onSubmit:" + onSubmitFunction()); String onClickBefore = (String)valueForBinding(Keys.onClickBefore); if (onClickBefore != null) _options.addObject(String.format("onClickBefore:'%s'", onClickBefore.replaceAll("'", "\\\\'"))); return _options.immutableClone(); } /** * Returns a comma separated string of AjaxUpload options. * * @return comma separated string of AjaxUpload options. */ public String ajaxUploadOptions() { return _ajaxUploadOptions().componentsJoinedByString(","); } /** * Builds an array of localized label strings * * @return array of label/text strings */ protected NSArray<String> _ajaxUploadLabels() { NSMutableArray<String> _labels = new NSMutableArray<>(); _labels.addObject(String.format("upload_canceling:'%s'", cancelingText())); _labels.addObject(String.format("upload_starting:'%s'", startingText())); _labels.addObject(String.format("upload_failed:'%s'", localizedStringForBinding(Keys.failedText, "Upload Failed"))); return _labels; } /** * Returns a comma separated string of the localized label strings. * * @return comma separated string of labels/text strings */ public String ajaxUploadLabels() { return _ajaxUploadLabels().componentsJoinedByString(","); } /** * Builds an array of AFU options * @return array of AFU options */ protected NSArray<String> _options() { NSMutableArray<String> _options = new NSMutableArray<>(String.format("refreshtime:%s", refreshTime())); _options.addObject("autosubmit:" + autoSubmit()); _options.addObject("allowcancel:" + valueForBinding(Keys.allowCancel)); _options.add("clearUploadProgressOnSuccess:" + clearUploadProgressOnSuccess()); String startedFunction = (String)valueForBinding(Keys.startedFunction); if (startedFunction != null) _options.addObject(String.format("startedFunction:%s", startedFunction)); String finishedFunction = (String)valueForBinding(Keys.finishedFunction); if (finishedFunction != null) _options.addObject(String.format("finishedFunction:%s", finishedFunction)); String failedFunction = (String)valueForBinding(Keys.failedFunction); if (failedFunction != null) _options.addObject(String.format("failedFunction:%s", failedFunction)); String canceledFunction = (String)valueForBinding(Keys.canceledFunction); if (canceledFunction != null) _options.addObject(String.format("canceledFunction:%s", canceledFunction)); String succeededFunction = (String)valueForBinding(Keys.succeededFunction); if (succeededFunction != null) _options.addObject(String.format("succeededFunction:%s", succeededFunction)); String clearedFunction = (String)valueForBinding(Keys.clearedFunction); if (clearedFunction != null) _options.addObject(String.format("clearedFunction:%s", clearedFunction)); return _options; } /** * Return a comma separated string of the AFU options * @return comma separated string of options */ public String options() { return _options().componentsJoinedByString(","); } // INLINE JS FUNCTIONS /** * JS Function called when the AjaxUpload registers a change * * @return string JS Function called when the AjaxUpload registers a change */ public String onChangeFunction() { String result = "function(file, extension) { AUP.prepare('"+ id() +"', file, extension); }"; return result; } /** * JS Function called when the AjaxUploader submits * * @return string JS Function called when the AjaxUploader submits */ public String onSubmitFunction() { String result = "function(){ AUP.start('" + id() + "'); }"; return result; } /** * Generate a dictionary containing the current state of the upload. * * @return a dictionary containing the current state of the upload. */ public NSDictionary<String, ?> uploadState() { NSMutableDictionary<String, ?> stateObj = new NSMutableDictionary<>(); AjaxUploadProgress progress = uploadProgress(); if (progress != null) { stateObj.takeValueForKey(progressAmount(), "progress"); stateObj.takeValueForKey(progress.fileName(), "filename"); } refreshState(); stateObj.takeValueForKey(Integer.valueOf(state.ordinal()), "state"); if (state == UploadState.CANCELED) { stateObj.takeValueForKey(cancelUrl(), "cancelUrl"); } log.debug("AjaxFlexibleFileUpload2.uploadState: {}", stateObj); return stateObj.immutableClone(); } /** * Refresh the current state, call the finished handlers if we are succeeded, failed, or canceled */ private void refreshState() { state = UploadState.DORMANT; AjaxUploadProgress progress = uploadProgress(); if (progress != null) { if (progress.isStarted()) { state = UploadState.INPROGRESS; if (progress.isDone()) { state = UploadState.FINISHED; if (progress.isSucceeded()) { state = UploadState.SUCCEEDED; uploadSucceeded(); } if (progress.isFailed()) { state = UploadState.FAILED; uploadFailed(); } if (progress.isCanceled()) { state = UploadState.CANCELED; uploadCanceled(); } } } else { state = UploadState.STARTED; // isDone can happen when a file with no EOF is upload // isFailed can happen when the upload request handler throws an // exception before the upload started (wrong file extension uploaded or exceeds file size) if (progress.isDone() || progress.isFailed()) { state = UploadState.FAILED; uploadFailed(); } } } log.debug("AjaxFlexibleFileUpload.refreshState: {}", state); } /** * JS function bound to the cancel button * * @return JS function bound to the cancel button */ public String cancelUploadFunction() { return String.format("AUP.cancel('%s');", id()); } /** * JS function bound to the manual upload button * * @return JS function bound to the manual upload button */ public String manualSubmitUploadFunction() { return String.format("AUP.submit('%s');", id()); } /** * JS function bound to the clear button * * @return JS function bound to the clear button */ public String clearUploadFunction() { return String.format("AUP.clear('%s');", id()); } // AJAX IDS /** * Element id for the cancel button * * @return id for the cancel button */ public String cancelButtonId() { return String.format("AFUCancelButton%s", id()); } /** * Element id for the clear button * * @return id for the clear button */ public String clearUploadButtonId() { return String.format("AFUClearButton%s", id()); } /** * Element id for the manual upload submit button * * @return id for the upload button */ public String submitUploadButtonId() { return String.format("AFUSubmitUploadButton%s", id()); } /** * Element id for the select file button * * @return id for the select file button */ public String selectFileButtonId() { return String.format("AFUSelectFileButton%s", id()); } /** * Element id for the select file button wrapper div * * @return id for the select file button wrapper div */ public String selectFileButtonWrapperId() { return String.format("AFUSelectFileButtonWrapper%s", id()); } /** * Element id for the file object wrapper div * * @return id for the file object wrapper div */ public String fileObjectId() { return String.format("AFUFileObject%s", id()); } /** * Element id for the progress bar wrapper div * * @return id for the progress bar wrapper div */ public String progressWrapperId() { return String.format("AFUProgressBarWrapper%s", id()); } /** * Element id for the progress bar value inner div * * @return id for the progress bar value inner div */ public String progressBarValueId() { return String.format("AFUProgressBarValue%s", id()); } /** * Unique identifier for the ajax proxy object for this upload component * * @return identifier for the ajax proxy object for this upload component */ public String ajaxProxyName() { return "jsonrpc" + id(); } /** * Unique identifier for the inner update container * * @return identifier for the inner update container */ public String innerUpdateContainerId() { return "AFUIC" + id(); } /** * Unique identifier for the outer update container * * @return identifier for the outer update container */ public String outerUpdateContainerId() { return "AFUOC" + id(); } /** * Unique identifier for the select files button * * @return identifier for the select files button */ public String uploadButtonId() { return "AFUUB" + id(); } /** * Unique identifier for the iframe generated by the AjaxUploader.js * * @return identifier for the iframe generated by the AjaxUploader.js */ public String iframeId() { return "AFUIF" + id(); } /** * Unique identifier for the fileName container * * @return identifier for the fileName container */ public String fileNameId() { return "AFUFileNameWrapper" + id(); } /** * Unique identifier for the upload name * * @return identifier for the upload name */ public String uploadName() { return "AFUUN" + id(); } // IFRAME URLS /** * Returns a closeHTTPSession DA action URL passed to the iframe to cancel the client-side upload * * @return URL sent to the iframe to cancel */ public String cancelUrl() { NSDictionary<String, Object> queryParams = new NSDictionary<>(Boolean.FALSE, WOApplication.application().sessionIdKey()); String url = context()._directActionURL("ERXDirectAction/closeHTTPSession", queryParams, ERXRequest.isRequestSecure(context().request()), 0, false); log.debug("URL: {}", url); return url; } // ACTIONS /** * Action called by the cancel upload button */ public void cancelUpload() { if (uploadProgress() != null) { uploadProgress().cancel(); } state = UploadState.CANCELED; } /** * Action called by the clear button, resets the uploader for a new file selection * * @return results of action */ public WOActionResults clearFileResults() { clearUploadProgress(); WOActionResults results = (WOActionResults) valueForBinding("clearedAction"); return results; } /** * Hook for add-in action called when an upload is canceled * * @return results of action */ @Override public WOActionResults uploadCanceled() { clearUploadProgress(); return super.uploadCanceled(); } /** * Hook for add-in action called when an upload fails * * @return results of action */ @Override public WOActionResults uploadFailed() { if (_progress != null && _progress.failure() != null && canSetValueForBinding("failure")) setValueForBinding(_progress.failure(), "failure"); clearUploadProgress(); return super.uploadFailed(); } /** * Hook for add-in action called when an upload succeeds. */ @Override public WOActionResults uploadSucceeded() { WOActionResults result = super.uploadSucceeded(); clearUploadProgress(); return result; } /** * Helper to reset the uploader and unregister the AjaxProgress object */ public void clearUploadProgress() { if (_progress != null) { AjaxUploadProgress.unregisterProgress(session(), _progress); } _progress = null; } // ACCESSORS /** * Returns the AjaxUploadProgress for this uploader */ @Override public AjaxUploadProgress uploadProgress() { if (_progress == null) { _progress = (AjaxUploadProgress)AjaxUploadProgress.progress(session(), id()); } return _progress; } /** * Boolean which determines whether the upload should occur automatically after a file is selected. * * @return value for 'autoSubmit' binding */ public Boolean autoSubmit() { if (_autoSubmit == null) { _autoSubmit = ERXValueUtilities.BooleanValueWithDefault(valueForBinding(Keys.autoSubmit), Boolean.TRUE); } return _autoSubmit; } public Boolean clearUploadProgressOnSuccess() { if (_clearUploadProgressOnSuccess == null) { _clearUploadProgressOnSuccess = ERXValueUtilities.BooleanValueWithDefault(valueForBinding(Keys.clearUploadProgressOnSuccess), Boolean.FALSE); } return _clearUploadProgressOnSuccess; } public Boolean allowCancel() { if (_allowCancel == null) { _allowCancel = ERXValueUtilities.BooleanValueWithDefault(valueForBinding(Keys.allowCancel), Boolean.FALSE); } return _allowCancel; } /** * Calculate the current progress amount ( 0-100 ) * * @return current progress amount ( 0-100 ) */ public Integer progressAmount() { Integer amount = null; AjaxUploadProgress progress = uploadProgress(); if (progress != null) { if (!progress.isSucceeded()) { int percent = (int)(progress.percentage() * 100); amount = Integer.valueOf(percent); } else { amount = Integer.valueOf(100); } } return amount; } /** * Returns the value for the binding 'refreshTime' * * The binding takes milliseconds between refreshes, this returns seconds * * @return value of the 'refreshTime' binding converted to seconds */ public String refreshTime() { if (_refreshTime == null) { double tempValue = ERXValueUtilities.intValueWithDefault(valueForBinding(Keys.refreshTime), 1000); _refreshTime = String.valueOf(tempValue); } return _refreshTime; } /** * Utility to localize labels with current localizer * @param value * @return localized value */ private String localizedString(String value) { return ERXLocalizer.currentLocalizer().localizedStringForKeyWithDefault(value); } /** * Utility to return localized value from stringValueForBinding * @param key * @param defaultValue * @return localized value of binding key or defaultValue */ private String localizedStringForBinding(String key, String defaultValue) { return localizedString(valueForStringBinding(key, defaultValue)); } /** * Label for the upload button * * @return string value for 'uploadLabel' binding */ @Override public String uploadLabel() { if (_uploadLabel == null) { _uploadLabel = localizedStringForBinding(Keys.uploadLabel, "Upload"); } return _uploadLabel; } /** * Label for the clear button * * @return string value for 'clearLabel' binding */ public String clearLabel() { if (_clearLabel == null) { _clearLabel = localizedStringForBinding(Keys.clearLabel, "Clear"); } return _clearLabel; } /** * Label for the cancel button * * @return string value for 'cancelLabel' binding */ public String cancelLabel() { if (_cancelLabel == null) { _cancelLabel = localizedStringForBinding(Keys.cancelLabel, "Cancel"); } return _cancelLabel; } /** * Label for the select file button * * @return string value for 'selectFileLabel' binding */ public String selectFileLabel() { if (_selectFileLabel == null) { _selectFileLabel = localizedStringForBinding(Keys.selectFileLabel, "Select File..."); } return _selectFileLabel; } /** * CSS Class for the select file button * * @return string value for 'selectFileButtonClass' binding */ public String selectFileButtonClass() { if (_selectFileButtonClass == null) { _selectFileButtonClass = valueForStringBinding(Keys.selectFileButtonClass, "Button ObjButton SelectFileObjButton"); } return _selectFileButtonClass; } /** * CSS Class for the upload file button * * @return string value for 'uploadButtonClass' binding */ public String uploadButtonClass() { if (_uploadButtonClass == null) { _uploadButtonClass = valueForStringBinding(Keys.uploadButtonClass, "Button ObjButton UploadFileObjButton"); } return _uploadButtonClass; } /** * CSS Class for the cancel upload button * * @return string value for 'cancelButtonClass' binding */ public String cancelButtonClass() { if (_cancelButtonClass == null) { _cancelButtonClass = valueForStringBinding(Keys.cancelButtonClass, "Button ObjButton CancelUploadObjButton"); } return _cancelButtonClass; } /** * CSS Class for the clear upload button * * @return string value for 'clearButtonClass' binding */ public String clearButtonClass() { if (_clearButtonClass == null) { _clearButtonClass = valueForStringBinding(Keys.clearButtonClass, "Button ObjButton ClearUploadObjButton"); } return _clearButtonClass; } }