1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2014, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @overview 26 * Provides a framework for uploading multiple files; the callbacks allow the upload to be stored as an 27 * attachment or saved document. 28 * 29 */ 30 31 /** 32 * Creates the ZmUploadManager 33 * @class 34 * This class represents the file uploading, with document creation 35 * 36 */ 37 ZmUploadManager = function() { 38 }; 39 40 41 ZmUploadManager.prototype.constructor = ZmUploadManager; 42 43 /** 44 * Returns a string representation of the object. 45 * 46 * @return {String} a string representation of the object 47 */ 48 ZmUploadManager.prototype.toString = 49 function() { 50 return "ZmUploadManager"; 51 }; 52 53 // Constants 54 55 ZmUploadManager.ERROR_INVALID_SIZE = "invalidSize"; 56 ZmUploadManager.ERROR_INVALID_EXTENSION = "invalidExtension"; 57 ZmUploadManager.ERROR_INVALID_FILENAME = "invalidFilename"; 58 59 /** 60 * uploadMyComputerFile serializes a set of files uploads. The responses are accumulated, and progress is provided to the 61 * current view, if any. Once all files are uploaded, a custom callback is run (if provided) to finish the upload. 62 * 63 * @param {object} params params to customize the upload flow: 64 * attachment True => Mail msg attachment, False => File Upload 65 * uploadFolder Folder to save associated document into 66 * files: raw File object from the external HTML5 drag and drop 67 * notes: Notes associated with each of the files being added 68 * allResponses: Placeholder, initialized here and passed to the chained calls, accumulating responses 69 * start: current index into files 70 * curView: target view for the drag and drop 71 * url url to use (optional, currently only from ZmImportExportController) 72 * stateChangeCallback callback to use from _handleUploadResponse which is the onreadystatechange listener, instead of the normal code here. (optiona, see ZmImportExportController) 73 * preAllCallback: Run prior to All uploads 74 * initOneUploadCallback: Run prior to a single upload 75 * progressCallback: Run by the upload code to provide upload progress 76 * errorCallback: Run upon an error 77 * completeOneCallback: Run when a single upload completes 78 * completeAllCallback: Run when the last file has completed its upload 79 * 80 */ 81 ZmUploadManager.prototype.upload = 82 function(params) { 83 if (!params.files) { 84 return; 85 } 86 params.start = params.start || 0; 87 88 try { 89 this.upLoadC = this.upLoadC + 1; 90 var file = params.files[params.start]; 91 var fileName = file.name || file.fileName; 92 93 if (!params.allResponses) { 94 // First file to upload. Do an preparation prior to uploading the files 95 params.allResponses = []; 96 if (params.preAllCallback) { 97 params.preAllCallback.run(fileName); 98 } 99 // Determine the total number of bytes to be upload across all the files 100 params.totalSize = this._getTotalUploadSize(params); 101 params.uploadedSize = 0; 102 } 103 104 if (params.start > 0) { 105 // Increment the total number of bytes upload with the previous file that completed. 106 params.uploadedSize += params.currentFileSize; 107 } 108 // Set the upload size of the current file 109 params.currentFileSize = file.size || file.fileSize || 0; 110 111 // Initiate the first upload 112 var req = new XMLHttpRequest(); // we do not call this function in IE 113 var uri = params.url || (params.attachment ? (appCtxt.get(ZmSetting.CSFE_ATTACHMENT_UPLOAD_URI) + "?fmt=extended,raw") : appCtxt.get(ZmSetting.CSFE_UPLOAD_URI)); 114 req.open("POST", uri, true); 115 req.setRequestHeader("Cache-Control", "no-cache"); 116 req.setRequestHeader("X-Requested-With", "XMLHttpRequest"); 117 req.setRequestHeader("Content-Type", (file.type || "application/octet-stream") + ";"); 118 req.setRequestHeader("Content-Disposition", 'attachment; filename="'+ AjxUtil.convertToEntities(fileName) + '"'); 119 if (window.csrfToken) { 120 req.setRequestHeader("X-Zimbra-Csrf-Token", window.csrfToken); 121 } 122 123 if (params.initOneUploadCallback) { 124 params.initOneUploadCallback.run(); 125 } 126 127 DBG.println(AjxDebug.DBG1,"Uploading file: " + fileName + " file type" + (file.type || "application/octet-stream") ); 128 this._uploadAttReq = req; 129 if (AjxEnv.supportsHTML5File) { 130 if (params.progressCallback) { 131 req.upload.addEventListener("progress", params.progressCallback, false); 132 } 133 } 134 else { 135 if (params.curView) { 136 var progress = function (obj) { 137 var viewObj = obj; 138 viewObj.si = window.setInterval (function(){viewObj._progress();}, 500); 139 }; 140 progress(params.curView); 141 } 142 } 143 144 req.onreadystatechange = this._handleUploadResponse.bind(this, req, fileName, params); 145 req.send(file); 146 delete req; 147 } catch(exp) { 148 DBG.println("Error while uploading file: " + fileName); 149 DBG.println("Exception: " + exp); 150 151 if (params.errorCallback) { 152 params.errorCallback.run(); 153 } 154 this._popupErrorDialog(ZmMsg.importErrorUpload); 155 this.upLoadC = this.upLoadC - 1; 156 return false; 157 } 158 159 }; 160 161 ZmUploadManager.prototype._getTotalUploadSize = function(params) { 162 // Determine the total number of bytes to be upload across all the files 163 var totalSize = 0; 164 for (var i = 0; i < params.files.length; i++) { 165 var file = params.files[i]; 166 var size = file.size || file.fileSize || 0; // fileSize: Safari 167 totalSize += size; 168 } 169 return totalSize; 170 } 171 172 ZmUploadManager.prototype._handleUploadResponse = 173 function(req, fileName, params){ 174 if (params.stateChangeCallback) { 175 return params.stateChangeCallback(req); 176 } 177 var curView = params.curView; 178 var files = params.files; 179 var start = params.start; 180 var allResponses = params.allResponses; 181 if (req.readyState === 4) { 182 var response = null; 183 var aid = null; 184 var status = req.status; 185 if (status === 200) { 186 if (params.attachment) { 187 // Sent via CSFE_ATTACHMENT_UPLOAD_URI 188 var resp = eval("["+req.responseText+"]"); 189 response = resp.length && resp[2]; 190 if (response) { 191 response = response[0]; 192 if (response) { 193 aid = response.aid; 194 } 195 } 196 } else { 197 // Sent via CSFE_UPLOAD_URI 198 // <UGLY> - the server assumes the javascript object/function it will communicate with - AjxPost. It invokes it with: 199 // function doit() { window.parent._uploadManager.loaded(200,'null','<uploadId>'); } and then <body onload='doit()'> 200 // We need to extract the uploadId param from the function call 201 var functionStr = "loaded("; 202 var response = req.responseText; 203 if (response) { 204 // Get the parameter text between 'loaded(' and ')'; 205 var paramText = response.substr(response.indexOf(functionStr) + functionStr.length); 206 paramText = paramText.substr(0, paramText.indexOf(")")); 207 // Convert to an array of params. Third one is the upload id 208 var serverParams = paramText.split(','); 209 var serverParamArray = eval( "["+ serverParams +"]" ); 210 status = serverParamArray[0]; 211 aid = serverParamArray && serverParamArray.length && serverParamArray[2]; 212 response = { aid: aid }; 213 } 214 // </UGLY> 215 } 216 } 217 if (response || this._uploadAttReq.aborted) { 218 allResponses.push(response || null); 219 if (aid) { 220 DBG.println(AjxDebug.DBG1,"Uploaded file: " + fileName + "Successfully."); 221 } 222 if (start < files.length - 1) { 223 // Still some file uploads to perform 224 if (params.completeOneCallback) { 225 params.completeOneCallback.run(files, start + 1, aid); 226 } 227 // Start the next upload 228 params.start++; 229 this.upload(params); 230 } 231 else { 232 // Uploads are all done 233 this._completeAll(params, allResponses, status); 234 } 235 } 236 else { 237 DBG.println("Error while uploading file: " + fileName + " response is null."); 238 239 // Uploads are all done 240 this._completeAll(params, allResponses, status); 241 242 var msgDlg = appCtxt.getMsgDialog(); 243 this.upLoadC = this.upLoadC - 1; 244 msgDlg.setMessage(ZmMsg.importErrorUpload, DwtMessageDialog.CRITICAL_STYLE); 245 msgDlg.popup(); 246 return false; 247 } 248 } 249 250 }; 251 252 ZmUploadManager.prototype._completeAll = 253 function(params, allResponses, status) { 254 if (params.completeAllCallback) { 255 // Run a custom callback (like for mail ComposeView, which is doing attachment handling) 256 params.completeAllCallback.run(allResponses, params, status) 257 } 258 } 259 260 ZmUploadManager.prototype._popupErrorDialog = function(message) { 261 var dialog = appCtxt.getMsgDialog(); 262 dialog.setMessage(message, DwtMessageDialog.CRITICAL_STYLE); 263 dialog.popup(); 264 }; 265 266 267 // --- Upload File Validation ------------------------------------------- 268 ZmUploadManager.prototype.getErrors = function(file, maxSize, errors, extensions){ 269 var error = { errorCodes:[], filename: AjxStringUtil.htmlEncode(file.name) }; 270 var valid = true; 271 var size = file.size || file.fileSize || 0; // fileSize: Safari 272 if (size && (size > maxSize)) { 273 valid = false; 274 error.errorCodes.push( ZmUploadManager.ERROR_INVALID_SIZE ); 275 } 276 if (!this._checkExtension(file.name, extensions)) { 277 valid = false; 278 error.errorCodes.push( ZmUploadManager.ERROR_INVALID_EXTENSION ); 279 } 280 if (ZmAppCtxt.INVALID_NAME_CHARS_RE.test(file.name)) { 281 valid = false; 282 error.errorCodes.push( ZmUploadManager.ERROR_INVALID_FILENAME ); 283 } 284 285 return valid ? null : error; 286 }; 287 288 ZmUploadManager.prototype._checkExtension = 289 function(filename, extensions) { 290 if (!extensions) return true; 291 var ext = filename.replace(/^.*\./,"").toUpperCase(); 292 for (var i = 0; i < extensions.length; i++) { 293 if (extensions[i] == ext) { 294 return true; 295 } 296 } 297 return false; 298 }; 299 300 ZmUploadManager.prototype.createUploadErrorMsg = 301 function(errors, maxSize, lineBreak) { 302 var errorSummary = {}; 303 var errorCodes; 304 var errorCode; 305 for (var i = 0; i < errors.length; i++) { 306 errorCodes = errors[i].errorCodes; 307 for (var j = 0; j < errorCodes.length; j++) { 308 errorCode = errorCodes[j]; 309 if (!errorSummary[errorCode]) { 310 errorSummary[errorCode] = []; 311 } 312 errorSummary[errorCode].push(errors[i].filename); 313 } 314 } 315 316 var errorMsg = [ZmMsg.uploadFailed]; 317 if (errorSummary.invalidExtension) { 318 var extensions = this._formatUploadErrorList(this._extensions); 319 errorMsg.push("* " + AjxMessageFormat.format(ZmMsg.errorNotAllowedFile, [ extensions ])); 320 } 321 var msgFormat, errorFilenames; 322 if (errorSummary.invalidFilename) { 323 msgFormat = (errorSummary.invalidFilename.length > 1) ? ZmMsg.uploadInvalidNames : ZmMsg.uploadInvalidName; 324 errorFilenames = this._formatUploadErrorList(errorSummary.invalidFilename); 325 errorMsg.push("* " + AjxMessageFormat.format(msgFormat, [ errorFilenames ] )); 326 } 327 if (errorSummary.invalidSize) { 328 msgFormat = (errorSummary.invalidSize.length > 1) ? ZmMsg.uploadSizeError : ZmMsg.singleUploadSizeError; 329 errorFilenames = this._formatUploadErrorList(errorSummary.invalidSize); 330 errorMsg.push("* " + AjxMessageFormat.format(msgFormat, [ errorFilenames, AjxUtil.formatSize(maxSize)] )); 331 } 332 return errorMsg.join(lineBreak); 333 }; 334 335 ZmUploadManager.prototype._formatUploadErrorList = 336 function(errorObjList) { 337 var errorObjText = ""; 338 if (errorObjList) { 339 if (!errorObjList.length) { 340 errorObjText = errorObjList; 341 } else { 342 if (errorObjList.length == 1) { 343 errorObjText = errorObjList[0]; 344 } else { 345 var lastObj = errorObjList.slice(-1); 346 errorObjList = errorObjList.slice(0, errorObjList.length - 1); 347 var initialErrorObjs = errorObjList.join(", "); 348 errorObjText = AjxMessageFormat.format(ZmMsg.pluralList, [ initialErrorObjs, lastObj] ); 349 } 350 } 351 } 352 return errorObjText; 353 }; 354