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