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.
pull/6612/head
Aleksander Machniak 5 years ago committed by GitHub
parent 8f62aed866
commit 0492b1f6e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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.

@ -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;
}

@ -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 = '<span>' + this.get_label('uploading' + (numfiles > 1 ? 'many' : '')) + '</span>',
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 = $('<li>');
if (!att.complete && att.html.indexOf('<') < 0)
att.html = '<span class="uploading">' + att.html + '</span>';
if (!att.complete && this.env.loadingicon)
att.html = '<img src="'+this.env.loadingicon+'" alt="" class="uploading" />' + att.html;
if (!att.complete && att.frame) {
if (!att.complete) {
label = this.get_label('cancel');
att.html = '<a title="'+label+'" onclick="return rcmail.cancel_attachment_upload(\''+name+'\', \''+att.frame+'\');" href="#cancelupload" class="cancelupload">'
att.html = '<a title="'+label+'" onclick="return rcmail.cancel_attachment_upload(\''+name+'\');" href="#cancelupload" class="cancelupload">'
+ (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="'+label+'" />' : '<span class="inner">' + label + '</span>') + '</a>' + 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 = $('<input>').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 = $('<span>').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 = $('<span>').text(multiple ? ref.get_label('uploadingmany') : files[0].name).html();
content = $('<span>').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);
}
};

@ -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 = {};
}

@ -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;

@ -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);

Loading…
Cancel
Save