1/* 2 * jQuery File Upload Plugin 3 * https://github.com/blueimp/jQuery-File-Upload 4 * 5 * © Sebastian Tschan 6 * https://blueimp.net 7 * 8 * Licensed under the MIT license: 9 * https://opensource.org/licenses/MIT 10 */ 11 12/* global define, require */ 13/* eslint-disable new-cap */ 14 15(function (factory) { 16 'use strict'; 17 if (typeof define === 'function' && define.amd) { 18 // Register as an anonymous AMD module: 19 define(['jquery', 'jquery-ui/ui/widget'], factory); 20 } else if (typeof exports === 'object') { 21 // Node/CommonJS: 22 factory(require('jquery'), require('./vendor/jquery.ui.widget')); 23 } else { 24 // Browser globals: 25 factory(window.jQuery); 26 } 27})(function ($) { 28 'use strict'; 29 30 // Detect file input support, based on 31 // https://viljamis.com/2012/file-upload-support-on-mobile/ 32 $.support.fileInput = !( 33 new RegExp( 34 // Handle devices which give false positives for the feature detection: 35 '(Android (1\\.[0156]|2\\.[01]))' + 36 '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + 37 '|(w(eb)?OSBrowser)|(webOS)' + 38 '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' 39 ).test(window.navigator.userAgent) || 40 // Feature detection for all other devices: 41 $('<input type="file"/>').prop('disabled') 42 ); 43 44 // The FileReader API is not actually used, but works as feature detection, 45 // as some Safari versions (5?) support XHR file uploads via the FormData API, 46 // but not non-multipart XHR file uploads. 47 // window.XMLHttpRequestUpload is not available on IE10, so we check for 48 // window.ProgressEvent instead to detect XHR2 file upload capability: 49 $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); 50 $.support.xhrFormDataFileUpload = !!window.FormData; 51 52 // Detect support for Blob slicing (required for chunked uploads): 53 $.support.blobSlice = 54 window.Blob && 55 (Blob.prototype.slice || 56 Blob.prototype.webkitSlice || 57 Blob.prototype.mozSlice); 58 59 /** 60 * Helper function to create drag handlers for dragover/dragenter/dragleave 61 * 62 * @param {string} type Event type 63 * @returns {Function} Drag handler 64 */ 65 function getDragHandler(type) { 66 var isDragOver = type === 'dragover'; 67 return function (e) { 68 e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; 69 var dataTransfer = e.dataTransfer; 70 if ( 71 dataTransfer && 72 $.inArray('Files', dataTransfer.types) !== -1 && 73 this._trigger(type, $.Event(type, { delegatedEvent: e })) !== false 74 ) { 75 e.preventDefault(); 76 if (isDragOver) { 77 dataTransfer.dropEffect = 'copy'; 78 } 79 } 80 }; 81 } 82 83 // The fileupload widget listens for change events on file input fields defined 84 // via fileInput setting and paste or drop events of the given dropZone. 85 // In addition to the default jQuery Widget methods, the fileupload widget 86 // exposes the "add" and "send" methods, to add or directly send files using 87 // the fileupload API. 88 // By default, files added via file input selection, paste, drag & drop or 89 // "add" method are uploaded immediately, but it is possible to override 90 // the "add" callback option to queue file uploads. 91 $.widget('blueimp.fileupload', { 92 options: { 93 // The drop target element(s), by the default the complete document. 94 // Set to null to disable drag & drop support: 95 dropZone: $(document), 96 // The paste target element(s), by the default undefined. 97 // Set to a DOM node or jQuery object to enable file pasting: 98 pasteZone: undefined, 99 // The file input field(s), that are listened to for change events. 100 // If undefined, it is set to the file input fields inside 101 // of the widget element on plugin initialization. 102 // Set to null to disable the change listener. 103 fileInput: undefined, 104 // By default, the file input field is replaced with a clone after 105 // each input field change event. This is required for iframe transport 106 // queues and allows change events to be fired for the same file 107 // selection, but can be disabled by setting the following option to fal
107se: 108 replaceFileInput: true, 109 // The parameter name for the file form data (the request argument name). 110 // If undefined or empty, the name property of the file input field is 111 // used, or "files[]" if the file input name property is also empty, 112 // can be a string or an array of strings: 113 paramName: undefined, 114 // By default, each file of a selection is uploaded using an individual 115 // request for XHR type uploads. Set to false to upload file 116 // selections in one request each: 117 singleFileUploads: true, 118 // To limit the number of files uploaded with one XHR request, 119 // set the following option to an integer greater than 0: 120 limitMultiFileUploads: undefined, 121 // The following option limits the number of files uploaded with one 122 // XHR request to keep the request size under or equal to the defined 123 // limit in bytes: 124 limitMultiFileUploadSize: undefined, 125 // Multipart file uploads add a number of bytes to each uploaded file, 126 // therefore the following option adds an overhead for each file used 127 // in the limitMultiFileUploadSize configuration: 128 limitMultiFileUploadSizeOverhead: 512, 129 // Set the following option to true to issue all file upload requests 130 // in a sequential order: 131 sequentialUploads: false, 132 // To limit the number of concurrent uploads, 133 // set the following option to an integer greater than 0: 134 limitConcurrentUploads: undefined, 135 // Set the following option to true to force iframe transport uploads: 136 forceIframeTransport: false, 137 // Set the following option to the location of a redirect url on the 138 // origin server, for cross-domain iframe transport uploads: 139 redirect: undefined, 140 // The parameter name for the redirect url, sent as part of the form 141 // data and set to 'redirect' if this option is empty: 142 redirectParamName: undefined, 143 // Set the following option to the location of a postMessage window, 144 // to enable postMessage transport uploads: 145 postMessage: undefined, 146 // By default, XHR file uploads are sent as multipart/form-data. 147 // The iframe transport is always using multipart/form-data. 148 // Set to false to enable non-multipart XHR uploads: 149 multipart: true, 150 // To upload large files in smaller chunks, set the following option 151 // to a preferred maximum chunk size. If set to 0, null or undefined, 152 // or the browser does not support the required Blob API, files will 153 // be uploaded as a whole. 154 maxChunkSize: undefined, 155 // When a non-multipart upload or a chunked multipart upload has been 156 // aborted, this option can be used to resume the upload by setting 157 // it to the size of the already uploaded bytes. This option is most 158 // useful when modifying the options object inside of the "add" or 159 // "send" callbacks, as the options are cloned for each file upload. 160 uploadedBytes: undefined, 161 // By default, failed (abort or error) file uploads are removed from the 162 // global progress calculation. Set the following option to false to 163 // prevent recalculating the global progress data: 164 recalculateProgress: true, 165 // Interval in milliseconds to calculate and trigger progress events: 166 progressInterval: 100, 167 // Interval in milliseconds to calculate progress bitrate: 168 bitrateInterval: 500, 169 // By default, uploads are started automatically when adding files: 170 autoUpload: true, 171 // By default, duplicate file names are expected to be handled on 172 // the server-side. If this is not possible (e.g. when uploading 173 // files directly to Amazon S3), the following option can be set to 174 // an empty object or an object mapping existing filenames, e.g.: 175 // { "image.jpg": true, "image (1).jpg": true } 176 // If it is set, all files will be uploaded with unique filenames, 177 // adding increasing number suffixes if necessary, e.g.: 178 // "image (2).jpg" 179 uniqueFilenames: undefined, 180 181 // Error and info messages: 182 messages: { 183 uploadedBytes: 'Uploaded bytes exceed file size' 184 }, 185
186 // Translation function, gets the message key to be translated 187 // and an object with context specific data as arguments: 188 i18n: function (message, context) { 189 // eslint-disable-next-line no-param-reassign 190 message = this.messages[message] || message.toString(); 191 if (context) { 192 $.each(context, function (key, value) { 193 // eslint-disable-next-line no-param-reassign 194 message = message.replace('{' + key + '}', value); 195 }); 196 } 197 return message; 198 }, 199 200 // Additional form data to be sent along with the file uploads can be set 201 // using this option, which accepts an array of objects with name and 202 // value properties, a function returning such an array, a FormData 203 // object (for XHR file uploads), or a simple object. 204 // The form of the first fileInput is given as parameter to the function: 205 formData: function (form) { 206 return form.serializeArray(); 207 }, 208 209 // The add callback is invoked as soon as files are added to the fileupload 210 // widget (via file input selection, drag & drop, paste or add API call). 211 // If the singleFileUploads option is enabled, this callback will be 212 // called once for each file in the selection for XHR file uploads, else 213 // once for each file selection. 214 // 215 // The upload starts when the submit method is invoked on the data parameter. 216 // The data object contains a files property holding the added files 217 // and allows you to override plugin options as well as define ajax settings. 218 // 219 // Listeners for this callback can also be bound the following way: 220 // .on('fileuploadadd', func); 221 // 222 // data.submit() returns a Promise object and allows to attach additional 223 // handlers using jQuery's Deferred callbacks: 224 // data.submit().done(func).fail(func).always(func); 225 add: function (e, data) { 226 if (e.isDefaultPrevented()) { 227 return false; 228 } 229 if ( 230 data.autoUpload || 231 (data.autoUpload !== false && 232 $(this).fileupload('option', 'autoUpload')) 233 ) { 234 data.process().done(function () { 235 data.submit(); 236 }); 237 } 238 }, 239 240 // Other callbacks: 241 242 // Callback for the submit event of each file upload: 243 // submit: function (e, data) {}, // .on('fileuploadsubmit', func); 244 245 // Callback for the start of each file upload request: 246 // send: function (e, data) {}, // .on('fileuploadsend', func); 247 248 // Callback for successful uploads: 249 // done: function (e, data) {}, // .on('fileuploaddone', func); 250 251 // Callback for failed (abort or error) uploads: 252 // fail: function (e, data) {}, // .on('fileuploadfail', func); 253 254 // Callback for completed (success, abort or error) requests: 255 // always: function (e, data) {}, // .on('fileuploadalways', func); 256 257 // Callback for upload progress events: 258 // progress: function (e, data) {}, // .on('fileuploadprogress', func); 259 260 // Callback for global upload progress events: 261 // progressall: function (e, data) {}, // .on('fileuploadprogressall', func); 262 263 // Callback for uploads start, equivalent to the global ajaxStart event: 264 // start: function (e) {}, // .on('fileuploadstart', func); 265 266 // Callback for uploads stop, equivalent to the global ajaxStop event: 267 // stop: function (e) {}, // .on('fileuploadstop', func); 268 269 // Callback for change events of the fileInput(s): 270 // change: function (e, data) {}, // .on('fileuploadchange', func); 271 272 // Callback for paste events to the pasteZone(s): 273 // paste: function (e, data) {}, // .on('fileuploadpaste', func); 274 275 // Callback for drop events of the dropZone(s): 276 // drop: function (e, data) {}, // .on('fileuploaddrop', func); 277 278 // Callback for dragover events of the dropZone(s): 279 // dragover: function (e) {}, // .on('fileuploaddragover', func); 280 281 // Callback before the start of each chunk upload request (before form data initialization): 282 // chunkbeforesend: function (e, data) {}, // .on('fileuploadchunkbeforesend', func); 283 284 // Callback for the start of each chunk upload request: 285 // chunksend: function (e, data) {}, // .on('fileuploadchunksend', func); 286 287 // Callback for successful chunk uploads: 288 // chunkdone: function (e, data) {}, // .on('fileuploadchunkdone', func); 289 290 // Callback for failed (abort or error) chunk uploads: 291 // chunkfail: function (e, data) {}, // .on('fileuploadchunkfail', func); 292 293 // Callback for completed (success, abort or error) chunk upload requests: 294 // chunkalways: function (e, data) {}, // .on('fileuploadchunkalways', func); 295 296 // The plugin options are used as settings object for the ajax calls. 297 // The following are jQuery ajax settings required for the file uploads: 298 processData: false, 299 contentType: false, 300 cache: false, 301 timeout: 0, 302 303 /* WPCP PATCH: Local resumable-upload recovery settings. Disabled by default for other consumers. */ 304 chunkTransferIdleTimeout: 0, 305 chunkResponseTimeout: 0, 306 maxChunkRetries: 0, 307 maxOffsetQueryAttempts: 0 308 }, 309 310 // jQuery versions before 1.8 require promise.pipe if the return value is 311 // used, as promise.then in older versions has a different behavior, see: 312 // https://blog.jquery.com/2012/08/09/jquery-1-8-released/ 313 // https://bugs.jquery.com/ticket/11010 314 // https://github.com/blueimp/jQuery-File-Upload/pull/3435 315 _promisePipe: (function () { 316 var parts = $.fn.jquery.split('.'); 317 return Number(parts[0]) > 1 || Number(parts[1]) > 7 ? 'then' : 'pipe'; 318 })(), 319 320 // A list of options that require reinitializing event listeners and/or 321 // special initialization code: 322 _specialOptions: [ 323 'fileInput', 324 'dropZone', 325 'pasteZone', 326 'multipart', 327 'forceIframeTransport' 328 ], 329 330 _blobSlice: 331 $.support.blobSlice && 332 function () { 333 var slice = this.slice || this.webkitSlice || this.mozSlice; 334 return slice.apply(this, arguments); 335 }, 336 337 _BitrateTimer: function () { 338 this.timestamp = Date.now ? Date.now() : new Date().getTime(); 339 this.loaded = 0; 340 this.bitrate = 0; 341 this.getBitrate = function (now, loaded, interval) { 342 var timeDiff = now - this.timestamp; 343 if (!this.bitrate || !interval || timeDiff > interval) { 344 this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; 345 this.loaded = loaded; 346 this.timestamp = now; 347 } 348 return this.bitrate; 349 }; 350 }, 351 352 _isXHRUpload: function (options) { 353 return ( 354 !options.forceIframeTransport && 355 ((!options.multipart && $.support.xhrFileUpload) || 356 $.support.xhrFormDataFileUpload) 357 ); 358 }, 359 360 _getFormData: function (options) { 361 var formData; 362 if ($.type(options.formData) === 'function') { 363 return options.formData(options.form); 364 } 365 if ($.isArray(options.formData)) { 366 return options.formData; 367 } 368 if ($.type(options.formData) === 'object') { 369 formData = []; 370 $.each(options.formData, function (name, value) { 371 formData.push({ name: name, value: value });
372 }); 373 return formData; 374 } 375 return []; 376 }, 377 378 _getTotal: function (files) { 379 var total = 0; 380 $.each(files, function (index, file) { 381 total += file.size || 1; 382 }); 383 return total; 384 }, 385 386 _initProgressObject: function (obj) { 387 var progress = { 388 loaded: 0, 389 total: 0, 390 bitrate: 0 391 }; 392 if (obj._progress) { 393 $.extend(obj._progress, progress); 394 } else { 395 obj._progress = progress; 396 } 397 }, 398 399 _initResponseObject: function (obj) { 400 var prop; 401 if (obj._response) { 402 for (prop in obj._response) { 403 if (Object.prototype.hasOwnProperty.call(obj._response, prop)) { 404 delete obj._response[prop]; 405 } 406 } 407 } else { 408 obj._response = {}; 409 } 410 }, 411 412 _onProgress: function (e, data) { 413 if (e.lengthComputable) { 414 var now = Date.now ? Date.now() : new Date().getTime(), 415 loaded; 416 if ( 417 data._time && 418 data.progressInterval && 419 now - data._time < data.progressInterval && 420 e.loaded !== e.total 421 ) { 422 return; 423 } 424 data._time = now; 425 loaded = 426 Math.floor( 427 (e.loaded / e.total) * (data.chunkSize || data._progress.total) 428 ) + (data.uploadedBytes || 0); 429 // Add the difference from the previously loaded state 430 // to the global loaded counter: 431 this._progress.loaded += loaded - data._progress.loaded; 432 this._progress.bitrate = this._bitrateTimer.getBitrate( 433 now, 434 this._progress.loaded, 435 data.bitrateInterval 436 ); 437 data._progress.loaded = data.loaded = loaded; 438 data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( 439 now, 440 loaded, 441 data.bitrateInterval 442 ); 443 // Trigger a custom progress event with a total data property set 444 // to the file size(s) of the current upload and a loaded data 445 // property calculated accordingly: 446 this._trigger( 447 'progress', 448 $.Event('progress', { delegatedEvent: e }), 449 data 450 ); 451 // Trigger a global progress event for all current file uploads, 452 // including ajax calls queued for sequential file uploads: 453 this._trigger( 454 'progressall', 455 $.Event('progressall', { delegatedEvent: e }), 456 this._progress 457 ); 458 } 459 }, 460 461 _initProgressListener: function (options) { 462 var that = this, 463 xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(), 464 watchdog; 465 // Accesss to the native XHR object is required to add event listeners 466 // for the upload progress event: 467 if (xhr.upload) { 468 /* WPCP PATCH: Abort only when upload bytes stop advancing or API's response never arrives. */ 469 if ( 470 options.chunkSize && 471 (options.chunkTransferIdleTimeout || options.chunkResponseTimeout) 472 ) { 473 watchdog = { 474 active: true, 475 lastLoaded: 0, 476 phase: 'transferring', 477 timer: null 478 }; 479 options._chunkWatchdog = watchdog; 480 watchdog.schedule = function (timeout) { 481 window.clearTimeout(watchdog.timer); 482 watchdog.timer = window.setTimeout(function () { 483 if (!watchdog.active) { 484 return; 485 } 486 watchdog.active = false; 487 options.watchdogAbort = true; 488 xhr.abort(); 489 }, timeout); 490 }; 491 watchdog.schedule(options.chunkTransferIdleTimeout); 492 } 493 $(xhr.upload).on('progress', function (e) { 494 var oe = e.originalEvent; 495 // Make sure the progress event properties get copied over: 496 e.lengthComputable = oe.lengthComputable; 497 e.loaded = oe.loaded; 498 e.total = oe.total; 499 /* WPCP PATCH: Progress events with unchanged byte counts must not mask a stalled transfer. */ 500 if (watchdog && watchdog.active && oe.loaded > watchdog.lastLoaded) { 501 watchdog.lastLoaded = oe.loaded; 502 if ( 503 oe.loaded >= options.chunkSize && 504 watchdog.phase !== 'awaiting-response' 505 ) { 506 watchdog.phase = 'awaiting-response'; 507 watchdog.schedule(options.chunkResponseTimeout); 508 } else if (watchdog.phase === 'transferring') { 509 watchdog.schedule(options.chunkTransferIdleTimeout); 510 } 511 } 512 that._onProgress(e, options); 513 }); 514 options.xhr = function () { 515 return xhr; 516 }; 517 } 518 }, 519 520 _deinitProgressListener: function (options) { 521 var xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); 522 /* WPCP PATCH: The watchdog belongs to one native chunk request and must not outlive it. */ 523 if (options._chunkWatchdog) { 524 options._chunkWatchdog.active = false; 525 window.clearTimeout(options._chunkWatchdog.timer); 526 options._chunkWatchdog = null; 527 } 528 if (xhr.upload) { 529 $(xhr.upload).off('progress'); 530 } 531 }, 532 533 _isInstanceOf: function (type, obj) { 534 // Cross-frame instanceof check 535 return Object.prototype.toString.call(obj) === '[object ' + type + ']'; 536 }, 537 538 _getUniqueFilename: function (name, map) { 539 // eslint-disable-next-line no-param-reassign 540 name = String(name); 541 if (map[name]) { 542 // eslint-disable-next-line no-param-reassign 543 name = name.replace(/(?: \(([\d]+)\))?(\.[^.]+)?$/, function ( 544 _, 545 p1, 546 p2 547 ) { 548 var index = p1 ? Number(p1) + 1 : 1; 549 var ext = p2 || ''; 550 return ' (' + index + ')' + ext; 551 }); 552 return this._getUniqueFilename(name, map); 553 } 554 map[name] = true; 555 return name; 556 }, 557 558 _initXHRData: function (options) { 559 var that = this, 560 formData, 561 file = options.files[0], 562 // Ignore non-multipart setting if not supported:
563 multipart = options.multipart || !$.support.xhrFileUpload, 564 paramName = 565 $.type(options.paramName) === 'array' 566 ? options.paramName[0] 567 : options.paramName; 568 options.headers = $.extend({}, options.headers); 569 if (options.contentRange) { 570 options.headers['Content-Range'] = options.contentRange; 571 } 572 if (!multipart || options.blob || !this._isInstanceOf('File', file)) { 573 options.headers['Content-Disposition'] = 574 'attachment; filename="' + 575 encodeURI(file.uploadName || file.name) + 576 '"'; 577 } 578 if (!multipart) { 579 options.contentType = file.type || 'application/octet-stream'; 580 options.data = options.blob || file; 581 } else if ($.support.xhrFormDataFileUpload) { 582 if (options.postMessage) { 583 // window.postMessage does not allow sending FormData 584 // objects, so we just add the File/Blob objects to 585 // the formData array and let the postMessage window 586 // create the FormData object out of this array: 587 formData = this._getFormData(options); 588 if (options.blob) { 589 formData.push({ 590 name: paramName, 591 value: options.blob 592 }); 593 } else { 594 $.each(options.files, function (index, file) { 595 formData.push({ 596 name: 597 ($.type(options.paramName) === 'array' && 598 options.paramName[index]) || 599 paramName, 600 value: file 601 }); 602 }); 603 } 604 } else { 605 if (that._isInstanceOf('FormData', options.formData)) { 606 formData = options.formData; 607 } else { 608 formData = new FormData(); 609 $.each(this._getFormData(options), function (index, field) { 610 formData.append(field.name, field.value); 611 }); 612 } 613 if (options.blob) { 614 formData.append( 615 paramName, 616 options.blob, 617 file.uploadName || file.name 618 ); 619 } else { 620 $.each(options.files, function (index, file) { 621 // This check allows the tests to run with 622 // dummy objects: 623 if ( 624 that._isInstanceOf('File', file) || 625 that._isInstanceOf('Blob', file) 626 ) { 627 var fileName = file.uploadName || file.name; 628 if (options.uniqueFilenames) { 629 fileName = that._getUniqueFilename( 630 fileName, 631 options.uniqueFilenames 632 ); 633 } 634 formData.append( 635 ($.type(options.paramName) === 'array' && 636 options.paramName[index]) || 637 paramName, 638 file, 639 fileName 640 ); 641 } 642 }); 643 } 644 } 645 options.data = formData; 646 } 647 // Blob reference is not needed anymore, free memory: 648 options.blob = null; 649 }, 650 651 _initIframeSettings: function (options) { 652 var targetHost = $('<a></a>').prop('href', options.url).prop('host'); 653 // Setting the dataType to iframe enables the iframe transport: 654 options.dataType = 'iframe ' + (options.dataType || ''); 655 // The iframe transport accepts a serialized array as form data: 656 options.formData = this._getFormData(options); 657 // Add redirect url to form data on cross-domain uploads: 658 if (options.redirect && targetHost && targetHost !== location.host) { 659 options.formData.push({ 660 name: options.redirectParamName || 'redirect', 661 value: options.redirect 662 }); 663 } 664 }, 665 666 _initDataSettings: function (options) { 667 if (this._isXHRUpload(options)) { 668 if (!this._chunkedUpload(options, true)) { 669 if (!options.data) { 670 this._initXHRData(options); 671 } 672 this._initProgressListener(options); 673 } 674 if (options.postMessage) { 675 // Setting the dataType to postmessage enables the 676 // postMessage transport: 677 options.dataType = 'postmessage ' + (options.dataType || ''); 678 } 679 } else { 680 this._initIframeSettings(options); 681 } 682 }, 683 684 _getParamName: function (options) { 685 var fileInput = $(options.fileInput), 686 paramName = options.paramName; 687 if (!paramName) { 688 paramName = []; 689 fileInput.each(function () { 690 var input = $(this), 691 name = input.prop('name') || 'files[]', 692 i = (input.prop('files') || [1]).length; 693 while (i) { 694 paramName.push(name); 695 i -= 1; 696 } 697 }); 698 if (!paramName.length) { 699 paramName = [fileInput.prop('name') || 'files[]']; 700 } 701 } else if (!$.isArray(paramName)) { 702 paramName = [paramName]; 703 } 704 return paramName; 705 }, 706 707 _initFormSettings: function (options) { 708 // Retrieve missing options from the input field and the 709 // associated form, if available: 710 if (!options.form || !options.form.length) { 711 options.form = $(options.fileInput.prop('form'));
712 // If the given file input doesn't have an associated form, 713 // use the default widget file input's form: 714 if (!options.form.length) { 715 options.form = $(this.options.fileInput.prop('form')); 716 } 717 } 718 options.paramName = this._getParamName(options); 719 if (!options.url) { 720 options.url = options.form.prop('action') || location.href; 721 } 722 // The HTTP request method must be "POST" or "PUT": 723 options.type = ( 724 options.type || 725 ($.type(options.form.prop('method')) === 'string' && 726 options.form.prop('method')) || 727 '' 728 ).toUpperCase(); 729 if ( 730 options.type !== 'POST' && 731 options.type !== 'PUT' && 732 options.type !== 'PATCH' 733 ) { 734 options.type = 'POST'; 735 } 736 if (!options.formAcceptCharset) { 737 options.formAcceptCharset = options.form.attr('accept-charset'); 738 } 739 }, 740 741 _getAJAXSettings: function (data) { 742 var options = $.extend({}, this.options, data); 743 this._initFormSettings(options); 744 this._initDataSettings(options); 745 return options; 746 }, 747 748 // jQuery 1.6 doesn't provide .state(), 749 // while jQuery 1.8+ removed .isRejected() and .isResolved(): 750 _getDeferredState: function (deferred) { 751 if (deferred.state) { 752 return deferred.state(); 753 } 754 if (deferred.isResolved()) { 755 return 'resolved'; 756 } 757 if (deferred.isRejected()) { 758 return 'rejected'; 759 } 760 return 'pending'; 761 }, 762 763 // Maps jqXHR callbacks to the equivalent 764 // methods of the given Promise object: 765 _enhancePromise: function (promise) { 766 promise.success = promise.done; 767 promise.error = promise.fail; 768 promise.complete = promise.always; 769 return promise; 770 }, 771 772 // Creates and returns a Promise object enhanced with 773 // the jqXHR methods abort, success, error and complete: 774 _getXHRPromise: function (resolveOrReject, context, args) { 775 var dfd = $.Deferred(), 776 promise = dfd.promise(); 777 // eslint-disable-next-line no-param-reassign 778 context = context || this.options.context || promise; 779 if (resolveOrReject === true) { 780 dfd.resolveWith(context, args); 781 } else if (resolveOrReject === false) { 782 dfd.rejectWith(context, args); 783 } 784 promise.abort = dfd.promise; 785 return this._enhancePromise(promise); 786 }, 787 788 // Adds convenience methods to the data callback argument: 789 _addConvenienceMethods: function (e, data) { 790 var that = this, 791 getPromise = function (args) { 792 return $.Deferred().resolveWith(that, args).promise(); 793 }; 794 data.process = function (resolveFunc, rejectFunc) { 795 if (resolveFunc || rejectFunc) { 796 data._processQueue = this._processQueue = (this._processQueue || 797 getPromise([this])) 798 [that._promisePipe](function () { 799 if (data.errorThrown) { 800 return $.Deferred().rejectWith(that, [data]).promise(); 801 } 802 return getPromise(arguments); 803 }) 804 [that._promisePipe](resolveFunc, rejectFunc); 805 } 806 return this._processQueue || getPromise([this]); 807 }; 808 data.submit = function () { 809 if (this.state() !== 'pending') { 810 data.jqXHR = this.jqXHR = 811 that._trigger( 812 'submit', 813 $.Event('submit', { delegatedEvent: e }), 814 this 815 ) !== false && that._onSend(e, this); 816 } 817 return this.jqXHR || that._getXHRPromise(); 818 }; 819 data.abort = function () { 820 if (this.jqXHR) { 821 return this.jqXHR.abort(); 822 } 823 this.errorThrown = 'abort'; 824 that._trigger('fail', null, this); 825 return that._getXHRPromise(false); 826 }; 827 data.state = function () { 828 if (this.jqXHR) { 829 return that._getDeferredState(this.jqXHR); 830 } 831 if (this._processQueue) { 832 return that._getDeferredState(this._processQueue); 833 } 834 }; 835 data.processing = function () { 836 return ( 837 !this.jqXHR && 838 this._processQueue && 839 that._getDeferredState(this._processQueue) === 'pending' 840 ); 841 }; 842 data.progress = function () { 843 return this._progress; 844 }; 845 data.response = function () { 846 return this._response; 847 }; 848 }, 849 850 // Parses the Range header from the server response 851 // and returns the uploaded bytes: 852 _getUploadedBytes: function (jqXHR) { 853 var range = jqXHR.getResponseHeader('Range'), 854 match, 855 upperBytesPos; 856 /* WPCP PATCH: Preserve missing and malformed headers so resumable uploads never guess an offset. */ 857 if (range === null) { 858 return null; 859 } 860 match = /^bytes=0-(\d+)$/i.exec(range); 861 if (!match) { 862 return false; 863 } 864 upperBytesPos = parseInt(match[1], 10); 865 return isNaN(upperBytesPos) ? false : upperBytesPos + 1; 866 }, 867 868 /* WPCP PATCH: Google rate limits use a 403 with a machine-readable error reason. */ 869 _getGoogleErrorReason: function (jqXHR) { 870 var response = jqXHR.responseJSON, 871 error; 872 if (!response && jqXHR.responseText) { 873 try { 874 response = JSON.parse(jqXHR.responseText); 875 } catch (ignore) { 876 response = null; 877 } 878 } 879 error = response && response.error; 880 if (error && error.errors && error.errors.length) { 881 return error.errors[0].reason || null; 882 } 883 return error && error.reason ? error.reason : null; 884 }, 885 886 /* WPCP PATCH: Use an allowlist so invalid resumable sessions can never enter a retry loop. */
887 _classifyChunkFailure: function (jqXHR, watchdogAbort) { 888 var status = jqXHR && jqXHR.status ? jqXHR.status : 0, 889 reason = jqXHR ? this._getGoogleErrorReason(jqXHR) : null, 890 retryable = 891 status === 0 || 892 status === 408 || 893 status === 429 || 894 status >= 500 || 895 (status === 403 && 896 (reason === 'rateLimitExceeded' || 897 reason === 'userRateLimitExceeded')), 898 category = 'generic'; 899 if (watchdogAbort) { 900 category = 'stalled'; 901 } else if ( 902 status === 400 || 903 status === 401 || 904 status === 404 || 905 status === 410 906 ) { 907 category = 'session'; 908 } else if ( 909 reason === 'rateLimitExceeded' || 910 reason === 'userRateLimitExceeded' 911 ) { 912 category = 'rate-limited'; 913 } 914 return { 915 category: category, 916 reason: reason, 917 retryable: retryable, 918 status: status 919 }; 920 }, 921 922 // Uploads a file in multiple, sequential requests 923 // by splitting the file up in multiple blob chunks. 924 // If the second parameter is true, only tests if the file 925 // should be uploaded in chunks, but does not invoke any 926 // upload requests: 927 _chunkedUpload: function (options, testOnly) { 928 options.uploadedBytes = options.uploadedBytes || 0; 929 var that = this, 930 file = options.files[0], 931 fs = file.size, 932 ub = options.uploadedBytes, 933 mcs = options.maxChunkSize || fs, 934 slice = this._blobSlice, 935 dfd = $.Deferred(), 936 promise = dfd.promise(), 937 jqXHR, 938 upload, 939 queryOffset, 940 scheduleRetry, 941 rejectUpload; 942 if ( 943 !( 944 this._isXHRUpload(options) && 945 slice && 946 (ub || ($.type(mcs) === 'function' ? mcs(options) : mcs) < fs) 947 ) || 948 options.data 949 ) { 950 return false; 951 } 952 if (testOnly) { 953 return true; 954 } 955 if (ub >= fs) { 956 file.error = options.i18n('uploadedBytes'); 957 return this._getXHRPromise(false, options.context, [ 958 null, 959 'error', 960 file.error 961 ]); 962 } 963 /* WPCP PATCH: Keep recovery state on the outer options object because chunk options are cloned. */ 964 options.chunkRetryCount = 0; 965 options.chunkRequestActive = false; 966 options.chunkRequestGeneration = 0; 967 options.chunkRetryGeneration = 0; 968 options.chunkRetryTimer = null; 969 970 rejectUpload = function (o, jqXHR, textStatus, errorThrown, failure) { 971 o.failureCategory = failure.category; 972 o.failureReason = failure.reason; 973 o.failureStatus = failure.status; 974 o.jqXHR = jqXHR; 975 o.textStatus = textStatus; 976 o.errorThrown = errorThrown; 977 that._trigger('chunkfail', null, o); 978 that._trigger('chunkalways', null, o); 979 dfd.rejectWith(o.context, [jqXHR, textStatus, errorThrown]); 980 }; 981 982 scheduleRetry = function (o, failure, retryAction) { 983 var retryGeneration, 984 delay, 985 retryNumber; 986 if (!failure.retryable) { 987 rejectUpload(o, o.jqXHR, o.textStatus, o.errorThrown, failure); 988 return; 989 } 990 options.chunkRetryCount += 1; 991 if (options.chunkRetryCount > options.maxChunkRetries) { 992 rejectUpload(o, o.jqXHR, o.textStatus, o.errorThrown, failure); 993 return; 994 } 995 retryNumber = options.chunkRetryCount; 996 delay = Math.min( 997 Math.pow(2, retryNumber - 1) * 1000 + Math.floor(Math.random() * 1000), 998 64000 999 ); 1000 retryGeneration = ++options.chunkRetryGeneration; 1001 o.chunkRetryCount = retryNumber; 1002 o.retryDelay = delay; 1003 that._trigger('chunkretry', null, o); 1004 options.chunkRetryTimer = window.setTimeout(function () { 1005 if ( 1006 options.cancelledByUser || 1007 retryGeneration !== options.chunkRetryGeneration || 1008 options.chunkRequestActive 1009 ) { 1010 return; 1011 } 1012 options.chunkRetryTimer = null; 1013 retryAction(); 1014 }, delay); 1015 }; 1016 1017 queryOffset = function (o, requestGeneration, failure, attempt) { 1018 var offsetQuery; 1019 if ( 1020 options.cancelledByUser || 1021 requestGeneration !== options.chunkRequestGeneration || 1022 options.chunkRequestActive 1023 ) { 1024 return; 1025 } 1026 options.chunkRequestActive = true; 1027 offsetQuery = $.ajax({
1028 cache: false, 1029 contentType: false, 1030 data: null, 1031 headers: { 1032 'Content-Range': 'bytes */' + fs 1033 }, 1034 processData: false, 1035 type: 'PUT', 1036 url: o.url 1037 }); 1038 jqXHR = offsetQuery; 1039 offsetQuery 1040 .done(function (result, textStatus, queryJqXHR) { 1041 if (options.cancelledByUser) { 1042 options.chunkRequestActive = false; 1043 rejectUpload( 1044 o, 1045 queryJqXHR, 1046 textStatus, 1047 'abort', 1048 that._classifyChunkFailure(queryJqXHR, false) 1049 ); 1050 return; 1051 } 1052 if (requestGeneration !== options.chunkRequestGeneration) { 1053 return; 1054 } 1055 options.chunkRequestActive = false; 1056 if (queryJqXHR.status !== 200 && queryJqXHR.status !== 201) { 1057 rejectUpload( 1058 o, 1059 queryJqXHR, 1060 textStatus, 1061 'Unexpected resumable upload response', 1062 that._classifyChunkFailure(queryJqXHR, false) 1063 ); 1064 return; 1065 } 1066 options.chunkRetryCount = 0; 1067 o.result = result; 1068 o.textStatus = textStatus; 1069 o.jqXHR = queryJqXHR; 1070 dfd.resolveWith(o.context, [result, textStatus, queryJqXHR]); 1071 }) 1072 .fail(function (queryJqXHR, textStatus, errorThrown) { 1073 var confirmedOffset, 1074 previousOffset, 1075 queryFailure; 1076 if (options.cancelledByUser) { 1077 options.chunkRequestActive = false; 1078 rejectUpload( 1079 o, 1080 queryJqXHR, 1081 textStatus, 1082 errorThrown, 1083 that._classifyChunkFailure(queryJqXHR, false) 1084 ); 1085 return; 1086 } 1087 if (requestGeneration !== options.chunkRequestGeneration) { 1088 return; 1089 } 1090 options.chunkRequestActive = false; 1091 if (queryJqXHR.status === 308) { 1092 confirmedOffset = that._getUploadedBytes(queryJqXHR); 1093 if (confirmedOffset === null) { 1094 if (ub > 0) { 1095 if (!options.corsRangeHeaderWarningLogged) { 1096 options.corsRangeHeaderWarningLogged = true; 1097 o.corsRangeHeaderHidden = true; 1098 that._trigger('chunkcorsrangehidden', null, o); 1099 } 1100 rejectUpload( 1101 o, 1102 queryJqXHR, 1103 textStatus, 1104 errorThrown, 1105 that._classifyChunkFailure(queryJqXHR, false) 1106 ); 1107 return; 1108 } 1109 confirmedOffset = 0; 1110 } else if (confirmedOffset === false || confirmedOffset < ub) { 1111 rejectUpload( 1112 o, 1113 queryJqXHR, 1114 textStatus, 1115 errorThrown, 1116 that._classifyChunkFailure(queryJqXHR, false) 1117 ); 1118 return; 1119 } 1120 if (confirmedOffset > ub) { 1121 previousOffset = ub; 1122 ub = confirmedOffset; 1123 options.chunkRetryCount = 0; 1124 if (o._progress.loaded < ub) { 1125 that._onProgress( 1126 $.Event('progress', { 1127 lengthComputable: true, 1128 loaded: ub - previousOffset, 1129 total: o.chunkSize 1130 }), 1131 o 1132 ); 1133 } 1134 options.uploadedBytes = o.uploadedBytes = ub; 1135 if (ub < fs) { 1136 upload(); 1137 } else { 1138 rejectUpload( 1139 o, 1140 queryJqXHR, 1141 textStatus, 1142 'Unexpected completed resumable upload response',
1143 that._classifyChunkFailure(queryJqXHR, false) 1144 ); 1145 } 1146 return; 1147 } 1148 scheduleRetry(o, failure, upload); 1149 return; 1150 } 1151 queryFailure = that._classifyChunkFailure(queryJqXHR, false); 1152 o.jqXHR = queryJqXHR; 1153 o.textStatus = textStatus; 1154 o.errorThrown = errorThrown; 1155 if ( 1156 queryFailure.retryable && 1157 attempt < options.maxOffsetQueryAttempts 1158 ) { 1159 o.offsetQueryAttempt = attempt + 1; 1160 o.retryDelay = Math.min( 1161 Math.pow(2, attempt - 1) * 1000 + Math.floor(Math.random() * 1000), 1162 64000 1163 ); 1164 that._trigger('chunkretry', null, o); 1165 window.setTimeout(function () { 1166 queryOffset(o, requestGeneration, failure, attempt + 1); 1167 }, o.retryDelay); 1168 return; 1169 } 1170 scheduleRetry(o, queryFailure, upload); 1171 }); 1172 }; 1173 1174 // The chunk upload method: 1175 upload = function () { 1176 // Clone the options object for each chunk upload: 1177 var o = $.extend({}, options), 1178 currentLoaded = o._progress.loaded, 1179 requestGeneration; 1180 if (options.cancelledByUser || options.chunkRequestActive) { 1181 return; 1182 } 1183 requestGeneration = ++options.chunkRequestGeneration; 1184 options.chunkRequestActive = true; 1185 o.blob = slice.call( 1186 file, 1187 ub, 1188 ub + ($.type(mcs) === 'function' ? mcs(o) : mcs), 1189 file.type 1190 ); 1191 // Store the current chunk size, as the blob itself 1192 // will be dereferenced after data processing: 1193 o.chunkSize = o.blob.size; 1194 // Expose the chunk bytes position range: 1195 o.contentRange = 1196 'bytes ' + ub + '-' + (ub + o.chunkSize - 1) + '/' + fs; 1197 // Trigger chunkbeforesend to allow form data to be updated for this chunk 1198 that._trigger('chunkbeforesend', null, o); 1199 // Process the upload data (the blob and potential form data): 1200 that._initXHRData(o); 1201 // Add progress listeners for this chunk upload: 1202 that._initProgressListener(o); 1203 jqXHR = ( 1204 (that._trigger('chunksend', null, o) !== false && $.ajax(o)) || 1205 that._getXHRPromise(false, o.context) 1206 ) 1207 .done(function (result, textStatus, jqXHR) { 1208 if (options.cancelledByUser) { 1209 options.chunkRequestActive = false; 1210 rejectUpload( 1211 o, 1212 jqXHR, 1213 textStatus, 1214 'abort', 1215 that._classifyChunkFailure(jqXHR, false) 1216 ); 1217 return; 1218 } 1219 if ( 1220 requestGeneration !== options.chunkRequestGeneration || 1221 !options.chunkRequestActive 1222 ) { 1223 return; 1224 } 1225 options.chunkRequestActive = false; 1226 if (jqXHR.status !== 200 && jqXHR.status !== 201) { 1227 rejectUpload( 1228 o, 1229 jqXHR, 1230 textStatus, 1231 'Unexpected resumable upload response', 1232 that._classifyChunkFailure(jqXHR, false) 1233 ); 1234 return; 1235 } 1236 ub = fs; 1237 options.chunkRetryCount = 0; 1238 // Create a progress event if no final progress event 1239 // with loaded equaling total has been triggered 1240 // for this chunk: 1241 if (currentLoaded + o.chunkSize - o._progress.loaded) { 1242 that._onProgress( 1243 $.Event('progress', { 1244 lengthComputable: true, 1245 loaded: ub - o.uploadedBytes, 1246 total: ub - o.uploadedBytes 1247 }), 1248 o 1249 ); 1250 } 1251 options.uploadedBytes = o.uploadedBytes = ub; 1252 o.result = result; 1253 o.textStatus = textStatus; 1254 o.jqXHR = jqXHR; 1255 that._trigger('chunkdone', null, o); 1256 that._trigger('chunkalways', null, o); 1257 if (ub < fs) { 1258 // File upload not yet complete, 1259 // continue with the next chunk: 1260 upload(); 1261 } else { 1262 dfd.resolveWith(o.context, [result, textStatus, jqXHR]); 1263 } 1264 }) 1265 .fail(function (jqXHR, textStatus, errorThrown) { 1266 var confirmedOffset, 1267 previousOffset, 1268 failure; 1269 if (options.cancelledByUser) { 1270 options.chunkRequestActive = false; 1271 rejectUpload( 1272 o, 1273 jqXHR, 1274 textStatus, 1275 errorThrown,
1276 that._classifyChunkFailure(jqXHR, false) 1277 ); 1278 return; 1279 } 1280 if ( 1281 requestGeneration !== options.chunkRequestGeneration || 1282 !options.chunkRequestActive 1283 ) { 1284 return; 1285 } 1286 options.chunkRequestActive = false; 1287 o.jqXHR = jqXHR; 1288 o.textStatus = textStatus; 1289 o.errorThrown = errorThrown; 1290 failure = that._classifyChunkFailure(jqXHR, o.watchdogAbort); 1291 /* WPCP PATCH: A normal Google 308 confirms this chunk and must not incur an offset-query request. */ 1292 if (jqXHR.status === 308) { 1293 confirmedOffset = that._getUploadedBytes(jqXHR); 1294 if (confirmedOffset === null) { 1295 if (ub > 0) { 1296 if (!options.corsRangeHeaderWarningLogged) { 1297 options.corsRangeHeaderWarningLogged = true; 1298 o.corsRangeHeaderHidden = true; 1299 that._trigger('chunkcorsrangehidden', null, o); 1300 } 1301 rejectUpload(o, jqXHR, textStatus, errorThrown, failure); 1302 return; 1303 } 1304 /* WPCP PATCH: Google legitimately omits Range for zero received bytes, which is safe to retry from zero. */ 1305 scheduleRetry( 1306 o, 1307 $.extend({}, failure, { retryable: true }), 1308 upload 1309 ); 1310 return; 1311 } else if (confirmedOffset === false || confirmedOffset <= ub) { 1312 rejectUpload(o, jqXHR, textStatus, errorThrown, failure); 1313 return; 1314 } 1315 previousOffset = ub; 1316 ub = confirmedOffset; 1317 options.chunkRetryCount = 0; 1318 if (o._progress.loaded < ub) { 1319 that._onProgress( 1320 $.Event('progress', { 1321 lengthComputable: true, 1322 loaded: ub - previousOffset, 1323 total: o.chunkSize 1324 }), 1325 o 1326 ); 1327 } 1328 options.uploadedBytes = o.uploadedBytes = ub; 1329 o.result = errorThrown; 1330 that._trigger('chunkdone', null, o); 1331 that._trigger('chunkalways', null, o); 1332 if (ub < fs) { 1333 upload(); 1334 } else { 1335 dfd.resolveWith(o.context, [errorThrown, textStatus, jqXHR]); 1336 } 1337 return; 1338 } 1339 queryOffset(o, requestGeneration, failure, 1); 1340 }) 1341 .always(function () { 1342 that._deinitProgressListener(o); 1343 }); 1344 }; 1345 this._enhancePromise(promise); 1346 promise.abort = function () { 1347 /* WPCP PATCH: A user cancellation must invalidate delayed recovery before aborting the active request. */ 1348 if (promise.cancelledByUser || options.cancelledByUser) { 1349 options.cancelledByUser = true; 1350 options.chunkRequestGeneration += 1; 1351 options.chunkRetryGeneration += 1; 1352 window.clearTimeout(options.chunkRetryTimer); 1353 if (!options.chunkRequestActive) { 1354 dfd.rejectWith(options.context, [null, 'abort', 'abort']); 1355 return promise; 1356 } 1357 } 1358 return jqXHR && jqXHR.abort ? jqXHR.abort() : promise; 1359 }; 1360 upload(); 1361 return promise; 1362 }, 1363 1364 _beforeSend: function (e, data) { 1365 if (this._active === 0) { 1366 // the start callback is triggered when an upload starts 1367 // and no other uploads are currently running, 1368 // equivalent to the global ajaxStart event: 1369 this._trigger('start'); 1370 // Set timer for global bitrate progress calculation: 1371 this._bitrateTimer = new this._BitrateTimer(); 1372 // Reset the global progress values: 1373 this._progress.loaded = this._progress.total = 0; 1374 this._progress.bitrate = 0; 1375 } 1376 // Make sure the container objects for the .response() and 1377 // .progress() methods on the data object are available 1378 // and reset to their initial state: 1379 this._initResponseObject(data); 1380 this._initProgressObject(data); 1381 data._progress.loaded = data.loaded = data.uploadedBytes || 0; 1382 data._progress.total = data.total = this._getTotal(data.files) || 1; 1383 data._progress.bitrate = data.bitrate = 0; 1384 this._active += 1; 1385 // Initialize the global progress values: 1386 this._progress.loaded += data.loaded; 1387 this._progress.total += data.total; 1388 }, 1389 1390 _onDone: function (result, textStatus, jqXHR, options) { 1391 var total = options._progress.total, 1392 response = options._response; 1393 if (options._progress.loaded < total) { 1394 // Create a progress event if no final progress event 1395 // with loaded equaling total has been triggered: 1396 this._onProgress( 1397 $.Event('progress', { 1398 lengthComputable: true, 1399 loaded: total, 1400 total: total 1401 }), 1402 options 1403 ); 1404 } 1405 response.result = options.result = result; 1406 response.textStatus = options.textStatus = textStatus; 1407 response.jqXHR = options.jqXHR = jqXHR; 1408 this._trigger('done', null, options); 1409 }, 1410 1411 _onFail: function (jqXHR, textStatus, errorThrown, options) { 1412 var response = options._response; 1413 if (options.recalculateProgress) { 1414 // Remove the failed (error or abort) file upload from 1415 // the global progress calculation: 1416 this._progress.loaded -= options._progress.loaded;
1417 this._progress.total -= options._progress.total; 1418 } 1419 response.jqXHR = options.jqXHR = jqXHR; 1420 response.textStatus = options.textStatus = textStatus; 1421 response.errorThrown = options.errorThrown = errorThrown; 1422 this._trigger('fail', null, options); 1423 }, 1424 1425 _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { 1426 // jqXHRorResult, textStatus and jqXHRorError are added to the 1427 // options object via done and fail callbacks 1428 this._trigger('always', null, options); 1429 }, 1430 1431 _onSend: function (e, data) { 1432 if (!data.submit) { 1433 this._addConvenienceMethods(e, data); 1434 } 1435 var that = this, 1436 jqXHR, 1437 aborted, 1438 slot, 1439 pipe, 1440 options = that._getAJAXSettings(data), 1441 send = function () { 1442 that._sending += 1; 1443 // Set timer for bitrate progress calculation: 1444 options._bitrateTimer = new that._BitrateTimer(); 1445 jqXHR = 1446 jqXHR || 1447 ( 1448 ((aborted || 1449 that._trigger( 1450 'send', 1451 $.Event('send', { delegatedEvent: e }), 1452 options 1453 ) === false) && 1454 that._getXHRPromise(false, options.context, aborted)) || 1455 that._chunkedUpload(options) || 1456 $.ajax(options) 1457 ) 1458 .done(function (result, textStatus, jqXHR) { 1459 that._onDone(result, textStatus, jqXHR, options); 1460 }) 1461 .fail(function (jqXHR, textStatus, errorThrown) { 1462 that._onFail(jqXHR, textStatus, errorThrown, options); 1463 }) 1464 .always(function (jqXHRorResult, textStatus, jqXHRorError) { 1465 that._deinitProgressListener(options); 1466 that._onAlways( 1467 jqXHRorResult, 1468 textStatus, 1469 jqXHRorError, 1470 options 1471 ); 1472 that._sending -= 1; 1473 that._active -= 1; 1474 if ( 1475 options.limitConcurrentUploads && 1476 options.limitConcurrentUploads > that._sending 1477 ) { 1478 // Start the next queued upload, 1479 // that has not been aborted: 1480 var nextSlot = that._slots.shift(); 1481 while (nextSlot) { 1482 if (that._getDeferredState(nextSlot) === 'pending') { 1483 nextSlot.resolve(); 1484 break; 1485 } 1486 nextSlot = that._slots.shift(); 1487 } 1488 } 1489 if (that._active === 0) { 1490 // The stop callback is triggered when all uploads have 1491 // been completed, equivalent to the global ajaxStop event: 1492 that._trigger('stop'); 1493 } 1494 }); 1495 return jqXHR; 1496 }; 1497 this._beforeSend(e, options); 1498 if ( 1499 this.options.sequentialUploads || 1500 (this.options.limitConcurrentUploads && 1501 this.options.limitConcurrentUploads <= this._sending) 1502 ) { 1503 if (this.options.limitConcurrentUploads > 1) { 1504 slot = $.Deferred(); 1505 this._slots.push(slot); 1506 pipe = slot[that._promisePipe](send); 1507 } else { 1508 this._sequence = this._sequence[that._promisePipe](send, send); 1509 pipe = this._sequence; 1510 } 1511 // Return the piped Promise object, enhanced with an abort method, 1512 // which is delegated to the jqXHR object of the current upload, 1513 // and jqXHR callbacks mapped to the equivalent Promise methods: 1514 pipe.abort = function () { 1515 aborted = [undefined, 'abort', 'abort']; 1516 /* WPCP PATCH: Queued uploads return this pipe, so cancellation must reach the shared chunk options. */ 1517 options.cancelledByUser = true; 1518 if (!jqXHR) { 1519 if (slot) { 1520 slot.rejectWith(options.context, aborted); 1521 } 1522 return send(); 1523 } 1524 return jqXHR.abort(); 1525 }; 1526 return this._enhancePromise(pipe); 1527 } 1528 return send(); 1529 }, 1530 1531 _onAdd: function (e, data) { 1532 var that = this, 1533 result = true, 1534 options = $.extend({}, this.options, data), 1535 files = data.files, 1536 filesLength = files.length, 1537 limit = options.limitMultiFileUploads, 1538 limitSize = options.limitMultiFileUploadSize, 1539 overhead = options.limitMultiFileUploadSizeOverhead, 1540 batchSize = 0, 1541 paramName = this._getParamName(options), 1542 paramNameSet, 1543 paramNameSlice, 1544 fileSet, 1545 i, 1546 j = 0; 1547 if (!filesLength) { 1548 return false; 1549 } 1550 if (limitSize && files[0].size === undefined) { 1551 limitSize = undefined; 1552 } 1553 if ( 1554 !(options.singleFileUploads || limit || limitSize) || 1555 !this._isXHRUpload(options) 1556 ) { 1557 fileSet = [files]; 1558 paramNameSet = [paramName]; 1559 } else if (!(options.singleFileUploads || limitSize) && limit) { 1560 fileSet = []; 1561 paramNameSet = []; 1562 for (i = 0; i < filesLength; i += limit) { 1563 fileSet.push(files.slice(i, i + limit)); 1564 paramNameSlice = paramName.slice(i, i + limit); 1565 if (!paramNameSlice.length) { 1566 paramNameSlice = paramName; 1567 } 1568 paramNameSet.push(paramNameSlice); 1569 } 1570 } else if (!options.singleFileUploads && limitSize) { 1571 fileSet = []; 1572 paramNameSet = []; 1573 for (i = 0; i < filesLength; i = i + 1) {
1574 batchSize += files[i].size + overhead; 1575 if ( 1576 i + 1 === filesLength || 1577 batchSize + files[i + 1].size + overhead > limitSize || 1578 (limit && i + 1 - j >= limit) 1579 ) { 1580 fileSet.push(files.slice(j, i + 1)); 1581 paramNameSlice = paramName.slice(j, i + 1); 1582 if (!paramNameSlice.length) { 1583 paramNameSlice = paramName; 1584 } 1585 paramNameSet.push(paramNameSlice); 1586 j = i + 1; 1587 batchSize = 0; 1588 } 1589 } 1590 } else { 1591 paramNameSet = paramName; 1592 } 1593 data.originalFiles = files; 1594 $.each(fileSet || files, function (index, element) { 1595 var newData = $.extend({}, data); 1596 newData.files = fileSet ? element : [element]; 1597 newData.paramName = paramNameSet[index]; 1598 that._initResponseObject(newData); 1599 that._initProgressObject(newData); 1600 that._addConvenienceMethods(e, newData); 1601 result = that._trigger( 1602 'add', 1603 $.Event('add', { delegatedEvent: e }), 1604 newData 1605 ); 1606 return result; 1607 }); 1608 return result; 1609 }, 1610 1611 _replaceFileInput: function (data) { 1612 var input = data.fileInput, 1613 inputClone = input.clone(true), 1614 restoreFocus = input.is(document.activeElement); 1615 // Add a reference for the new cloned file input to the data argument: 1616 data.fileInputClone = inputClone; 1617 $('<form></form>').append(inputClone)[0].reset(); 1618 // Detaching allows to insert the fileInput on another form 1619 // without loosing the file input value: 1620 input.after(inputClone).detach(); 1621 // If the fileInput had focus before it was detached, 1622 // restore focus to the inputClone. 1623 if (restoreFocus) { 1624 inputClone.trigger('focus'); 1625 } 1626 // Avoid memory leaks with the detached file input: 1627 $.cleanData(input.off('remove')); 1628 // Replace the original file input element in the fileInput 1629 // elements set with the clone, which has been copied including 1630 // event handlers: 1631 this.options.fileInput = this.options.fileInput.map(function (i, el) { 1632 if (el === input[0]) { 1633 return inputClone[0]; 1634 } 1635 return el; 1636 }); 1637 // If the widget has been initialized on the file input itself, 1638 // override this.element with the file input clone: 1639 if (input[0] === this.element[0]) { 1640 this.element = inputClone; 1641 } 1642 }, 1643 1644 _handleFileTreeEntry: function (entry, path) { 1645 var that = this, 1646 dfd = $.Deferred(), 1647 entries = [], 1648 dirReader, 1649 errorHandler = function (e) { 1650 if (e && !e.entry) { 1651 e.entry = entry; 1652 } 1653 // Since $.when returns immediately if one 1654 // Deferred is rejected, we use resolve instead. 1655 // This allows valid files and invalid items 1656 // to be returned together in one set: 1657 dfd.resolve([e]); 1658 }, 1659 successHandler = function (entries) { 1660 that 1661 ._handleFileTreeEntries(entries, path + entry.name + '/') 1662 .done(function (files) { 1663 dfd.resolve(files); 1664 }) 1665 .fail(errorHandler); 1666 }, 1667 readEntries = function () { 1668 dirReader.readEntries(function (results) { 1669 if (!results.length) { 1670 successHandler(entries); 1671 } else { 1672 entries = entries.concat(results); 1673 readEntries(); 1674 } 1675 }, errorHandler); 1676 }; 1677 // eslint-disable-next-line no-param-reassign 1678 path = path || ''; 1679 if (entry.isFile) { 1680 if (entry._file) { 1681 // Workaround for Chrome bug #149735 1682 entry._file.relativePath = path; 1683 dfd.resolve(entry._file); 1684 } else { 1685 entry.file(function (file) { 1686 file.relativePath = path; 1687 dfd.resolve(file); 1688 }, errorHandler); 1689 } 1690 } else if (entry.isDirectory) { 1691 dirReader = entry.createReader(); 1692 readEntries(); 1693 } else { 1694 // Return an empty list for file system items 1695 // other than files or directories: 1696 dfd.resolve([]); 1697 } 1698 return dfd.promise(); 1699 }, 1700 1701 _handleFileTreeEntries: function (entries, path) { 1702 var that = this; 1703 return $.when 1704 .apply( 1705 $, 1706 $.map(entries, function (entry) { 1707 return that._handleFileTreeEntry(entry, path); 1708 }) 1709 ) 1710 [this._promisePipe](function () { 1711 return Array.prototype.concat.apply([], arguments); 1712 }); 1713 }, 1714 1715 _getDroppedFiles: function (dataTransfer) { 1716 // eslint-disable-next-line no-param-reassign 1717 dataTransfer = dataTransfer || {}; 1718 var items = dataTransfer.items; 1719 if ( 1720 items && 1721 items.length && 1722 (items[0].webkitGetAsEntry || items[0].getAsEntry) 1723 ) { 1724 return this._handleFileTreeEntries( 1725 $.map(items, function (item) { 1726 var entry; 1727 if (item.webkitGetAsEntry) { 1728 entry = item.webkitGetAsEntry(); 1729 if (entry) { 1730 // Workaround for Chrome bug #149735: 1731 entry._file = item.getAsFile(); 1732 } 1733 return entry; 1734 } 1735 return item.getAsEntry(); 1736 }) 1737 ); 1738 } 1739 return $.Deferred().resolve($.makeArray(dataTransfer.files)).promise(); 1740 }, 1741 1742 _getSingleFileInputFiles: function (fileInput) { 1743 // eslint-disable-next-line no-param-reassign 1744 fileInput = $(fileInput); 1745 var entries = 1746 fileInput.prop('webkitEntries') || fileInput.prop('entries'), 1747 files, 1748 value;
1749 if (entries && entries.length) { 1750 return this._handleFileTreeEntries(entries); 1751 } 1752 files = $.makeArray(fileInput.prop('files')); 1753 if (!files.length) { 1754 value = fileInput.prop('value'); 1755 if (!value) { 1756 return $.Deferred().resolve([]).promise(); 1757 } 1758 // If the files property is not available, the browser does not 1759 // support the File API and we add a pseudo File object with 1760 // the input value as name with path information removed: 1761 files = [{ name: value.replace(/^.*\\/, '') }]; 1762 } else if (files[0].name === undefined && files[0].fileName) { 1763 // File normalization for Safari 4 and Firefox 3: 1764 $.each(files, function (index, file) { 1765 file.name = file.fileName; 1766 file.size = file.fileSize; 1767 }); 1768 } 1769 return $.Deferred().resolve(files).promise(); 1770 }, 1771 1772 _getFileInputFiles: function (fileInput) { 1773 if (!(fileInput instanceof $) || fileInput.length === 1) { 1774 return this._getSingleFileInputFiles(fileInput); 1775 } 1776 return $.when 1777 .apply($, $.map(fileInput, this._getSingleFileInputFiles)) 1778 [this._promisePipe](function () { 1779 return Array.prototype.concat.apply([], arguments); 1780 }); 1781 }, 1782 1783 _onChange: function (e) { 1784 var that = this, 1785 data = { 1786 fileInput: $(e.target), 1787 form: $(e.target.form) 1788 }; 1789 this._getFileInputFiles(data.fileInput).always(function (files) { 1790 data.files = files; 1791 if (that.options.replaceFileInput) { 1792 that._replaceFileInput(data); 1793 } 1794 if ( 1795 that._trigger( 1796 'change', 1797 $.Event('change', { delegatedEvent: e }), 1798 data 1799 ) !== false 1800 ) { 1801 that._onAdd(e, data); 1802 } 1803 }); 1804 }, 1805 1806 _onPaste: function (e) { 1807 var items = 1808 e.originalEvent && 1809 e.originalEvent.clipboardData && 1810 e.originalEvent.clipboardData.items, 1811 data = { files: [] }; 1812 if (items && items.length) { 1813 $.each(items, function (index, item) { 1814 var file = item.getAsFile && item.getAsFile(); 1815 if (file) { 1816 data.files.push(file); 1817 } 1818 }); 1819 if ( 1820 this._trigger( 1821 'paste', 1822 $.Event('paste', { delegatedEvent: e }), 1823 data 1824 ) !== false 1825 ) { 1826 this._onAdd(e, data); 1827 } 1828 } 1829 }, 1830 1831 _onDrop: function (e) { 1832 e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; 1833 var that = this, 1834 dataTransfer = e.dataTransfer, 1835 data = {}; 1836 if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { 1837 e.preventDefault(); 1838 this._getDroppedFiles(dataTransfer).always(function (files) { 1839 data.files = files; 1840 if ( 1841 that._trigger( 1842 'drop', 1843 $.Event('drop', { delegatedEvent: e }), 1844 data 1845 ) !== false 1846 ) { 1847 that._onAdd(e, data); 1848 } 1849 }); 1850 } 1851 }, 1852 1853 _onDragOver: getDragHandler('dragover'), 1854 1855 _onDragEnter: getDragHandler('dragenter'), 1856 1857 _onDragLeave: getDragHandler('dragleave'), 1858 1859 _initEventHandlers: function () { 1860 if (this._isXHRUpload(this.options)) { 1861 this._on(this.options.dropZone, { 1862 dragover: this._onDragOver, 1863 drop: this._onDrop, 1864 // event.preventDefault() on dragenter is required for IE10+: 1865 dragenter: this._onDragEnter, 1866 // dragleave is not required, but added for completeness: 1867 dragleave: this._onDragLeave 1868 }); 1869 this._on(this.options.pasteZone, { 1870 paste: this._onPaste 1871 }); 1872 } 1873 if ($.support.fileInput) { 1874 this._on(this.options.fileInput, { 1875 change: this._onChange 1876 }); 1877 } 1878 }, 1879 1880 _destroyEventHandlers: function () { 1881 this._off(this.options.dropZone, 'dragenter dragleave dragover drop'); 1882 this._off(this.options.pasteZone, 'paste'); 1883 this._off(this.options.fileInput, 'change'); 1884 }, 1885 1886 _destroy: function () { 1887 this._destroyEventHandlers(); 1888 }, 1889 1890 _setOption: function (key, value) { 1891 var reinit = $.inArray(key, this._specialOptions) !== -1; 1892 if (reinit) { 1893 this._destroyEventHandlers(); 1894 } 1895 this._super(key, value); 1896 if (reinit) { 1897 this._initSpecialOptions(); 1898 this._initEventHandlers(); 1899 } 1900 }, 1901 1902 _initSpecialOptions: function () { 1903 var options = this.options; 1904 if (options.fileInput === undefined) { 1905 options.fileInput = this.element.is('input[type="file"]') 1906 ? this.element 1907 : this.element.find('input[type="file"]'); 1908 } else if (!(options.fileInput instanceof $)) { 1909 options.fileInput = $(options.fileInput); 1910 } 1911 if (!(options.dropZone instanceof $)) { 1912 options.dropZone = $(options.dropZone); 1913 } 1914 if (!(options.pasteZone instanceof $)) { 1915 options.pasteZone = $(options.pasteZone); 1916 } 1917 }, 1918 1919 _getRegExp: function (str) { 1920 var parts = str.split('/'), 1921 modifiers = parts.pop(); 1922 parts.shift(); 1923 return new RegExp(parts.join('/'), modifiers); 1924 }, 1925 1926 _isRegExpOption: function (key, value) { 1927 return ( 1928 key !== 'url' && 1929 $.type(value) === 'string' && 1930 /^\/.*\/[igm]{0,3}$/.test(value) 1931 ); 1932 }, 1933 1934 _initDataAttributes: function () { 1935 var that = this, 1936 options = this.options, 1937 data = this.element.data(); 1938 // Initialize options set via HTML5 data-attributes: 1939 $.each(this.element[0].attributes, function (index, attr) { 1940 var key = attr.name.toLowerCase(), 1941 value; 1942 if (/^data-/.test(key)) { 1943 // Convert hyphen-ated key to camelCase: 1944 key = key.slice(5).replace(/-[a-z]/g, function (str) { 1945 return str.charAt(1).toUpperCase(); 1946 }); 1947 value = data[key]; 1948 if (that._isRegExpOption(key, value)) { 1949 value = that._getRegExp(value); 1950 } 1951 options[key] = value; 1952 } 1953 }); 1954 }, 1955 1956 _create: function () { 1957 this._initDataAttributes(); 1958 this._initSpecialOptions(); 1959 this._slots = []; 1960 this._sequence = this._getXHRPromise(true); 1961 this._sending = this._active = 0; 1962 this._initProgressObject(this); 1963 this._initEventHandlers(); 1964 }, 1965 1966 // This method is exposed to the widget API and allows to query 1967 // the number of active uploads: 1968 active: function () { 1969 return this._active; 1970 }, 1971 1972 // This method is exposed to the widget API and allows to query 1973 // the widget upload progress. 1974 // It returns an object with loaded, total and bitrate properties 1975 // for the running uploads: 1976 progress: function () { 1977 return this._progress; 1978 }, 1979 1980 // This method is exposed to the widget API and allows adding files 1981 // using the fileupload API. The data parameter accepts an object which 1982 // must have a files property and can contain additional options: 1983 // .fileupload('add', {files: filesList}); 1984 add: function (data) { 1985 var that = this; 1986 if (!data || this.options.disabled) { 1987 return; 1988 } 1989 if (data.fileInput && !data.files) { 1990 this._getFileInputFiles(data.fileInput).always(function (files) { 1991 data.files = files; 1992 that._onAdd(null, data); 1993 }); 1994 } else { 1995 data.files = $.makeArray(data.files); 1996 this._onAdd(null, data); 1997 } 1998 }, 1999 2000 // This method is exposed to the widget API and allows sending files 2001 // using the fileupload API. The data parameter accepts an object which 2002 // must have a files or fileInput property and can contain additional options: 2003 // .fileupload('send', {files: filesList}); 2004 // The method returns a Promise object for the file upload call. 2005 send: function (data) { 2006 if (data && !this.options.disabled) { 2007 if (data.fileInput && !data.files) { 2008 var that = this, 2009 dfd = $.Deferred(), 2010 promise = dfd.promise(), 2011 jqXHR, 2012 aborted; 2013 promise.abort = function () { 2014 aborted = true; 2015 if (jqXHR) { 2016 return jqXHR.abort(); 2017 } 2018 dfd.reject(null, 'abort', 'abort'); 2019 return promise; 2020 }; 2021 this._getFileInputFiles(data.fileInput).always(function (files) { 2022 if (aborted) { 2023 return; 2024 } 2025 if (!files.length) { 2026 dfd.reject(); 2027 return; 2028 } 2029 data.files = files; 2030 jqXHR = that._onSend(null, data); 2031 jqXHR.then( 2032 function (result, textStatus, jqXHR) { 2033 dfd.resolve(result, textStatus, jqXHR); 2034 }, 2035 function (jqXHR, textStatus, errorThrown) { 2036 dfd.reject(jqXHR, textStatus, errorThrown); 2037 } 2038 ); 2039 }); 2040 return this._enhancePromise(promise); 2041 } 2042 data.files = $.makeArray(data.files); 2043 if (data.files.length) { 2044 return this._onSend(null, data); 2045 } 2046 } 2047 return this._getXHRPromise(false, data && data.context); 2048 } 2049 }); 2050});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.