diff --git a/.eslintignore b/.eslintignore index c90c8347..da0c0a2e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,3 @@ build/* dist/* node_modules/* - -js/components/ascribe_uploader/vendor/* diff --git a/js/components/ascribe_uploader/vendor/s3.fine-uploader.js b/js/components/ascribe_uploader/vendor/s3.fine-uploader.js deleted file mode 100644 index b939d655..00000000 --- a/js/components/ascribe_uploader/vendor/s3.fine-uploader.js +++ /dev/null @@ -1,14475 +0,0 @@ -/*! -* Fine Uploader -* -* Copyright 2015, Widen Enterprises, Inc. info@fineuploader.com -* -* Version: 5.3.0 -* -* Homepage: http://fineuploader.com -* -* Repository: git://github.com/FineUploader/fine-uploader.git -* -* Licensed only under the Widen Commercial License (http://fineuploader.com/licensing). -*/ - - -/*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest, Blob, Storage, ActiveXObject */ -/* jshint -W079 */ -var qq = function(element) { - "use strict"; - - return { - hide: function() { - element.style.display = "none"; - return this; - }, - - /** Returns the function which detaches attached event */ - attach: function(type, fn) { - if (element.addEventListener) { - element.addEventListener(type, fn, false); - } else if (element.attachEvent) { - element.attachEvent("on" + type, fn); - } - return function() { - qq(element).detach(type, fn); - }; - }, - - detach: function(type, fn) { - if (element.removeEventListener) { - element.removeEventListener(type, fn, false); - } else if (element.attachEvent) { - element.detachEvent("on" + type, fn); - } - return this; - }, - - contains: function(descendant) { - // The [W3C spec](http://www.w3.org/TR/domcore/#dom-node-contains) - // says a `null` (or ostensibly `undefined`) parameter - // passed into `Node.contains` should result in a false return value. - // IE7 throws an exception if the parameter is `undefined` though. - if (!descendant) { - return false; - } - - // compareposition returns false in this case - if (element === descendant) { - return true; - } - - if (element.contains) { - return element.contains(descendant); - } else { - /*jslint bitwise: true*/ - return !!(descendant.compareDocumentPosition(element) & 8); - } - }, - - /** - * Insert this element before elementB. - */ - insertBefore: function(elementB) { - elementB.parentNode.insertBefore(element, elementB); - return this; - }, - - remove: function() { - element.parentNode.removeChild(element); - return this; - }, - - /** - * Sets styles for an element. - * Fixes opacity in IE6-8. - */ - css: function(styles) { - /*jshint eqnull: true*/ - if (element.style == null) { - throw new qq.Error("Can't apply style to node as it is not on the HTMLElement prototype chain!"); - } - - /*jshint -W116*/ - if (styles.opacity != null) { - if (typeof element.style.opacity !== "string" && typeof (element.filters) !== "undefined") { - styles.filter = "alpha(opacity=" + Math.round(100 * styles.opacity) + ")"; - } - } - qq.extend(element.style, styles); - - return this; - }, - - hasClass: function(name, considerParent) { - var re = new RegExp("(^| )" + name + "( |$)"); - return re.test(element.className) || !!(considerParent && re.test(element.parentNode.className)); - }, - - addClass: function(name) { - if (!qq(element).hasClass(name)) { - element.className += " " + name; - } - return this; - }, - - removeClass: function(name) { - var re = new RegExp("(^| )" + name + "( |$)"); - element.className = element.className.replace(re, " ").replace(/^\s+|\s+$/g, ""); - return this; - }, - - getByClass: function(className) { - var candidates, - result = []; - - if (element.querySelectorAll) { - return element.querySelectorAll("." + className); - } - - candidates = element.getElementsByTagName("*"); - - qq.each(candidates, function(idx, val) { - if (qq(val).hasClass(className)) { - result.push(val); - } - }); - return result; - }, - - children: function() { - var children = [], - child = element.firstChild; - - while (child) { - if (child.nodeType === 1) { - children.push(child); - } - child = child.nextSibling; - } - - return children; - }, - - setText: function(text) { - element.innerText = text; - element.textContent = text; - return this; - }, - - clearText: function() { - return qq(element).setText(""); - }, - - // Returns true if the attribute exists on the element - // AND the value of the attribute is NOT "false" (case-insensitive) - hasAttribute: function(attrName) { - var attrVal; - - if (element.hasAttribute) { - - if (!element.hasAttribute(attrName)) { - return false; - } - - /*jshint -W116*/ - return (/^false$/i).exec(element.getAttribute(attrName)) == null; - } - else { - attrVal = element[attrName]; - - if (attrVal === undefined) { - return false; - } - - /*jshint -W116*/ - return (/^false$/i).exec(attrVal) == null; - } - } - }; -}; - -(function() { - "use strict"; - - qq.canvasToBlob = function(canvas, mime, quality) { - return qq.dataUriToBlob(canvas.toDataURL(mime, quality)); - }; - - qq.dataUriToBlob = function(dataUri) { - var arrayBuffer, byteString, - createBlob = function(data, mime) { - var BlobBuilder = window.BlobBuilder || - window.WebKitBlobBuilder || - window.MozBlobBuilder || - window.MSBlobBuilder, - blobBuilder = BlobBuilder && new BlobBuilder(); - - if (blobBuilder) { - blobBuilder.append(data); - return blobBuilder.getBlob(mime); - } - else { - return new Blob([data], {type: mime}); - } - }, - intArray, mimeString; - - // convert base64 to raw binary data held in a string - if (dataUri.split(",")[0].indexOf("base64") >= 0) { - byteString = atob(dataUri.split(",")[1]); - } - else { - byteString = decodeURI(dataUri.split(",")[1]); - } - - // extract the MIME - mimeString = dataUri.split(",")[0] - .split(":")[1] - .split(";")[0]; - - // write the bytes of the binary string to an ArrayBuffer - arrayBuffer = new ArrayBuffer(byteString.length); - intArray = new Uint8Array(arrayBuffer); - qq.each(byteString, function(idx, character) { - intArray[idx] = character.charCodeAt(0); - }); - - return createBlob(arrayBuffer, mimeString); - }; - - qq.log = function(message, level) { - if (window.console) { - if (!level || level === "info") { - window.console.log(message); - } - else - { - if (window.console[level]) { - window.console[level](message); - } - else { - window.console.log("<" + level + "> " + message); - } - } - } - }; - - qq.isObject = function(variable) { - return variable && !variable.nodeType && Object.prototype.toString.call(variable) === "[object Object]"; - }; - - qq.isFunction = function(variable) { - return typeof (variable) === "function"; - }; - - /** - * Check the type of a value. Is it an "array"? - * - * @param value value to test. - * @returns true if the value is an array or associated with an `ArrayBuffer` - */ - qq.isArray = function(value) { - return Object.prototype.toString.call(value) === "[object Array]" || - (value && window.ArrayBuffer && value.buffer && value.buffer.constructor === ArrayBuffer); - }; - - // Looks for an object on a `DataTransfer` object that is associated with drop events when utilizing the Filesystem API. - qq.isItemList = function(maybeItemList) { - return Object.prototype.toString.call(maybeItemList) === "[object DataTransferItemList]"; - }; - - // Looks for an object on a `NodeList` or an `HTMLCollection`|`HTMLFormElement`|`HTMLSelectElement` - // object that is associated with collections of Nodes. - qq.isNodeList = function(maybeNodeList) { - return Object.prototype.toString.call(maybeNodeList) === "[object NodeList]" || - // If `HTMLCollection` is the actual type of the object, we must determine this - // by checking for expected properties/methods on the object - (maybeNodeList.item && maybeNodeList.namedItem); - }; - - qq.isString = function(maybeString) { - return Object.prototype.toString.call(maybeString) === "[object String]"; - }; - - qq.trimStr = function(string) { - if (String.prototype.trim) { - return string.trim(); - } - - return string.replace(/^\s+|\s+$/g, ""); - }; - - /** - * @param str String to format. - * @returns {string} A string, swapping argument values with the associated occurrence of {} in the passed string. - */ - qq.format = function(str) { - - var args = Array.prototype.slice.call(arguments, 1), - newStr = str, - nextIdxToReplace = newStr.indexOf("{}"); - - qq.each(args, function(idx, val) { - var strBefore = newStr.substring(0, nextIdxToReplace), - strAfter = newStr.substring(nextIdxToReplace + 2); - - newStr = strBefore + val + strAfter; - nextIdxToReplace = newStr.indexOf("{}", nextIdxToReplace + val.length); - - // End the loop if we have run out of tokens (when the arguments exceed the # of tokens) - if (nextIdxToReplace < 0) { - return false; - } - }); - - return newStr; - }; - - qq.isFile = function(maybeFile) { - return window.File && Object.prototype.toString.call(maybeFile) === "[object File]"; - }; - - qq.isFileList = function(maybeFileList) { - return window.FileList && Object.prototype.toString.call(maybeFileList) === "[object FileList]"; - }; - - qq.isFileOrInput = function(maybeFileOrInput) { - return qq.isFile(maybeFileOrInput) || qq.isInput(maybeFileOrInput); - }; - - qq.isInput = function(maybeInput, notFile) { - var evaluateType = function(type) { - var normalizedType = type.toLowerCase(); - - if (notFile) { - return normalizedType !== "file"; - } - - return normalizedType === "file"; - }; - - if (window.HTMLInputElement) { - if (Object.prototype.toString.call(maybeInput) === "[object HTMLInputElement]") { - if (maybeInput.type && evaluateType(maybeInput.type)) { - return true; - } - } - } - if (maybeInput.tagName) { - if (maybeInput.tagName.toLowerCase() === "input") { - if (maybeInput.type && evaluateType(maybeInput.type)) { - return true; - } - } - } - - return false; - }; - - qq.isBlob = function(maybeBlob) { - if (window.Blob && Object.prototype.toString.call(maybeBlob) === "[object Blob]") { - return true; - } - }; - - qq.isXhrUploadSupported = function() { - var input = document.createElement("input"); - input.type = "file"; - - return ( - input.multiple !== undefined && - typeof File !== "undefined" && - typeof FormData !== "undefined" && - typeof (qq.createXhrInstance()).upload !== "undefined"); - }; - - // Fall back to ActiveX is native XHR is disabled (possible in any version of IE). - qq.createXhrInstance = function() { - if (window.XMLHttpRequest) { - return new XMLHttpRequest(); - } - - try { - return new ActiveXObject("MSXML2.XMLHTTP.3.0"); - } - catch (error) { - qq.log("Neither XHR or ActiveX are supported!", "error"); - return null; - } - }; - - qq.isFolderDropSupported = function(dataTransfer) { - return dataTransfer.items && - dataTransfer.items.length > 0 && - dataTransfer.items[0].webkitGetAsEntry; - }; - - qq.isFileChunkingSupported = function() { - return !qq.androidStock() && //Android's stock browser cannot upload Blobs correctly - qq.isXhrUploadSupported() && - (File.prototype.slice !== undefined || File.prototype.webkitSlice !== undefined || File.prototype.mozSlice !== undefined); - }; - - qq.sliceBlob = function(fileOrBlob, start, end) { - var slicer = fileOrBlob.slice || fileOrBlob.mozSlice || fileOrBlob.webkitSlice; - - return slicer.call(fileOrBlob, start, end); - }; - - qq.arrayBufferToHex = function(buffer) { - var bytesAsHex = "", - bytes = new Uint8Array(buffer); - - qq.each(bytes, function(idx, byt) { - var byteAsHexStr = byt.toString(16); - - if (byteAsHexStr.length < 2) { - byteAsHexStr = "0" + byteAsHexStr; - } - - bytesAsHex += byteAsHexStr; - }); - - return bytesAsHex; - }; - - qq.readBlobToHex = function(blob, startOffset, length) { - var initialBlob = qq.sliceBlob(blob, startOffset, startOffset + length), - fileReader = new FileReader(), - promise = new qq.Promise(); - - fileReader.onload = function() { - promise.success(qq.arrayBufferToHex(fileReader.result)); - }; - - fileReader.onerror = promise.failure; - - fileReader.readAsArrayBuffer(initialBlob); - - return promise; - }; - - qq.extend = function(first, second, extendNested) { - qq.each(second, function(prop, val) { - if (extendNested && qq.isObject(val)) { - if (first[prop] === undefined) { - first[prop] = {}; - } - qq.extend(first[prop], val, true); - } - else { - first[prop] = val; - } - }); - - return first; - }; - - /** - * Allow properties in one object to override properties in another, - * keeping track of the original values from the target object. - * - * Note that the pre-overriden properties to be overriden by the source will be passed into the `sourceFn` when it is invoked. - * - * @param target Update properties in this object from some source - * @param sourceFn A function that, when invoked, will return properties that will replace properties with the same name in the target. - * @returns {object} The target object - */ - qq.override = function(target, sourceFn) { - var super_ = {}, - source = sourceFn(super_); - - qq.each(source, function(srcPropName, srcPropVal) { - if (target[srcPropName] !== undefined) { - super_[srcPropName] = target[srcPropName]; - } - - target[srcPropName] = srcPropVal; - }); - - return target; - }; - - /** - * Searches for a given element (elt) in the array, returns -1 if it is not present. - */ - qq.indexOf = function(arr, elt, from) { - if (arr.indexOf) { - return arr.indexOf(elt, from); - } - - from = from || 0; - var len = arr.length; - - if (from < 0) { - from += len; - } - - for (; from < len; from += 1) { - if (arr.hasOwnProperty(from) && arr[from] === elt) { - return from; - } - } - return -1; - }; - - //this is a version 4 UUID - qq.getUniqueId = function() { - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { - /*jslint eqeq: true, bitwise: true*/ - var r = Math.random() * 16 | 0, v = c == "x" ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); - }; - - // - // Browsers and platforms detection - qq.ie = function() { - return navigator.userAgent.indexOf("MSIE") !== -1 || - navigator.userAgent.indexOf("Trident") !== -1; - }; - - qq.ie7 = function() { - return navigator.userAgent.indexOf("MSIE 7") !== -1; - }; - - qq.ie8 = function() { - return navigator.userAgent.indexOf("MSIE 8") !== -1; - }; - - qq.ie10 = function() { - return navigator.userAgent.indexOf("MSIE 10") !== -1; - }; - - qq.ie11 = function() { - return qq.ie() && navigator.userAgent.indexOf("rv:11") !== -1; - }; - - qq.safari = function() { - return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1; - }; - - qq.chrome = function() { - return navigator.vendor !== undefined && navigator.vendor.indexOf("Google") !== -1; - }; - - qq.opera = function() { - return navigator.vendor !== undefined && navigator.vendor.indexOf("Opera") !== -1; - }; - - qq.firefox = function() { - return (!qq.ie11() && navigator.userAgent.indexOf("Mozilla") !== -1 && navigator.vendor !== undefined && navigator.vendor === ""); - }; - - qq.windows = function() { - return navigator.platform === "Win32"; - }; - - qq.android = function() { - return navigator.userAgent.toLowerCase().indexOf("android") !== -1; - }; - - // We need to identify the Android stock browser via the UA string to work around various bugs in this browser, - // such as the one that prevents a `Blob` from being uploaded. - qq.androidStock = function() { - return qq.android() && navigator.userAgent.toLowerCase().indexOf("chrome") < 0; - }; - - qq.ios6 = function() { - return qq.ios() && navigator.userAgent.indexOf(" OS 6_") !== -1; - }; - - qq.ios7 = function() { - return qq.ios() && navigator.userAgent.indexOf(" OS 7_") !== -1; - }; - - qq.ios8 = function() { - return qq.ios() && navigator.userAgent.indexOf(" OS 8_") !== -1; - }; - - // iOS 8.0.0 - qq.ios800 = function() { - return qq.ios() && navigator.userAgent.indexOf(" OS 8_0 ") !== -1; - }; - - qq.ios = function() { - /*jshint -W014 */ - return navigator.userAgent.indexOf("iPad") !== -1 - || navigator.userAgent.indexOf("iPod") !== -1 - || navigator.userAgent.indexOf("iPhone") !== -1; - }; - - qq.iosChrome = function() { - return qq.ios() && navigator.userAgent.indexOf("CriOS") !== -1; - }; - - qq.iosSafari = function() { - return qq.ios() && !qq.iosChrome() && navigator.userAgent.indexOf("Safari") !== -1; - }; - - qq.iosSafariWebView = function() { - return qq.ios() && !qq.iosChrome() && !qq.iosSafari(); - }; - - // - // Events - - qq.preventDefault = function(e) { - if (e.preventDefault) { - e.preventDefault(); - } else { - e.returnValue = false; - } - }; - - /** - * Creates and returns element from html string - * Uses innerHTML to create an element - */ - qq.toElement = (function() { - var div = document.createElement("div"); - return function(html) { - div.innerHTML = html; - var element = div.firstChild; - div.removeChild(element); - return element; - }; - }()); - - //key and value are passed to callback for each entry in the iterable item - qq.each = function(iterableItem, callback) { - var keyOrIndex, retVal; - - if (iterableItem) { - // Iterate through [`Storage`](http://www.w3.org/TR/webstorage/#the-storage-interface) items - if (window.Storage && iterableItem.constructor === window.Storage) { - for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { - retVal = callback(iterableItem.key(keyOrIndex), iterableItem.getItem(iterableItem.key(keyOrIndex))); - if (retVal === false) { - break; - } - } - } - // `DataTransferItemList` & `NodeList` objects are array-like and should be treated as arrays - // when iterating over items inside the object. - else if (qq.isArray(iterableItem) || qq.isItemList(iterableItem) || qq.isNodeList(iterableItem)) { - for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { - retVal = callback(keyOrIndex, iterableItem[keyOrIndex]); - if (retVal === false) { - break; - } - } - } - else if (qq.isString(iterableItem)) { - for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { - retVal = callback(keyOrIndex, iterableItem.charAt(keyOrIndex)); - if (retVal === false) { - break; - } - } - } - else { - for (keyOrIndex in iterableItem) { - if (Object.prototype.hasOwnProperty.call(iterableItem, keyOrIndex)) { - retVal = callback(keyOrIndex, iterableItem[keyOrIndex]); - if (retVal === false) { - break; - } - } - } - } - } - }; - - //include any args that should be passed to the new function after the context arg - qq.bind = function(oldFunc, context) { - if (qq.isFunction(oldFunc)) { - var args = Array.prototype.slice.call(arguments, 2); - - return function() { - var newArgs = qq.extend([], args); - if (arguments.length) { - newArgs = newArgs.concat(Array.prototype.slice.call(arguments)); - } - return oldFunc.apply(context, newArgs); - }; - } - - throw new Error("first parameter must be a function!"); - }; - - /** - * obj2url() takes a json-object as argument and generates - * a querystring. pretty much like jQuery.param() - * - * how to use: - * - * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');` - * - * will result in: - * - * `http://any.url/upload?otherParam=value&a=b&c=d` - * - * @param Object JSON-Object - * @param String current querystring-part - * @return String encoded querystring - */ - qq.obj2url = function(obj, temp, prefixDone) { - /*jshint laxbreak: true*/ - var uristrings = [], - prefix = "&", - add = function(nextObj, i) { - var nextTemp = temp - ? (/\[\]$/.test(temp)) // prevent double-encoding - ? temp - : temp + "[" + i + "]" - : i; - if ((nextTemp !== "undefined") && (i !== "undefined")) { - uristrings.push( - (typeof nextObj === "object") - ? qq.obj2url(nextObj, nextTemp, true) - : (Object.prototype.toString.call(nextObj) === "[object Function]") - ? encodeURIComponent(nextTemp) + "=" + encodeURIComponent(nextObj()) - : encodeURIComponent(nextTemp) + "=" + encodeURIComponent(nextObj) - ); - } - }; - - if (!prefixDone && temp) { - prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? "" : "&" : "?"; - uristrings.push(temp); - uristrings.push(qq.obj2url(obj)); - } else if ((Object.prototype.toString.call(obj) === "[object Array]") && (typeof obj !== "undefined")) { - qq.each(obj, function(idx, val) { - add(val, idx); - }); - } else if ((typeof obj !== "undefined") && (obj !== null) && (typeof obj === "object")) { - qq.each(obj, function(prop, val) { - add(val, prop); - }); - } else { - uristrings.push(encodeURIComponent(temp) + "=" + encodeURIComponent(obj)); - } - - if (temp) { - return uristrings.join(prefix); - } else { - return uristrings.join(prefix) - .replace(/^&/, "") - .replace(/%20/g, "+"); - } - }; - - qq.obj2FormData = function(obj, formData, arrayKeyName) { - if (!formData) { - formData = new FormData(); - } - - qq.each(obj, function(key, val) { - key = arrayKeyName ? arrayKeyName + "[" + key + "]" : key; - - if (qq.isObject(val)) { - qq.obj2FormData(val, formData, key); - } - else if (qq.isFunction(val)) { - formData.append(key, val()); - } - else { - formData.append(key, val); - } - }); - - return formData; - }; - - qq.obj2Inputs = function(obj, form) { - var input; - - if (!form) { - form = document.createElement("form"); - } - - qq.obj2FormData(obj, { - append: function(key, val) { - input = document.createElement("input"); - input.setAttribute("name", key); - input.setAttribute("value", val); - form.appendChild(input); - } - }); - - return form; - }; - - /** - * Not recommended for use outside of Fine Uploader since this falls back to an unchecked eval if JSON.parse is not - * implemented. For a more secure JSON.parse polyfill, use Douglas Crockford's json2.js. - */ - qq.parseJson = function(json) { - /*jshint evil: true*/ - if (window.JSON && qq.isFunction(JSON.parse)) { - return JSON.parse(json); - } else { - return eval("(" + json + ")"); - } - }; - - /** - * Retrieve the extension of a file, if it exists. - * - * @param filename - * @returns {string || undefined} - */ - qq.getExtension = function(filename) { - var extIdx = filename.lastIndexOf(".") + 1; - - if (extIdx > 0) { - return filename.substr(extIdx, filename.length - extIdx); - } - }; - - qq.getFilename = function(blobOrFileInput) { - /*jslint regexp: true*/ - - if (qq.isInput(blobOrFileInput)) { - // get input value and remove path to normalize - return blobOrFileInput.value.replace(/.*(\/|\\)/, ""); - } - else if (qq.isFile(blobOrFileInput)) { - if (blobOrFileInput.fileName !== null && blobOrFileInput.fileName !== undefined) { - return blobOrFileInput.fileName; - } - } - - return blobOrFileInput.name; - }; - - /** - * A generic module which supports object disposing in dispose() method. - * */ - qq.DisposeSupport = function() { - var disposers = []; - - return { - /** Run all registered disposers */ - dispose: function() { - var disposer; - do { - disposer = disposers.shift(); - if (disposer) { - disposer(); - } - } - while (disposer); - }, - - /** Attach event handler and register de-attacher as a disposer */ - attach: function() { - var args = arguments; - /*jslint undef:true*/ - this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1))); - }, - - /** Add disposer to the collection */ - addDisposer: function(disposeFunction) { - disposers.push(disposeFunction); - } - }; - }; -}()); - -/* globals qq */ -/** - * Fine Uploader top-level Error container. Inherits from `Error`. - */ -(function() { - "use strict"; - - qq.Error = function(message) { - this.message = "[Fine Uploader " + qq.version + "] " + message; - }; - - qq.Error.prototype = new Error(); -}()); - -/*global qq */ -qq.version = "5.3.0"; - -/* globals qq */ -qq.supportedFeatures = (function() { - "use strict"; - - var supportsUploading, - supportsUploadingBlobs, - supportsFileDrop, - supportsAjaxFileUploading, - supportsFolderDrop, - supportsChunking, - supportsResume, - supportsUploadViaPaste, - supportsUploadCors, - supportsDeleteFileXdr, - supportsDeleteFileCorsXhr, - supportsDeleteFileCors, - supportsFolderSelection, - supportsImagePreviews, - supportsUploadProgress; - - function testSupportsFileInputElement() { - var supported = true, - tempInput; - - try { - tempInput = document.createElement("input"); - tempInput.type = "file"; - qq(tempInput).hide(); - - if (tempInput.disabled) { - supported = false; - } - } - catch (ex) { - supported = false; - } - - return supported; - } - - //only way to test for Filesystem API support since webkit does not expose the DataTransfer interface - function isChrome21OrHigher() { - return (qq.chrome() || qq.opera()) && - navigator.userAgent.match(/Chrome\/[2][1-9]|Chrome\/[3-9][0-9]/) !== undefined; - } - - //only way to test for complete Clipboard API support at this time - function isChrome14OrHigher() { - return (qq.chrome() || qq.opera()) && - navigator.userAgent.match(/Chrome\/[1][4-9]|Chrome\/[2-9][0-9]/) !== undefined; - } - - //Ensure we can send cross-origin `XMLHttpRequest`s - function isCrossOriginXhrSupported() { - if (window.XMLHttpRequest) { - var xhr = qq.createXhrInstance(); - - //Commonly accepted test for XHR CORS support. - return xhr.withCredentials !== undefined; - } - - return false; - } - - //Test for (terrible) cross-origin ajax transport fallback for IE9 and IE8 - function isXdrSupported() { - return window.XDomainRequest !== undefined; - } - - // CORS Ajax requests are supported if it is either possible to send credentialed `XMLHttpRequest`s, - // or if `XDomainRequest` is an available alternative. - function isCrossOriginAjaxSupported() { - if (isCrossOriginXhrSupported()) { - return true; - } - - return isXdrSupported(); - } - - function isFolderSelectionSupported() { - // We know that folder selection is only supported in Chrome via this proprietary attribute for now - return document.createElement("input").webkitdirectory !== undefined; - } - - function isLocalStorageSupported() { - try { - return !!window.localStorage; - } - catch (error) { - // probably caught a security exception, so no localStorage for you - return false; - } - } - - function isDragAndDropSupported() { - var span = document.createElement("span"); - - return ("draggable" in span || ("ondragstart" in span && "ondrop" in span)) && - !qq.android() && !qq.ios(); - } - - supportsUploading = testSupportsFileInputElement(); - - supportsAjaxFileUploading = supportsUploading && qq.isXhrUploadSupported(); - - supportsUploadingBlobs = supportsAjaxFileUploading && !qq.androidStock(); - - supportsFileDrop = supportsAjaxFileUploading && isDragAndDropSupported(); - - supportsFolderDrop = supportsFileDrop && isChrome21OrHigher(); - - supportsChunking = supportsAjaxFileUploading && qq.isFileChunkingSupported(); - - supportsResume = supportsAjaxFileUploading && supportsChunking && isLocalStorageSupported(); - - supportsUploadViaPaste = supportsAjaxFileUploading && isChrome14OrHigher(); - - supportsUploadCors = supportsUploading && (window.postMessage !== undefined || supportsAjaxFileUploading); - - supportsDeleteFileCorsXhr = isCrossOriginXhrSupported(); - - supportsDeleteFileXdr = isXdrSupported(); - - supportsDeleteFileCors = isCrossOriginAjaxSupported(); - - supportsFolderSelection = isFolderSelectionSupported(); - - supportsImagePreviews = supportsAjaxFileUploading && window.FileReader !== undefined; - - supportsUploadProgress = (function() { - if (supportsAjaxFileUploading) { - return !qq.androidStock() && !qq.iosChrome(); - } - return false; - }()); - - return { - ajaxUploading: supportsAjaxFileUploading, - blobUploading: supportsUploadingBlobs, - canDetermineSize: supportsAjaxFileUploading, - chunking: supportsChunking, - deleteFileCors: supportsDeleteFileCors, - deleteFileCorsXdr: supportsDeleteFileXdr, //NOTE: will also return true in IE10, where XDR is also supported - deleteFileCorsXhr: supportsDeleteFileCorsXhr, - dialogElement: !!window.HTMLDialogElement, - fileDrop: supportsFileDrop, - folderDrop: supportsFolderDrop, - folderSelection: supportsFolderSelection, - imagePreviews: supportsImagePreviews, - imageValidation: supportsImagePreviews, - itemSizeValidation: supportsAjaxFileUploading, - pause: supportsChunking, - progressBar: supportsUploadProgress, - resume: supportsResume, - scaling: supportsImagePreviews && supportsUploadingBlobs, - tiffPreviews: qq.safari(), // Not the best solution, but simple and probably accurate enough (for now) - unlimitedScaledImageSize: !qq.ios(), // false simply indicates that there is some known limit - uploading: supportsUploading, - uploadCors: supportsUploadCors, - uploadCustomHeaders: supportsAjaxFileUploading, - uploadNonMultipart: supportsAjaxFileUploading, - uploadViaPaste: supportsUploadViaPaste - }; - -}()); - -/*globals qq*/ - -// Is the passed object a promise instance? -qq.isGenericPromise = function(maybePromise) { - "use strict"; - return !!(maybePromise && maybePromise.then && qq.isFunction(maybePromise.then)); -}; - -qq.Promise = function() { - "use strict"; - - var successArgs, failureArgs, - successCallbacks = [], - failureCallbacks = [], - doneCallbacks = [], - state = 0; - - qq.extend(this, { - then: function(onSuccess, onFailure) { - if (state === 0) { - if (onSuccess) { - successCallbacks.push(onSuccess); - } - if (onFailure) { - failureCallbacks.push(onFailure); - } - } - else if (state === -1) { - onFailure && onFailure.apply(null, failureArgs); - } - else if (onSuccess) { - onSuccess.apply(null, successArgs); - } - - return this; - }, - - done: function(callback) { - if (state === 0) { - doneCallbacks.push(callback); - } - else { - callback.apply(null, failureArgs === undefined ? successArgs : failureArgs); - } - - return this; - }, - - success: function() { - state = 1; - successArgs = arguments; - - if (successCallbacks.length) { - qq.each(successCallbacks, function(idx, callback) { - callback.apply(null, successArgs); - }); - } - - if (doneCallbacks.length) { - qq.each(doneCallbacks, function(idx, callback) { - callback.apply(null, successArgs); - }); - } - - return this; - }, - - failure: function() { - state = -1; - failureArgs = arguments; - - if (failureCallbacks.length) { - qq.each(failureCallbacks, function(idx, callback) { - callback.apply(null, failureArgs); - }); - } - - if (doneCallbacks.length) { - qq.each(doneCallbacks, function(idx, callback) { - callback.apply(null, failureArgs); - }); - } - - return this; - } - }); -}; - -/* globals qq */ -/** - * Placeholder for a Blob that will be generated on-demand. - * - * @param referenceBlob Parent of the generated blob - * @param onCreate Function to invoke when the blob must be created. Must be promissory. - * @constructor - */ -qq.BlobProxy = function(referenceBlob, onCreate) { - "use strict"; - - qq.extend(this, { - referenceBlob: referenceBlob, - - create: function() { - return onCreate(referenceBlob); - } - }); -}; - -/*globals qq*/ - -/** - * This module represents an upload or "Select File(s)" button. It's job is to embed an opaque `` - * element as a child of a provided "container" element. This "container" element (`options.element`) is used to provide - * a custom style for the `` element. The ability to change the style of the container element is also - * provided here by adding CSS classes to the container on hover/focus. - * - * TODO Eliminate the mouseover and mouseout event handlers since the :hover CSS pseudo-class should now be - * available on all supported browsers. - * - * @param o Options to override the default values - */ -qq.UploadButton = function(o) { - "use strict"; - - var self = this, - - disposeSupport = new qq.DisposeSupport(), - - options = { - // "Container" element - element: null, - - // If true adds `multiple` attribute to `` - multiple: false, - - // Corresponds to the `accept` attribute on the associated `` - acceptFiles: null, - - // A true value allows folders to be selected, if supported by the UA - folders: false, - - // `name` attribute of `` - name: "qqfile", - - // Called when the browser invokes the onchange handler on the `` - onChange: function(input) {}, - - ios8BrowserCrashWorkaround: false, - - // **This option will be removed** in the future as the :hover CSS pseudo-class is available on all supported browsers - hoverClass: "qq-upload-button-hover", - - focusClass: "qq-upload-button-focus" - }, - input, buttonId; - - // Overrides any of the default option values with any option values passed in during construction. - qq.extend(options, o); - - buttonId = qq.getUniqueId(); - - // Embed an opaque `` element as a child of `options.element`. - function createInput() { - var input = document.createElement("input"); - - input.setAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME, buttonId); - input.setAttribute("title", "file input"); - - self.setMultiple(options.multiple, input); - - if (options.folders && qq.supportedFeatures.folderSelection) { - // selecting directories is only possible in Chrome now, via a vendor-specific prefixed attribute - input.setAttribute("webkitdirectory", ""); - } - - if (options.acceptFiles) { - input.setAttribute("accept", options.acceptFiles); - } - - input.setAttribute("type", "file"); - input.setAttribute("name", options.name); - - qq(input).css({ - position: "absolute", - // in Opera only 'browse' button - // is clickable and it is located at - // the right side of the input - right: 0, - top: 0, - fontFamily: "Arial", - // It's especially important to make this an arbitrarily large value - // to ensure the rendered input button in IE takes up the entire - // space of the container element. Otherwise, the left side of the - // button will require a double-click to invoke the file chooser. - // In other browsers, this might cause other issues, so a large font-size - // is only used in IE. There is a bug in IE8 where the opacity style is ignored - // in some cases when the font-size is large. So, this workaround is not applied - // to IE8. - fontSize: qq.ie() && !qq.ie8() ? "3500px" : "118px", - margin: 0, - padding: 0, - cursor: "pointer", - opacity: 0 - }); - - // Setting the file input's height to 100% in IE7 causes - // most of the visible button to be unclickable. - !qq.ie7() && qq(input).css({height: "100%"}); - - options.element.appendChild(input); - - disposeSupport.attach(input, "change", function() { - options.onChange(input); - }); - - // **These event handlers will be removed** in the future as the :hover CSS pseudo-class is available on all supported browsers - disposeSupport.attach(input, "mouseover", function() { - qq(options.element).addClass(options.hoverClass); - }); - disposeSupport.attach(input, "mouseout", function() { - qq(options.element).removeClass(options.hoverClass); - }); - - disposeSupport.attach(input, "focus", function() { - qq(options.element).addClass(options.focusClass); - }); - disposeSupport.attach(input, "blur", function() { - qq(options.element).removeClass(options.focusClass); - }); - - return input; - } - - // Make button suitable container for input - qq(options.element).css({ - position: "relative", - overflow: "hidden", - // Make sure browse button is in the right side in Internet Explorer - direction: "ltr" - }); - - // Exposed API - qq.extend(this, { - getInput: function() { - return input; - }, - - getButtonId: function() { - return buttonId; - }, - - setMultiple: function(isMultiple, optInput) { - var input = optInput || this.getInput(); - - // Temporary workaround for bug in in iOS8 UIWebView that causes the browser to crash - // before the file chooser appears if the file input doesn't contain a multiple attribute. - // See #1283. - if (options.ios8BrowserCrashWorkaround && qq.ios8() && (qq.iosChrome() || qq.iosSafariWebView())) { - input.setAttribute("multiple", ""); - } - - else { - if (isMultiple) { - input.setAttribute("multiple", ""); - } - else { - input.removeAttribute("multiple"); - } - } - }, - - setAcceptFiles: function(acceptFiles) { - if (acceptFiles !== options.acceptFiles) { - input.setAttribute("accept", acceptFiles); - } - }, - - reset: function() { - if (input.parentNode) { - qq(input).remove(); - } - - qq(options.element).removeClass(options.focusClass); - input = null; - input = createInput(); - } - }); - - input = createInput(); -}; - -qq.UploadButton.BUTTON_ID_ATTR_NAME = "qq-button-id"; - -/*globals qq */ -qq.UploadData = function(uploaderProxy) { - "use strict"; - - var data = [], - byUuid = {}, - byStatus = {}, - byProxyGroupId = {}, - byBatchId = {}; - - function getDataByIds(idOrIds) { - if (qq.isArray(idOrIds)) { - var entries = []; - - qq.each(idOrIds, function(idx, id) { - entries.push(data[id]); - }); - - return entries; - } - - return data[idOrIds]; - } - - function getDataByUuids(uuids) { - if (qq.isArray(uuids)) { - var entries = []; - - qq.each(uuids, function(idx, uuid) { - entries.push(data[byUuid[uuid]]); - }); - - return entries; - } - - return data[byUuid[uuids]]; - } - - function getDataByStatus(status) { - var statusResults = [], - statuses = [].concat(status); - - qq.each(statuses, function(index, statusEnum) { - var statusResultIndexes = byStatus[statusEnum]; - - if (statusResultIndexes !== undefined) { - qq.each(statusResultIndexes, function(i, dataIndex) { - statusResults.push(data[dataIndex]); - }); - } - }); - - return statusResults; - } - - qq.extend(this, { - /** - * Adds a new file to the data cache for tracking purposes. - * - * @param spec Data that describes this file. Possible properties are: - * - * - uuid: Initial UUID for this file. - * - name: Initial name of this file. - * - size: Size of this file, omit if this cannot be determined - * - status: Initial `qq.status` for this file. Omit for `qq.status.SUBMITTING`. - * - batchId: ID of the batch this file belongs to - * - proxyGroupId: ID of the proxy group associated with this file - * - * @returns {number} Internal ID for this file. - */ - addFile: function(spec) { - var status = spec.status || qq.status.SUBMITTING, - id = data.push({ - name: spec.name, - originalName: spec.name, - uuid: spec.uuid, - size: spec.size == null ? -1 : spec.size, - status: status - }) - 1; - - if (spec.batchId) { - data[id].batchId = spec.batchId; - - if (byBatchId[spec.batchId] === undefined) { - byBatchId[spec.batchId] = []; - } - byBatchId[spec.batchId].push(id); - } - - if (spec.proxyGroupId) { - data[id].proxyGroupId = spec.proxyGroupId; - - if (byProxyGroupId[spec.proxyGroupId] === undefined) { - byProxyGroupId[spec.proxyGroupId] = []; - } - byProxyGroupId[spec.proxyGroupId].push(id); - } - - data[id].id = id; - byUuid[spec.uuid] = id; - - if (byStatus[status] === undefined) { - byStatus[status] = []; - } - byStatus[status].push(id); - - uploaderProxy.onStatusChange(id, null, status); - - return id; - }, - - retrieve: function(optionalFilter) { - if (qq.isObject(optionalFilter) && data.length) { - if (optionalFilter.id !== undefined) { - return getDataByIds(optionalFilter.id); - } - - else if (optionalFilter.uuid !== undefined) { - return getDataByUuids(optionalFilter.uuid); - } - - else if (optionalFilter.status) { - return getDataByStatus(optionalFilter.status); - } - } - else { - return qq.extend([], data, true); - } - }, - - reset: function() { - data = []; - byUuid = {}; - byStatus = {}; - byBatchId = {}; - }, - - setStatus: function(id, newStatus) { - var oldStatus = data[id].status, - byStatusOldStatusIndex = qq.indexOf(byStatus[oldStatus], id); - - byStatus[oldStatus].splice(byStatusOldStatusIndex, 1); - - data[id].status = newStatus; - - if (byStatus[newStatus] === undefined) { - byStatus[newStatus] = []; - } - byStatus[newStatus].push(id); - - uploaderProxy.onStatusChange(id, oldStatus, newStatus); - }, - - uuidChanged: function(id, newUuid) { - var oldUuid = data[id].uuid; - - data[id].uuid = newUuid; - byUuid[newUuid] = id; - delete byUuid[oldUuid]; - }, - - updateName: function(id, newName) { - data[id].name = newName; - }, - - updateSize: function(id, newSize) { - data[id].size = newSize; - }, - - // Only applicable if this file has a parent that we may want to reference later. - setParentId: function(targetId, parentId) { - data[targetId].parentId = parentId; - }, - - getIdsInProxyGroup: function(id) { - var proxyGroupId = data[id].proxyGroupId; - - if (proxyGroupId) { - return byProxyGroupId[proxyGroupId]; - } - return []; - }, - - getIdsInBatch: function(id) { - var batchId = data[id].batchId; - - return byBatchId[batchId]; - } - }); -}; - -qq.status = { - SUBMITTING: "submitting", - SUBMITTED: "submitted", - REJECTED: "rejected", - QUEUED: "queued", - CANCELED: "canceled", - PAUSED: "paused", - UPLOADING: "uploading", - UPLOAD_RETRYING: "retrying upload", - UPLOAD_SUCCESSFUL: "upload successful", - UPLOAD_FAILED: "upload failed", - DELETE_FAILED: "delete failed", - DELETING: "deleting", - DELETED: "deleted" -}; - -/*globals qq*/ -/** - * Defines the public API for FineUploaderBasic mode. - */ -(function() { - "use strict"; - - qq.basePublicApi = { - // DEPRECATED - TODO REMOVE IN NEXT MAJOR RELEASE (replaced by addFiles) - addBlobs: function(blobDataOrArray, params, endpoint) { - this.addFiles(blobDataOrArray, params, endpoint); - }, - - addFiles: function(data, params, endpoint) { - this._maybeHandleIos8SafariWorkaround(); - - var batchId = this._storedIds.length === 0 ? qq.getUniqueId() : this._currentBatchId, - - processBlob = qq.bind(function(blob) { - this._handleNewFile({ - blob: blob, - name: this._options.blobs.defaultName - }, batchId, verifiedFiles); - }, this), - - processBlobData = qq.bind(function(blobData) { - this._handleNewFile(blobData, batchId, verifiedFiles); - }, this), - - processCanvas = qq.bind(function(canvas) { - var blob = qq.canvasToBlob(canvas); - - this._handleNewFile({ - blob: blob, - name: this._options.blobs.defaultName + ".png" - }, batchId, verifiedFiles); - }, this), - - processCanvasData = qq.bind(function(canvasData) { - var normalizedQuality = canvasData.quality && canvasData.quality / 100, - blob = qq.canvasToBlob(canvasData.canvas, canvasData.type, normalizedQuality); - - this._handleNewFile({ - blob: blob, - name: canvasData.name - }, batchId, verifiedFiles); - }, this), - - processFileOrInput = qq.bind(function(fileOrInput) { - if (qq.isInput(fileOrInput) && qq.supportedFeatures.ajaxUploading) { - var files = Array.prototype.slice.call(fileOrInput.files), - self = this; - - qq.each(files, function(idx, file) { - self._handleNewFile(file, batchId, verifiedFiles); - }); - } - else { - this._handleNewFile(fileOrInput, batchId, verifiedFiles); - } - }, this), - - normalizeData = function() { - if (qq.isFileList(data)) { - data = Array.prototype.slice.call(data); - } - data = [].concat(data); - }, - - self = this, - verifiedFiles = []; - - this._currentBatchId = batchId; - - if (data) { - normalizeData(); - - qq.each(data, function(idx, fileContainer) { - if (qq.isFileOrInput(fileContainer)) { - processFileOrInput(fileContainer); - } - else if (qq.isBlob(fileContainer)) { - processBlob(fileContainer); - } - else if (qq.isObject(fileContainer)) { - if (fileContainer.blob && fileContainer.name) { - processBlobData(fileContainer); - } - else if (fileContainer.canvas && fileContainer.name) { - processCanvasData(fileContainer); - } - } - else if (fileContainer.tagName && fileContainer.tagName.toLowerCase() === "canvas") { - processCanvas(fileContainer); - } - else { - self.log(fileContainer + " is not a valid file container! Ignoring!", "warn"); - } - }); - - this.log("Received " + verifiedFiles.length + " files."); - this._prepareItemsForUpload(verifiedFiles, params, endpoint); - } - }, - - 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(); - }, - - clearStoredFiles: function() { - this._storedIds = []; - }, - - continueUpload: function(id) { - var uploadData = this._uploadData.retrieve({id: id}); - - if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) { - return false; - } - - if (uploadData.status === qq.status.PAUSED) { - this.log(qq.format("Paused file ID {} ({}) will be continued. Not paused.", id, this.getName(id))); - this._uploadFile(id); - return true; - } - else { - this.log(qq.format("Ignoring continue for file ID {} ({}). Not paused.", id, this.getName(id)), "error"); - } - - return false; - }, - - deleteFile: function(id) { - return this._onSubmitDelete(id); - }, - - // TODO document? - doesExist: function(fileOrBlobId) { - return this._handler.isValid(fileOrBlobId); - }, - - // Generate a variable size thumbnail on an img or canvas, - // returning a promise that is fulfilled when the attempt completes. - // Thumbnail can either be based off of a URL for an image returned - // by the server in the upload response, or the associated `Blob`. - drawThumbnail: function(fileId, imgOrCanvas, maxSize, fromServer) { - var promiseToReturn = new qq.Promise(), - fileOrUrl, options; - - if (this._imageGenerator) { - fileOrUrl = this._thumbnailUrls[fileId]; - options = { - scale: maxSize > 0, - maxSize: maxSize > 0 ? maxSize : null - }; - - // If client-side preview generation is possible - // and we are not specifically looking for the image URl returned by the server... - if (!fromServer && qq.supportedFeatures.imagePreviews) { - fileOrUrl = this.getFile(fileId); - } - - /* jshint eqeqeq:false,eqnull:true */ - if (fileOrUrl == null) { - promiseToReturn.failure({container: imgOrCanvas, error: "File or URL not found."}); - } - else { - this._imageGenerator.generate(fileOrUrl, imgOrCanvas, options).then( - function success(modifiedContainer) { - promiseToReturn.success(modifiedContainer); - }, - - function failure(container, reason) { - promiseToReturn.failure({container: container, error: reason || "Problem generating thumbnail"}); - } - ); - } - } - else { - promiseToReturn.failure({container: imgOrCanvas, error: "Missing image generator module"}); - } - - return promiseToReturn; - }, - - getButton: function(fileId) { - return this._getButton(this._buttonIdsForFileIds[fileId]); - }, - - getEndpoint: function(fileId) { - return this._endpointStore.get(fileId); - }, - - getFile: function(fileOrBlobId) { - return this._handler.getFile(fileOrBlobId) || null; - }, - - getInProgress: function() { - return this._uploadData.retrieve({ - status: [ - qq.status.UPLOADING, - qq.status.UPLOAD_RETRYING, - qq.status.QUEUED - ] - }).length; - }, - - getName: function(id) { - return this._uploadData.retrieve({id: id}).name; - }, - - // Parent ID for a specific file, or null if this is the parent, or if it has no parent. - getParentId: function(id) { - var uploadDataEntry = this.getUploads({id: id}), - parentId = null; - - if (uploadDataEntry) { - if (uploadDataEntry.parentId !== undefined) { - parentId = uploadDataEntry.parentId; - } - } - - return parentId; - }, - - getResumableFilesData: function() { - return this._handler.getResumableFilesData(); - }, - - getSize: function(id) { - return this._uploadData.retrieve({id: id}).size; - }, - - getNetUploads: function() { - return this._netUploaded; - }, - - getRemainingAllowedItems: function() { - var allowedItems = this._currentItemLimit; - - if (allowedItems > 0) { - return allowedItems - this._netUploadedOrQueued; - } - - return null; - }, - - getUploads: function(optionalFilter) { - return this._uploadData.retrieve(optionalFilter); - }, - - getUuid: function(id) { - return this._uploadData.retrieve({id: id}).uuid; - }, - - log: function(str, level) { - if (this._options.debug && (!level || level === "info")) { - qq.log("[Fine Uploader " + qq.version + "] " + str); - } - else if (level && level !== "info") { - qq.log("[Fine Uploader " + qq.version + "] " + str, level); - - } - }, - - pauseUpload: function(id) { - var uploadData = this._uploadData.retrieve({id: id}); - - if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) { - return false; - } - - // Pause only really makes sense if the file is uploading or retrying - if (qq.indexOf([qq.status.UPLOADING, qq.status.UPLOAD_RETRYING], uploadData.status) >= 0) { - if (this._handler.pause(id)) { - this._uploadData.setStatus(id, qq.status.PAUSED); - return true; - } - else { - this.log(qq.format("Unable to pause file ID {} ({}).", id, this.getName(id)), "error"); - } - } - else { - this.log(qq.format("Ignoring pause for file ID {} ({}). Not in progress.", id, this.getName(id)), "error"); - } - - return false; - }, - - reset: function() { - this.log("Resetting uploader..."); - - this._handler.reset(); - this._storedIds = []; - this._autoRetries = []; - this._retryTimeouts = []; - this._preventRetries = []; - this._thumbnailUrls = []; - - qq.each(this._buttons, function(idx, button) { - button.reset(); - }); - - this._paramsStore.reset(); - this._endpointStore.reset(); - this._netUploadedOrQueued = 0; - this._netUploaded = 0; - this._uploadData.reset(); - this._buttonIdsForFileIds = []; - - this._pasteHandler && this._pasteHandler.reset(); - this._options.session.refreshOnReset && this._refreshSessionData(); - - this._succeededSinceLastAllComplete = []; - this._failedSinceLastAllComplete = []; - - this._totalProgress && this._totalProgress.reset(); - }, - - retry: function(id) { - return this._manualRetry(id); - }, - - scaleImage: function(id, specs) { - var self = this; - - return qq.Scaler.prototype.scaleImage(id, specs, { - log: qq.bind(self.log, self), - getFile: qq.bind(self.getFile, self), - uploadData: self._uploadData - }); - }, - - setCustomHeaders: function(headers, id) { - this._customHeadersStore.set(headers, id); - }, - - setDeleteFileCustomHeaders: function(headers, id) { - this._deleteFileCustomHeadersStore.set(headers, id); - }, - - setDeleteFileEndpoint: function(endpoint, id) { - this._deleteFileEndpointStore.set(endpoint, id); - }, - - setDeleteFileParams: function(params, id) { - this._deleteFileParamsStore.set(params, id); - }, - - // Re-sets the default endpoint, an endpoint for a specific file, or an endpoint for a specific button - setEndpoint: function(endpoint, id) { - this._endpointStore.set(endpoint, id); - }, - - setForm: function(elementOrId) { - this._updateFormSupportAndParams(elementOrId); - }, - - setItemLimit: function(newItemLimit) { - this._currentItemLimit = newItemLimit; - }, - - setName: function(id, newName) { - this._uploadData.updateName(id, newName); - }, - - setParams: function(params, id) { - this._paramsStore.set(params, id); - }, - - setUuid: function(id, newUuid) { - return this._uploadData.uuidChanged(id, newUuid); - }, - - uploadStoredFiles: function() { - if (this._storedIds.length === 0) { - this._itemError("noFilesError"); - } - else { - this._uploadStoredFiles(); - } - } - }; - - /** - * Defines the private (internal) API for FineUploaderBasic mode. - */ - qq.basePrivateApi = { - // Updates internal state with a file record (not backed by a live file). Returns the assigned ID. - _addCannedFile: function(sessionData) { - var id = this._uploadData.addFile({ - uuid: sessionData.uuid, - name: sessionData.name, - size: sessionData.size, - status: qq.status.UPLOAD_SUCCESSFUL - }); - - sessionData.deleteFileEndpoint && this.setDeleteFileEndpoint(sessionData.deleteFileEndpoint, id); - sessionData.deleteFileParams && this.setDeleteFileParams(sessionData.deleteFileParams, id); - - if (sessionData.thumbnailUrl) { - this._thumbnailUrls[id] = sessionData.thumbnailUrl; - } - - this._netUploaded++; - this._netUploadedOrQueued++; - - return id; - }, - - _annotateWithButtonId: function(file, associatedInput) { - if (qq.isFile(file)) { - file.qqButtonId = this._getButtonId(associatedInput); - } - }, - - _batchError: function(message) { - this._options.callbacks.onError(null, null, message, undefined); - }, - - _createDeleteHandler: function() { - var self = this; - - return new qq.DeleteFileAjaxRequester({ - method: this._options.deleteFile.method.toUpperCase(), - maxConnections: this._options.maxConnections, - uuidParamName: this._options.request.uuidName, - customHeaders: this._deleteFileCustomHeadersStore, - paramsStore: this._deleteFileParamsStore, - endpointStore: this._deleteFileEndpointStore, - cors: this._options.cors, - log: qq.bind(self.log, self), - onDelete: function(id) { - self._onDelete(id); - self._options.callbacks.onDelete(id); - }, - onDeleteComplete: function(id, xhrOrXdr, isError) { - self._onDeleteComplete(id, xhrOrXdr, isError); - self._options.callbacks.onDeleteComplete(id, xhrOrXdr, isError); - } - - }); - }, - - _createPasteHandler: function() { - var self = this; - - return new qq.PasteSupport({ - targetElement: this._options.paste.targetElement, - callbacks: { - log: qq.bind(self.log, self), - pasteReceived: function(blob) { - self._handleCheckedCallback({ - name: "onPasteReceived", - callback: qq.bind(self._options.callbacks.onPasteReceived, self, blob), - onSuccess: qq.bind(self._handlePasteSuccess, self, blob), - identifier: "pasted image" - }); - } - } - }); - }, - - _createStore: function(initialValue, _readOnlyValues_) { - var store = {}, - catchall = initialValue, - perIdReadOnlyValues = {}, - readOnlyValues = _readOnlyValues_, - copy = function(orig) { - if (qq.isObject(orig)) { - return qq.extend({}, orig); - } - return orig; - }, - getReadOnlyValues = function() { - if (qq.isFunction(readOnlyValues)) { - return readOnlyValues(); - } - return readOnlyValues; - }, - includeReadOnlyValues = function(id, existing) { - if (readOnlyValues && qq.isObject(existing)) { - qq.extend(existing, getReadOnlyValues()); - } - - if (perIdReadOnlyValues[id]) { - qq.extend(existing, perIdReadOnlyValues[id]); - } - }; - - return { - set: function(val, id) { - /*jshint eqeqeq: true, eqnull: true*/ - if (id == null) { - store = {}; - catchall = copy(val); - } - else { - store[id] = copy(val); - } - }, - - get: function(id) { - var values; - - /*jshint eqeqeq: true, eqnull: true*/ - if (id != null && store[id]) { - values = store[id]; - } - else { - values = copy(catchall); - } - - includeReadOnlyValues(id, values); - - return copy(values); - }, - - addReadOnly: function(id, values) { - // Only applicable to Object stores - if (qq.isObject(store)) { - // If null ID, apply readonly values to all files - if (id === null) { - if (qq.isFunction(values)) { - readOnlyValues = values; - } - else { - readOnlyValues = readOnlyValues || {}; - qq.extend(readOnlyValues, values); - } - } - else { - perIdReadOnlyValues[id] = perIdReadOnlyValues[id] || {}; - qq.extend(perIdReadOnlyValues[id], values); - } - } - }, - - remove: function(fileId) { - return delete store[fileId]; - }, - - reset: function() { - store = {}; - perIdReadOnlyValues = {}; - catchall = initialValue; - } - }; - }, - - _createUploadDataTracker: function() { - var self = this; - - return new qq.UploadData({ - getName: function(id) { - return self.getName(id); - }, - getUuid: function(id) { - return self.getUuid(id); - }, - getSize: function(id) { - return self.getSize(id); - }, - onStatusChange: function(id, oldStatus, newStatus) { - self._onUploadStatusChange(id, oldStatus, newStatus); - self._options.callbacks.onStatusChange(id, oldStatus, newStatus); - self._maybeAllComplete(id, newStatus); - - if (self._totalProgress) { - setTimeout(function() { - self._totalProgress.onStatusChange(id, oldStatus, newStatus); - }, 0); - } - } - }); - }, - - /** - * Generate a tracked upload button. - * - * @param spec Object containing a required `element` property - * along with optional `multiple`, `accept`, and `folders`. - * @returns {qq.UploadButton} - * @private - */ - _createUploadButton: function(spec) { - var self = this, - acceptFiles = spec.accept || this._options.validation.acceptFiles, - allowedExtensions = spec.allowedExtensions || this._options.validation.allowedExtensions, - button; - - function allowMultiple() { - if (qq.supportedFeatures.ajaxUploading) { - // Workaround for bug in iOS7+ (see #1039) - if (self._options.workarounds.iosEmptyVideos && - qq.ios() && - !qq.ios6() && - self._isAllowedExtension(allowedExtensions, ".mov")) { - - return false; - } - - if (spec.multiple === undefined) { - return self._options.multiple; - } - - return spec.multiple; - } - - return false; - } - - button = new qq.UploadButton({ - element: spec.element, - folders: spec.folders, - name: this._options.request.inputName, - multiple: allowMultiple(), - acceptFiles: acceptFiles, - onChange: function(input) { - self._onInputChange(input); - }, - hoverClass: this._options.classes.buttonHover, - focusClass: this._options.classes.buttonFocus, - ios8BrowserCrashWorkaround: this._options.workarounds.ios8BrowserCrash - }); - - this._disposeSupport.addDisposer(function() { - button.dispose(); - }); - - self._buttons.push(button); - - return button; - }, - - _createUploadHandler: function(additionalOptions, namespace) { - var self = this, - lastOnProgress = {}, - options = { - debug: this._options.debug, - maxConnections: this._options.maxConnections, - cors: this._options.cors, - paramsStore: this._paramsStore, - endpointStore: this._endpointStore, - chunking: this._options.chunking, - resume: this._options.resume, - blobs: this._options.blobs, - log: qq.bind(self.log, self), - preventRetryParam: this._options.retry.preventRetryResponseProperty, - onProgress: function(id, name, loaded, total) { - if (loaded < 0 || total < 0) { - return; - } - - if (lastOnProgress[id]) { - if (lastOnProgress[id].loaded !== loaded || lastOnProgress[id].total !== total) { - self._onProgress(id, name, loaded, total); - self._options.callbacks.onProgress(id, name, loaded, total); - } - } - else { - self._onProgress(id, name, loaded, total); - self._options.callbacks.onProgress(id, name, loaded, total); - } - - lastOnProgress[id] = {loaded: loaded, total: total}; - - }, - onComplete: function(id, name, result, xhr) { - delete lastOnProgress[id]; - - var status = self.getUploads({id: id}).status, - retVal; - - // This is to deal with some observed cases where the XHR readyStateChange handler is - // invoked by the browser multiple times for the same XHR instance with the same state - // readyState value. Higher level: don't invoke complete-related code if we've already - // done this. - if (status === qq.status.UPLOAD_SUCCESSFUL || status === qq.status.UPLOAD_FAILED) { - return; - } - - retVal = self._onComplete(id, name, result, xhr); - - // If the internal `_onComplete` handler returns a promise, don't invoke the `onComplete` callback - // until the promise has been fulfilled. - if (retVal instanceof qq.Promise) { - retVal.done(function() { - self._options.callbacks.onComplete(id, name, result, xhr); - }); - } - else { - self._options.callbacks.onComplete(id, name, result, xhr); - } - }, - onCancel: function(id, name, cancelFinalizationEffort) { - var promise = new qq.Promise(); - - self._handleCheckedCallback({ - name: "onCancel", - callback: qq.bind(self._options.callbacks.onCancel, self, id, name), - onFailure: promise.failure, - onSuccess: function() { - cancelFinalizationEffort.then(function() { - self._onCancel(id, name); - }); - - promise.success(); - }, - identifier: id - }); - - return promise; - }, - onUploadPrep: qq.bind(this._onUploadPrep, this), - onUpload: function(id, name) { - self._onUpload(id, name); - self._options.callbacks.onUpload(id, name); - }, - onUploadChunk: function(id, name, chunkData) { - self._onUploadChunk(id, chunkData); - self._options.callbacks.onUploadChunk(id, name, chunkData); - }, - onUploadChunkSuccess: function(id, chunkData, result, xhr) { - self._options.callbacks.onUploadChunkSuccess.apply(self, arguments); - }, - onResume: function(id, name, chunkData) { - return self._options.callbacks.onResume(id, name, chunkData); - }, - onAutoRetry: function(id, name, responseJSON, xhr) { - return self._onAutoRetry.apply(self, arguments); - }, - onUuidChanged: function(id, newUuid) { - self.log("Server requested UUID change from '" + self.getUuid(id) + "' to '" + newUuid + "'"); - self.setUuid(id, newUuid); - }, - getName: qq.bind(self.getName, self), - getUuid: qq.bind(self.getUuid, self), - getSize: qq.bind(self.getSize, self), - setSize: qq.bind(self._setSize, self), - getDataByUuid: function(uuid) { - return self.getUploads({uuid: uuid}); - }, - isQueued: function(id) { - var status = self.getUploads({id: id}).status; - return status === qq.status.QUEUED || - status === qq.status.SUBMITTED || - status === qq.status.UPLOAD_RETRYING || - status === qq.status.PAUSED; - }, - getIdsInProxyGroup: self._uploadData.getIdsInProxyGroup, - getIdsInBatch: self._uploadData.getIdsInBatch - }; - - qq.each(this._options.request, function(prop, val) { - options[prop] = val; - }); - - options.customHeaders = this._customHeadersStore; - - if (additionalOptions) { - qq.each(additionalOptions, function(key, val) { - options[key] = val; - }); - } - - return new qq.UploadHandlerController(options, namespace); - }, - - _fileOrBlobRejected: function(id) { - this._netUploadedOrQueued--; - this._uploadData.setStatus(id, qq.status.REJECTED); - }, - - _formatSize: function(bytes) { - var i = -1; - do { - bytes = bytes / 1000; - i++; - } while (bytes > 999); - - return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i]; - }, - - // Creates an internal object that tracks various properties of each extra button, - // and then actually creates the extra button. - _generateExtraButtonSpecs: function() { - var self = this; - - this._extraButtonSpecs = {}; - - qq.each(this._options.extraButtons, function(idx, extraButtonOptionEntry) { - var multiple = extraButtonOptionEntry.multiple, - validation = qq.extend({}, self._options.validation, true), - extraButtonSpec = qq.extend({}, extraButtonOptionEntry); - - if (multiple === undefined) { - multiple = self._options.multiple; - } - - if (extraButtonSpec.validation) { - qq.extend(validation, extraButtonOptionEntry.validation, true); - } - - qq.extend(extraButtonSpec, { - multiple: multiple, - validation: validation - }, true); - - self._initExtraButton(extraButtonSpec); - }); - }, - - _getButton: function(buttonId) { - var extraButtonsSpec = this._extraButtonSpecs[buttonId]; - - if (extraButtonsSpec) { - return extraButtonsSpec.element; - } - else if (buttonId === this._defaultButtonId) { - return this._options.button; - } - }, - - /** - * Gets the internally used tracking ID for a button. - * - * @param buttonOrFileInputOrFile `File`, ``, or a button container element - * @returns {*} The button's ID, or undefined if no ID is recoverable - * @private - */ - _getButtonId: function(buttonOrFileInputOrFile) { - var inputs, fileInput, - fileBlobOrInput = buttonOrFileInputOrFile; - - // We want the reference file/blob here if this is a proxy (a file that will be generated on-demand later) - if (fileBlobOrInput instanceof qq.BlobProxy) { - fileBlobOrInput = fileBlobOrInput.referenceBlob; - } - - // If the item is a `Blob` it will never be associated with a button or drop zone. - if (fileBlobOrInput && !qq.isBlob(fileBlobOrInput)) { - if (qq.isFile(fileBlobOrInput)) { - return fileBlobOrInput.qqButtonId; - } - else if (fileBlobOrInput.tagName.toLowerCase() === "input" && - fileBlobOrInput.type.toLowerCase() === "file") { - - return fileBlobOrInput.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME); - } - - inputs = fileBlobOrInput.getElementsByTagName("input"); - - qq.each(inputs, function(idx, input) { - if (input.getAttribute("type") === "file") { - fileInput = input; - return false; - } - }); - - if (fileInput) { - return fileInput.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME); - } - } - }, - - _getNotFinished: function() { - return this._uploadData.retrieve({ - status: [ - qq.status.UPLOADING, - qq.status.UPLOAD_RETRYING, - qq.status.QUEUED, - qq.status.SUBMITTING, - qq.status.SUBMITTED, - qq.status.PAUSED - ] - }).length; - }, - - // Get the validation options for this button. Could be the default validation option - // or a specific one assigned to this particular button. - _getValidationBase: function(buttonId) { - var extraButtonSpec = this._extraButtonSpecs[buttonId]; - - return extraButtonSpec ? extraButtonSpec.validation : this._options.validation; - }, - - _getValidationDescriptor: function(fileWrapper) { - if (fileWrapper.file instanceof qq.BlobProxy) { - return { - name: qq.getFilename(fileWrapper.file.referenceBlob), - size: fileWrapper.file.referenceBlob.size - }; - } - - return { - name: this.getUploads({id: fileWrapper.id}).name, - size: this.getUploads({id: fileWrapper.id}).size - }; - }, - - _getValidationDescriptors: function(fileWrappers) { - var self = this, - fileDescriptors = []; - - qq.each(fileWrappers, function(idx, fileWrapper) { - fileDescriptors.push(self._getValidationDescriptor(fileWrapper)); - }); - - return fileDescriptors; - }, - - // Allows camera access on either the default or an extra button for iOS devices. - _handleCameraAccess: function() { - if (this._options.camera.ios && qq.ios()) { - var acceptIosCamera = "image/*;capture=camera", - button = this._options.camera.button, - buttonId = button ? this._getButtonId(button) : this._defaultButtonId, - optionRoot = this._options; - - // If we are not targeting the default button, it is an "extra" button - if (buttonId && buttonId !== this._defaultButtonId) { - optionRoot = this._extraButtonSpecs[buttonId]; - } - - // Camera access won't work in iOS if the `multiple` attribute is present on the file input - optionRoot.multiple = false; - - // update the options - if (optionRoot.validation.acceptFiles === null) { - optionRoot.validation.acceptFiles = acceptIosCamera; - } - else { - optionRoot.validation.acceptFiles += "," + acceptIosCamera; - } - - // update the already-created button - qq.each(this._buttons, function(idx, button) { - if (button.getButtonId() === buttonId) { - button.setMultiple(optionRoot.multiple); - button.setAcceptFiles(optionRoot.acceptFiles); - - return false; - } - }); - } - }, - - _handleCheckedCallback: function(details) { - var self = this, - callbackRetVal = details.callback(); - - if (qq.isGenericPromise(callbackRetVal)) { - this.log(details.name + " - waiting for " + details.name + " promise to be fulfilled for " + details.identifier); - return callbackRetVal.then( - function(successParam) { - self.log(details.name + " promise success for " + details.identifier); - details.onSuccess(successParam); - }, - function() { - if (details.onFailure) { - self.log(details.name + " promise failure for " + details.identifier); - details.onFailure(); - } - else { - self.log(details.name + " promise failure for " + details.identifier); - } - }); - } - - if (callbackRetVal !== false) { - details.onSuccess(callbackRetVal); - } - else { - if (details.onFailure) { - this.log(details.name + " - return value was 'false' for " + details.identifier + ". Invoking failure callback."); - details.onFailure(); - } - else { - this.log(details.name + " - return value was 'false' for " + details.identifier + ". Will not proceed."); - } - } - - return callbackRetVal; - }, - - // Updates internal state when a new file has been received, and adds it along with its ID to a passed array. - _handleNewFile: function(file, batchId, newFileWrapperList) { - var self = this, - uuid = qq.getUniqueId(), - size = -1, - name = qq.getFilename(file), - actualFile = file.blob || file, - handler = this._customNewFileHandler ? - this._customNewFileHandler : - qq.bind(self._handleNewFileGeneric, self); - - if (!qq.isInput(actualFile) && actualFile.size >= 0) { - size = actualFile.size; - } - - handler(actualFile, name, uuid, size, newFileWrapperList, batchId, this._options.request.uuidName, { - uploadData: self._uploadData, - paramsStore: self._paramsStore, - addFileToHandler: function(id, file) { - self._handler.add(id, file); - self._netUploadedOrQueued++; - self._trackButton(id); - } - }); - }, - - _handleNewFileGeneric: function(file, name, uuid, size, fileList, batchId) { - var id = this._uploadData.addFile({uuid: uuid, name: name, size: size, batchId: batchId}); - - this._handler.add(id, file); - this._trackButton(id); - - this._netUploadedOrQueued++; - - fileList.push({id: id, file: file}); - }, - - _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.addFiles({ - name: name, - blob: blob - }); - }, - - // Creates an extra button element - _initExtraButton: function(spec) { - var button = this._createUploadButton({ - element: spec.element, - multiple: spec.multiple, - accept: spec.validation.acceptFiles, - folders: spec.folders, - allowedExtensions: spec.validation.allowedExtensions - }); - - this._extraButtonSpecs[button.getButtonId()] = spec; - }, - - _initFormSupportAndParams: function() { - this._formSupport = qq.FormSupport && new qq.FormSupport( - this._options.form, qq.bind(this.uploadStoredFiles, this), qq.bind(this.log, this) - ); - - if (this._formSupport && this._formSupport.attachedToForm) { - this._paramsStore = this._createStore( - this._options.request.params, this._formSupport.getFormInputsAsObject - ); - - this._options.autoUpload = this._formSupport.newAutoUpload; - if (this._formSupport.newEndpoint) { - this._options.request.endpoint = this._formSupport.newEndpoint; - } - } - else { - this._paramsStore = this._createStore(this._options.request.params); - } - }, - - _isDeletePossible: function() { - if (!qq.DeleteFileAjaxRequester || !this._options.deleteFile.enabled) { - return false; - } - - if (this._options.cors.expected) { - if (qq.supportedFeatures.deleteFileCorsXhr) { - return true; - } - - if (qq.supportedFeatures.deleteFileCorsXdr && this._options.cors.allowXdr) { - return true; - } - - return false; - } - - return true; - }, - - _isAllowedExtension: function(allowed, fileName) { - var 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; - }, - - /** - * Constructs and returns a message that describes an item/file error. Also calls `onError` callback. - * - * @param code REQUIRED - a code that corresponds to a stock message describing this type of error - * @param maybeNameOrNames names of the items that have failed, if applicable - * @param item `File`, `Blob`, or `` - * @private - */ - _itemError: function(code, maybeNameOrNames, item) { - var message = this._options.messages[code], - allowedExtensions = [], - names = [].concat(maybeNameOrNames), - name = names[0], - buttonId = this._getButtonId(item), - validationBase = this._getValidationBase(buttonId), - extensionsForMessage, placeholderMatch; - - function r(name, replacement) { message = message.replace(name, replacement); } - - qq.each(validationBase.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(validationBase.sizeLimit)); - r("{minSizeLimit}", this._formatSize(validationBase.minSizeLimit)); - - placeholderMatch = message.match(/(\{\w+\})/g); - if (placeholderMatch !== null) { - qq.each(placeholderMatch, function(idx, placeholder) { - r(placeholder, names[idx]); - }); - } - - this._options.callbacks.onError(null, name, message, undefined); - - return message; - }, - - /** - * Conditionally orders a manual retry of a failed upload. - * - * @param id File ID of the failed upload - * @param callback Optional callback to invoke if a retry is prudent. - * In lieu of asking the upload handler to retry. - * @returns {boolean} true if a manual retry will occur - * @private - */ - _manualRetry: function(id, callback) { - if (this._onBeforeManualRetry(id)) { - this._netUploadedOrQueued++; - this._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING); - - if (callback) { - callback(id); - } - else { - this._handler.retry(id); - } - - return true; - } - }, - - _maybeAllComplete: function(id, status) { - var self = this, - notFinished = this._getNotFinished(); - - if (status === qq.status.UPLOAD_SUCCESSFUL) { - this._succeededSinceLastAllComplete.push(id); - } - else if (status === qq.status.UPLOAD_FAILED) { - this._failedSinceLastAllComplete.push(id); - } - - if (notFinished === 0 && - (this._succeededSinceLastAllComplete.length || this._failedSinceLastAllComplete.length)) { - // Attempt to ensure onAllComplete is not invoked before other callbacks, such as onCancel & onComplete - setTimeout(function() { - self._onAllComplete(self._succeededSinceLastAllComplete, self._failedSinceLastAllComplete); - }, 0); - } - }, - - _maybeHandleIos8SafariWorkaround: function() { - var self = this; - - if (this._options.workarounds.ios8SafariUploads && qq.ios800() && qq.iosSafari()) { - setTimeout(function() { - window.alert(self._options.messages.unsupportedBrowserIos8Safari); - }, 0); - throw new qq.Error(this._options.messages.unsupportedBrowserIos8Safari); - } - }, - - _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 : this._options.text.defaultResponseError; - this._options.callbacks.onError(id, name, errorReason, xhr); - } - } - }, - - _maybeProcessNextItemAfterOnValidateCallback: function(validItem, items, index, params, endpoint) { - var self = this; - - if (items.length > index) { - if (validItem || !this._options.validation.stopOnFirstInvalidFile) { - //use setTimeout to prevent a stack overflow with a large number of files in the batch & non-promissory callbacks - setTimeout(function() { - var validationDescriptor = self._getValidationDescriptor(items[index]), - buttonId = self._getButtonId(items[index].file), - button = self._getButton(buttonId); - - self._handleCheckedCallback({ - name: "onValidate", - callback: qq.bind(self._options.callbacks.onValidate, self, validationDescriptor, button), - onSuccess: qq.bind(self._onValidateCallbackSuccess, self, items, index, params, endpoint), - onFailure: qq.bind(self._onValidateCallbackFailure, self, items, index, params, endpoint), - identifier: "Item '" + validationDescriptor.name + "', size: " + validationDescriptor.size - }); - }, 0); - } - else if (!validItem) { - for (; index < items.length; index++) { - self._fileOrBlobRejected(items[index].id); - } - } - } - }, - - _onAllComplete: function(successful, failed) { - this._totalProgress && this._totalProgress.onAllComplete(successful, failed, this._preventRetries); - - this._options.callbacks.onAllComplete(qq.extend([], successful), qq.extend([], failed)); - - this._succeededSinceLastAllComplete = []; - this._failedSinceLastAllComplete = []; - }, - - /** - * Attempt to automatically retry a failed upload. - * - * @param id The file ID of the failed upload - * @param name The name of the file associated with the failed upload - * @param responseJSON Response from the server, parsed into a javascript object - * @param xhr Ajax transport used to send the failed request - * @param callback Optional callback to be invoked if a retry is prudent. - * Invoked in lieu of asking the upload handler to retry. - * @returns {boolean} true if an auto-retry will occur - * @private - */ - _onAutoRetry: function(id, name, responseJSON, xhr, callback) { - var self = this; - - self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty]; - - if (self._shouldAutoRetry(id, name, responseJSON)) { - self._maybeParseAndSendUploadError.apply(self, arguments); - self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id]); - self._onBeforeAutoRetry(id, name); - - self._retryTimeouts[id] = setTimeout(function() { - self.log("Retrying " + name + "..."); - self._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING); - - if (callback) { - callback(id); - } - else { - self._handler.retry(id); - } - }, self._options.retry.autoAttemptDelay * 1000); - - return true; - } - }, - - _onBeforeAutoRetry: function(id, name) { - this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "..."); - }, - - //return false if we should not attempt the requested retry - _onBeforeManualRetry: function(id) { - var itemLimit = this._currentItemLimit, - fileName; - - if (this._preventRetries[id]) { - this.log("Retries are forbidden for id " + id, "warn"); - return false; - } - else if (this._handler.isValid(id)) { - fileName = this.getName(id); - - if (this._options.callbacks.onManualRetry(id, fileName) === false) { - return false; - } - - if (itemLimit > 0 && this._netUploadedOrQueued + 1 > itemLimit) { - this._itemError("retryFailTooManyItems"); - return false; - } - - this.log("Retrying upload for '" + fileName + "' (id: " + id + ")..."); - return true; - } - else { - this.log("'" + id + "' is not a valid file ID", "error"); - return false; - } - }, - - _onCancel: function(id, name) { - this._netUploadedOrQueued--; - - clearTimeout(this._retryTimeouts[id]); - - var storedItemIndex = qq.indexOf(this._storedIds, id); - if (!this._options.autoUpload && storedItemIndex >= 0) { - this._storedIds.splice(storedItemIndex, 1); - } - - this._uploadData.setStatus(id, qq.status.CANCELED); - }, - - _onComplete: function(id, name, result, xhr) { - if (!result.success) { - this._netUploadedOrQueued--; - this._uploadData.setStatus(id, qq.status.UPLOAD_FAILED); - - if (result[this._options.retry.preventRetryResponseProperty] === true) { - this._preventRetries[id] = true; - } - } - else { - if (result.thumbnailUrl) { - this._thumbnailUrls[id] = result.thumbnailUrl; - } - - this._netUploaded++; - this._uploadData.setStatus(id, qq.status.UPLOAD_SUCCESSFUL); - } - - this._maybeParseAndSendUploadError(id, name, result, xhr); - - return result.success ? true : false; - }, - - _onDelete: function(id) { - this._uploadData.setStatus(id, qq.status.DELETING); - }, - - _onDeleteComplete: function(id, xhrOrXdr, isError) { - var name = this.getName(id); - - if (isError) { - this._uploadData.setStatus(id, qq.status.DELETE_FAILED); - this.log("Delete request for '" + name + "' has failed.", "error"); - - // For error reporing, we only have accesss to the response status if this is not - // an `XDomainRequest`. - if (xhrOrXdr.withCredentials === undefined) { - this._options.callbacks.onError(id, name, "Delete request failed", xhrOrXdr); - } - else { - this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhrOrXdr.status, xhrOrXdr); - } - } - else { - this._netUploadedOrQueued--; - this._netUploaded--; - this._handler.expunge(id); - this._uploadData.setStatus(id, qq.status.DELETED); - this.log("Delete request for '" + name + "' has succeeded."); - } - }, - - _onInputChange: function(input) { - var fileIndex; - - if (qq.supportedFeatures.ajaxUploading) { - for (fileIndex = 0; fileIndex < input.files.length; fileIndex++) { - this._annotateWithButtonId(input.files[fileIndex], input); - } - - this.addFiles(input.files); - } - // Android 2.3.x will fire `onchange` even if no file has been selected - else if (input.value.length > 0) { - this.addFiles(input); - } - - qq.each(this._buttons, function(idx, button) { - button.reset(); - }); - }, - - _onProgress: function(id, name, loaded, total) { - this._totalProgress && this._totalProgress.onIndividualProgress(id, loaded, total); - }, - - _onSubmit: function(id, name) { - //nothing to do yet in core uploader - }, - - _onSubmitCallbackSuccess: function(id, name) { - this._onSubmit.apply(this, arguments); - this._uploadData.setStatus(id, qq.status.SUBMITTED); - this._onSubmitted.apply(this, arguments); - - if (this._options.autoUpload) { - this._options.callbacks.onSubmitted.apply(this, arguments); - this._uploadFile(id); - } - else { - this._storeForLater(id); - this._options.callbacks.onSubmitted.apply(this, arguments); - } - }, - - _onSubmitDelete: function(id, onSuccessCallback, additionalMandatedParams) { - var uuid = this.getUuid(id), - adjustedOnSuccessCallback; - - if (onSuccessCallback) { - adjustedOnSuccessCallback = qq.bind(onSuccessCallback, this, id, uuid, additionalMandatedParams); - } - - if (this._isDeletePossible()) { - this._handleCheckedCallback({ - name: "onSubmitDelete", - callback: qq.bind(this._options.callbacks.onSubmitDelete, this, id), - onSuccess: adjustedOnSuccessCallback || - qq.bind(this._deleteHandler.sendDelete, this, id, uuid, additionalMandatedParams), - identifier: id - }); - return true; - } - 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; - } - }, - - _onSubmitted: function(id) { - //nothing to do in the base uploader - }, - - _onTotalProgress: function(loaded, total) { - this._options.callbacks.onTotalProgress(loaded, total); - }, - - _onUploadPrep: function(id) { - // nothing to do in the core uploader for now - }, - - _onUpload: function(id, name) { - this._uploadData.setStatus(id, qq.status.UPLOADING); - }, - - _onUploadChunk: function(id, chunkData) { - //nothing to do in the base uploader - }, - - _onUploadStatusChange: function(id, oldStatus, newStatus) { - // Make sure a "queued" retry attempt is canceled if the upload has been paused - if (newStatus === qq.status.PAUSED) { - clearTimeout(this._retryTimeouts[id]); - } - }, - - _onValidateBatchCallbackFailure: function(fileWrappers) { - var self = this; - - qq.each(fileWrappers, function(idx, fileWrapper) { - self._fileOrBlobRejected(fileWrapper.id); - }); - }, - - _onValidateBatchCallbackSuccess: function(validationDescriptors, items, params, endpoint, button) { - var errorMessage, - itemLimit = this._currentItemLimit, - proposedNetFilesUploadedOrQueued = this._netUploadedOrQueued; - - if (itemLimit === 0 || proposedNetFilesUploadedOrQueued <= itemLimit) { - if (items.length > 0) { - this._handleCheckedCallback({ - name: "onValidate", - callback: qq.bind(this._options.callbacks.onValidate, this, validationDescriptors[0], button), - onSuccess: qq.bind(this._onValidateCallbackSuccess, this, items, 0, params, endpoint), - onFailure: qq.bind(this._onValidateCallbackFailure, this, items, 0, params, endpoint), - identifier: "Item '" + items[0].file.name + "', size: " + items[0].file.size - }); - } - else { - this._itemError("noFilesError"); - } - } - else { - this._onValidateBatchCallbackFailure(items); - errorMessage = this._options.messages.tooManyItemsError - .replace(/\{netItems\}/g, proposedNetFilesUploadedOrQueued) - .replace(/\{itemLimit\}/g, itemLimit); - this._batchError(errorMessage); - } - }, - - _onValidateCallbackFailure: function(items, index, params, endpoint) { - var nextIndex = index + 1; - - this._fileOrBlobRejected(items[index].id, items[index].file.name); - - this._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint); - }, - - _onValidateCallbackSuccess: function(items, index, params, endpoint) { - var self = this, - nextIndex = index + 1, - validationDescriptor = this._getValidationDescriptor(items[index]); - - this._validateFileOrBlobData(items[index], validationDescriptor) - .then( - function() { - self._upload(items[index].id, params, endpoint); - self._maybeProcessNextItemAfterOnValidateCallback(true, items, nextIndex, params, endpoint); - }, - function() { - self._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint); - } - ); - }, - - _prepareItemsForUpload: function(items, params, endpoint) { - if (items.length === 0) { - this._itemError("noFilesError"); - return; - } - - var validationDescriptors = this._getValidationDescriptors(items), - buttonId = this._getButtonId(items[0].file), - button = this._getButton(buttonId); - - this._handleCheckedCallback({ - name: "onValidateBatch", - callback: qq.bind(this._options.callbacks.onValidateBatch, this, validationDescriptors, button), - onSuccess: qq.bind(this._onValidateBatchCallbackSuccess, this, validationDescriptors, items, params, endpoint, button), - onFailure: qq.bind(this._onValidateBatchCallbackFailure, this, items), - identifier: "batch validation" - }); - }, - - _preventLeaveInProgress: function() { - var self = this; - - this._disposeSupport.attach(window, "beforeunload", function(e) { - if (self.getInProgress()) { - e = e || window.event; - // for ie, ff - e.returnValue = self._options.messages.onLeave; - // for webkit - return self._options.messages.onLeave; - } - }); - }, - - // Attempts to refresh session data only if the `qq.Session` module exists - // and a session endpoint has been specified. The `onSessionRequestComplete` - // callback will be invoked once the refresh is complete. - _refreshSessionData: function() { - var self = this, - options = this._options.session; - - /* jshint eqnull:true */ - if (qq.Session && this._options.session.endpoint != null) { - if (!this._session) { - qq.extend(options, this._options.cors); - - options.log = qq.bind(this.log, this); - options.addFileRecord = qq.bind(this._addCannedFile, this); - - this._session = new qq.Session(options); - } - - setTimeout(function() { - self._session.refresh().then(function(response, xhrOrXdr) { - - self._options.callbacks.onSessionRequestComplete(response, true, xhrOrXdr); - - }, function(response, xhrOrXdr) { - - self._options.callbacks.onSessionRequestComplete(response, false, xhrOrXdr); - }); - }, 0); - } - }, - - _setSize: function(id, newSize) { - this._uploadData.updateSize(id, newSize); - this._totalProgress && this._totalProgress.onNewSize(id); - }, - - _shouldAutoRetry: function(id, name, responseJSON) { - var uploadData = this._uploadData.retrieve({id: id}); - - /*jshint laxbreak: true */ - if (!this._preventRetries[id] - && this._options.retry.enableAuto - && uploadData.status !== qq.status.PAUSED) { - - if (this._autoRetries[id] === undefined) { - this._autoRetries[id] = 0; - } - - if (this._autoRetries[id] < this._options.retry.maxAutoAttempts) { - this._autoRetries[id] += 1; - return true; - } - } - - return false; - }, - - _storeForLater: function(id) { - this._storedIds.push(id); - }, - - // Maps a file with the button that was used to select it. - _trackButton: function(id) { - var buttonId; - - if (qq.supportedFeatures.ajaxUploading) { - buttonId = this._handler.getFile(id).qqButtonId; - } - else { - buttonId = this._getButtonId(this._handler.getInput(id)); - } - - if (buttonId) { - this._buttonIdsForFileIds[id] = buttonId; - } - }, - - _updateFormSupportAndParams: function(formElementOrId) { - this._options.form.element = formElementOrId; - - this._formSupport = qq.FormSupport && new qq.FormSupport( - this._options.form, qq.bind(this.uploadStoredFiles, this), qq.bind(this.log, this) - ); - - if (this._formSupport && this._formSupport.attachedToForm) { - this._paramsStore.addReadOnly(null, this._formSupport.getFormInputsAsObject); - - this._options.autoUpload = this._formSupport.newAutoUpload; - if (this._formSupport.newEndpoint) { - this.setEndpoint(this._formSupport.newEndpoint); - } - } - }, - - _upload: function(id, params, endpoint) { - var name = this.getName(id); - - if (params) { - this.setParams(params, id); - } - - if (endpoint) { - this.setEndpoint(endpoint, id); - } - - this._handleCheckedCallback({ - name: "onSubmit", - callback: qq.bind(this._options.callbacks.onSubmit, this, id, name), - onSuccess: qq.bind(this._onSubmitCallbackSuccess, this, id, name), - onFailure: qq.bind(this._fileOrBlobRejected, this, id, name), - identifier: id - }); - }, - - _uploadFile: function(id) { - if (!this._handler.upload(id)) { - this._uploadData.setStatus(id, qq.status.QUEUED); - } - }, - - _uploadStoredFiles: function() { - var idToUpload, stillSubmitting, - self = this; - - while (this._storedIds.length) { - idToUpload = this._storedIds.shift(); - this._uploadFile(idToUpload); - } - - // If we are still waiting for some files to clear validation, attempt to upload these again in a bit - stillSubmitting = this.getUploads({status: qq.status.SUBMITTING}).length; - if (stillSubmitting) { - qq.log("Still waiting for " + stillSubmitting + " files to clear submit queue. Will re-parse stored IDs array shortly."); - setTimeout(function() { - self._uploadStoredFiles(); - }, 1000); - } - }, - - /** - * Performs some internal validation checks on an item, defined in the `validation` option. - * - * @param fileWrapper Wrapper containing a `file` along with an `id` - * @param validationDescriptor Normalized information about the item (`size`, `name`). - * @returns qq.Promise with appropriate callbacks invoked depending on the validity of the file - * @private - */ - _validateFileOrBlobData: function(fileWrapper, validationDescriptor) { - var self = this, - file = (function() { - if (fileWrapper.file instanceof qq.BlobProxy) { - return fileWrapper.file.referenceBlob; - } - return fileWrapper.file; - }()), - name = validationDescriptor.name, - size = validationDescriptor.size, - buttonId = this._getButtonId(fileWrapper.file), - validationBase = this._getValidationBase(buttonId), - validityChecker = new qq.Promise(); - - validityChecker.then( - function() {}, - function() { - self._fileOrBlobRejected(fileWrapper.id, name); - }); - - if (qq.isFileOrInput(file) && !this._isAllowedExtension(validationBase.allowedExtensions, name)) { - this._itemError("typeError", name, file); - return validityChecker.failure(); - } - - if (size === 0) { - this._itemError("emptyError", name, file); - return validityChecker.failure(); - } - - if (size > 0 && validationBase.sizeLimit && size > validationBase.sizeLimit) { - this._itemError("sizeError", name, file); - return validityChecker.failure(); - } - - if (size > 0 && size < validationBase.minSizeLimit) { - this._itemError("minSizeError", name, file); - return validityChecker.failure(); - } - - if (qq.ImageValidation && qq.supportedFeatures.imagePreviews && qq.isFile(file)) { - new qq.ImageValidation(file, qq.bind(self.log, self)).validate(validationBase.image).then( - validityChecker.success, - function(errorCode) { - self._itemError(errorCode + "ImageError", name, file); - validityChecker.failure(); - } - ); - } - else { - validityChecker.success(); - } - - return validityChecker; - }, - - _wrapCallbacks: function() { - var self, safeCallback, prop; - - self = this; - - safeCallback = function(name, callback, args) { - var errorMsg; - - try { - return callback.apply(self, args); - } - catch (exception) { - errorMsg = exception.message || exception.toString(); - self.log("Caught exception in '" + name + "' callback - " + errorMsg, "error"); - } - }; - - /* jshint forin: false, loopfunc: true */ - for (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); - }; - }()); - } - } - }; -}()); - -/*globals qq*/ -(function() { - "use strict"; - - qq.FineUploaderBasic = function(o) { - var self = this; - - // These options define FineUploaderBasic mode. - this._options = { - debug: false, - button: null, - multiple: true, - maxConnections: 3, - disableCancelForFormUploads: false, - autoUpload: true, - - request: { - customHeaders: {}, - endpoint: "/server/upload", - filenameParam: "qqfilename", - forceMultipart: true, - inputName: "qqfile", - method: "POST", - params: {}, - paramsInBody: true, - totalFileSizeName: "qqtotalfilesize", - uuidName: "qquuid" - }, - - validation: { - allowedExtensions: [], - sizeLimit: 0, - minSizeLimit: 0, - itemLimit: 0, - stopOnFirstInvalidFile: true, - acceptFiles: null, - image: { - maxHeight: 0, - maxWidth: 0, - minHeight: 0, - minWidth: 0 - } - }, - - callbacks: { - onSubmit: function(id, name) {}, - onSubmitted: function(id, name) {}, - onComplete: function(id, name, responseJSON, maybeXhr) {}, - onAllComplete: function(successful, failed) {}, - onCancel: function(id, name) {}, - onUpload: function(id, name) {}, - onUploadChunk: function(id, name, chunkData) {}, - onUploadChunkSuccess: function(id, chunkData, responseJSON, xhr) {}, - onResume: function(id, fileName, chunkData) {}, - onProgress: function(id, name, loaded, total) {}, - onTotalProgress: function(loaded, total) {}, - onError: function(id, name, reason, maybeXhrOrXdr) {}, - onAutoRetry: function(id, name, attemptNumber) {}, - onManualRetry: function(id, name) {}, - onValidateBatch: function(fileOrBlobData) {}, - onValidate: function(fileOrBlobData) {}, - onSubmitDelete: function(id) {}, - onDelete: function(id) {}, - onDeleteComplete: function(id, xhrOrXdr, isError) {}, - onPasteReceived: function(blob) {}, - onStatusChange: function(id, oldStatus, newStatus) {}, - onSessionRequestComplete: function(response, success, xhrOrXdr) {} - }, - - 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}.", - maxHeightImageError: "Image is too tall.", - maxWidthImageError: "Image is too wide.", - minHeightImageError: "Image is not tall enough.", - minWidthImageError: "Image is not wide enough.", - retryFailTooManyItems: "Retry failed - you have reached your file limit.", - onLeave: "The files are being uploaded, if you leave now the upload will be canceled.", - unsupportedBrowserIos8Safari: "Unrecoverable error - this browser does not permit file uploading of any kind due to serious bugs in iOS8 Safari. Please use iOS8 Chrome until Apple fixes these issues." - }, - - retry: { - enableAuto: false, - maxAutoAttempts: 3, - autoAttemptDelay: 5, - preventRetryResponseProperty: "preventRetry" - }, - - classes: { - buttonHover: "qq-upload-button-hover", - buttonFocus: "qq-upload-button-focus" - }, - - chunking: { - enabled: false, - concurrent: { - enabled: false - }, - mandatory: false, - paramNames: { - partIndex: "qqpartindex", - partByteOffset: "qqpartbyteoffset", - chunkSize: "qqchunksize", - totalFileSize: "qqtotalfilesize", - totalParts: "qqtotalparts" - }, - partSize: 2000000, - // only relevant for traditional endpoints, only required when concurrent.enabled === true - success: { - endpoint: null - } - }, - - resume: { - enabled: false, - recordsExpireIn: 7, //days - paramNames: { - resuming: "qqresume" - } - }, - - formatFileName: function(fileOrBlobName) { - return fileOrBlobName; - }, - - text: { - defaultResponseError: "Upload failure reason unknown", - sizeSymbols: ["kB", "MB", "GB", "TB", "PB", "EB"] - }, - - deleteFile: { - enabled: false, - method: "DELETE", - endpoint: "/server/upload", - customHeaders: {}, - params: {} - }, - - cors: { - expected: false, - sendCredentials: false, - allowXdr: false - }, - - blobs: { - defaultName: "misc_data" - }, - - paste: { - targetElement: null, - defaultName: "pasted_image" - }, - - camera: { - ios: false, - - // if ios is true: button is null means target the default button, otherwise target the button specified - button: null - }, - - // This refers to additional upload buttons to be handled by Fine Uploader. - // Each element is an object, containing `element` as the only required - // property. The `element` must be a container that will ultimately - // contain an invisible `` created by Fine Uploader. - // Optional properties of each object include `multiple`, `validation`, - // and `folders`. - extraButtons: [], - - // Depends on the session module. Used to query the server for an initial file list - // during initialization and optionally after a `reset`. - session: { - endpoint: null, - params: {}, - customHeaders: {}, - refreshOnReset: true - }, - - // Send parameters associated with an existing form along with the files - form: { - // Element ID, HTMLElement, or null - element: "qq-form", - - // Overrides the base `autoUpload`, unless `element` is null. - autoUpload: false, - - // true = upload files on form submission (and squelch submit event) - interceptSubmit: true - }, - - // scale images client side, upload a new file for each scaled version - scaling: { - // send the original file as well - sendOriginal: true, - - // fox orientation for scaled images - orient: true, - - // If null, scaled image type will match reference image type. This value will be referred to - // for any size record that does not specific a type. - defaultType: null, - - defaultQuality: 80, - - failureText: "Failed to scale", - - includeExif: false, - - // metadata about each requested scaled version - sizes: [] - }, - - workarounds: { - iosEmptyVideos: true, - ios8SafariUploads: true, - ios8BrowserCrash: false - } - }; - - // Replace any default options with user defined ones - qq.extend(this._options, o, true); - - this._buttons = []; - this._extraButtonSpecs = {}; - this._buttonIdsForFileIds = []; - - this._wrapCallbacks(); - this._disposeSupport = new qq.DisposeSupport(); - - this._storedIds = []; - this._autoRetries = []; - this._retryTimeouts = []; - this._preventRetries = []; - this._thumbnailUrls = []; - - this._netUploadedOrQueued = 0; - this._netUploaded = 0; - this._uploadData = this._createUploadDataTracker(); - - this._initFormSupportAndParams(); - - this._customHeadersStore = this._createStore(this._options.request.customHeaders); - this._deleteFileCustomHeadersStore = this._createStore(this._options.deleteFile.customHeaders); - - this._deleteFileParamsStore = this._createStore(this._options.deleteFile.params); - - this._endpointStore = this._createStore(this._options.request.endpoint); - this._deleteFileEndpointStore = this._createStore(this._options.deleteFile.endpoint); - - this._handler = this._createUploadHandler(); - - this._deleteHandler = qq.DeleteFileAjaxRequester && this._createDeleteHandler(); - - if (this._options.button) { - this._defaultButtonId = this._createUploadButton({element: this._options.button}).getButtonId(); - } - - this._generateExtraButtonSpecs(); - - this._handleCameraAccess(); - - if (this._options.paste.targetElement) { - if (qq.PasteSupport) { - this._pasteHandler = this._createPasteHandler(); - } - else { - this.log("Paste support module not found", "error"); - } - } - - this._preventLeaveInProgress(); - - this._imageGenerator = qq.ImageGenerator && new qq.ImageGenerator(qq.bind(this.log, this)); - this._refreshSessionData(); - - this._succeededSinceLastAllComplete = []; - this._failedSinceLastAllComplete = []; - - this._scaler = (qq.Scaler && new qq.Scaler(this._options.scaling, qq.bind(this.log, this))) || {}; - if (this._scaler.enabled) { - this._customNewFileHandler = qq.bind(this._scaler.handleNewFile, this._scaler); - } - - if (qq.TotalProgress && qq.supportedFeatures.progressBar) { - this._totalProgress = new qq.TotalProgress( - qq.bind(this._onTotalProgress, this), - - function(id) { - var entry = self._uploadData.retrieve({id: id}); - return (entry && entry.size) || 0; - } - ); - } - - this._currentItemLimit = this._options.validation.itemLimit; - }; - - // Define the private & public API methods. - qq.FineUploaderBasic.prototype = qq.basePublicApi; - qq.extend(qq.FineUploaderBasic.prototype, qq.basePrivateApi); -}()); - -/*globals qq, XDomainRequest*/ -/** Generic class for sending non-upload ajax requests and handling the associated responses **/ -qq.AjaxRequester = function(o) { - "use strict"; - - var log, shouldParamsBeInQueryString, - queue = [], - requestData = {}, - options = { - acceptHeader: null, - validMethods: ["PATCH", "POST", "PUT"], - method: "POST", - contentType: "application/x-www-form-urlencoded", - maxConnections: 3, - customHeaders: {}, - endpointStore: {}, - paramsStore: {}, - mandatedParams: {}, - allowXRequestedWithAndCacheControl: true, - successfulResponseCodes: { - DELETE: [200, 202, 204], - PATCH: [200, 201, 202, 203, 204], - POST: [200, 201, 202, 203, 204], - PUT: [200, 201, 202, 203, 204], - GET: [200] - }, - cors: { - expected: false, - sendCredentials: false - }, - log: function(str, level) {}, - onSend: function(id) {}, - onComplete: function(id, xhrOrXdr, isError) {}, - onProgress: null - }; - - qq.extend(options, o); - log = options.log; - - if (qq.indexOf(options.validMethods, options.method) < 0) { - throw new Error("'" + options.method + "' is not a supported method for this type of request!"); - } - - // [Simple methods](http://www.w3.org/TR/cors/#simple-method) - // are defined by the W3C in the CORS spec as a list of methods that, in part, - // make a CORS request eligible to be exempt from preflighting. - function isSimpleMethod() { - return qq.indexOf(["GET", "POST", "HEAD"], options.method) >= 0; - } - - // [Simple headers](http://www.w3.org/TR/cors/#simple-header) - // are defined by the W3C in the CORS spec as a list of headers that, in part, - // make a CORS request eligible to be exempt from preflighting. - function containsNonSimpleHeaders(headers) { - var containsNonSimple = false; - - qq.each(containsNonSimple, function(idx, header) { - if (qq.indexOf(["Accept", "Accept-Language", "Content-Language", "Content-Type"], header) < 0) { - containsNonSimple = true; - return false; - } - }); - - return containsNonSimple; - } - - function isXdr(xhr) { - //The `withCredentials` test is a commonly accepted way to determine if XHR supports CORS. - return options.cors.expected && xhr.withCredentials === undefined; - } - - // Returns either a new `XMLHttpRequest` or `XDomainRequest` instance. - function getCorsAjaxTransport() { - var xhrOrXdr; - - if (window.XMLHttpRequest || window.ActiveXObject) { - xhrOrXdr = qq.createXhrInstance(); - - if (xhrOrXdr.withCredentials === undefined) { - xhrOrXdr = new XDomainRequest(); - } - } - - return xhrOrXdr; - } - - // Returns either a new XHR/XDR instance, or an existing one for the associated `File` or `Blob`. - function getXhrOrXdr(id, suppliedXhr) { - var xhrOrXdr = requestData[id].xhr; - - if (!xhrOrXdr) { - if (suppliedXhr) { - xhrOrXdr = suppliedXhr; - } - else { - if (options.cors.expected) { - xhrOrXdr = getCorsAjaxTransport(); - } - else { - xhrOrXdr = qq.createXhrInstance(); - } - } - - requestData[id].xhr = xhrOrXdr; - } - - return xhrOrXdr; - } - - // Removes element from queue, sends next request - function dequeue(id) { - var i = qq.indexOf(queue, id), - max = options.maxConnections, - nextId; - - delete requestData[id]; - queue.splice(i, 1); - - if (queue.length >= max && i < max) { - nextId = queue[max - 1]; - sendRequest(nextId); - } - } - - function onComplete(id, xdrError) { - var xhr = getXhrOrXdr(id), - method = options.method, - isError = xdrError === true; - - dequeue(id); - - if (isError) { - log(method + " request for " + id + " has failed", "error"); - } - else if (!isXdr(xhr) && !isResponseSuccessful(xhr.status)) { - isError = true; - log(method + " request for " + id + " has failed - response code " + xhr.status, "error"); - } - - options.onComplete(id, xhr, isError); - } - - function getParams(id) { - var onDemandParams = requestData[id].additionalParams, - mandatedParams = options.mandatedParams, - params; - - if (options.paramsStore.get) { - params = options.paramsStore.get(id); - } - - if (onDemandParams) { - qq.each(onDemandParams, function(name, val) { - params = params || {}; - params[name] = val; - }); - } - - if (mandatedParams) { - qq.each(mandatedParams, function(name, val) { - params = params || {}; - params[name] = val; - }); - } - - return params; - } - - function sendRequest(id, optXhr) { - var xhr = getXhrOrXdr(id, optXhr), - method = options.method, - params = getParams(id), - payload = requestData[id].payload, - url; - - options.onSend(id); - - url = createUrl(id, params); - - // XDR and XHR status detection APIs differ a bit. - if (isXdr(xhr)) { - xhr.onload = getXdrLoadHandler(id); - xhr.onerror = getXdrErrorHandler(id); - } - else { - xhr.onreadystatechange = getXhrReadyStateChangeHandler(id); - } - - registerForUploadProgress(id); - - // The last parameter is assumed to be ignored if we are actually using `XDomainRequest`. - xhr.open(method, url, true); - - // Instruct the transport to send cookies along with the CORS request, - // unless we are using `XDomainRequest`, which is not capable of this. - if (options.cors.expected && options.cors.sendCredentials && !isXdr(xhr)) { - xhr.withCredentials = true; - } - - setHeaders(id); - - log("Sending " + method + " request for " + id); - - if (payload) { - xhr.send(payload); - } - else if (shouldParamsBeInQueryString || !params) { - xhr.send(); - } - else if (params && options.contentType && options.contentType.toLowerCase().indexOf("application/x-www-form-urlencoded") >= 0) { - xhr.send(qq.obj2url(params, "")); - } - else if (params && options.contentType && options.contentType.toLowerCase().indexOf("application/json") >= 0) { - xhr.send(JSON.stringify(params)); - } - else { - xhr.send(params); - } - - return xhr; - } - - function createUrl(id, params) { - var endpoint = options.endpointStore.get(id), - addToPath = requestData[id].addToPath; - - /*jshint -W116,-W041 */ - if (addToPath != undefined) { - endpoint += "/" + addToPath; - } - - if (shouldParamsBeInQueryString && params) { - return qq.obj2url(params, endpoint); - } - else { - return endpoint; - } - } - - // Invoked by the UA to indicate a number of possible states that describe - // a live `XMLHttpRequest` transport. - function getXhrReadyStateChangeHandler(id) { - return function() { - if (getXhrOrXdr(id).readyState === 4) { - onComplete(id); - } - }; - } - - function registerForUploadProgress(id) { - var onProgress = options.onProgress; - - if (onProgress) { - getXhrOrXdr(id).upload.onprogress = function(e) { - if (e.lengthComputable) { - onProgress(id, e.loaded, e.total); - } - }; - } - } - - // This will be called by IE to indicate **success** for an associated - // `XDomainRequest` transported request. - function getXdrLoadHandler(id) { - return function() { - onComplete(id); - }; - } - - // This will be called by IE to indicate **failure** for an associated - // `XDomainRequest` transported request. - function getXdrErrorHandler(id) { - return function() { - onComplete(id, true); - }; - } - - function setHeaders(id) { - var xhr = getXhrOrXdr(id), - customHeaders = options.customHeaders, - onDemandHeaders = requestData[id].additionalHeaders || {}, - method = options.method, - allHeaders = {}; - - // If XDomainRequest is being used, we can't set headers, so just ignore this block. - if (!isXdr(xhr)) { - options.acceptHeader && xhr.setRequestHeader("Accept", options.acceptHeader); - - // Only attempt to add X-Requested-With & Cache-Control if permitted - if (options.allowXRequestedWithAndCacheControl) { - // Do not add X-Requested-With & Cache-Control if this is a cross-origin request - // OR the cross-origin request contains a non-simple method or header. - // This is done to ensure a preflight is not triggered exclusively based on the - // addition of these 2 non-simple headers. - if (!options.cors.expected || (!isSimpleMethod() || containsNonSimpleHeaders(customHeaders))) { - xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); - xhr.setRequestHeader("Cache-Control", "no-cache"); - } - } - - if (options.contentType && (method === "POST" || method === "PUT")) { - xhr.setRequestHeader("Content-Type", options.contentType); - } - - qq.extend(allHeaders, qq.isFunction(customHeaders) ? customHeaders(id) : customHeaders); - qq.extend(allHeaders, onDemandHeaders); - - qq.each(allHeaders, function(name, val) { - xhr.setRequestHeader(name, val); - }); - } - } - - function isResponseSuccessful(responseCode) { - return qq.indexOf(options.successfulResponseCodes[options.method], responseCode) >= 0; - } - - function prepareToSend(id, optXhr, addToPath, additionalParams, additionalHeaders, payload) { - requestData[id] = { - addToPath: addToPath, - additionalParams: additionalParams, - additionalHeaders: additionalHeaders, - payload: payload - }; - - var len = queue.push(id); - - // if too many active connections, wait... - if (len <= options.maxConnections) { - return sendRequest(id, optXhr); - } - } - - shouldParamsBeInQueryString = options.method === "GET" || options.method === "DELETE"; - - qq.extend(this, { - // Start the process of sending the request. The ID refers to the file associated with the request. - initTransport: function(id) { - var path, params, headers, payload, cacheBuster; - - return { - // Optionally specify the end of the endpoint path for the request. - withPath: function(appendToPath) { - path = appendToPath; - return this; - }, - - // Optionally specify additional parameters to send along with the request. - // These will be added to the query string for GET/DELETE requests or the payload - // for POST/PUT requests. The Content-Type of the request will be used to determine - // how these parameters should be formatted as well. - withParams: function(additionalParams) { - params = additionalParams; - return this; - }, - - // Optionally specify additional headers to send along with the request. - withHeaders: function(additionalHeaders) { - headers = additionalHeaders; - return this; - }, - - // Optionally specify a payload/body for the request. - withPayload: function(thePayload) { - payload = thePayload; - return this; - }, - - // Appends a cache buster (timestamp) to the request URL as a query parameter (only if GET or DELETE) - withCacheBuster: function() { - cacheBuster = true; - return this; - }, - - // Send the constructed request. - send: function(optXhr) { - if (cacheBuster && qq.indexOf(["GET", "DELETE"], options.method) >= 0) { - params.qqtimestamp = new Date().getTime(); - } - - return prepareToSend(id, optXhr, path, params, headers, payload); - } - }; - }, - - canceled: function(id) { - dequeue(id); - } - }); -}; - -/* globals qq */ -/** - * Common upload handler functions. - * - * @constructor - */ -qq.UploadHandler = function(spec) { - "use strict"; - - var proxy = spec.proxy, - fileState = {}, - onCancel = proxy.onCancel, - getName = proxy.getName; - - qq.extend(this, { - add: function(id, fileItem) { - fileState[id] = fileItem; - fileState[id].temp = {}; - }, - - cancel: function(id) { - var self = this, - cancelFinalizationEffort = new qq.Promise(), - onCancelRetVal = onCancel(id, getName(id), cancelFinalizationEffort); - - onCancelRetVal.then(function() { - if (self.isValid(id)) { - fileState[id].canceled = true; - self.expunge(id); - } - cancelFinalizationEffort.success(); - }); - }, - - expunge: function(id) { - delete fileState[id]; - }, - - getThirdPartyFileId: function(id) { - return fileState[id].key; - }, - - isValid: function(id) { - return fileState[id] !== undefined; - }, - - reset: function() { - fileState = {}; - }, - - _getFileState: function(id) { - return fileState[id]; - }, - - _setThirdPartyFileId: function(id, thirdPartyFileId) { - fileState[id].key = thirdPartyFileId; - }, - - _wasCanceled: function(id) { - return !!fileState[id].canceled; - } - }); -}; - -/*globals qq*/ -/** - * Base upload handler module. Controls more specific handlers. - * - * @param o Options. Passed along to the specific handler submodule as well. - * @param namespace [optional] Namespace for the specific handler. - */ -qq.UploadHandlerController = function(o, namespace) { - "use strict"; - - var controller = this, - chunkingPossible = false, - concurrentChunkingPossible = false, - chunking, preventRetryResponse, log, handler, - - options = { - paramsStore: {}, - maxConnections: 3, // maximum number of concurrent uploads - chunking: { - enabled: false, - multiple: { - enabled: false - } - }, - log: function(str, level) {}, - onProgress: function(id, fileName, loaded, total) {}, - onComplete: function(id, fileName, response, xhr) {}, - onCancel: function(id, fileName) {}, - onUploadPrep: function(id) {}, // Called if non-trivial operations will be performed before onUpload - onUpload: function(id, fileName) {}, - onUploadChunk: function(id, fileName, chunkData) {}, - onUploadChunkSuccess: function(id, chunkData, response, xhr) {}, - onAutoRetry: function(id, fileName, response, xhr) {}, - onResume: function(id, fileName, chunkData) {}, - onUuidChanged: function(id, newUuid) {}, - getName: function(id) {}, - setSize: function(id, newSize) {}, - isQueued: function(id) {}, - getIdsInProxyGroup: function(id) {}, - getIdsInBatch: function(id) {} - }, - - chunked = { - // Called when each chunk has uploaded successfully - done: function(id, chunkIdx, response, xhr) { - var chunkData = handler._getChunkData(id, chunkIdx); - - handler._getFileState(id).attemptingResume = false; - - delete handler._getFileState(id).temp.chunkProgress[chunkIdx]; - handler._getFileState(id).loaded += chunkData.size; - - options.onUploadChunkSuccess(id, handler._getChunkDataForCallback(chunkData), response, xhr); - }, - - // Called when all chunks have been successfully uploaded and we want to ask the handler to perform any - // logic associated with closing out the file, such as combining the chunks. - finalize: function(id) { - var size = options.getSize(id), - name = options.getName(id); - - log("All chunks have been uploaded for " + id + " - finalizing...."); - handler.finalizeChunks(id).then( - function(response, xhr) { - log("Finalize successful for " + id); - - var normaizedResponse = upload.normalizeResponse(response, true); - - options.onProgress(id, name, size, size); - handler._maybeDeletePersistedChunkData(id); - upload.cleanup(id, normaizedResponse, xhr); - }, - function(response, xhr) { - var normaizedResponse = upload.normalizeResponse(response, false); - - log("Problem finalizing chunks for file ID " + id + " - " + normaizedResponse.error, "error"); - - if (normaizedResponse.reset) { - chunked.reset(id); - } - - if (!options.onAutoRetry(id, name, normaizedResponse, xhr)) { - upload.cleanup(id, normaizedResponse, xhr); - } - } - ); - }, - - hasMoreParts: function(id) { - return !!handler._getFileState(id).chunking.remaining.length; - }, - - nextPart: function(id) { - var nextIdx = handler._getFileState(id).chunking.remaining.shift(); - - if (nextIdx >= handler._getTotalChunks(id)) { - nextIdx = null; - } - - return nextIdx; - }, - - reset: function(id) { - log("Server or callback has ordered chunking effort to be restarted on next attempt for item ID " + id, "error"); - - handler._maybeDeletePersistedChunkData(id); - handler.reevaluateChunking(id); - handler._getFileState(id).loaded = 0; - }, - - sendNext: function(id) { - var size = options.getSize(id), - name = options.getName(id), - chunkIdx = chunked.nextPart(id), - chunkData = handler._getChunkData(id, chunkIdx), - resuming = handler._getFileState(id).attemptingResume, - inProgressChunks = handler._getFileState(id).chunking.inProgress || []; - - if (handler._getFileState(id).loaded == null) { - handler._getFileState(id).loaded = 0; - } - - // Don't follow-through with the resume attempt if the integrator returns false from onResume - if (resuming && options.onResume(id, name, chunkData) === false) { - chunked.reset(id); - chunkIdx = chunked.nextPart(id); - chunkData = handler._getChunkData(id, chunkIdx); - resuming = false; - } - - // If all chunks have already uploaded successfully, we must be re-attempting the finalize step. - if (chunkIdx == null && inProgressChunks.length === 0) { - chunked.finalize(id); - } - - // Send the next chunk - else { - log("Sending chunked upload request for item " + id + ": bytes " + (chunkData.start + 1) + "-" + chunkData.end + " of " + size); - options.onUploadChunk(id, name, handler._getChunkDataForCallback(chunkData)); - - inProgressChunks.push(chunkIdx); - handler._getFileState(id).chunking.inProgress = inProgressChunks; - - if (concurrentChunkingPossible) { - connectionManager.open(id, chunkIdx); - } - - if (concurrentChunkingPossible && connectionManager.available() && handler._getFileState(id).chunking.remaining.length) { - chunked.sendNext(id); - } - - handler.uploadChunk(id, chunkIdx, resuming).then( - // upload chunk success - function success(response, xhr) { - log("Chunked upload request succeeded for " + id + ", chunk " + chunkIdx); - - handler.clearCachedChunk(id, chunkIdx); - - var inProgressChunks = handler._getFileState(id).chunking.inProgress || [], - responseToReport = upload.normalizeResponse(response, true), - inProgressChunkIdx = qq.indexOf(inProgressChunks, chunkIdx); - - log(qq.format("Chunk {} for file {} uploaded successfully.", chunkIdx, id)); - - chunked.done(id, chunkIdx, responseToReport, xhr); - - if (inProgressChunkIdx >= 0) { - inProgressChunks.splice(inProgressChunkIdx, 1); - } - - handler._maybePersistChunkedState(id); - - if (!chunked.hasMoreParts(id) && inProgressChunks.length === 0) { - chunked.finalize(id); - } - else if (chunked.hasMoreParts(id)) { - chunked.sendNext(id); - } - }, - - // upload chunk failure - function failure(response, xhr) { - log("Chunked upload request failed for " + id + ", chunk " + chunkIdx); - - handler.clearCachedChunk(id, chunkIdx); - - var responseToReport = upload.normalizeResponse(response, false), - inProgressIdx; - - if (responseToReport.reset) { - chunked.reset(id); - } - else { - inProgressIdx = qq.indexOf(handler._getFileState(id).chunking.inProgress, chunkIdx); - if (inProgressIdx >= 0) { - handler._getFileState(id).chunking.inProgress.splice(inProgressIdx, 1); - handler._getFileState(id).chunking.remaining.unshift(chunkIdx); - } - } - - // We may have aborted all other in-progress chunks for this file due to a failure. - // If so, ignore the failures associated with those aborts. - if (!handler._getFileState(id).temp.ignoreFailure) { - // If this chunk has failed, we want to ignore all other failures of currently in-progress - // chunks since they will be explicitly aborted - if (concurrentChunkingPossible) { - handler._getFileState(id).temp.ignoreFailure = true; - - qq.each(handler._getXhrs(id), function(ckid, ckXhr) { - ckXhr.abort(); - }); - - // We must indicate that all aborted chunks are no longer in progress - handler.moveInProgressToRemaining(id); - - // Free up any connections used by these chunks, but don't allow any - // other files to take up the connections (until we have exhausted all auto-retries) - connectionManager.free(id, true); - } - - if (!options.onAutoRetry(id, name, responseToReport, xhr)) { - // If one chunk fails, abort all of the others to avoid odd race conditions that occur - // if a chunk succeeds immediately after one fails before we have determined if the upload - // is a failure or not. - upload.cleanup(id, responseToReport, xhr); - } - } - } - ) - .done(function() { - if (handler._getFileState(id)) { - handler.clearXhr(id, chunkIdx); - } - }) ; - } - } - }, - - connectionManager = { - _open: [], - _openChunks: {}, - _waiting: [], - - available: function() { - var max = options.maxConnections, - openChunkEntriesCount = 0, - openChunksCount = 0; - - qq.each(connectionManager._openChunks, function(fileId, openChunkIndexes) { - openChunkEntriesCount++; - openChunksCount += openChunkIndexes.length; - }); - - return max - (connectionManager._open.length - openChunkEntriesCount + openChunksCount); - }, - - /** - * Removes element from queue, starts upload of next - */ - free: function(id, dontAllowNext) { - var allowNext = !dontAllowNext, - waitingIndex = qq.indexOf(connectionManager._waiting, id), - connectionsIndex = qq.indexOf(connectionManager._open, id), - nextId; - - delete connectionManager._openChunks[id]; - - if (upload.getProxyOrBlob(id) instanceof qq.BlobProxy) { - log("Generated blob upload has ended for " + id + ", disposing generated blob."); - delete handler._getFileState(id).file; - } - - // If this file was not consuming a connection, it was just waiting, so remove it from the waiting array - if (waitingIndex >= 0) { - connectionManager._waiting.splice(waitingIndex, 1); - } - // If this file was consuming a connection, allow the next file to be uploaded - else if (allowNext && connectionsIndex >= 0) { - connectionManager._open.splice(connectionsIndex, 1); - - nextId = connectionManager._waiting.shift(); - if (nextId >= 0) { - connectionManager._open.push(nextId); - upload.start(nextId); - } - } - }, - - getWaitingOrConnected: function() { - var waitingOrConnected = []; - - // Chunked files may have multiple connections open per chunk (if concurrent chunking is enabled) - // We need to grab the file ID of any file that has at least one chunk consuming a connection. - qq.each(connectionManager._openChunks, function(fileId, chunks) { - if (chunks && chunks.length) { - waitingOrConnected.push(parseInt(fileId)); - } - }); - - // For non-chunked files, only one connection will be consumed per file. - // This is where we aggregate those file IDs. - qq.each(connectionManager._open, function(idx, fileId) { - if (!connectionManager._openChunks[fileId]) { - waitingOrConnected.push(parseInt(fileId)); - } - }); - - // There may be files waiting for a connection. - waitingOrConnected = waitingOrConnected.concat(connectionManager._waiting); - - return waitingOrConnected; - }, - - isUsingConnection: function(id) { - return qq.indexOf(connectionManager._open, id) >= 0; - }, - - open: function(id, chunkIdx) { - if (chunkIdx == null) { - connectionManager._waiting.push(id); - } - - if (connectionManager.available()) { - if (chunkIdx == null) { - connectionManager._waiting.pop(); - connectionManager._open.push(id); - } - else { - (function() { - var openChunksEntry = connectionManager._openChunks[id] || []; - openChunksEntry.push(chunkIdx); - connectionManager._openChunks[id] = openChunksEntry; - }()); - } - - return true; - } - - return false; - }, - - reset: function() { - connectionManager._waiting = []; - connectionManager._open = []; - } - }, - - simple = { - send: function(id, name) { - handler._getFileState(id).loaded = 0; - - log("Sending simple upload request for " + id); - handler.uploadFile(id).then( - function(response, optXhr) { - log("Simple upload request succeeded for " + id); - - var responseToReport = upload.normalizeResponse(response, true), - size = options.getSize(id); - - options.onProgress(id, name, size, size); - upload.maybeNewUuid(id, responseToReport); - upload.cleanup(id, responseToReport, optXhr); - }, - - function(response, optXhr) { - log("Simple upload request failed for " + id); - - var responseToReport = upload.normalizeResponse(response, false); - - if (!options.onAutoRetry(id, name, responseToReport, optXhr)) { - upload.cleanup(id, responseToReport, optXhr); - } - } - ); - } - }, - - upload = { - cancel: function(id) { - log("Cancelling " + id); - options.paramsStore.remove(id); - connectionManager.free(id); - }, - - cleanup: function(id, response, optXhr) { - var name = options.getName(id); - - options.onComplete(id, name, response, optXhr); - - if (handler._getFileState(id)) { - handler._clearXhrs && handler._clearXhrs(id); - } - - connectionManager.free(id); - }, - - // Returns a qq.BlobProxy, or an actual File/Blob if no proxy is involved, or undefined - // if none of these are available for the ID - getProxyOrBlob: function(id) { - return (handler.getProxy && handler.getProxy(id)) || - (handler.getFile && handler.getFile(id)); - }, - - initHandler: function() { - var handlerType = namespace ? qq[namespace] : qq.traditional, - handlerModuleSubtype = qq.supportedFeatures.ajaxUploading ? "Xhr" : "Form"; - - handler = new handlerType[handlerModuleSubtype + "UploadHandler"]( - options, - { - getDataByUuid: options.getDataByUuid, - getName: options.getName, - getSize: options.getSize, - getUuid: options.getUuid, - log: log, - onCancel: options.onCancel, - onProgress: options.onProgress, - onUuidChanged: options.onUuidChanged - } - ); - - if (handler._removeExpiredChunkingRecords) { - handler._removeExpiredChunkingRecords(); - } - }, - - isDeferredEligibleForUpload: function(id) { - return options.isQueued(id); - }, - - // For Blobs that are part of a group of generated images, along with a reference image, - // this will ensure the blobs in the group are uploaded in the order they were triggered, - // even if some async processing must be completed on one or more Blobs first. - maybeDefer: function(id, blob) { - // If we don't have a file/blob yet & no file/blob exists for this item, request it, - // and then submit the upload to the specific handler once the blob is available. - // ASSUMPTION: This condition will only ever be true if XHR uploading is supported. - if (blob && !handler.getFile(id) && blob instanceof qq.BlobProxy) { - - // Blob creation may take some time, so the caller may want to update the - // UI to indicate that an operation is in progress, even before the actual - // upload begins and an onUpload callback is invoked. - options.onUploadPrep(id); - - log("Attempting to generate a blob on-demand for " + id); - blob.create().then(function(generatedBlob) { - log("Generated an on-demand blob for " + id); - - // Update record associated with this file by providing the generated Blob - handler.updateBlob(id, generatedBlob); - - // Propagate the size for this generated Blob - options.setSize(id, generatedBlob.size); - - // Order handler to recalculate chunking possibility, if applicable - handler.reevaluateChunking(id); - - upload.maybeSendDeferredFiles(id); - }, - - // Blob could not be generated. Fail the upload & attempt to prevent retries. Also bubble error message. - function(errorMessage) { - var errorResponse = {}; - - if (errorMessage) { - errorResponse.error = errorMessage; - } - - log(qq.format("Failed to generate blob for ID {}. Error message: {}.", id, errorMessage), "error"); - - options.onComplete(id, options.getName(id), qq.extend(errorResponse, preventRetryResponse), null); - upload.maybeSendDeferredFiles(id); - connectionManager.free(id); - }); - } - else { - return upload.maybeSendDeferredFiles(id); - } - - return false; - }, - - // Upload any grouped blobs, in the proper order, that are ready to be uploaded - maybeSendDeferredFiles: function(id) { - var idsInGroup = options.getIdsInProxyGroup(id), - uploadedThisId = false; - - if (idsInGroup && idsInGroup.length) { - log("Maybe ready to upload proxy group file " + id); - - qq.each(idsInGroup, function(idx, idInGroup) { - if (upload.isDeferredEligibleForUpload(idInGroup) && !!handler.getFile(idInGroup)) { - uploadedThisId = idInGroup === id; - upload.now(idInGroup); - } - else if (upload.isDeferredEligibleForUpload(idInGroup)) { - return false; - } - }); - } - else { - uploadedThisId = true; - upload.now(id); - } - - return uploadedThisId; - }, - - maybeNewUuid: function(id, response) { - if (response.newUuid !== undefined) { - options.onUuidChanged(id, response.newUuid); - } - }, - - // The response coming from handler implementations may be in various formats. - // Instead of hoping a promise nested 5 levels deep will always return an object - // as its first param, let's just normalize the response here. - normalizeResponse: function(originalResponse, successful) { - var response = originalResponse; - - // The passed "response" param may not be a response at all. - // It could be a string, detailing the error, for example. - if (!qq.isObject(originalResponse)) { - response = {}; - - if (qq.isString(originalResponse) && !successful) { - response.error = originalResponse; - } - } - - response.success = successful; - - return response; - }, - - now: function(id) { - var name = options.getName(id); - - if (!controller.isValid(id)) { - throw new qq.Error(id + " is not a valid file ID to upload!"); - } - - options.onUpload(id, name); - - if (chunkingPossible && handler._shouldChunkThisFile(id)) { - chunked.sendNext(id); - } - else { - simple.send(id, name); - } - }, - - start: function(id) { - var blobToUpload = upload.getProxyOrBlob(id); - - if (blobToUpload) { - return upload.maybeDefer(id, blobToUpload); - } - else { - upload.now(id); - return true; - } - } - }; - - qq.extend(this, { - /** - * Adds file or file input to the queue - **/ - add: function(id, file) { - handler.add.apply(this, arguments); - }, - - /** - * Sends the file identified by id - */ - upload: function(id) { - if (connectionManager.open(id)) { - return upload.start(id); - } - return false; - }, - - retry: function(id) { - // On retry, if concurrent chunking has been enabled, we may have aborted all other in-progress chunks - // for a file when encountering a failed chunk upload. We then signaled the controller to ignore - // all failures associated with these aborts. We are now retrying, so we don't want to ignore - // any more failures at this point. - if (concurrentChunkingPossible) { - handler._getFileState(id).temp.ignoreFailure = false; - } - - // If we are attempting to retry a file that is already consuming a connection, this is likely an auto-retry. - // Just go ahead and ask the handler to upload again. - if (connectionManager.isUsingConnection(id)) { - return upload.start(id); - } - - // If we are attempting to retry a file that is not currently consuming a connection, - // this is likely a manual retry attempt. We will need to ensure a connection is available - // before the retry commences. - else { - return controller.upload(id); - } - }, - - /** - * Cancels file upload by id - */ - cancel: function(id) { - var cancelRetVal = handler.cancel(id); - - if (qq.isGenericPromise(cancelRetVal)) { - cancelRetVal.then(function() { - upload.cancel(id); - }); - } - else if (cancelRetVal !== false) { - upload.cancel(id); - } - }, - - /** - * Cancels all queued or in-progress uploads - */ - cancelAll: function() { - var waitingOrConnected = connectionManager.getWaitingOrConnected(), - i; - - // ensure files are cancelled in reverse order which they were added - // to avoid a flash of time where a queued file begins to upload before it is canceled - if (waitingOrConnected.length) { - for (i = waitingOrConnected.length - 1; i >= 0; i--) { - controller.cancel(waitingOrConnected[i]); - } - } - - connectionManager.reset(); - }, - - // Returns a File, Blob, or the Blob/File for the reference/parent file if the targeted blob is a proxy. - // Undefined if no file record is available. - getFile: function(id) { - if (handler.getProxy && handler.getProxy(id)) { - return handler.getProxy(id).referenceBlob; - } - - return handler.getFile && handler.getFile(id); - }, - - // Returns true if the Blob associated with the ID is related to a proxy s - isProxied: function(id) { - return !!(handler.getProxy && handler.getProxy(id)); - }, - - getInput: function(id) { - if (handler.getInput) { - return handler.getInput(id); - } - }, - - reset: function() { - log("Resetting upload handler"); - controller.cancelAll(); - connectionManager.reset(); - handler.reset(); - }, - - expunge: function(id) { - if (controller.isValid(id)) { - return handler.expunge(id); - } - }, - - /** - * Determine if the file exists. - */ - isValid: function(id) { - return handler.isValid(id); - }, - - getResumableFilesData: function() { - if (handler.getResumableFilesData) { - return handler.getResumableFilesData(); - } - return []; - }, - - /** - * This may or may not be implemented, depending on the handler. For handlers where a third-party ID is - * available (such as the "key" for Amazon S3), this will return that value. Otherwise, the return value - * will be undefined. - * - * @param id Internal file ID - * @returns {*} Some identifier used by a 3rd-party service involved in the upload process - */ - getThirdPartyFileId: function(id) { - if (controller.isValid(id)) { - return handler.getThirdPartyFileId(id); - } - }, - - /** - * Attempts to pause the associated upload if the specific handler supports this and the file is "valid". - * @param id ID of the upload/file to pause - * @returns {boolean} true if the upload was paused - */ - pause: function(id) { - if (controller.isResumable(id) && handler.pause && controller.isValid(id) && handler.pause(id)) { - connectionManager.free(id); - handler.moveInProgressToRemaining(id); - return true; - } - return false; - }, - - // True if the file is eligible for pause/resume. - isResumable: function(id) { - return !!handler.isResumable && handler.isResumable(id); - } - }); - - qq.extend(options, o); - log = options.log; - chunkingPossible = options.chunking.enabled && qq.supportedFeatures.chunking; - concurrentChunkingPossible = chunkingPossible && options.chunking.concurrent.enabled; - - preventRetryResponse = (function() { - var response = {}; - - response[options.preventRetryParam] = true; - - return response; - }()); - - upload.initHandler(); -}; - -/* globals qq */ -/** - * Common APIs exposed to creators of upload via form/iframe handlers. This is reused and possibly overridden - * in some cases by specific form upload handlers. - * - * @constructor - */ -qq.FormUploadHandler = function(spec) { - "use strict"; - - var options = spec.options, - handler = this, - proxy = spec.proxy, - formHandlerInstanceId = qq.getUniqueId(), - onloadCallbacks = {}, - detachLoadEvents = {}, - postMessageCallbackTimers = {}, - isCors = options.isCors, - inputName = options.inputName, - getUuid = proxy.getUuid, - log = proxy.log, - corsMessageReceiver = new qq.WindowReceiveMessage({log: log}); - - /** - * Remove any trace of the file from the handler. - * - * @param id ID of the associated file - */ - function expungeFile(id) { - delete detachLoadEvents[id]; - - // If we are dealing with CORS, we might still be waiting for a response from a loaded iframe. - // In that case, terminate the timer waiting for a message from the loaded iframe - // and stop listening for any more messages coming from this iframe. - if (isCors) { - clearTimeout(postMessageCallbackTimers[id]); - delete postMessageCallbackTimers[id]; - corsMessageReceiver.stopReceivingMessages(id); - } - - var iframe = document.getElementById(handler._getIframeName(id)); - if (iframe) { - // To cancel request set src to something else. We use src="javascript:false;" - // because it doesn't trigger ie6 prompt on https - /* jshint scripturl:true */ - iframe.setAttribute("src", "javascript:false;"); - - qq(iframe).remove(); - } - } - - /** - * @param iframeName `document`-unique Name of the associated iframe - * @returns {*} ID of the associated file - */ - function getFileIdForIframeName(iframeName) { - return iframeName.split("_")[0]; - } - - /** - * Generates an iframe to be used as a target for upload-related form submits. This also adds the iframe - * to the current `document`. Note that the iframe is hidden from view. - * - * @param name Name of the iframe. - * @returns {HTMLIFrameElement} The created iframe - */ - function initIframeForUpload(name) { - var iframe = qq.toElement("