diff options
Diffstat (limited to 'apps')
-rw-r--r-- | apps/files/ajax/upload.php | 62 | ||||
-rw-r--r-- | apps/files/css/files.css | 84 | ||||
-rw-r--r-- | apps/files/js/file-upload.js | 344 | ||||
-rw-r--r-- | apps/files/js/fileactions.js | 2 | ||||
-rw-r--r-- | apps/files/js/filelist.js | 219 | ||||
-rw-r--r-- | apps/files/js/files.js | 30 | ||||
-rw-r--r-- | apps/files/js/jquery.fileupload.js | 1023 | ||||
-rw-r--r-- | apps/files/js/jquery.iframe-transport.js | 70 | ||||
-rw-r--r-- | apps/files/templates/fileexists.html | 26 | ||||
-rw-r--r-- | apps/files_sharing/js/public.js | 5 |
10 files changed, 1373 insertions, 492 deletions
diff --git a/apps/files/ajax/upload.php b/apps/files/ajax/upload.php index 1d03cd89f83..12724c0c5bc 100644 --- a/apps/files/ajax/upload.php +++ b/apps/files/ajax/upload.php @@ -78,7 +78,7 @@ foreach ($_FILES['files']['error'] as $error) { } $files = $_FILES['files']; -$error = ''; +$error = false; $maxUploadFileSize = $storageStats['uploadMaxFilesize']; $maxHumanFileSize = OCP\Util::humanFileSize($maxUploadFileSize); @@ -98,33 +98,69 @@ $result = array(); if (strpos($dir, '..') === false) { $fileCount = count($files['name']); for ($i = 0; $i < $fileCount; $i++) { - $target = OCP\Files::buildNotExistingFileName(stripslashes($dir), $files['name'][$i]); // $path needs to be normalized - this failed within drag'n'drop upload to a sub-folder - $target = \OC\Files\Filesystem::normalizePath($target); - if (is_uploaded_file($files['tmp_name'][$i]) and \OC\Files\Filesystem::fromTmpFile($files['tmp_name'][$i], $target)) { + if (isset($_POST['resolution']) && $_POST['resolution']==='autorename') { + // append a number in brackets like 'filename (2).ext' + $target = OCP\Files::buildNotExistingFileName(stripslashes($dir), $files['name'][$i]); + } else { + $target = \OC\Files\Filesystem::normalizePath(stripslashes($dir).'/'.$files['name'][$i]); + } + + if ( ! \OC\Files\Filesystem::file_exists($target) + || (isset($_POST['resolution']) && $_POST['resolution']==='replace') + ) { + // upload and overwrite file + if (is_uploaded_file($files['tmp_name'][$i]) and \OC\Files\Filesystem::fromTmpFile($files['tmp_name'][$i], $target)) { + + // updated max file size after upload + $storageStats = \OCA\files\lib\Helper::buildFileStorageStatistics($dir); + + $meta = \OC\Files\Filesystem::getFileInfo($target); + if ($meta === false) { + $error = $l->t('Upload failed. Could not get file info.'); + } else { + $result[] = array('status' => 'success', + 'mime' => $meta['mimetype'], + 'mtime' => $meta['mtime'], + 'size' => $meta['size'], + 'id' => $meta['fileid'], + 'name' => basename($target), + 'originalname' => $files['tmp_name'][$i], + 'uploadMaxFilesize' => $maxUploadFileSize, + 'maxHumanFilesize' => $maxHumanFileSize + ); + } + + } else { + $error = $l->t('Upload failed. Could not find uploaded file'); + } + + } else { + // file already exists $meta = \OC\Files\Filesystem::getFileInfo($target); - // updated max file size after upload - $storageStats = \OCA\files\lib\Helper::buildFileStorageStatistics($dir); if ($meta === false) { - OCP\JSON::error(array('data' => array_merge(array('message' => $l->t('Upload failed')), $storageStats))); - exit(); + $error = $l->t('Upload failed. Could not get file info.'); } else { - $result[] = array('status' => 'success', + $result[] = array('status' => 'existserror', 'mime' => $meta['mimetype'], + 'mtime' => $meta['mtime'], 'size' => $meta['size'], 'id' => $meta['fileid'], 'name' => basename($target), - 'originalname' => $files['name'][$i], + 'originalname' => $files['tmp_name'][$i], 'uploadMaxFilesize' => $maxUploadFileSize, 'maxHumanFilesize' => $maxHumanFileSize ); } } } - OCP\JSON::encodedPrint($result); - exit(); } else { $error = $l->t('Invalid directory.'); } -OCP\JSON::error(array('data' => array_merge(array('message' => $error), $storageStats))); +if ($error === false) { + OCP\JSON::encodedPrint($result); + exit(); +} else { + OCP\JSON::error(array('data' => array_merge(array('message' => $error), $storageStats))); +} diff --git a/apps/files/css/files.css b/apps/files/css/files.css index 41d9808c56b..ff593fc4d24 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -358,3 +358,87 @@ table.dragshadow td.size { opacity: 0; } +.oc-dialog .fileexists table { + width: 100%; +} +.oc-dialog .fileexists th { + padding-left: 0; + padding-right: 0; +} +.oc-dialog .fileexists th input[type='checkbox'] { + margin-right: 3px; +} +.oc-dialog .fileexists th:first-child { + width: 230px; +} +.oc-dialog .fileexists th label { + font-weight: normal; + color:black; +} +.oc-dialog .fileexists th .count { + margin-left: 3px; +} +.oc-dialog .fileexists .conflict { + width: 100%; + height: 85px; +} +.oc-dialog .fileexists .conflict.template { + display: none; +} +.oc-dialog .fileexists .conflict .filename { + color:#777; + word-break: break-all; + clear: left; +} +.oc-dialog .fileexists .icon { + width: 64px; + height: 64px; + margin: 0px 5px 5px 5px; + background-repeat: no-repeat; + background-size: 64px 64px; + float: left; +} + +.oc-dialog .fileexists .replacement { + float: left; + width: 230px; +} +.oc-dialog .fileexists .original { + float: left; + width: 230px; +} +.oc-dialog .fileexists .conflicts { + overflow-y:scroll; + max-height: 225px; +} +.oc-dialog .fileexists .conflict input[type='checkbox'] { + float: left; +} + +.oc-dialog .fileexists .toggle { + background-image: url('%webroot%/core/img/actions/triangle-e.png'); + width: 16px; + height: 16px; +} +.oc-dialog .fileexists #allfileslabel { + float:right; +} +.oc-dialog .fileexists #allfiles { + vertical-align: bottom; + position: relative; + top: -3px; +} +.oc-dialog .fileexists #allfiles + span{ + vertical-align: bottom; +} + + + +.oc-dialog .oc-dialog-buttonrow { + width:100%; + text-align:right; +} + +.oc-dialog .oc-dialog-buttonrow .cancel { + float:left; +} diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index 6a53bebfcc0..9ea3658cc73 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -1,3 +1,12 @@ +/** + * The file upload code uses several hooks to interact with blueimps jQuery file upload library: + * 1. the core upload handling hooks are added when initializing the plugin, + * 2. if the browser supports progress events they are added in a separate set after the initialization + * 3. every app can add it's own triggers for fileupload + * - files adds d'n'd handlers and also reacts to done events to add new rows to the filelist + * - TODO pictures upload button + * - TODO music upload button + */ /** * Function that will allow us to know if Ajax uploads are supported @@ -26,51 +35,275 @@ function supportAjaxUploadWithProgress() { } } +/** + * keeps track of uploads in progress and implements callbacks for the conflicts dialog + * @type OC.Upload + */ +OC.Upload = { + _uploads: [], + /** + * cancels a single upload, + * @deprecated because it was only used when a file currently beeing uploaded was deleted. Now they are added after + * they have been uploaded. + * @param string dir + * @param string filename + * @returns unresolved + */ + cancelUpload:function(dir, filename) { + var self = this; + var deleted = false; + //FIXME _selections + jQuery.each(this._uploads, function(i, jqXHR) { + if (selection.dir === dir && selection.uploads[filename]) { + deleted = self.deleteSelectionUpload(selection, filename); + return false; // end searching through selections + } + }); + return deleted; + }, + /** + * deletes the jqHXR object from a data selection + * @param data data + */ + deleteUpload:function(data) { + delete data.jqXHR; + }, + /** + * cancels all uploads + */ + cancelUploads:function() { + console.log('canceling uploads'); + jQuery.each(this._uploads,function(i, jqXHR){ + jqXHR.abort(); + }); + this._uploads = []; + }, + rememberUpload:function(jqXHR){ + if (jqXHR) { + this._uploads.push(jqXHR); + } + }, + /** + * Checks the currently known uploads. + * returns true if any hxr has the state 'pending' + * @returns Boolean + */ + isProcessing:function(){ + var count = 0; + + jQuery.each(this._uploads,function(i, data){ + if (data.state() === 'pending') { + count++; + } + }); + return count > 0; + }, + /** + * callback for the conflicts dialog + * @param data + */ + onCancel:function(data) { + this.cancelUploads(); + }, + /** + * callback for the conflicts dialog + * calls onSkip, onReplace or onAutorename for each conflict + * @param conflicts list of conflict elements + */ + onContinue:function(conflicts) { + var self = this; + //iterate over all conflicts + jQuery.each(conflicts, function (i, conflict) { + conflict = $(conflict); + var keepOriginal = conflict.find('.original input[type="checkbox"]:checked').length === 1; + var keepReplacement = conflict.find('.replacement input[type="checkbox"]:checked').length === 1; + if (keepOriginal && keepReplacement) { + // when both selected -> autorename + self.onAutorename(conflict.data('data')); + } else if (keepReplacement) { + // when only replacement selected -> overwrite + self.onReplace(conflict.data('data')); + } else { + // when only original seleted -> skip + // when none selected -> skip + self.onSkip(conflict.data('data')); + } + }); + }, + /** + * handle skipping an upload + * @param data data + */ + onSkip:function(data){ + this.logStatus('skip', null, data); + this.deleteUpload(data); + }, + /** + * handle replacing a file on the server with an uploaded file + * @param data data + */ + onReplace:function(data){ + this.logStatus('replace', null, data); + data.data.append('resolution', 'replace'); + data.submit(); + }, + /** + * handle uploading a file and letting the server decide a new name + * @param data data + */ + onAutorename:function(data){ + this.logStatus('autorename', null, data); + if (data.data) { + data.data.append('resolution', 'autorename'); + } else { + data.formData.push({name:'resolution',value:'autorename'}); //hack for ie8 + } + data.submit(); + }, + logStatus:function(caption, e, data) { + console.log(caption); + console.log(data); + }, + /** + * TODO checks the list of existing files prior to uploading and shows a simple dialog to choose + * skip all, replace all or choosw which files to keep + * @param array selection of files to upload + * @param callbacks to call: + * onNoConflicts, + * onSkipConflicts, + * onReplaceConflicts, + * onChooseConflicts, + * onCancel + */ + checkExistingFiles: function (selection, callbacks){ + // TODO check filelist before uploading and show dialog on conflicts, use callbacks + callbacks.onNoConflicts(selection); + } +}; + $(document).ready(function() { - if ( $('#file_upload_start').length ) { + if ( $('#file_upload_start').exists() ) { + var file_upload_param = { dropZone: $('#content'), // restrict dropZone to content div + autoUpload: false, + sequentialUploads: true, //singleFileUploads is on by default, so the data.files array will always have length 1 + /** + * on first add of every selection + * - check all files of originalFiles array with files in dir + * - on conflict show dialog + * - skip all -> remember as single skip action for all conflicting files + * - replace all -> remember as single replace action for all conflicting files + * - choose -> show choose dialog + * - mark files to keep + * - when only existing -> remember as single skip action + * - when only new -> remember as single replace action + * - when both -> remember as single autorename action + * - start uploading selection + * @param {type} e + * @param {type} data + * @returns {Boolean} + */ add: function(e, data) { - - if(data.files[0].type === '' && data.files[0].size == 4096) - { + OC.Upload.logStatus('add', e, data); + var that = $(this); + + // we need to collect all data upload objects before starting the upload so we can check their existence + // and set individual conflict actions. unfortunately there is only one variable that we can use to identify + // the selection a data upload is part of, so we have to collect them in data.originalFiles + // turning singleFileUploads off is not an option because we want to gracefully handle server errors like + // already exists + + // create a container where we can store the data objects + if ( ! data.originalFiles.selection ) { + // initialize selection and remember number of files to upload + data.originalFiles.selection = { + uploads: [], + filesToUpload: data.originalFiles.length, + totalBytes: 0 + }; + } + var selection = data.originalFiles.selection; + + // add uploads + if ( selection.uploads.length < selection.filesToUpload ){ + // remember upload + selection.uploads.push(data); + } + + //examine file + var file = data.files[0]; + + if (file.type === '' && file.size === 4096) { data.textStatus = 'dirorzero'; - data.errorThrown = t('files','Unable to upload your file as it is a directory or has 0 bytes'); - var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); - fu._trigger('fail', e, data); - return true; //don't upload this file but go on with next in queue + data.errorThrown = t('files', 'Unable to upload {filename} as it is a directory or has 0 bytes', + {filename: file.name} + ); } - - var totalSize=0; - $.each(data.originalFiles, function(i,file){ - totalSize+=file.size; - }); - - if(totalSize>$('#max_upload').val()){ + + // add size + selection.totalBytes += file.size; + + //check max upload size + if (selection.totalBytes > $('#max_upload').val()) { data.textStatus = 'notenoughspace'; - data.errorThrown = t('files','Not enough space available'); - var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); + data.errorThrown = t('files', 'Not enough space available'); + } + + // end upload for whole selection on error + if (data.errorThrown) { + // trigger fileupload fail + var fu = that.data('blueimp-fileupload') || that.data('fileupload'); fu._trigger('fail', e, data); return false; //don't upload anything } - // start the actual file upload - var jqXHR = data.submit(); + // check existing files when all is collected + if ( selection.uploads.length >= selection.filesToUpload ) { + + //remove our selection hack: + delete data.originalFiles.selection; - // remember jqXHR to show warning to user when he navigates away but an upload is still in progress - if (typeof data.context !== 'undefined' && data.context.data('type') === 'dir') { - var dirName = data.context.data('file'); - if(typeof uploadingFiles[dirName] === 'undefined') { - uploadingFiles[dirName] = {}; - } - uploadingFiles[dirName][data.files[0].name] = jqXHR; - } else { - uploadingFiles[data.files[0].name] = jqXHR; + var callbacks = { + + onNoConflicts: function (selection) { + $.each(selection.uploads, function(i, upload) { + upload.submit(); + }); + }, + onSkipConflicts: function (selection) { + //TODO mark conflicting files as toskip + }, + onReplaceConflicts: function (selection) { + //TODO mark conflicting files as toreplace + }, + onChooseConflicts: function (selection) { + //TODO mark conflicting files as chosen + }, + onCancel: function (selection) { + $.each(selection.uploads, function(i, upload) { + upload.abort(); + }); + } + }; + + OC.Upload.checkExistingFiles(selection, callbacks); + } + + return true; // continue adding files + }, + /** + * called after the first add, does NOT have the data param + * @param e + */ + start: function(e) { + OC.Upload.logStatus('start', e, null); }, submit: function(e, data) { + OC.Upload.rememberUpload(data); if ( ! data.formData ) { // noone set update parameters, we set the minimum data.formData = { @@ -79,13 +312,8 @@ $(document).ready(function() { }; } }, - /** - * called after the first add, does NOT have the data param - * @param e - */ - start: function(e) { - }, fail: function(e, data) { + OC.Upload.logStatus('fail', e, data); if (typeof data.textStatus !== 'undefined' && data.textStatus !== 'success' ) { if (data.textStatus === 'abort') { $('#notification').text(t('files', 'Upload cancelled.')); @@ -99,7 +327,7 @@ $(document).ready(function() { $('#notification').fadeOut(); }, 5000); } - delete uploadingFiles[data.files[0].name]; + OC.Upload.deleteUpload(data); }, /** * called for every successful upload @@ -107,6 +335,7 @@ $(document).ready(function() { * @param data */ done:function(e, data) { + OC.Upload.logStatus('done', e, data); // handle different responses (json or body from iframe for ie) var response; if (typeof data.result === 'string') { @@ -117,21 +346,21 @@ $(document).ready(function() { } var result=$.parseJSON(response); - if(typeof result[0] !== 'undefined' && result[0].status === 'success') { - var filename = result[0].originalname; - - // delete jqXHR reference - if (typeof data.context !== 'undefined' && data.context.data('type') === 'dir') { - var dirName = data.context.data('file'); - delete uploadingFiles[dirName][filename]; - if ($.assocArraySize(uploadingFiles[dirName]) == 0) { - delete uploadingFiles[dirName]; - } - } else { - delete uploadingFiles[filename]; - } - var file = result[0]; - } else { + delete data.jqXHR; + + if(typeof result[0] === 'undefined') { + data.textStatus = 'servererror'; + data.errorThrown = t('files', 'Could not get result from server.'); + var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); + fu._trigger('fail', e, data); + } else if (result[0].status === 'existserror') { + //show "file already exists" dialog + var original = result[0]; + var replacement = data.files[0]; + var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); + OC.dialogs.fileexists(data, original, replacement, OC.Upload, fu); + } else if (result[0].status !== 'success') { + //delete data.jqXHR; data.textStatus = 'servererror'; data.errorThrown = t('files', result.data.message); var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); @@ -144,6 +373,7 @@ $(document).ready(function() { * @param data */ stop: function(e, data) { + OC.Upload.logStatus('stop', e, data); } }; @@ -155,6 +385,7 @@ $(document).ready(function() { // add progress handlers fileupload.on('fileuploadadd', function(e, data) { + OC.Upload.logStatus('progress handle fileuploadadd', e, data); //show cancel button //if(data.dataType !== 'iframe') { //FIXME when is iframe used? only for ie? // $('#uploadprogresswrapper input.stop').show(); @@ -162,23 +393,29 @@ $(document).ready(function() { }); // add progress handlers fileupload.on('fileuploadstart', function(e, data) { + OC.Upload.logStatus('progress handle fileuploadstart', e, data); $('#uploadprogresswrapper input.stop').show(); $('#uploadprogressbar').progressbar({value:0}); $('#uploadprogressbar').fadeIn(); }); fileupload.on('fileuploadprogress', function(e, data) { + OC.Upload.logStatus('progress handle fileuploadprogress', e, data); //TODO progressbar in row }); fileupload.on('fileuploadprogressall', function(e, data) { + OC.Upload.logStatus('progress handle fileuploadprogressall', e, data); var progress = (data.loaded / data.total) * 100; $('#uploadprogressbar').progressbar('value', progress); }); fileupload.on('fileuploadstop', function(e, data) { + OC.Upload.logStatus('progress handle fileuploadstop', e, data); + $('#uploadprogresswrapper input.stop').fadeOut(); $('#uploadprogressbar').fadeOut(); }); fileupload.on('fileuploadfail', function(e, data) { + OC.Upload.logStatus('progress handle fileuploadfail', e, data); //if user pressed cancel hide upload progress bar and cancel button if (data.errorThrown === 'abort') { $('#uploadprogresswrapper input.stop').fadeOut(); @@ -201,9 +438,9 @@ $(document).ready(function() { }; // warn user not to leave the page while upload is in progress - $(window).bind('beforeunload', function(e) { - if ($.assocArraySize(uploadingFiles) > 0) { - return t('files','File upload is in progress. Leaving the page now will cancel the upload.'); + $(window).on('beforeunload', function(e) { + if (OC.Upload.isProcessing()) { + return t('files', 'File upload is in progress. Leaving the page now will cancel the upload.'); } }); @@ -392,4 +629,5 @@ $(document).ready(function() { $('#new>a').click(); }); }); + window.file_upload_param = file_upload_param; }); diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 330fe86f6b3..af7c757276e 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -172,7 +172,7 @@ $(document).ready(function () { FileActions.register('all', 'Delete', OC.PERMISSION_DELETE, function () { return OC.imagePath('core', 'actions/delete'); }, function (filename) { - if (Files.cancelUpload(filename)) { + if (OC.Upload.cancelUpload($('#dir').val(), filename)) { if (filename.substr) { filename = [filename]; } diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index b50d46c98d3..ffdbe5ef01e 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -650,147 +650,160 @@ $(document).ready(function(){ // handle upload events var file_upload_start = $('#file_upload_start'); + file_upload_start.on('fileuploaddrop', function(e, data) { - // only handle drop to dir if fileList exists - if ($('#fileList').length > 0) { - var dropTarget = $(e.originalEvent.target).closest('tr'); - if(dropTarget && dropTarget.data('type') === 'dir') { // drag&drop upload to folder - var dirName = dropTarget.data('file'); - // update folder in form - data.formData = function(form) { - var formArray = form.serializeArray(); - // array index 0 contains the max files size - // array index 1 contains the request token - // array index 2 contains the directory - var parentDir = formArray[2]['value']; - if (parentDir === '/') { - formArray[2]['value'] += dirName; - } else { - formArray[2]['value'] += '/'+dirName; - } - return formArray; + OC.Upload.logStatus('filelist handle fileuploaddrop', e, data); + + var dropTarget = $(e.originalEvent.target).closest('tr'); + if(dropTarget && dropTarget.data('type') === 'dir') { // drag&drop upload to folder + + // remember as context + data.context = dropTarget; + + var dir = dropTarget.data('file'); + + // update folder in form + data.formData = function(form) { + var formArray = form.serializeArray(); + // array index 0 contains the max files size + // array index 1 contains the request token + // array index 2 contains the directory + var parentDir = formArray[2]['value']; + if (parentDir === '/') { + formArray[2]['value'] += dir; + } else { + formArray[2]['value'] += '/' + dir; } - } - } + + return formArray; + }; + } + }); file_upload_start.on('fileuploadadd', function(e, data) { - // only add to fileList if it exists - if ($('#fileList').length > 0) { + OC.Upload.logStatus('filelist handle fileuploadadd', e, data); - if(FileList.deleteFiles && FileList.deleteFiles.indexOf(data.files[0].name)!=-1){//finish delete if we are uploading a deleted file - FileList.finishDelete(null, true); //delete file before continuing + //finish delete if we are uploading a deleted file + if(FileList.deleteFiles && FileList.deleteFiles.indexOf(data.files[0].name)!==-1){ + FileList.finishDelete(null, true); //delete file before continuing + } + + // add ui visualization to existing folder + if(data.context && data.context.data('type') === 'dir') { + // add to existing folder + + // update upload counter ui + var uploadtext = data.context.find('.uploadtext'); + var currentUploads = parseInt(uploadtext.attr('currentUploads')); + currentUploads += 1; + uploadtext.attr('currentUploads', currentUploads); + + var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads); + if(currentUploads === 1) { + var img = OC.imagePath('core', 'loading.gif'); + data.context.find('td.filename').attr('style','background-image:url('+img+')'); + uploadtext.text(translatedText); + uploadtext.show(); + } else { + uploadtext.text(translatedText); } + } + + }); + /* + * when file upload done successfully add row to filelist + * update counter when uploading to sub folder + */ + file_upload_start.on('fileuploaddone', function(e, data) { + OC.Upload.logStatus('filelist handle fileuploaddone', e, data); + + var response; + if (typeof data.result === 'string') { + response = data.result; + } else { + // fetch response from iframe + response = data.result[0].body.innerText; + } + var result=$.parseJSON(response); - // add ui visualization to existing folder or as new stand-alone file? - var dropTarget = $(e.originalEvent.target).closest('tr'); - if(dropTarget && dropTarget.data('type') === 'dir') { - // add to existing folder - var dirName = dropTarget.data('file'); + if(typeof result[0] !== 'undefined' && result[0].status === 'success') { + var file = result[0]; - // set dir context - data.context = $('tr').filterAttr('data-type', 'dir').filterAttr('data-file', dirName); + if (data.context && data.context.data('type') === 'dir') { // update upload counter ui var uploadtext = data.context.find('.uploadtext'); var currentUploads = parseInt(uploadtext.attr('currentUploads')); - currentUploads += 1; + currentUploads -= 1; uploadtext.attr('currentUploads', currentUploads); var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads); - if(currentUploads === 1) { - var img = OC.imagePath('core', 'loading.gif'); + if(currentUploads === 0) { + var img = OC.imagePath('core', 'filetypes/folder.png'); data.context.find('td.filename').attr('style','background-image:url('+img+')'); uploadtext.text(translatedText); - uploadtext.show(); + uploadtext.hide(); } else { uploadtext.text(translatedText); } + + // update folder size + var size = parseInt(data.context.data('size')); + size += parseInt(file.size); + data.context.attr('data-size', size); + data.context.find('td.filesize').text(humanFileSize(size)); + } else { + // add as stand-alone row to filelist - var uniqueName = getUniqueName(data.files[0].name); - var size=t('files','Pending'); - if(data.files[0].size>=0){ + var size=t('files', 'Pending'); + if (data.files[0].size>=0){ size=data.files[0].size; } var date=new Date(); var param = {}; if ($('#publicUploadRequestToken').length) { - param.download_url = document.location.href + '&download&path=/' + $('#dir').val() + '/' + uniqueName; + param.download_url = document.location.href + '&download&path=/' + $('#dir').val() + '/' + file.name; } + //should the file exist in the list remove it + FileList.remove(file.name); + // create new file context - data.context = FileList.addFile(uniqueName,size,date,true,false,param); + data.context = FileList.addFile(file.name, file.size, date, false, false, param); + + // update file data + data.context.attr('data-mime',file.mime).attr('data-id',file.id); + var path = getPathForPreview(file.name); + lazyLoadPreview(path, file.mime, function(previewpath){ + data.context.find('td.filename').attr('style','background-image:url('+previewpath+')'); + }); } } }); - file_upload_start.on('fileuploaddone', function(e, data) { - // only update the fileList if it exists - if ($('#fileList').length > 0) { - var response; - if (typeof data.result === 'string') { - response = data.result; - } else { - // fetch response from iframe - response = data.result[0].body.innerText; - } - var result=$.parseJSON(response); - - if(typeof result[0] !== 'undefined' && result[0].status === 'success') { - var file = result[0]; - - if (data.context.data('type') === 'file') { - // update file data - data.context.attr('data-mime',file.mime).attr('data-id',file.id); - var size = data.context.data('size'); - if(size!=file.size){ - data.context.attr('data-size', file.size); - data.context.find('td.filesize').text(humanFileSize(file.size)); - } - if (FileList.loadingDone) { - FileList.loadingDone(file.name, file.id); - } - } else { - // update upload counter ui - var uploadtext = data.context.find('.uploadtext'); - var currentUploads = parseInt(uploadtext.attr('currentUploads')); - currentUploads -= 1; - uploadtext.attr('currentUploads', currentUploads); - if(currentUploads === 0) { - var img = OC.imagePath('core', 'filetypes/folder.png'); - data.context.find('td.filename').attr('style','background-image:url('+img+')'); - uploadtext.text(''); - uploadtext.hide(); - } else { - uploadtext.text(currentUploads + ' ' + t('files', 'files uploading')); - } + file_upload_start.on('fileuploadstop', function(e, data) { + OC.Upload.logStatus('filelist handle fileuploadstop', e, data); - // update folder size - var size = parseInt(data.context.data('size')); - size += parseInt(file.size); - data.context.attr('data-size', size); - data.context.find('td.filesize').text(humanFileSize(size)); - - } - } + //if user pressed cancel hide upload chrome + if (data.errorThrown === 'abort') { + //cleanup uploading to a dir + var uploadtext = $('tr .uploadtext'); + var img = OC.imagePath('core', 'filetypes/folder.png'); + uploadtext.parents('td.filename').attr('style','background-image:url('+img+')'); + uploadtext.fadeOut(); + uploadtext.attr('currentUploads', 0); } }); file_upload_start.on('fileuploadfail', function(e, data) { - // only update the fileList if it exists - // cleanup files, error notification has been shown by fileupload code - var tr = data.context; - if (typeof tr === 'undefined') { - tr = $('tr').filterAttr('data-file', data.files[0].name); - } - if (tr.attr('data-type') === 'dir') { + OC.Upload.logStatus('filelist handle fileuploadfail', e, data); + + //if user pressed cancel hide upload chrome + if (data.errorThrown === 'abort') { //cleanup uploading to a dir - var uploadtext = tr.find('.uploadtext'); + var uploadtext = $('tr .uploadtext'); var img = OC.imagePath('core', 'filetypes/folder.png'); - tr.find('td.filename').attr('style','background-image:url('+img+')'); - uploadtext.text(''); - uploadtext.hide(); //TODO really hide already - } else { - //remove file - tr.fadeOut(); - tr.remove(); + uploadtext.parents('td.filename').attr('style','background-image:url('+img+')'); + uploadtext.fadeOut(); + uploadtext.attr('currentUploads', 0); } }); diff --git a/apps/files/js/files.js b/apps/files/js/files.js index c2418cfa751..afbb14c5e00 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -1,31 +1,4 @@ -var uploadingFiles = {}; Files={ - cancelUpload:function(filename) { - if(uploadingFiles[filename]) { - uploadingFiles[filename].abort(); - delete uploadingFiles[filename]; - return true; - } - return false; - }, - cancelUploads:function() { - $.each(uploadingFiles,function(index,file) { - if(typeof file['abort'] === 'function') { - file.abort(); - var filename = $('tr').filterAttr('data-file',index); - filename.hide(); - filename.find('input[type="checkbox"]').removeAttr('checked'); - filename.removeClass('selected'); - } else { - $.each(file,function(i,f) { - f.abort(); - delete file[i]; - }); - } - delete uploadingFiles[index]; - }); - procesSelection(); - }, updateMaxUploadFilesize:function(response) { if(response == undefined) { return; @@ -208,7 +181,8 @@ $(document).ready(function() { // Trigger cancelling of file upload $('#uploadprogresswrapper .stop').on('click', function() { - Files.cancelUploads(); + OC.Upload.cancelUploads(); + procesSelection(); }); // Show trash bin diff --git a/apps/files/js/jquery.fileupload.js b/apps/files/js/jquery.fileupload.js index a89e9dc2c44..f9f6cc3a382 100644 --- a/apps/files/js/jquery.fileupload.js +++ b/apps/files/js/jquery.fileupload.js @@ -1,5 +1,5 @@ /* - * jQuery File Upload Plugin 5.9 + * jQuery File Upload Plugin 5.32.2 * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2010, Sebastian Tschan @@ -10,7 +10,7 @@ */ /*jslint nomen: true, unparam: true, regexp: true */ -/*global define, window, document, Blob, FormData, location */ +/*global define, window, document, location, File, Blob, FormData */ (function (factory) { 'use strict'; @@ -27,12 +27,28 @@ }(function ($) { 'use strict'; + // Detect file input support, based on + // http://viljamis.com/blog/2012/file-upload-support-on-mobile/ + $.support.fileInput = !(new RegExp( + // Handle devices which give false positives for the feature detection: + '(Android (1\\.[0156]|2\\.[01]))' + + '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + + '|(w(eb)?OSBrowser)|(webOS)' + + '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' + ).test(window.navigator.userAgent) || + // Feature detection for all other devices: + $('<input type="file">').prop('disabled')); + // The FileReader API is not actually used, but works as feature detection, // as e.g. Safari supports XHR file uploads via the FormData API, // but not non-multipart XHR file uploads: $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader); $.support.xhrFormDataFileUpload = !!window.FormData; + // Detect support for Blob slicing (required for chunked uploads): + $.support.blobSlice = window.Blob && (Blob.prototype.slice || + Blob.prototype.webkitSlice || Blob.prototype.mozSlice); + // The fileupload widget listens for change events on file input fields defined // via fileInput setting and paste or drop events of the given dropZone. // In addition to the default jQuery Widget methods, the fileupload widget @@ -44,17 +60,16 @@ $.widget('blueimp.fileupload', { options: { - // The namespace used for event handler binding on the dropZone and - // fileInput collections. - // If not set, the name of the widget ("fileupload") is used. - namespace: undefined, - // The drop target collection, by the default the complete document. - // Set to null or an empty collection to disable drag & drop support: + // The drop target element(s), by the default the complete document. + // Set to null to disable drag & drop support: dropZone: $(document), - // The file input field collection, that is listened for change events. + // The paste target element(s), by the default the complete document. + // Set to null to disable paste support: + pasteZone: $(document), + // The file input field(s), that are listened to for change events. // If undefined, it is set to the file input fields inside // of the widget element on plugin initialization. - // Set to null or an empty collection to disable the change listener. + // Set to null to disable the change listener. fileInput: undefined, // By default, the file input field is replaced with a clone after // each input field change event. This is required for iframe transport @@ -63,7 +78,8 @@ replaceFileInput: true, // The parameter name for the file form data (the request argument name). // If undefined or empty, the name property of the file input field is - // used, or "files[]" if the file input name property is also empty: + // used, or "files[]" if the file input name property is also empty, + // can be a string or an array of strings: paramName: undefined, // By default, each file of a selection is uploaded using an individual // request for XHR type uploads. Set to false to upload file @@ -108,6 +124,29 @@ // global progress calculation. Set the following option to false to // prevent recalculating the global progress data: recalculateProgress: true, + // Interval in milliseconds to calculate and trigger progress events: + progressInterval: 100, + // Interval in milliseconds to calculate progress bitrate: + bitrateInterval: 500, + // By default, uploads are started automatically when adding files: + autoUpload: true, + + // Error and info messages: + messages: { + uploadedBytes: 'Uploaded bytes exceed file size' + }, + + // Translation function, gets the message key to be translated + // and an object with context specific data as arguments: + i18n: function (message, context) { + message = this.messages[message] || message.toString(); + if (context) { + $.each(context, function (key, value) { + message = message.replace('{' + key + '}', value); + }); + } + return message; + }, // Additional form data to be sent along with the file uploads can be set // using this option, which accepts an array of objects with name and @@ -121,48 +160,81 @@ // The add callback is invoked as soon as files are added to the fileupload // widget (via file input selection, drag & drop, paste or add API call). // If the singleFileUploads option is enabled, this callback will be - // called once for each file in the selection for XHR file uplaods, else + // called once for each file in the selection for XHR file uploads, else // once for each file selection. + // // The upload starts when the submit method is invoked on the data parameter. // The data object contains a files property holding the added files - // and allows to override plugin options as well as define ajax settings. + // and allows you to override plugin options as well as define ajax settings. + // // Listeners for this callback can also be bound the following way: // .bind('fileuploadadd', func); + // // data.submit() returns a Promise object and allows to attach additional // handlers using jQuery's Deferred callbacks: // data.submit().done(func).fail(func).always(func); add: function (e, data) { - data.submit(); + if (data.autoUpload || (data.autoUpload !== false && + $(this).fileupload('option', 'autoUpload'))) { + data.process().done(function () { + data.submit(); + }); + } }, // Other callbacks: + // Callback for the submit event of each file upload: // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); + // Callback for the start of each file upload request: // send: function (e, data) {}, // .bind('fileuploadsend', func); + // Callback for successful uploads: // done: function (e, data) {}, // .bind('fileuploaddone', func); + // Callback for failed (abort or error) uploads: // fail: function (e, data) {}, // .bind('fileuploadfail', func); + // Callback for completed (success, abort or error) requests: // always: function (e, data) {}, // .bind('fileuploadalways', func); + // Callback for upload progress events: // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + // Callback for global upload progress events: // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + // Callback for uploads start, equivalent to the global ajaxStart event: // start: function (e) {}, // .bind('fileuploadstart', func); + // Callback for uploads stop, equivalent to the global ajaxStop event: // stop: function (e) {}, // .bind('fileuploadstop', func); - // Callback for change events of the fileInput collection: + + // Callback for change events of the fileInput(s): // change: function (e, data) {}, // .bind('fileuploadchange', func); - // Callback for paste events to the dropZone collection: + + // Callback for paste events to the pasteZone(s): // paste: function (e, data) {}, // .bind('fileuploadpaste', func); - // Callback for drop events of the dropZone collection: + + // Callback for drop events of the dropZone(s): // drop: function (e, data) {}, // .bind('fileuploaddrop', func); - // Callback for dragover events of the dropZone collection: + + // Callback for dragover events of the dropZone(s): // dragover: function (e) {}, // .bind('fileuploaddragover', func); + // Callback for the start of each chunk upload request: + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); + + // Callback for successful chunk uploads: + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); + + // Callback for failed (abort or error) chunk uploads: + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); + + // Callback for completed (success, abort or error) chunk upload requests: + // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func); + // The plugin options are used as settings object for the ajax calls. // The following are jQuery ajax settings required for the file uploads: processData: false, @@ -170,15 +242,36 @@ cache: false }, - // A list of options that require a refresh after assigning a new value: - _refreshOptionsList: [ - 'namespace', - 'dropZone', + // A list of options that require reinitializing event listeners and/or + // special initialization code: + _specialOptions: [ 'fileInput', + 'dropZone', + 'pasteZone', 'multipart', 'forceIframeTransport' ], + _blobSlice: $.support.blobSlice && function () { + var slice = this.slice || this.webkitSlice || this.mozSlice; + return slice.apply(this, arguments); + }, + + _BitrateTimer: function () { + this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime()); + this.loaded = 0; + this.bitrate = 0; + this.getBitrate = function (now, loaded, interval) { + var timeDiff = now - this.timestamp; + if (!this.bitrate || !interval || timeDiff > interval) { + this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; + this.loaded = loaded; + this.timestamp = now; + } + return this.bitrate; + }; + }, + _isXHRUpload: function (options) { return !options.forceIframeTransport && ((!options.multipart && $.support.xhrFileUpload) || @@ -189,9 +282,11 @@ var formData; if (typeof options.formData === 'function') { return options.formData(options.form); - } else if ($.isArray(options.formData)) { + } + if ($.isArray(options.formData)) { return options.formData; - } else if (options.formData) { + } + if ($.type(options.formData) === 'object') { formData = []; $.each(options.formData, function (name, value) { formData.push({name: name, value: value}); @@ -209,28 +304,66 @@ return total; }, + _initProgressObject: function (obj) { + var progress = { + loaded: 0, + total: 0, + bitrate: 0 + }; + if (obj._progress) { + $.extend(obj._progress, progress); + } else { + obj._progress = progress; + } + }, + + _initResponseObject: function (obj) { + var prop; + if (obj._response) { + for (prop in obj._response) { + if (obj._response.hasOwnProperty(prop)) { + delete obj._response[prop]; + } + } + } else { + obj._response = {}; + } + }, + _onProgress: function (e, data) { if (e.lengthComputable) { - var total = data.total || this._getTotal(data.files), - loaded = parseInt( - e.loaded / e.total * (data.chunkSize || total), - 10 - ) + (data.uploadedBytes || 0); - this._loaded += loaded - (data.loaded || data.uploadedBytes || 0); - data.lengthComputable = true; - data.loaded = loaded; - data.total = total; + var now = ((Date.now) ? Date.now() : (new Date()).getTime()), + loaded; + if (data._time && data.progressInterval && + (now - data._time < data.progressInterval) && + e.loaded !== e.total) { + return; + } + data._time = now; + loaded = Math.floor( + e.loaded / e.total * (data.chunkSize || data._progress.total) + ) + (data.uploadedBytes || 0); + // Add the difference from the previously loaded state + // to the global loaded counter: + this._progress.loaded += (loaded - data._progress.loaded); + this._progress.bitrate = this._bitrateTimer.getBitrate( + now, + this._progress.loaded, + data.bitrateInterval + ); + data._progress.loaded = data.loaded = loaded; + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( + now, + loaded, + data.bitrateInterval + ); // Trigger a custom progress event with a total data property set // to the file size(s) of the current upload and a loaded data // property calculated accordingly: this._trigger('progress', e, data); // Trigger a global progress event for all current file uploads, // including ajax calls queued for sequential file uploads: - this._trigger('progressall', e, { - lengthComputable: true, - loaded: this._loaded, - total: this._total - }); + this._trigger('progressall', e, this._progress); } }, @@ -254,34 +387,30 @@ } }, + _isInstanceOf: function (type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']'; + }, + _initXHRData: function (options) { - var formData, + var that = this, + formData, file = options.files[0], // Ignore non-multipart setting if not supported: - multipart = options.multipart || !$.support.xhrFileUpload; - if (!multipart || options.blob) { - // For non-multipart uploads and chunked uploads, - // file meta data is not part of the request body, - // so we transmit this data as part of the HTTP headers. - // For cross domain requests, these headers must be allowed - // via Access-Control-Allow-Headers or removed using - // the beforeSend callback: - options.headers = $.extend(options.headers, { - 'X-File-Name': file.name, - 'X-File-Type': file.type, - 'X-File-Size': file.size - }); - if (!options.blob) { - // Non-chunked non-multipart upload: - options.contentType = file.type; - options.data = file; - } else if (!multipart) { - // Chunked non-multipart upload: - options.contentType = 'application/octet-stream'; - options.data = options.blob; - } + multipart = options.multipart || !$.support.xhrFileUpload, + paramName = options.paramName[0]; + options.headers = options.headers || {}; + if (options.contentRange) { + options.headers['Content-Range'] = options.contentRange; + } + if (!multipart || options.blob || !this._isInstanceOf('File', file)) { + options.headers['Content-Disposition'] = 'attachment; filename="' + + encodeURI(file.name) + '"'; } - if (multipart && $.support.xhrFormDataFileUpload) { + if (!multipart) { + options.contentType = file.type; + options.data = options.blob || file; + } else if ($.support.xhrFormDataFileUpload) { if (options.postMessage) { // window.postMessage does not allow sending FormData // objects, so we just add the File/Blob objects to @@ -290,19 +419,19 @@ formData = this._getFormData(options); if (options.blob) { formData.push({ - name: options.paramName, + name: paramName, value: options.blob }); } else { $.each(options.files, function (index, file) { formData.push({ - name: options.paramName, + name: options.paramName[index] || paramName, value: file }); }); } } else { - if (options.formData instanceof FormData) { + if (that._isInstanceOf('FormData', options.formData)) { formData = options.formData; } else { formData = new FormData(); @@ -311,14 +440,18 @@ }); } if (options.blob) { - formData.append(options.paramName, options.blob, file.name); + formData.append(paramName, options.blob, file.name); } else { $.each(options.files, function (index, file) { - // File objects are also Blob instances. // This check allows the tests to run with // dummy objects: - if (file instanceof Blob) { - formData.append(options.paramName, file, file.name); + if (that._isInstanceOf('File', file) || + that._isInstanceOf('Blob', file)) { + formData.append( + options.paramName[index] || paramName, + file, + file.name + ); } }); } @@ -330,13 +463,13 @@ }, _initIframeSettings: function (options) { + var targetHost = $('<a></a>').prop('href', options.url).prop('host'); // Setting the dataType to iframe enables the iframe transport: options.dataType = 'iframe ' + (options.dataType || ''); // The iframe transport accepts a serialized array as form data: options.formData = this._getFormData(options); // Add redirect url to form data on cross-domain uploads: - if (options.redirect && $('<a></a>').prop('href', options.url) - .prop('host') !== location.host) { + if (options.redirect && targetHost && targetHost !== location.host) { options.formData.push({ name: options.redirectParamName || 'redirect', value: options.redirect @@ -358,29 +491,58 @@ options.dataType = 'postmessage ' + (options.dataType || ''); } } else { - this._initIframeSettings(options, 'iframe'); + this._initIframeSettings(options); } }, + _getParamName: function (options) { + var fileInput = $(options.fileInput), + paramName = options.paramName; + if (!paramName) { + paramName = []; + fileInput.each(function () { + var input = $(this), + name = input.prop('name') || 'files[]', + i = (input.prop('files') || [1]).length; + while (i) { + paramName.push(name); + i -= 1; + } + }); + if (!paramName.length) { + paramName = [fileInput.prop('name') || 'files[]']; + } + } else if (!$.isArray(paramName)) { + paramName = [paramName]; + } + return paramName; + }, + _initFormSettings: function (options) { // Retrieve missing options from the input field and the // associated form, if available: if (!options.form || !options.form.length) { options.form = $(options.fileInput.prop('form')); + // If the given file input doesn't have an associated form, + // use the default widget file input's form: + if (!options.form.length) { + options.form = $(this.options.fileInput.prop('form')); + } } - if (!options.paramName) { - options.paramName = options.fileInput.prop('name') || - 'files[]'; - } + options.paramName = this._getParamName(options); if (!options.url) { options.url = options.form.prop('action') || location.href; } // The HTTP request method must be "POST" or "PUT": options.type = (options.type || options.form.prop('method') || '') .toUpperCase(); - if (options.type !== 'POST' && options.type !== 'PUT') { + if (options.type !== 'POST' && options.type !== 'PUT' && + options.type !== 'PATCH') { options.type = 'POST'; } + if (!options.formAcceptCharset) { + options.formAcceptCharset = options.form.attr('accept-charset'); + } }, _getAJAXSettings: function (data) { @@ -390,6 +552,21 @@ return options; }, + // jQuery 1.6 doesn't provide .state(), + // while jQuery 1.8+ removed .isRejected() and .isResolved(): + _getDeferredState: function (deferred) { + if (deferred.state) { + return deferred.state(); + } + if (deferred.isResolved()) { + return 'resolved'; + } + if (deferred.isRejected()) { + return 'rejected'; + } + return 'pending'; + }, + // Maps jqXHR callbacks to the equivalent // methods of the given Promise object: _enhancePromise: function (promise) { @@ -414,24 +591,77 @@ return this._enhancePromise(promise); }, + // Adds convenience methods to the data callback argument: + _addConvenienceMethods: function (e, data) { + var that = this, + getPromise = function (data) { + return $.Deferred().resolveWith(that, [data]).promise(); + }; + data.process = function (resolveFunc, rejectFunc) { + if (resolveFunc || rejectFunc) { + data._processQueue = this._processQueue = + (this._processQueue || getPromise(this)) + .pipe(resolveFunc, rejectFunc); + } + return this._processQueue || getPromise(this); + }; + data.submit = function () { + if (this.state() !== 'pending') { + data.jqXHR = this.jqXHR = + (that._trigger('submit', e, this) !== false) && + that._onSend(e, this); + } + return this.jqXHR || that._getXHRPromise(); + }; + data.abort = function () { + if (this.jqXHR) { + return this.jqXHR.abort(); + } + return that._getXHRPromise(); + }; + data.state = function () { + if (this.jqXHR) { + return that._getDeferredState(this.jqXHR); + } + if (this._processQueue) { + return that._getDeferredState(this._processQueue); + } + }; + data.progress = function () { + return this._progress; + }; + data.response = function () { + return this._response; + }; + }, + + // Parses the Range header from the server response + // and returns the uploaded bytes: + _getUploadedBytes: function (jqXHR) { + var range = jqXHR.getResponseHeader('Range'), + parts = range && range.split('-'), + upperBytesPos = parts && parts.length > 1 && + parseInt(parts[1], 10); + return upperBytesPos && upperBytesPos + 1; + }, + // Uploads a file in multiple, sequential requests // by splitting the file up in multiple blob chunks. // If the second parameter is true, only tests if the file // should be uploaded in chunks, but does not invoke any // upload requests: _chunkedUpload: function (options, testOnly) { + options.uploadedBytes = options.uploadedBytes || 0; var that = this, file = options.files[0], fs = file.size, - ub = options.uploadedBytes = options.uploadedBytes || 0, + ub = options.uploadedBytes, mcs = options.maxChunkSize || fs, - // Use the Blob methods with the slice implementation - // according to the W3C Blob API specification: - slice = file.webkitSlice || file.mozSlice || file.slice, - upload, - n, + slice = this._blobSlice, + dfd = $.Deferred(), + promise = dfd.promise(), jqXHR, - pipe; + upload; if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || options.data) { return false; @@ -440,62 +670,84 @@ return true; } if (ub >= fs) { - file.error = 'uploadedBytes'; + file.error = options.i18n('uploadedBytes'); return this._getXHRPromise( false, options.context, [null, 'error', file.error] ); } - // n is the number of blobs to upload, - // calculated via filesize, uploaded bytes and max chunk size: - n = Math.ceil((fs - ub) / mcs); - // The chunk upload method accepting the chunk number as parameter: - upload = function (i) { - if (!i) { - return that._getXHRPromise(true, options.context); - } - // Upload the blobs in sequential order: - return upload(i -= 1).pipe(function () { - // Clone the options object for each chunk upload: - var o = $.extend({}, options); - o.blob = slice.call( - file, - ub + i * mcs, - ub + (i + 1) * mcs - ); - // Store the current chunk size, as the blob itself - // will be dereferenced after data processing: - o.chunkSize = o.blob.size; - // Process the upload data (the blob and potential form data): - that._initXHRData(o); - // Add progress listeners for this chunk upload: - that._initProgressListener(o); - jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context)) - .done(function () { - // Create a progress event if upload is done and - // no progress event has been invoked for this chunk: - if (!o.loaded) { - that._onProgress($.Event('progress', { - lengthComputable: true, - loaded: o.chunkSize, - total: o.chunkSize - }), o); - } - options.uploadedBytes = o.uploadedBytes += - o.chunkSize; - }); - return jqXHR; - }); + // The chunk upload method: + upload = function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options), + currentLoaded = o._progress.loaded; + o.blob = slice.call( + file, + ub, + ub + mcs, + file.type + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Expose the chunk bytes position range: + o.contentRange = 'bytes ' + ub + '-' + + (ub + o.chunkSize - 1) + '/' + fs; + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || + that._getXHRPromise(false, o.context)) + .done(function (result, textStatus, jqXHR) { + ub = that._getUploadedBytes(jqXHR) || + (ub + o.chunkSize); + // Create a progress event if no final progress event + // with loaded equaling total has been triggered + // for this chunk: + if (currentLoaded + o.chunkSize - o._progress.loaded) { + that._onProgress($.Event('progress', { + lengthComputable: true, + loaded: ub - o.uploadedBytes, + total: ub - o.uploadedBytes + }), o); + } + options.uploadedBytes = o.uploadedBytes = ub; + o.result = result; + o.textStatus = textStatus; + o.jqXHR = jqXHR; + that._trigger('chunkdone', null, o); + that._trigger('chunkalways', null, o); + if (ub < fs) { + // File upload not yet complete, + // continue with the next chunk: + upload(); + } else { + dfd.resolveWith( + o.context, + [result, textStatus, jqXHR] + ); + } + }) + .fail(function (jqXHR, textStatus, errorThrown) { + o.jqXHR = jqXHR; + o.textStatus = textStatus; + o.errorThrown = errorThrown; + that._trigger('chunkfail', null, o); + that._trigger('chunkalways', null, o); + dfd.rejectWith( + o.context, + [jqXHR, textStatus, errorThrown] + ); + }); }; - // Return the piped Promise object, enhanced with an abort method, - // which is delegated to the jqXHR object of the current upload, - // and jqXHR callbacks mapped to the equivalent Promise methods: - pipe = upload(n); - pipe.abort = function () { + this._enhancePromise(promise); + promise.abort = function () { return jqXHR.abort(); }; - return this._enhancePromise(pipe); + upload(); + return promise; }, _beforeSend: function (e, data) { @@ -504,99 +756,113 @@ // and no other uploads are currently running, // equivalent to the global ajaxStart event: this._trigger('start'); + // Set timer for global bitrate progress calculation: + this._bitrateTimer = new this._BitrateTimer(); + // Reset the global progress values: + this._progress.loaded = this._progress.total = 0; + this._progress.bitrate = 0; } + // Make sure the container objects for the .response() and + // .progress() methods on the data object are available + // and reset to their initial state: + this._initResponseObject(data); + this._initProgressObject(data); + data._progress.loaded = data.loaded = data.uploadedBytes || 0; + data._progress.total = data.total = this._getTotal(data.files) || 1; + data._progress.bitrate = data.bitrate = 0; this._active += 1; // Initialize the global progress values: - this._loaded += data.uploadedBytes || 0; - this._total += this._getTotal(data.files); + this._progress.loaded += data.loaded; + this._progress.total += data.total; }, _onDone: function (result, textStatus, jqXHR, options) { - if (!this._isXHRUpload(options)) { - // Create a progress event for each iframe load: + var total = options._progress.total, + response = options._response; + if (options._progress.loaded < total) { + // Create a progress event if no final progress event + // with loaded equaling total has been triggered: this._onProgress($.Event('progress', { lengthComputable: true, - loaded: 1, - total: 1 + loaded: total, + total: total }), options); } - options.result = result; - options.textStatus = textStatus; - options.jqXHR = jqXHR; + response.result = options.result = result; + response.textStatus = options.textStatus = textStatus; + response.jqXHR = options.jqXHR = jqXHR; this._trigger('done', null, options); }, _onFail: function (jqXHR, textStatus, errorThrown, options) { - options.jqXHR = jqXHR; - options.textStatus = textStatus; - options.errorThrown = errorThrown; - this._trigger('fail', null, options); + var response = options._response; if (options.recalculateProgress) { // Remove the failed (error or abort) file upload from // the global progress calculation: - this._loaded -= options.loaded || options.uploadedBytes || 0; - this._total -= options.total || this._getTotal(options.files); + this._progress.loaded -= options._progress.loaded; + this._progress.total -= options._progress.total; } + response.jqXHR = options.jqXHR = jqXHR; + response.textStatus = options.textStatus = textStatus; + response.errorThrown = options.errorThrown = errorThrown; + this._trigger('fail', null, options); }, _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { - this._active -= 1; - options.textStatus = textStatus; - if (jqXHRorError && jqXHRorError.always) { - options.jqXHR = jqXHRorError; - options.result = jqXHRorResult; - } else { - options.jqXHR = jqXHRorResult; - options.errorThrown = jqXHRorError; - } + // jqXHRorResult, textStatus and jqXHRorError are added to the + // options object via done and fail callbacks this._trigger('always', null, options); - if (this._active === 0) { - // The stop callback is triggered when all uploads have - // been completed, equivalent to the global ajaxStop event: - this._trigger('stop'); - // Reset the global progress values: - this._loaded = this._total = 0; - } }, _onSend: function (e, data) { + if (!data.submit) { + this._addConvenienceMethods(e, data); + } var that = this, jqXHR, + aborted, slot, pipe, options = that._getAJAXSettings(data), - send = function (resolve, args) { + send = function () { that._sending += 1; + // Set timer for bitrate progress calculation: + options._bitrateTimer = new that._BitrateTimer(); jqXHR = jqXHR || ( - (resolve !== false && - that._trigger('send', e, options) !== false && - (that._chunkedUpload(options) || $.ajax(options))) || - that._getXHRPromise(false, options.context, args) + ((aborted || that._trigger('send', e, options) === false) && + that._getXHRPromise(false, options.context, aborted)) || + that._chunkedUpload(options) || $.ajax(options) ).done(function (result, textStatus, jqXHR) { that._onDone(result, textStatus, jqXHR, options); }).fail(function (jqXHR, textStatus, errorThrown) { that._onFail(jqXHR, textStatus, errorThrown, options); }).always(function (jqXHRorResult, textStatus, jqXHRorError) { - that._sending -= 1; that._onAlways( jqXHRorResult, textStatus, jqXHRorError, options ); + that._sending -= 1; + that._active -= 1; if (options.limitConcurrentUploads && options.limitConcurrentUploads > that._sending) { // Start the next queued upload, // that has not been aborted: var nextSlot = that._slots.shift(); while (nextSlot) { - if (!nextSlot.isRejected()) { + if (that._getDeferredState(nextSlot) === 'pending') { nextSlot.resolve(); break; } nextSlot = that._slots.shift(); } } + if (that._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + that._trigger('stop'); + } }); return jqXHR; }; @@ -609,18 +875,19 @@ this._slots.push(slot); pipe = slot.pipe(send); } else { - pipe = (this._sequence = this._sequence.pipe(send, send)); + this._sequence = this._sequence.pipe(send, send); + pipe = this._sequence; } // Return the piped Promise object, enhanced with an abort method, // which is delegated to the jqXHR object of the current upload, // and jqXHR callbacks mapped to the equivalent Promise methods: pipe.abort = function () { - var args = [undefined, 'abort', 'abort']; + aborted = [undefined, 'abort', 'abort']; if (!jqXHR) { if (slot) { - slot.rejectWith(args); + slot.rejectWith(options.context, aborted); } - return send(false, args); + return send(); } return jqXHR.abort(); }; @@ -634,40 +901,43 @@ result = true, options = $.extend({}, this.options, data), limit = options.limitMultiFileUploads, + paramName = this._getParamName(options), + paramNameSet, + paramNameSlice, fileSet, i; if (!(options.singleFileUploads || limit) || !this._isXHRUpload(options)) { fileSet = [data.files]; + paramNameSet = [paramName]; } else if (!options.singleFileUploads && limit) { fileSet = []; + paramNameSet = []; for (i = 0; i < data.files.length; i += limit) { fileSet.push(data.files.slice(i, i + limit)); + paramNameSlice = paramName.slice(i, i + limit); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); } + } else { + paramNameSet = paramName; } data.originalFiles = data.files; $.each(fileSet || data.files, function (index, element) { - var files = fileSet ? element : [element], - newData = $.extend({}, data, {files: files}); - newData.submit = function () { - newData.jqXHR = this.jqXHR = - (that._trigger('submit', e, this) !== false) && - that._onSend(e, this); - return this.jqXHR; - }; - return (result = that._trigger('add', e, newData)); + var newData = $.extend({}, data); + newData.files = fileSet ? element : [element]; + newData.paramName = paramNameSet[index]; + that._initResponseObject(newData); + that._initProgressObject(newData); + that._addConvenienceMethods(e, newData); + result = that._trigger('add', e, newData); + return result; }); return result; }, - // File Normalization for Gecko 1.9.1 (Firefox 3.5) support: - _normalizeFile: function (index, file) { - if (file.name === undefined && file.size === undefined) { - file.name = file.fileName; - file.size = file.fileSize; - } - }, - _replaceFileInput: function (input) { var inputClone = input.clone(true); $('<form></form>').append(inputClone)[0].reset(); @@ -677,7 +947,7 @@ // Avoid memory leaks with the detached file input: $.cleanData(input.unbind('remove')); // Replace the original file input element in the fileInput - // collection with the clone, which has been copied including + // elements set with the clone, which has been copied including // event handlers: this.options.fileInput = this.options.fileInput.map(function (i, el) { if (el === input[0]) { @@ -692,102 +962,229 @@ } }, - _onChange: function (e) { - var that = e.data.fileupload, - data = { - files: $.each($.makeArray(e.target.files), that._normalizeFile), - fileInput: $(e.target), - form: $(e.target.form) - }; - if (!data.files.length) { + _handleFileTreeEntry: function (entry, path) { + var that = this, + dfd = $.Deferred(), + errorHandler = function (e) { + if (e && !e.entry) { + e.entry = entry; + } + // Since $.when returns immediately if one + // Deferred is rejected, we use resolve instead. + // This allows valid files and invalid items + // to be returned together in one set: + dfd.resolve([e]); + }, + dirReader; + path = path || ''; + if (entry.isFile) { + if (entry._file) { + // Workaround for Chrome bug #149735 + entry._file.relativePath = path; + dfd.resolve(entry._file); + } else { + entry.file(function (file) { + file.relativePath = path; + dfd.resolve(file); + }, errorHandler); + } + } else if (entry.isDirectory) { + dirReader = entry.createReader(); + dirReader.readEntries(function (entries) { + that._handleFileTreeEntries( + entries, + path + entry.name + '/' + ).done(function (files) { + dfd.resolve(files); + }).fail(errorHandler); + }, errorHandler); + } else { + // Return an empy list for file system items + // other than files or directories: + dfd.resolve([]); + } + return dfd.promise(); + }, + + _handleFileTreeEntries: function (entries, path) { + var that = this; + return $.when.apply( + $, + $.map(entries, function (entry) { + return that._handleFileTreeEntry(entry, path); + }) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _getDroppedFiles: function (dataTransfer) { + dataTransfer = dataTransfer || {}; + var items = dataTransfer.items; + if (items && items.length && (items[0].webkitGetAsEntry || + items[0].getAsEntry)) { + return this._handleFileTreeEntries( + $.map(items, function (item) { + var entry; + if (item.webkitGetAsEntry) { + entry = item.webkitGetAsEntry(); + if (entry) { + // Workaround for Chrome bug #149735: + entry._file = item.getAsFile(); + } + return entry; + } + return item.getAsEntry(); + }) + ); + } + return $.Deferred().resolve( + $.makeArray(dataTransfer.files) + ).promise(); + }, + + _getSingleFileInputFiles: function (fileInput) { + fileInput = $(fileInput); + var entries = fileInput.prop('webkitEntries') || + fileInput.prop('entries'), + files, + value; + if (entries && entries.length) { + return this._handleFileTreeEntries(entries); + } + files = $.makeArray(fileInput.prop('files')); + if (!files.length) { + value = fileInput.prop('value'); + if (!value) { + return $.Deferred().resolve([]).promise(); + } // If the files property is not available, the browser does not // support the File API and we add a pseudo File object with // the input value as name with path information removed: - data.files = [{name: e.target.value.replace(/^.*\\/, '')}]; - } - if (that.options.replaceFileInput) { - that._replaceFileInput(data.fileInput); + files = [{name: value.replace(/^.*\\/, '')}]; + } else if (files[0].name === undefined && files[0].fileName) { + // File normalization for Safari 4 and Firefox 3: + $.each(files, function (index, file) { + file.name = file.fileName; + file.size = file.fileSize; + }); } - if (that._trigger('change', e, data) === false || - that._onAdd(e, data) === false) { - return false; + return $.Deferred().resolve(files).promise(); + }, + + _getFileInputFiles: function (fileInput) { + if (!(fileInput instanceof $) || fileInput.length === 1) { + return this._getSingleFileInputFiles(fileInput); } + return $.when.apply( + $, + $.map(fileInput, this._getSingleFileInputFiles) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _onChange: function (e) { + var that = this, + data = { + fileInput: $(e.target), + form: $(e.target.form) + }; + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + if (that.options.replaceFileInput) { + that._replaceFileInput(data.fileInput); + } + if (that._trigger('change', e, data) !== false) { + that._onAdd(e, data); + } + }); }, _onPaste: function (e) { - var that = e.data.fileupload, - cbd = e.originalEvent.clipboardData, - items = (cbd && cbd.items) || [], + var items = e.originalEvent && e.originalEvent.clipboardData && + e.originalEvent.clipboardData.items, data = {files: []}; - $.each(items, function (index, item) { - var file = item.getAsFile && item.getAsFile(); - if (file) { - data.files.push(file); + if (items && items.length) { + $.each(items, function (index, item) { + var file = item.getAsFile && item.getAsFile(); + if (file) { + data.files.push(file); + } + }); + if (this._trigger('paste', e, data) === false || + this._onAdd(e, data) === false) { + return false; } - }); - if (that._trigger('paste', e, data) === false || - that._onAdd(e, data) === false) { - return false; } }, _onDrop: function (e) { - var that = e.data.fileupload, - dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer, - data = { - files: $.each( - $.makeArray(dataTransfer && dataTransfer.files), - that._normalizeFile - ) - }; - if (that._trigger('drop', e, data) === false || - that._onAdd(e, data) === false) { - return false; + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var that = this, + dataTransfer = e.dataTransfer, + data = {}; + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + e.preventDefault(); + this._getDroppedFiles(dataTransfer).always(function (files) { + data.files = files; + if (that._trigger('drop', e, data) !== false) { + that._onAdd(e, data); + } + }); } - e.preventDefault(); }, _onDragOver: function (e) { - var that = e.data.fileupload, - dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer; - if (that._trigger('dragover', e) === false) { - return false; - } + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var dataTransfer = e.dataTransfer; if (dataTransfer) { - dataTransfer.dropEffect = dataTransfer.effectAllowed = 'copy'; + if (this._trigger('dragover', e) === false) { + return false; + } + if ($.inArray('Files', dataTransfer.types) !== -1) { + dataTransfer.dropEffect = 'copy'; + e.preventDefault(); + } } - e.preventDefault(); }, _initEventHandlers: function () { - var ns = this.options.namespace; if (this._isXHRUpload(this.options)) { - this.options.dropZone - .bind('dragover.' + ns, {fileupload: this}, this._onDragOver) - .bind('drop.' + ns, {fileupload: this}, this._onDrop) - .bind('paste.' + ns, {fileupload: this}, this._onPaste); + this._on(this.options.dropZone, { + dragover: this._onDragOver, + drop: this._onDrop + }); + this._on(this.options.pasteZone, { + paste: this._onPaste + }); + } + if ($.support.fileInput) { + this._on(this.options.fileInput, { + change: this._onChange + }); } - this.options.fileInput - .bind('change.' + ns, {fileupload: this}, this._onChange); }, _destroyEventHandlers: function () { - var ns = this.options.namespace; - this.options.dropZone - .unbind('dragover.' + ns, this._onDragOver) - .unbind('drop.' + ns, this._onDrop) - .unbind('paste.' + ns, this._onPaste); - this.options.fileInput - .unbind('change.' + ns, this._onChange); + this._off(this.options.dropZone, 'dragover drop'); + this._off(this.options.pasteZone, 'paste'); + this._off(this.options.fileInput, 'change'); }, _setOption: function (key, value) { - var refresh = $.inArray(key, this._refreshOptionsList) !== -1; - if (refresh) { + var reinit = $.inArray(key, this._specialOptions) !== -1; + if (reinit) { this._destroyEventHandlers(); } - $.Widget.prototype._setOption.call(this, key, value); - if (refresh) { + this._super(key, value); + if (reinit) { this._initSpecialOptions(); this._initEventHandlers(); } @@ -796,42 +1193,68 @@ _initSpecialOptions: function () { var options = this.options; if (options.fileInput === undefined) { - options.fileInput = this.element.is('input:file') ? - this.element : this.element.find('input:file'); + options.fileInput = this.element.is('input[type="file"]') ? + this.element : this.element.find('input[type="file"]'); } else if (!(options.fileInput instanceof $)) { options.fileInput = $(options.fileInput); } if (!(options.dropZone instanceof $)) { options.dropZone = $(options.dropZone); } + if (!(options.pasteZone instanceof $)) { + options.pasteZone = $(options.pasteZone); + } + }, + + _getRegExp: function (str) { + var parts = str.split('/'), + modifiers = parts.pop(); + parts.shift(); + return new RegExp(parts.join('/'), modifiers); + }, + + _isRegExpOption: function (key, value) { + return key !== 'url' && $.type(value) === 'string' && + /^\/.*\/[igm]{0,3}$/.test(value); + }, + + _initDataAttributes: function () { + var that = this, + options = this.options; + // Initialize options set via HTML5 data-attributes: + $.each( + $(this.element[0].cloneNode(false)).data(), + function (key, value) { + if (that._isRegExpOption(key, value)) { + value = that._getRegExp(value); + } + options[key] = value; + } + ); }, _create: function () { - var options = this.options, - dataOpts = $.extend({}, this.element.data()); - dataOpts[this.widgetName] = undefined; - $.extend(options, dataOpts); - options.namespace = options.namespace || this.widgetName; + this._initDataAttributes(); this._initSpecialOptions(); this._slots = []; this._sequence = this._getXHRPromise(true); - this._sending = this._active = this._loaded = this._total = 0; + this._sending = this._active = 0; + this._initProgressObject(this); this._initEventHandlers(); }, - destroy: function () { - this._destroyEventHandlers(); - $.Widget.prototype.destroy.call(this); - }, - - enable: function () { - $.Widget.prototype.enable.call(this); - this._initEventHandlers(); + // This method is exposed to the widget API and allows to query + // the number of active uploads: + active: function () { + return this._active; }, - disable: function () { - this._destroyEventHandlers(); - $.Widget.prototype.disable.call(this); + // This method is exposed to the widget API and allows to query + // the widget upload progress. + // It returns an object with loaded, total and bitrate properties + // for the running uploads: + progress: function () { + return this._progress; }, // This method is exposed to the widget API and allows adding files @@ -839,21 +1262,65 @@ // must have a files property and can contain additional options: // .fileupload('add', {files: filesList}); add: function (data) { + var that = this; if (!data || this.options.disabled) { return; } - data.files = $.each($.makeArray(data.files), this._normalizeFile); - this._onAdd(null, data); + if (data.fileInput && !data.files) { + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + that._onAdd(null, data); + }); + } else { + data.files = $.makeArray(data.files); + this._onAdd(null, data); + } }, // This method is exposed to the widget API and allows sending files // using the fileupload API. The data parameter accepts an object which - // must have a files property and can contain additional options: + // must have a files or fileInput property and can contain additional options: // .fileupload('send', {files: filesList}); // The method returns a Promise object for the file upload call. send: function (data) { if (data && !this.options.disabled) { - data.files = $.each($.makeArray(data.files), this._normalizeFile); + if (data.fileInput && !data.files) { + var that = this, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + aborted; + promise.abort = function () { + aborted = true; + if (jqXHR) { + return jqXHR.abort(); + } + dfd.reject(null, 'abort', 'abort'); + return promise; + }; + this._getFileInputFiles(data.fileInput).always( + function (files) { + if (aborted) { + return; + } + if (!files.length) { + dfd.reject(); + return; + } + data.files = files; + jqXHR = that._onSend(null, data).then( + function (result, textStatus, jqXHR) { + dfd.resolve(result, textStatus, jqXHR); + }, + function (jqXHR, textStatus, errorThrown) { + dfd.reject(jqXHR, textStatus, errorThrown); + } + ); + } + ); + return this._enhancePromise(promise); + } + data.files = $.makeArray(data.files); if (data.files.length) { return this._onSend(null, data); } @@ -863,4 +1330,4 @@ }); -})); +}));
\ No newline at end of file diff --git a/apps/files/js/jquery.iframe-transport.js b/apps/files/js/jquery.iframe-transport.js index d85c0c11297..5c9df77976b 100644 --- a/apps/files/js/jquery.iframe-transport.js +++ b/apps/files/js/jquery.iframe-transport.js @@ -1,5 +1,5 @@ /* - * jQuery Iframe Transport Plugin 1.3 + * jQuery Iframe Transport Plugin 1.7 * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2011, Sebastian Tschan @@ -30,27 +30,45 @@ // The iframe transport accepts three additional options: // options.fileInput: a jQuery collection of file input fields // options.paramName: the parameter name for the file form data, - // overrides the name property of the file input field(s) + // overrides the name property of the file input field(s), + // can be a string or an array of strings. // options.formData: an array of objects with name and value properties, // equivalent to the return data of .serializeArray(), e.g.: // [{name: 'a', value: 1}, {name: 'b', value: 2}] $.ajaxTransport('iframe', function (options) { - if (options.async && (options.type === 'POST' || options.type === 'GET')) { + if (options.async) { var form, - iframe; + iframe, + addParamChar; return { send: function (_, completeCallback) { form = $('<form style="display:none;"></form>'); + form.attr('accept-charset', options.formAcceptCharset); + addParamChar = /\?/.test(options.url) ? '&' : '?'; + // XDomainRequest only supports GET and POST: + if (options.type === 'DELETE') { + options.url = options.url + addParamChar + '_method=DELETE'; + options.type = 'POST'; + } else if (options.type === 'PUT') { + options.url = options.url + addParamChar + '_method=PUT'; + options.type = 'POST'; + } else if (options.type === 'PATCH') { + options.url = options.url + addParamChar + '_method=PATCH'; + options.type = 'POST'; + } // javascript:false as initial iframe src // prevents warning popups on HTTPS in IE6. // IE versions below IE8 cannot set the name property of // elements that have already been added to the DOM, // so we set the name along with the iframe HTML markup: + counter += 1; iframe = $( '<iframe src="javascript:false;" name="iframe-transport-' + - (counter += 1) + '"></iframe>' + counter + '"></iframe>' ).bind('load', function () { - var fileInputClones; + var fileInputClones, + paramNames = $.isArray(options.paramName) ? + options.paramName : [options.paramName]; iframe .unbind('load') .bind('load', function () { @@ -79,7 +97,12 @@ // (happens on form submits to iframe targets): $('<iframe src="javascript:false;"></iframe>') .appendTo(form); - form.remove(); + window.setTimeout(function () { + // Removing the form in a setTimeout call + // allows Chrome's developer tools to display + // the response result + form.remove(); + }, 0); }); form .prop('target', iframe.prop('name')) @@ -101,8 +124,11 @@ return fileInputClones[index]; }); if (options.paramName) { - options.fileInput.each(function () { - $(this).prop('name', options.paramName); + options.fileInput.each(function (index) { + $(this).prop( + 'name', + paramNames[index] || options.paramName + ); }); } // Appending the file input fields to the hidden form @@ -144,22 +170,36 @@ }); // The iframe transport returns the iframe content document as response. - // The following adds converters from iframe to text, json, html, and script: + // The following adds converters from iframe to text, json, html, xml + // and script. + // Please note that the Content-Type for JSON responses has to be text/plain + // or text/html, if the browser doesn't include application/json in the + // Accept header, else IE will show a download dialog. + // The Content-Type for XML responses on the other hand has to be always + // application/xml or text/xml, so IE properly parses the XML response. + // See also + // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation $.ajaxSetup({ converters: { 'iframe text': function (iframe) { - return $(iframe[0].body).text(); + return iframe && $(iframe[0].body).text(); }, 'iframe json': function (iframe) { - return $.parseJSON($(iframe[0].body).text()); + return iframe && $.parseJSON($(iframe[0].body).text()); }, 'iframe html': function (iframe) { - return $(iframe[0].body).html(); + return iframe && $(iframe[0].body).html(); + }, + 'iframe xml': function (iframe) { + var xmlDoc = iframe && iframe[0]; + return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc : + $.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || + $(xmlDoc.body).html()); }, 'iframe script': function (iframe) { - return $.globalEval($(iframe[0].body).text()); + return iframe && $.globalEval($(iframe[0].body).text()); } } }); -})); +}));
\ No newline at end of file diff --git a/apps/files/templates/fileexists.html b/apps/files/templates/fileexists.html new file mode 100644 index 00000000000..a5b2fb76908 --- /dev/null +++ b/apps/files/templates/fileexists.html @@ -0,0 +1,26 @@ +<div id="{dialog_name}" title="{title}" class="fileexists"> + <span class="why">{why}<!-- Which files do you want to keep --></span><br/> + <span class="what">{what}<!-- If you select both versions, the copied file will have a number added to its name. --></span><br/> + <br/> + <table> + <th><label><input class="allnewfiles" type="checkbox" />New Files<span class="count"></span></label></th> + <th><label><input class="allexistingfiles" type="checkbox" />Already existing files<span class="count"></span></label></th> + </table> + <div class="conflicts"> + <div class="conflict template"> + <div class="filename"></div> + <div class="replacement"> + <input type="checkbox" /> + <span class="svg icon"></span> + <div class="mtime"></div> + <div class="size"></div> + </div> + <div class="original"> + <input type="checkbox" /> + <span class="svg icon"></span> + <div class="mtime"></div> + <div class="size"></div> + </div> + </div> + </div> +</div> diff --git a/apps/files_sharing/js/public.js b/apps/files_sharing/js/public.js index acabc9a5c10..67320b1cdb6 100644 --- a/apps/files_sharing/js/public.js +++ b/apps/files_sharing/js/public.js @@ -62,6 +62,9 @@ $(document).ready(function() { $('#controls').append($('#additional_controls div#uploadprogresswrapper')); // Cancel upload trigger - $('#cancel_upload_button').click(Files.cancelUploads); + $('#cancel_upload_button').click(function() { + OC.Upload.cancelUploads(); + procesSelection(); + }); }); |