From 0492b1f6e5589bcde05aaaf5dd77d30cc8a42c59 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Sun, 3 Feb 2019 09:49:02 +0100 Subject: [PATCH] HTML5 Upload Progress (#6177) (#6583) Replaced all old upload progress code in favour of ajax upload progress. Instead of posting a hidden iframe, we now use AJAX (as we did for drag-n-drop). Removed code for old browsers. Now we support IE >= 10, Firefox > 4. Upload progress may not work in some more, but support is quite good. --- config/defaults.inc.php | 5 - program/include/rcmail.php | 90 +-------- program/js/app.js | 315 +++++++++++------------------ program/js/editor.js | 2 +- program/steps/mail/attachments.inc | 5 - program/steps/settings/upload.inc | 5 - 6 files changed, 126 insertions(+), 296 deletions(-) diff --git a/config/defaults.inc.php b/config/defaults.inc.php index fb7b9a542..b60a0d04d 100644 --- a/config/defaults.inc.php +++ b/config/defaults.inc.php @@ -796,11 +796,6 @@ $config['max_pagesize'] = 200; // Minimal value of user's 'refresh_interval' setting (in seconds) $config['min_refresh_interval'] = 60; -// Enables files upload indicator. Requires APC installed and enabled apc.rfc1867 option. -// By default refresh time is set to 1 second. You can set this value to true -// or any integer value indicating number of seconds. -$config['upload_progress'] = false; - // Specifies for how many seconds the Undo button will be available // after object delete action. Currently used with supporting address book sources. // Setting it to 0, disables the feature. diff --git a/program/include/rcmail.php b/program/include/rcmail.php index e94d9019b..f9d925eb2 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -2030,76 +2030,12 @@ class rcmail extends rcube /** * File upload progress handler. + * + * @deprecated We're using HTML5 upload progress */ public function upload_progress() { - $params = array( - 'action' => $this->action, - 'name' => rcube_utils::get_input_value('_progress', rcube_utils::INPUT_GET), - ); - - if (function_exists('uploadprogress_get_info')) { - $status = uploadprogress_get_info($params['name']); - - if (!empty($status)) { - $params['current'] = $status['bytes_uploaded']; - $params['total'] = $status['bytes_total']; - } - } - - if (!isset($status) && filter_var(ini_get('apc.rfc1867'), FILTER_VALIDATE_BOOLEAN) - && ini_get('apc.rfc1867_name') - ) { - $prefix = ini_get('apc.rfc1867_prefix'); - $status = apc_fetch($prefix . $params['name']); - - if (!empty($status)) { - $params['current'] = $status['current']; - $params['total'] = $status['total']; - } - } - - if (!isset($status) && filter_var(ini_get('session.upload_progress.enabled'), FILTER_VALIDATE_BOOLEAN) - && ini_get('session.upload_progress.name') - ) { - $key = ini_get('session.upload_progress.prefix') . $params['name']; - - $params['total'] = $_SESSION[$key]['content_length']; - $params['current'] = $_SESSION[$key]['bytes_processed']; - } - - if (!empty($params['total'])) { - $total = $this->show_bytes($params['total'], $unit); - switch ($unit) { - case 'GB': - $gb = $params['current']/1073741824; - $current = sprintf($gb >= 10 ? "%d" : "%.1f", $gb); - break; - case 'MB': - $mb = $params['current']/1048576; - $current = sprintf($mb >= 10 ? "%d" : "%.1f", $mb); - break; - case 'KB': - $current = round($params['current']/1024); - break; - case 'B': - default: - $current = $params['current']; - break; - } - - $params['percent'] = round($params['current']/$params['total']*100); - $params['text'] = $this->gettext(array( - 'name' => 'uploadprogress', - 'vars' => array( - 'percent' => $params['percent'] . '%', - 'current' => $current, - 'total' => $total - ) - )); - } - - $this->output->command('upload_progress_update', $params); + // NOOP $this->output->send(); } @@ -2112,24 +2048,6 @@ class rcmail extends rcube */ public function upload_init($max_size = null) { - // Enable upload progress bar - if ($seconds = $this->config->get('upload_progress')) { - if (function_exists('uploadprogress_get_info')) { - $field_name = 'UPLOAD_IDENTIFIER'; - } - if (!$field_name && filter_var(ini_get('apc.rfc1867'), FILTER_VALIDATE_BOOLEAN)) { - $field_name = ini_get('apc.rfc1867_name'); - } - if (!$field_name && filter_var(ini_get('session.upload_progress.enabled'), FILTER_VALIDATE_BOOLEAN)) { - $field_name = ini_get('session.upload_progress.name'); - } - - if ($field_name) { - $this->output->set_env('upload_progress_name', $field_name); - $this->output->set_env('upload_progress_time', (int) $seconds); - } - } - // find max filesize value $max_filesize = rcube_utils::max_upload_size(); if ($max_size && $max_size < $max_filesize) { @@ -2147,6 +2065,8 @@ class rcmail extends rcube 'name' => 'filecounterror', 'vars' => array('count' => $max_filecount)))); } + $this->output->add_label('uploadprogress', 'GB', 'MB', 'KB', 'B'); + return $max_filesize_txt; } diff --git a/program/js/app.js b/program/js/app.js index 19958cd6c..57726a026 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -50,6 +50,7 @@ function rcube_webmail() this.menu_buttons = {}; this.entity_selectors = []; this.image_style = {}; + this.uploads = {}; // webmail client settings this.dblclick_time = 500; @@ -684,7 +685,7 @@ function rcube_webmail() } // activate html5 file drop feature (if browser supports it and if configured) - if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) { + if (this.gui_objects.filedrop && this.env.filedrop && window.FormData) { $(document.body).on('dragover dragleave drop', function(e) { return ref.document_drag_hover(e, e.type == 'dragover'); }); $(this.gui_objects.filedrop).addClass('droptarget') .on('dragover dragleave', function(e) { return ref.file_drag_hover(e, e.type == 'dragover'); }) @@ -1393,8 +1394,6 @@ function rcube_webmail() var form = props || this.gui_objects.importform, importlock = this.set_busy(true, 'importwait'); - $('[name="_unlock"]', form).val(importlock); - if (!(flag = this.upload_file(form, 'import', importlock))) { this.set_busy(false, null, importlock); if (flag !== false) @@ -5314,77 +5313,21 @@ function rcube_webmail() // upload (attachment) file this.upload_file = function(form, action, lock) { - if (!form) - return; - - // count files and size on capable browser - var size = 0, numfiles = 0; - - $.each($(form).get(0).elements || [], function() { - if (this.type != 'file') - return; - - var i, files = this.files ? this.files.length : (this.value ? 1 : 0); - - // check file size - if (this.files) { - for (i=0; i < files; i++) - size += this.files[i].size; - } - - numfiles += files; - }); - - // create hidden iframe and post upload form - if (numfiles) { - if (this.env.max_filesize && this.env.filesizeerror && size > this.env.max_filesize) { - this.display_message(this.env.filesizeerror, 'error'); - return false; - } - - if (this.env.max_filecount && this.env.filecounterror && numfiles > this.env.max_filecount) { - this.display_message(this.env.filecounterror, 'error'); - return false; - } - - var frame_name = this.async_upload_form(form, action || 'upload', function(e) { - var d, content = ''; - try { - if (this.contentDocument) { - d = this.contentDocument; - } else if (this.contentWindow) { - d = this.contentWindow.document; - } - content = d.childNodes[1].innerHTML; - } catch (err) {} - - if (!content.match(/add2attachment/) && (!bw.opera || (ref.env.uploadframe && ref.env.uploadframe == e.data.ts))) { - if (!content.match(/display_message/)) - ref.display_message('fileuploaderror', 'error'); - ref.remove_from_attachment_list(e.data.ts); - - if (lock) - ref.set_busy(false, null, lock); + if (form) { + var fname, files = []; + $('input', form).each(function() { + if (this.files) { + fname = this.name; + for (var i=0; i < this.files.length; i++) + files.push(this.files[i]); } - // Opera hack: handle double onload - if (bw.opera) - ref.env.uploadframe = e.data.ts; }); - // display upload indicator and cancel button - var content = '' + this.get_label('uploading' + (numfiles > 1 ? 'many' : '')) + '', - ts = frame_name.replace(/^rcmupload/, ''); - - this.add2attachment_list(ts, { name:'', html:content, classname:'uploading', frame:frame_name, complete:false }); - - // upload progress support - if (this.env.upload_progress_time) { - this.upload_progress_start('upload', ts); - } - - // set reference to the form object - this.gui_objects.attachmentform = form; - return true; + return this.file_upload(files, {_id: this.env.compose_id || ''}, { + name: fname, + action: action, + lock: lock + }); } }; @@ -5408,12 +5351,15 @@ function rcube_webmail() var label, indicator, li = $('
  • '); + if (!att.complete && att.html.indexOf('<') < 0) + att.html = '' + att.html + ''; + if (!att.complete && this.env.loadingicon) att.html = '' + att.html; - if (!att.complete && att.frame) { + if (!att.complete) { label = this.get_label('cancel'); - att.html = '' + att.html = '' + (this.env.cancelicon ? ''+label+'' : '' + label + '') + '' + att.html; } @@ -5453,35 +5399,16 @@ function rcube_webmail() return false; }; - this.cancel_attachment_upload = function(name, frame_name) + this.cancel_attachment_upload = function(name) { - if (!name || !frame_name) + if (!name || !this.uploads[name]) return false; this.remove_from_attachment_list(name); - $("iframe[name='"+frame_name+"']").remove(); + this.uploads[name].abort(); return false; }; - this.upload_progress_start = function(action, name) - { - setTimeout(function() { ref.http_request(action, {_progress: name}); }, - this.env.upload_progress_time * 1000); - }; - - this.upload_progress_update = function(param) - { - var elem = $('#'+param.name + ' > span'); - - if (!elem.length || !param.text) - return; - - elem.text(param.text); - - if (!param.done) - this.upload_progress_start(param.action, param.name); - }; - // rename uploaded attachment (in compose) this.rename_attachment = function(id) { @@ -9069,7 +8996,7 @@ function rcube_webmail() return; if (response.unlock) - this.set_busy(false); + this.set_busy(false, null, response.unlock); this.triggerEvent('responsebefore', {response: response}); this.triggerEvent('responsebefore'+response.action, {response: response}); @@ -9472,19 +9399,6 @@ function rcube_webmail() frame_name = 'rcmupload' + ts, frame = this.dummy_iframe(frame_name); - // upload progress support - if (this.env.upload_progress_name) { - var fname = this.env.upload_progress_name, - field = $('input[name='+fname+']', form); - - if (!field.length) { - field = $('').attr({type: 'hidden', name: fname}); - field.prependTo(form); - } - - field.val(ts); - } - // handle upload errors by parsing iframe content in onload frame.on('load', {ts:ts}, onload); @@ -9532,19 +9446,14 @@ function rcube_webmail() this.file_drag_hover(e, false); // prepare multipart form data composition - var uri, size = 0, numfiles = 0, + var uri, files = e.target.files || e.dataTransfer.files, - formdata = window.FormData ? new FormData() : null, - fieldname = (this.env.filedrop.fieldname || '_file') + (this.env.filedrop.single ? '' : '[]'), - boundary = '------multipartformboundary' + (new Date).getTime(), - dashdash = '--', crlf = '\r\n', - multipart = dashdash + boundary + crlf, args = {_id: this.env.compose_id || this.env.cid || '', _remote: 1, _from: this.env.action}; if (!files || !files.length) { // Roundcube attachment, pass its uri to the backend and attach if (uri = e.dataTransfer.getData('roundcube-uri')) { - var ts = new Date().getTime(), + var ts = 'upload' + new Date().getTime(), // jQuery way to escape filename (#1490530) content = $('').text(e.dataTransfer.getData('roundcube-name') || this.get_label('attaching')).html(); @@ -9557,110 +9466,126 @@ function rcube_webmail() this.http_post(this.env.filedrop.action || 'upload', args); } + return; } - // inline function to submit the files to the server - var submit_data = function() { - if (ref.env.max_filesize && ref.env.filesizeerror && size > ref.env.max_filesize) { - ref.display_message(ref.env.filesizeerror, 'error'); - return; + this.file_upload(files, args, { + name: (this.env.filedrop.fieldname || '_file') + (this.env.filedrop.single ? '' : '[]'), + single: this.env.filedrop.single, + filter: this.env.filedrop.filter, + action: ref.env.filedrop.action + }); + }; + + // Files upload using ajax + this.file_upload = function(files, post_args, props) + { + if (!window.FormData || !files || !files.length) + return false; + + var f, i, fname, size = 0, numfiles = 0, + formdata = new FormData(), + fieldname = props.name || '_file[]', + limit = props.single ? 1 : files.length; + args = $.extend({_remote: 1, _from: this.env.action}, post_args || {}); + + // add files to form data + for (i=0; numfiles < limit && (f = files[i]); i++) { + // filter by file type if requested + if (props.filter && !f.type.match(new RegExp(props.filter))) { + // TODO: show message to user + continue; } - if (ref.env.max_filecount && ref.env.filecounterror && numfiles > ref.env.max_filecount) { - ref.display_message(ref.env.filecounterror, 'error'); - return; + formdata.append(fieldname, f); + size += f.size; + fname = f.name; + numfiles++; + } + + if (numfiles) { + if (this.env.max_filesize && this.env.filesizeerror && size > this.env.max_filesize) { + this.display_message(this.env.filesizeerror, 'error'); + return false; } - var multiple = files.length > 1, - ts = new Date().getTime(), + if (this.env.max_filecount && this.env.filecounterror && numfiles > this.env.max_filecount) { + this.display_message(this.env.filecounterror, 'error'); + return false; + } + + var ts = 'upload' + new Date().getTime(), + label = numfiles > 1 ? this.get_label('uploadingmany') : fname, // jQuery way to escape filename (#1490530) - content = $('').text(multiple ? ref.get_label('uploadingmany') : files[0].name).html(); + content = $('').text(label).html(); // add to attachments list - if (!ref.add2attachment_list(ts, { name:'', html:content, classname:'uploading', complete:false })) - ref.file_upload_id = ref.set_busy(true, 'uploading'); - - // complete multipart content and post request - multipart += dashdash + boundary + dashdash + crlf; + if (!this.add2attachment_list(ts, {name: '', html: content, classname: 'uploading', complete: false}) && !props.lock) + props.lock = this.file_upload_id = this.set_busy(true, 'uploading'); args._uploadid = ts; + args._unlock = props.lock; - $.ajax({ + this.uploads[ts] = $.ajax({ type: 'POST', dataType: 'json', - url: ref.url(ref.env.filedrop.action || 'upload', args), - contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary, + url: this.url(props.action || 'upload', args), + contentType: false, processData: false, timeout: 0, // disable default timeout set in ajaxSetup() - data: formdata || multipart, - headers: {'X-Roundcube-Request': ref.env.request_token}, - xhr: function() { var xhr = jQuery.ajaxSettings.xhr(); if (!formdata && xhr.sendAsBinary) xhr.send = xhr.sendAsBinary; return xhr; }, - success: function(data){ ref.http_response(data); }, - error: function(o, status, err) { ref.http_error(o, status, err, null, 'attachment'); } + data: formdata, + headers: {'X-Roundcube-Request': this.env.request_token}, + xhr: function() { + var xhr = $.ajaxSettings.xhr(); + if (xhr.upload && ref.labels.uploadprogress) { + xhr.upload.onprogress = function(e) { + var msg = ref.file_upload_msg(e.loaded, e.total); + if (msg) { + $('#' + ts).find('.uploading').text(msg); + } + }; + } + return xhr; + }, + success: function(data) { + delete ref.uploads[ts]; + ref.http_response(data); + }, + error: function(o, status, err) { + delete ref.uploads[ts]; + ref.remove_from_attachment_list(ts); + ref.http_error(o, status, err, props.lock, 'attachment'); + } }); - }; + } - // get contents of all dropped files - var last = this.env.filedrop.single ? 0 : files.length - 1; - for (var j=0, i=0, f; j <= last && (f = files[i]); i++) { - if (!f.name) f.name = f.fileName; - if (!f.size) f.size = f.fileSize; - if (!f.type) f.type = 'application/octet-stream'; + return true; + }; - // file name contains non-ASCII characters, do UTF8-binary string conversion. - if (!formdata && /[^\x20-\x7E]/.test(f.name)) - f.name_bin = unescape(encodeURIComponent(f.name)); + this.file_upload_msg = function(current, total) + { + if (total && current < total) { + var percent = Math.round(current/total * 100), + label = ref.get_label('uploadprogress'); - // filter by file type if requested - if (this.env.filedrop.filter && !f.type.match(new RegExp(this.env.filedrop.filter))) { - // TODO: show message to user - continue; + if (total >= 1073741824) { + total = parseFloat(total/1073741824).toFixed(1) + ' ' . this.get_label('GB'); + current = parseFloat(current/1073741824).toFixed(1); } - - size += f.size; - numfiles++; - - // do it the easy way with FormData (FF 4+, Chrome 5+, Safari 5+) - if (formdata) { - formdata.append(fieldname, f); - if (j == last) - return submit_data(); - } - // use FileReader supporetd by Firefox 3.6 - else if (window.FileReader) { - var reader = new FileReader(); - - // closure to pass file properties to async callback function - reader.onload = (function(file, j) { - return function(e) { - multipart += 'Content-Disposition: form-data; name="' + fieldname + '"'; - multipart += '; filename="' + (f.name_bin || file.name) + '"' + crlf; - multipart += 'Content-Length: ' + file.size + crlf; - multipart += 'Content-Type: ' + file.type + crlf + crlf; - multipart += reader.result + crlf; - multipart += dashdash + boundary + crlf; - - if (j == last) // we're done, submit the data - return submit_data(); - } - })(f,j); - reader.readAsBinaryString(f); + else if (total >= 1048576) { + total = parseFloat(total/1048576).toFixed(1) + ' ' + this.get_label('MB'); + current = parseFloat(current/1048576).toFixed(1); } - // Firefox 3 - else if (f.getAsBinary) { - multipart += 'Content-Disposition: form-data; name="' + fieldname + '"'; - multipart += '; filename="' + (f.name_bin || f.name) + '"' + crlf; - multipart += 'Content-Length: ' + f.size + crlf; - multipart += 'Content-Type: ' + f.type + crlf + crlf; - multipart += f.getAsBinary() + crlf; - multipart += dashdash + boundary +crlf; - - if (j == last) - return submit_data(); + else if (total >= 1024) { + total = parseInt(total/1024) + ' ' + this.get_label('KB'); + current = parseInt(current/1024); + } + else { + total = total + ' ' + this.get_label('B'); } - j++; + return label.replace('$percent', percent + '%').replace('$current', current).replace('$total', total); } }; diff --git a/program/js/editor.js b/program/js/editor.js index 91123defd..191196eb6 100644 --- a/program/js/editor.js +++ b/program/js/editor.js @@ -729,7 +729,7 @@ function rcube_text_editor(config, id) }); // enable drag-n-drop area - if ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData) { + if (window.FormData) { if (!rcmail.env.filedrop) { rcmail.env.filedrop = {}; } diff --git a/program/steps/mail/attachments.inc b/program/steps/mail/attachments.inc index 2297cc0a8..e5a07217a 100644 --- a/program/steps/mail/attachments.inc +++ b/program/steps/mail/attachments.inc @@ -19,11 +19,6 @@ +-----------------------------------------------------------------------+ */ -// Upload progress update -if (!empty($_GET['_progress'])) { - $RCMAIL->upload_progress(); -} - $COMPOSE_ID = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); $COMPOSE = null; diff --git a/program/steps/settings/upload.inc b/program/steps/settings/upload.inc index 5e91f1d20..01ee433e3 100644 --- a/program/steps/settings/upload.inc +++ b/program/steps/settings/upload.inc @@ -19,11 +19,6 @@ +-----------------------------------------------------------------------+ */ -// Upload progress update -if (!empty($_GET['_progress'])) { - $RCMAIL->upload_progress(); -} - $from = rcube_utils::get_input_value('_from', rcube_utils::INPUT_GET); $type = preg_replace('/(add|edit)-/', '', $from);