qq.FineUploaderBasic = function(o){ var that = this; this._options = { debug: false, button: null, multiple: true, maxConnections: 3, disableCancelForFormUploads: false, autoUpload: true, request: { endpoint: '/server/upload', params: {}, paramsInBody: true, customHeaders: {}, forceMultipart: true, inputName: 'qqfile', uuidName: 'qquuid', totalFileSizeName: 'qqtotalfilesize' }, validation: { allowedExtensions: [], sizeLimit: 0, minSizeLimit: 0, itemLimit: 0, stopOnFirstInvalidFile: true }, callbacks: { onSubmit: function(id, name){}, onSubmitted: function(id, name){}, onComplete: function(id, name, responseJSON){}, onCancel: function(id, name){}, onUpload: function(id, name){}, onUploadChunk: function(id, name, chunkData){}, onResume: function(id, fileName, chunkData){}, onProgress: function(id, name, loaded, total){}, onError: function(id, name, reason, maybeXhr) {}, onAutoRetry: function(id, name, attemptNumber) {}, onManualRetry: function(id, name) {}, onValidateBatch: function(fileOrBlobData) {}, onValidate: function(fileOrBlobData) {}, onSubmitDelete: function(id) {}, onDelete: function(id){}, onDeleteComplete: function(id, xhr, isError){}, onPasteReceived: function(blob) { return new qq.Promise().success(); } }, messages: { typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.", sizeError: "{file} is too large, maximum file size is {sizeLimit}.", minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.", emptyError: "{file} is empty, please select files again without it.", noFilesError: "No files to upload.", tooManyItemsError: "Too many items ({netItems}) would be uploaded. Item limit is {itemLimit}.", retryFailTooManyItems: "Retry failed - you have reached your file limit.", onLeave: "The files are being uploaded, if you leave now the upload will be cancelled." }, retry: { enableAuto: false, maxAutoAttempts: 3, autoAttemptDelay: 5, preventRetryResponseProperty: 'preventRetry' }, classes: { buttonHover: 'qq-upload-button-hover', buttonFocus: 'qq-upload-button-focus' }, chunking: { enabled: false, partSize: 2000000, paramNames: { partIndex: 'qqpartindex', partByteOffset: 'qqpartbyteoffset', chunkSize: 'qqchunksize', totalFileSize: 'qqtotalfilesize', totalParts: 'qqtotalparts', filename: 'qqfilename' } }, resume: { enabled: false, id: null, cookiesExpireIn: 7, //days paramNames: { resuming: "qqresume" } }, formatFileName: function(fileOrBlobName) { if (fileOrBlobName.length > 33) { fileOrBlobName = fileOrBlobName.slice(0, 19) + '...' + fileOrBlobName.slice(-14); } return fileOrBlobName; }, text: { sizeSymbols: ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'] }, deleteFile : { enabled: false, endpoint: '/server/upload', customHeaders: {}, params: {} }, cors: { expected: false, sendCredentials: false }, blobs: { defaultName: 'misc_data', paramNames: { name: 'qqblobname' } }, paste: { targetElement: null, defaultName: 'pasted_image' } }; qq.extend(this._options, o, true); this._wrapCallbacks(); this._disposeSupport = new qq.DisposeSupport(); this._filesInProgress = []; this._storedIds = []; this._autoRetries = []; this._retryTimeouts = []; this._preventRetries = []; this._netFilesUploadedOrQueued = 0; this._paramsStore = this._createParamsStore("request"); this._deleteFileParamsStore = this._createParamsStore("deleteFile"); this._endpointStore = this._createEndpointStore("request"); this._deleteFileEndpointStore = this._createEndpointStore("deleteFile"); this._handler = this._createUploadHandler(); this._deleteHandler = this._createDeleteHandler(); if (this._options.button){ this._button = this._createUploadButton(this._options.button); } if (this._options.paste.targetElement) { this._pasteHandler = this._createPasteHandler(); } this._preventLeaveInProgress(); }; qq.FineUploaderBasic.prototype = { log: function(str, level) { if (this._options.debug && (!level || level === 'info')) { qq.log('[FineUploader] ' + str); } else if (level && level !== 'info') { qq.log('[FineUploader] ' + str, level); } }, setParams: function(params, id) { /*jshint eqeqeq: true, eqnull: true*/ if (id == null) { this._options.request.params = params; } else { this._paramsStore.setParams(params, id); } }, setDeleteFileParams: function(params, id) { /*jshint eqeqeq: true, eqnull: true*/ if (id == null) { this._options.deleteFile.params = params; } else { this._deleteFileParamsStore.setParams(params, id); } }, setEndpoint: function(endpoint, id) { /*jshint eqeqeq: true, eqnull: true*/ if (id == null) { this._options.request.endpoint = endpoint; } else { this._endpointStore.setEndpoint(endpoint, id); } }, getInProgress: function(){ return this._filesInProgress.length; }, uploadStoredFiles: function(){ "use strict"; var idToUpload; while(this._storedIds.length) { idToUpload = this._storedIds.shift(); this._filesInProgress.push(idToUpload); this._handler.upload(idToUpload); } }, clearStoredFiles: function(){ this._storedIds = []; }, retry: function(id) { if (this._onBeforeManualRetry(id)) { this._netFilesUploadedOrQueued++; this._handler.retry(id); return true; } else { return false; } }, cancel: function(id) { this._handler.cancel(id); }, cancelAll: function() { var storedIdsCopy = [], self = this; qq.extend(storedIdsCopy, this._storedIds); qq.each(storedIdsCopy, function(idx, storedFileId) { self.cancel(storedFileId); }); this._handler.cancelAll(); }, reset: function() { this.log("Resetting uploader..."); this._handler.reset(); this._filesInProgress = []; this._storedIds = []; this._autoRetries = []; this._retryTimeouts = []; this._preventRetries = []; this._button.reset(); this._paramsStore.reset(); this._endpointStore.reset(); this._netFilesUploadedOrQueued = 0; if (this._pasteHandler) { this._pasteHandler.reset(); } }, addFiles: function(filesBlobDataOrInputs) { var self = this, verifiedFilesOrInputs = [], index, fileOrInput; if (filesBlobDataOrInputs) { if (!window.FileList || !(filesBlobDataOrInputs instanceof FileList)) { filesBlobDataOrInputs = [].concat(filesBlobDataOrInputs); } for (index = 0; index < filesBlobDataOrInputs.length; index+=1) { fileOrInput = filesBlobDataOrInputs[index]; if (qq.isFileOrInput(fileOrInput)) { verifiedFilesOrInputs.push(fileOrInput); } else { self.log(fileOrInput + ' is not a File or INPUT element! Ignoring!', 'warn'); } } this.log('Processing ' + verifiedFilesOrInputs.length + ' files or inputs...'); this._uploadFileOrBlobDataList(verifiedFilesOrInputs); } }, addBlobs: function(blobDataOrArray) { if (blobDataOrArray) { var blobDataArray = [].concat(blobDataOrArray), verifiedBlobDataList = [], self = this; qq.each(blobDataArray, function(idx, blobData) { if (qq.isBlob(blobData) && !qq.isFileOrInput(blobData)) { verifiedBlobDataList.push({ blob: blobData, name: self._options.blobs.defaultName }); } else if (qq.isObject(blobData) && blobData.blob && blobData.name) { verifiedBlobDataList.push(blobData); } else { self.log("addBlobs: entry at index " + idx + " is not a Blob or a BlobData object", "error"); } }); this._uploadFileOrBlobDataList(verifiedBlobDataList); } else { this.log("undefined or non-array parameter passed into addBlobs", "error"); } }, getUuid: function(id) { return this._handler.getUuid(id); }, getResumableFilesData: function() { return this._handler.getResumableFilesData(); }, getSize: function(id) { return this._handler.getSize(id); }, getName: function(id) { return this._handler.getName(id); }, getFile: function(fileOrBlobId) { return this._handler.getFile(fileOrBlobId); }, deleteFile: function(id) { this._onSubmitDelete(id); }, setDeleteFileEndpoint: function(endpoint, id) { /*jshint eqeqeq: true, eqnull: true*/ if (id == null) { this._options.deleteFile.endpoint = endpoint; } else { this._deleteFileEndpointStore.setEndpoint(endpoint, id); } }, getPromissoryCallbackNames: function() { return ["onPasteReceived"]; }, _createUploadButton: function(element){ var self = this; var button = new qq.UploadButton({ element: element, multiple: this._options.multiple && qq.isXhrUploadSupported(), acceptFiles: this._options.validation.acceptFiles, onChange: function(input){ self._onInputChange(input); }, hoverClass: this._options.classes.buttonHover, focusClass: this._options.classes.buttonFocus }); this._disposeSupport.addDisposer(function() { button.dispose(); }); return button; }, _createUploadHandler: function(){ var self = this; return new qq.UploadHandler({ debug: this._options.debug, forceMultipart: this._options.request.forceMultipart, maxConnections: this._options.maxConnections, customHeaders: this._options.request.customHeaders, inputName: this._options.request.inputName, uuidParamName: this._options.request.uuidName, totalFileSizeParamName: this._options.request.totalFileSizeName, cors: this._options.cors, demoMode: this._options.demoMode, paramsInBody: this._options.request.paramsInBody, paramsStore: this._paramsStore, endpointStore: this._endpointStore, chunking: this._options.chunking, resume: this._options.resume, blobs: this._options.blobs, log: function(str, level) { self.log(str, level); }, onProgress: function(id, name, loaded, total){ self._onProgress(id, name, loaded, total); self._options.callbacks.onProgress(id, name, loaded, total); }, onComplete: function(id, name, result, xhr){ self._onComplete(id, name, result, xhr); self._options.callbacks.onComplete(id, name, result); }, onCancel: function(id, name){ self._onCancel(id, name); self._options.callbacks.onCancel(id, name); }, onUpload: function(id, name){ self._onUpload(id, name); self._options.callbacks.onUpload(id, name); }, onUploadChunk: function(id, name, chunkData){ self._options.callbacks.onUploadChunk(id, name, chunkData); }, onResume: function(id, name, chunkData) { return self._options.callbacks.onResume(id, name, chunkData); }, onAutoRetry: function(id, name, responseJSON, xhr) { self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty]; if (self._shouldAutoRetry(id, name, responseJSON)) { self._maybeParseAndSendUploadError(id, name, responseJSON, xhr); self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id] + 1); self._onBeforeAutoRetry(id, name); self._retryTimeouts[id] = setTimeout(function() { self._onAutoRetry(id, name, responseJSON) }, self._options.retry.autoAttemptDelay * 1000); return true; } else { return false; } } }); }, _createDeleteHandler: function() { var self = this; return new qq.DeleteFileAjaxRequestor({ maxConnections: this._options.maxConnections, customHeaders: this._options.deleteFile.customHeaders, paramsStore: this._deleteFileParamsStore, endpointStore: this._deleteFileEndpointStore, demoMode: this._options.demoMode, cors: this._options.cors, log: function(str, level) { self.log(str, level); }, onDelete: function(id) { self._onDelete(id); self._options.callbacks.onDelete(id); }, onDeleteComplete: function(id, xhr, isError) { self._onDeleteComplete(id, xhr, isError); self._options.callbacks.onDeleteComplete(id, xhr, isError); } }); }, _createPasteHandler: function() { var self = this; return new qq.PasteSupport({ targetElement: this._options.paste.targetElement, callbacks: { log: function(str, level) { self.log(str, level); }, pasteReceived: function(blob) { var pasteReceivedCallback = self._options.callbacks.onPasteReceived, promise = pasteReceivedCallback(blob); if (promise.then) { promise.then(function(successData) { self._handlePasteSuccess(blob, successData); }, function(failureData) { self.log("Ignoring pasted image per paste received callback. Reason = '" + failureData + "'"); }); } else { self.log("Promise contract not fulfilled in pasteReceived callback handler! Ignoring pasted item.", "error"); } } } }); }, _handlePasteSuccess: function(blob, extSuppliedName) { var extension = blob.type.split("/")[1], name = extSuppliedName; /*jshint eqeqeq: true, eqnull: true*/ if (name == null) { name = this._options.paste.defaultName; } name += '.' + extension; this.addBlobs({ name: name, blob: blob }); }, _preventLeaveInProgress: function(){ var self = this; this._disposeSupport.attach(window, 'beforeunload', function(e){ if (!self._filesInProgress.length){return;} var e = e || window.event; // for ie, ff e.returnValue = self._options.messages.onLeave; // for webkit return self._options.messages.onLeave; }); }, _onSubmit: function(id, name) { this._netFilesUploadedOrQueued++; if (this._options.autoUpload) { this._filesInProgress.push(id); } }, _onProgress: function(id, name, loaded, total){ }, _onComplete: function(id, name, result, xhr) { if (!result.success) { this._netFilesUploadedOrQueued--; } this._removeFromFilesInProgress(id); this._maybeParseAndSendUploadError(id, name, result, xhr); }, _onCancel: function(id, name){ this._netFilesUploadedOrQueued--; this._removeFromFilesInProgress(id); clearTimeout(this._retryTimeouts[id]); var storedItemIndex = qq.indexOf(this._storedIds, id); if (!this._options.autoUpload && storedItemIndex >= 0) { this._storedIds.splice(storedItemIndex, 1); } }, _isDeletePossible: function() { return (this._options.deleteFile.enabled && (!this._options.cors.expected || (this._options.cors.expected && (qq.ie10() || !qq.ie())) ) ); }, _onSubmitDelete: function(id) { if (this._isDeletePossible()) { if (this._options.callbacks.onSubmitDelete(id) !== false) { this._deleteHandler.sendDelete(id, this.getUuid(id)); } } else { this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " + "due to CORS on a user agent that does not support pre-flighting.", "warn"); return false; } }, _onDelete: function(fileId) {}, _onDeleteComplete: function(id, xhr, isError) { var name = this._handler.getName(id); if (isError) { this.log("Delete request for '" + name + "' has failed.", "error"); this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhr.status, xhr); } else { this._netFilesUploadedOrQueued--; this.log("Delete request for '" + name + "' has succeeded."); } }, _removeFromFilesInProgress: function(id) { var index = qq.indexOf(this._filesInProgress, id); if (index >= 0) { this._filesInProgress.splice(index, 1); } }, _onUpload: function(id, name){}, _onInputChange: function(input){ if (qq.isXhrUploadSupported()){ this.addFiles(input.files); } else { this.addFiles(input); } this._button.reset(); }, _onBeforeAutoRetry: function(id, name) { this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "..."); }, _onAutoRetry: function(id, name, responseJSON) { this.log("Retrying " + name + "..."); this._autoRetries[id]++; this._handler.retry(id); }, _shouldAutoRetry: function(id, name, responseJSON) { if (!this._preventRetries[id] && this._options.retry.enableAuto) { if (this._autoRetries[id] === undefined) { this._autoRetries[id] = 0; } return this._autoRetries[id] < this._options.retry.maxAutoAttempts; } return false; }, //return false if we should not attempt the requested retry _onBeforeManualRetry: function(id) { var itemLimit = this._options.validation.itemLimit; if (this._preventRetries[id]) { this.log("Retries are forbidden for id " + id, 'warn'); return false; } else if (this._handler.isValid(id)) { var fileName = this._handler.getName(id); if (this._options.callbacks.onManualRetry(id, fileName) === false) { return false; } if (itemLimit > 0 && this._netFilesUploadedOrQueued+1 > itemLimit) { this._itemError("retryFailTooManyItems", ""); return false; } this.log("Retrying upload for '" + fileName + "' (id: " + id + ")..."); this._filesInProgress.push(id); return true; } else { this.log("'" + id + "' is not a valid file ID", 'error'); return false; } }, _maybeParseAndSendUploadError: function(id, name, response, xhr) { //assuming no one will actually set the response code to something other than 200 and still set 'success' to true if (!response.success){ if (xhr && xhr.status !== 200 && !response.error) { this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status, xhr); } else { var errorReason = response.error ? response.error : "Upload failure reason unknown"; this._options.callbacks.onError(id, name, errorReason, xhr); } } }, _uploadFileOrBlobDataList: function(fileOrBlobDataList){ var index, validationDescriptors = this._getValidationDescriptors(fileOrBlobDataList), batchValid = this._isBatchValid(validationDescriptors); if (batchValid) { if (fileOrBlobDataList.length > 0) { for (index = 0; index < fileOrBlobDataList.length; index++){ if (this._validateFileOrBlobData(fileOrBlobDataList[index])){ this._upload(fileOrBlobDataList[index]); } else { if (this._options.validation.stopOnFirstInvalidFile){ return; } } } } else { this._itemError("noFilesError", ""); } } }, _upload: function(blobOrFileContainer){ var id = this._handler.add(blobOrFileContainer); var name = this._handler.getName(id); if (this._options.callbacks.onSubmit(id, name) !== false) { this._onSubmit(id, name); this._options.callbacks.onSubmitted(id, name); if (this._options.autoUpload) { this._handler.upload(id); } else { this._storeForLater(id); } } }, _storeForLater: function(id) { this._storedIds.push(id); }, _isBatchValid: function(validationDescriptors) { //first, defer the check to the callback (ask the integrator) var errorMessage, itemLimit = this._options.validation.itemLimit, proposedNetFilesUploadedOrQueued = this._netFilesUploadedOrQueued + validationDescriptors.length, batchValid = this._options.callbacks.onValidateBatch(validationDescriptors) !== false; //if the callback hasn't rejected the batch, run some internal tests on the batch next if (batchValid) { if (itemLimit === 0 || proposedNetFilesUploadedOrQueued <= itemLimit) { batchValid = true; } else { batchValid = false; errorMessage = this._options.messages.tooManyItemsError .replace(/\{netItems\}/g, proposedNetFilesUploadedOrQueued) .replace(/\{itemLimit\}/g, itemLimit); this._batchError(errorMessage); } } return batchValid; }, _validateFileOrBlobData: function(fileOrBlobData){ var validationDescriptor, name, size; validationDescriptor = this._getValidationDescriptor(fileOrBlobData); name = validationDescriptor.name; size = validationDescriptor.size; if (this._options.callbacks.onValidate(validationDescriptor) === false) { return false; } if (qq.isFileOrInput(fileOrBlobData) && !this._isAllowedExtension(name)){ this._itemError('typeError', name); return false; } else if (size === 0){ this._itemError('emptyError', name); return false; } else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){ this._itemError('sizeError', name); return false; } else if (size && size < this._options.validation.minSizeLimit){ this._itemError('minSizeError', name); return false; } return true; }, _itemError: function(code, name) { var message = this._options.messages[code], allowedExtensions = [], extensionsForMessage; function r(name, replacement){ message = message.replace(name, replacement); } qq.each(this._options.validation.allowedExtensions, function(idx, allowedExtension) { /** * If an argument is not a string, ignore it. Added when a possible issue with MooTools hijacking the * `allowedExtensions` array was discovered. See case #735 in the issue tracker for more details. */ if (qq.isString(allowedExtension)) { allowedExtensions.push(allowedExtension); } }); extensionsForMessage = allowedExtensions.join(', ').toLowerCase(); r('{file}', this._options.formatFileName(name)); r('{extensions}', extensionsForMessage); r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit)); r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit)); this._options.callbacks.onError(null, name, message); return message; }, _batchError: function(message) { this._options.callbacks.onError(null, null, message); }, _isAllowedExtension: function(fileName){ var allowed = this._options.validation.allowedExtensions, valid = false; if (!allowed.length) { return true; } qq.each(allowed, function(idx, allowedExt) { /** * If an argument is not a string, ignore it. Added when a possible issue with MooTools hijacking the * `allowedExtensions` array was discovered. See case #735 in the issue tracker for more details. */ if (qq.isString(allowedExt)) { /*jshint eqeqeq: true, eqnull: true*/ var extRegex = new RegExp('\\.' + allowedExt + "$", 'i'); if (fileName.match(extRegex) != null) { valid = true; return false; } } }); return valid; }, _formatSize: function(bytes){ var i = -1; do { bytes = bytes / 1024; i++; } while (bytes > 99); return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i]; }, _wrapCallbacks: function() { var self, safeCallback; self = this; safeCallback = function(name, callback, args) { try { return callback.apply(self, args); } catch (exception) { self.log("Caught exception in '" + name + "' callback - " + exception.message, 'error'); } }; for (var prop in this._options.callbacks) { (function() { var callbackName, callbackFunc; callbackName = prop; callbackFunc = self._options.callbacks[callbackName]; self._options.callbacks[callbackName] = function() { return safeCallback(callbackName, callbackFunc, arguments); }; }()); } }, _parseFileOrBlobDataName: function(fileOrBlobData) { var name; if (qq.isFileOrInput(fileOrBlobData)) { if (fileOrBlobData.value) { // it is a file input // get input value and remove path to normalize name = fileOrBlobData.value.replace(/.*(\/|\\)/, ""); } else { // fix missing properties in Safari 4 and firefox 11.0a2 name = (fileOrBlobData.fileName !== null && fileOrBlobData.fileName !== undefined) ? fileOrBlobData.fileName : fileOrBlobData.name; } } else { name = fileOrBlobData.name; } return name; }, _parseFileOrBlobDataSize: function(fileOrBlobData) { var size; if (qq.isFileOrInput(fileOrBlobData)) { if (!fileOrBlobData.value){ // fix missing properties in Safari 4 and firefox 11.0a2 size = (fileOrBlobData.fileSize !== null && fileOrBlobData.fileSize !== undefined) ? fileOrBlobData.fileSize : fileOrBlobData.size; } } else { size = fileOrBlobData.blob.size; } return size; }, _getValidationDescriptor: function(fileOrBlobData) { var name, size, fileDescriptor; fileDescriptor = {}; name = this._parseFileOrBlobDataName(fileOrBlobData); size = this._parseFileOrBlobDataSize(fileOrBlobData); fileDescriptor.name = name; if (size) { fileDescriptor.size = size; } return fileDescriptor; }, _getValidationDescriptors: function(files) { var self = this, fileDescriptors = []; qq.each(files, function(idx, file) { fileDescriptors.push(self._getValidationDescriptor(file)); }); return fileDescriptors; }, _createParamsStore: function(type) { var paramsStore = {}, self = this; return { setParams: function(params, id) { var paramsCopy = {}; qq.extend(paramsCopy, params); paramsStore[id] = paramsCopy; }, getParams: function(id) { /*jshint eqeqeq: true, eqnull: true*/ var paramsCopy = {}; if (id != null && paramsStore[id]) { qq.extend(paramsCopy, paramsStore[id]); } else { qq.extend(paramsCopy, self._options[type].params); } return paramsCopy; }, remove: function(fileId) { return delete paramsStore[fileId]; }, reset: function() { paramsStore = {}; } }; }, _createEndpointStore: function(type) { var endpointStore = {}, self = this; return { setEndpoint: function(endpoint, id) { endpointStore[id] = endpoint; }, getEndpoint: function(id) { /*jshint eqeqeq: true, eqnull: true*/ if (id != null && endpointStore[id]) { return endpointStore[id]; } return self._options[type].endpoint; }, remove: function(fileId) { return delete endpointStore[fileId]; }, reset: function() { endpointStore = {}; } }; } };