From 85e60ada1558798669b29225aa530b4ba9310cdc Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Sun, 10 Nov 2013 14:04:33 +0100 Subject: First version of the local storage compose data saving feature; some behavioral improvements and encrytion are still to be added --- index.php | 14 +-- program/include/rcmail.php | 3 + program/js/app.js | 208 +++++++++++++++++++++++++++++++- program/lib/Roundcube/rcube_user.php | 8 ++ program/localization/en_US/labels.inc | 3 + program/localization/en_US/messages.inc | 1 + program/steps/mail/compose.inc | 6 +- program/steps/mail/sendmail.inc | 1 + skins/larry/ui.js | 6 +- 9 files changed, 237 insertions(+), 13 deletions(-) diff --git a/index.php b/index.php index 1719abc48..03c3f4322 100644 --- a/index.php +++ b/index.php @@ -193,12 +193,6 @@ if (empty($RCMAIL->user->ID)) { $session_error = true; } - if ($OUTPUT->ajax_call) - $OUTPUT->redirect(array('_err' => 'session'), 2000); - - if (!empty($_REQUEST['_framed'])) - $OUTPUT->command('redirect', $RCMAIL->url(array('_err' => 'session'))); - // check if installer is still active if ($RCMAIL->config->get('enable_installer') && is_readable('./installer/index.php')) { $OUTPUT->add_footer(html::div(array('style' => "background:#ef9398; border:2px solid #dc5757; padding:0.5em; margin:2em auto; width:50em"), @@ -211,8 +205,14 @@ if (empty($RCMAIL->user->ID)) { ); } - if ($session_error || $_REQUEST['_err'] == 'session') + if ($session_error || $_REQUEST['_err'] == 'session') { $OUTPUT->show_message('sessionerror', 'error', null, true, -1); + } + + if ($OUTPUT->ajax_call || !empty($_REQUEST['_framed'])) { + $OUTPUT->command('session_error', $RCMAIL->url(array('_err' => 'session'))); + $OUTPUT->send('iframe'); + } $plugin = $RCMAIL->plugins->exec_hook('unauthenticated', array('task' => 'login', 'error' => $session_error)); diff --git a/program/include/rcmail.php b/program/include/rcmail.php index 4b3f13760..8abe87303 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -413,6 +413,9 @@ class rcmail extends rcube $this->output->set_env('comm_path', $this->comm_path); $this->output->set_charset(RCUBE_CHARSET); + if ($this->user && $this->user->ID) + $this->output->set_env('user_id', $this->user->get_hash()); + // add some basic labels to client $this->output->add_label('loading', 'servererror', 'requesttimedout', 'refreshing'); diff --git a/program/js/app.js b/program/js/app.js index f7fd7cea0..81c66ba3e 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -187,6 +187,8 @@ function rcube_webmail() if (this.env.permaurl) this.enable_command('permaurl', 'extwin', true); + this.local_storage_prefix = 'roundcube.' + (this.env.user_id || 'anonymous') + '.'; + switch (this.task) { case 'mail': @@ -578,9 +580,12 @@ function rcube_webmail() } // check input before leaving compose step - if (this.task == 'mail' && this.env.action == 'compose' && $.inArray(command, this.env.compose_commands)<0) { + if (this.task == 'mail' && this.env.action == 'compose' && $.inArray(command, this.env.compose_commands) < 0 && !this.env.server_error) { if (this.cmp_hash != this.compose_field_hash() && !confirm(this.get_label('notsentwarning'))) return false; + + // remove copy from local storage if compose screen is left intentionally + this.remove_compose_data(this.env.compose_id); } // process external commands @@ -615,10 +620,10 @@ function rcube_webmail() break; // commands to switch task + case 'logout': case 'mail': case 'addressbook': case 'settings': - case 'logout': this.switch_task(command); break; @@ -638,6 +643,7 @@ function rcube_webmail() var form = this.gui_objects.messageform, win = this.open_window(''); + this.save_compose_form_local(); $("input[name='_action']", form).val('compose'); form.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 }); form.target = win.name; @@ -1292,8 +1298,10 @@ function rcube_webmail() return; var url = this.get_task_url(task); - if (task=='mail') + if (task == 'mail') url += '&_mbox=INBOX'; + else if (task == 'logout') + this.clear_compose_data(); this.redirect(url); }; @@ -3117,6 +3125,53 @@ function rcube_webmail() } } + // check for locally stored compose data + if (window.localStorage) { + var index = this.local_storage_get_item('compose.index', []); + + for (var key, i = 0; i < index.length; i++) { + key = index[i], formdata = this.local_storage_get_item('compose.' + key, null, true); + // restore saved copy of current compose_id + if (formdata && formdata.changed && key == this.env.compose_id) { + this.restore_compose_form(key, html_mode); + break; + } + // show dialog asking to restore the message + if (formdata && formdata.changed && formdata.session != this.env.session_id) { + this.show_popup_dialog( + this.get_label('restoresavedcomposedata') + .replace('$date', new Date(formdata.changed).toLocaleString()) + .replace('$subject', formdata._subject) + .replace(/\n/g, '
'), + this.get_label('restoremessage'), + [{ + text: this.get_label('restore'), + click: function(){ + ref.restore_compose_form(key, html_mode); + ref.remove_compose_data(key); // remove old copy + ref.save_compose_form_local(); // save under current compose_id + $(this).dialog('close'); + } + }, + { + text: this.get_label('delete'), + click: function(){ + ref.remove_compose_data(key); + $(this).dialog('close'); + } + }, + { + text: this.get_label('cancel'), + click: function(){ + $(this).dialog('close'); + } + }] + ); + break; + } + } + } + if (input_to.val() == '') input_to.focus(); else if (input_subject.val() == '') @@ -3554,6 +3609,8 @@ function rcube_webmail() this.env.draft_id = id; $("input[name='_draft_saveid']").val(id); + + this.remove_compose_data(this.env.compose_id); }; this.auto_save_start = function() @@ -3561,6 +3618,11 @@ function rcube_webmail() if (this.env.draft_autosave) this.save_timer = setTimeout(function(){ ref.command("savedraft"); }, this.env.draft_autosave * 1000); + // save compose form content to local storage every 10 seconds + // TODO: track typing activity and only save on changes + if (!this.local_save_timer && window.localStorage) + this.local_save_timer = setInterval(function(){ ref.save_compose_form_local(); }, 10000); + // Unlock interface now that saving is complete this.busy = false; }; @@ -3589,6 +3651,109 @@ function rcube_webmail() return str; }; + // store the contents of the compose form to localstorage + this.save_compose_form_local = function() + { + var formdata = { session:this.env.session_id, changed:new Date().getTime() }, + ed, empty = true; + + // get fresh content from editor + if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody))) { + tinyMCE.triggerSave(); + } + + $('input, select, textarea', this.gui_objects.messageform).each(function(i, elem){ + switch (elem.tagName.toLowerCase()) { + case 'input': + if (elem.type == 'button' || elem.type == 'submit' || (elem.type == 'hidden' && elem.name != '_is_html')) { + break; + } + formdata[elem.name] = elem.type != 'checkbox' || elem.checked ? elem.value : ''; + + if (formdata[elem.name] != '' && elem.type != 'hidden') + empty = false; + break; + + case 'select': + formdata[elem.name] = $('option:checked', elem).val(); + break; + + default: + formdata[elem.name] = $(elem).val(); + } + }); + + if (window.localStorage && !empty) { + var index = this.local_storage_get_item('compose.index', []), + key = this.env.compose_id; + + if (index.indexOf(key) < 0) { + index.push(key); + } + this.local_storage_set_item('compose.' + key, formdata, true); + this.local_storage_set_item('compose.index', index); + } + }; + + // write stored compose data back to form + this.restore_compose_form = function(key, html_mode) + { + var ed, formdata = this.local_storage_get_item('compose.' + key, true); + + if (formdata && typeof formdata == 'object') { + $.each(formdata, function(k, value){ + if (k[0] == '_') { + var elem = $("*[name='"+k+"']"); + if (elem[0] && elem[0].type == 'checkbox') { + elem.prop('checked', value != ''); + } + else { + elem.val(value); + } + } + }); + + // initialize HTML editor + if (formdata._is_html == '1') { + if (!html_mode) { + tinyMCE.execCommand('mceAddControl', false, this.env.composebody); + this.triggerEvent('aftertoggle-editor', { mode:'html' }); + } + } + else if (html_mode) { + tinyMCE.execCommand('mceRemoveControl', false, this.env.composebody); + this.triggerEvent('aftertoggle-editor', { mode:'plain' }); + } + } + }; + + // remove stored compose data from localStorage + this.remove_compose_data = function(key) + { + if (window.localStorage) { + var index = this.local_storage_get_item('compose.index', []); + + if (index.indexOf(key) >= 0) { + this.local_storage_remove_item('compose.' + key); + this.local_storage_set_item('compose.index', $.grep(index, function(val,i){ return val != key; })); + } + } + }; + + // clear all stored compose data of this user + this.clear_compose_data = function() + { + if (window.localStorage) { + var index = this.local_storage_get_item('compose.index', []); + + for (var i=0; i < index.length; i++) { + this.local_storage_remove_item('compose.' + index[i]); + } + this.local_storage_remove_item('compose.index'); + } + } + + this.change_identity = function(obj, show_sig) { if (!obj || !obj.options) @@ -6709,6 +6874,20 @@ function rcube_webmail() setTimeout(function(){ ref.keep_alive(); ref.start_keepalive(); }, 30000); }; + // handler for session errors detected on the server + this.session_error = function(redirect_url) + { + this.env.server_error = 401; + + // save message in local storage and do not redirect + if (this.env.action == 'compose') { + this.save_compose_form_local(); + } + else if (redirect_url) { + window.setTimeout(function(){ ref.redirect(redirect_url, true); }, 2000); + } + }; + // callback when an iframe finished loading this.iframe_loaded = function(unlock) { @@ -7230,7 +7409,28 @@ function rcube_webmail() this.set_cookie = function(name, value, expires) { setCookie(name, value, expires, this.env.cookie_path, this.env.cookie_domain, this.env.cookie_secure); - } + }; + + // wrapper for localStorage.getItem(key) + this.local_storage_get_item = function(key, deflt, encrypted) + { + // TODO: add encryption + var item = localStorage.getItem(this.local_storage_prefix + key); + return item !== null ? JSON.parse(item) : (deflt || null); + }; + + // wrapper for localStorage.setItem(key, data) + this.local_storage_set_item = function(key, data, encrypted) + { + // TODO: add encryption + return localStorage.setItem(this.local_storage_prefix + key, JSON.stringify(data)); + }; + + // wrapper for localStorage.removeItem(key) + this.local_storage_remove_item = function(key) + { + return localStorage.removeItem(this.local_storage_prefix + key); + }; } // end object rcube_webmail diff --git a/program/lib/Roundcube/rcube_user.php b/program/lib/Roundcube/rcube_user.php index 57f63361d..3e4be0ab9 100644 --- a/program/lib/Roundcube/rcube_user.php +++ b/program/lib/Roundcube/rcube_user.php @@ -221,6 +221,14 @@ class rcube_user return false; } + /** + * Generate a unique hash to identify this user which + */ + function get_hash() + { + $key = substr($this->rc->config->get('des_key'), 1, 4); + return md5($this->data['user_id'] . $key . $this->data['username'] . '@' . $this->data['mail_host']); + } /** * Get default identity of this user diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index 8f221a3a9..92ec82617 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -232,6 +232,9 @@ $labels['checkspelling'] = 'Check spelling'; $labels['resumeediting'] = 'Resume editing'; $labels['revertto'] = 'Revert to'; +$labels['restore'] = 'Restore'; +$labels['restoremessage'] = 'Restore message?'; + $labels['responses'] = 'Responses'; $labels['insertresponse'] = 'Insert a response'; $labels['manageresponses'] = 'Manage responses'; diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc index 033c820d1..a36d9ab62 100644 --- a/program/localization/en_US/messages.inc +++ b/program/localization/en_US/messages.inc @@ -84,6 +84,7 @@ $messages['norecipientwarning'] = 'Please enter at least one recipient.'; $messages['nosubjectwarning'] = 'The "Subject" field is empty. Would you like to enter one now?'; $messages['nobodywarning'] = 'Send this message without text?'; $messages['notsentwarning'] = 'Message has not been sent. Do you want to discard your message?'; +$messages['restoresavedcomposedata'] = 'A previously composed but unsent message was found.\n\nSubject: $subject\nSaved: $date\n\nDo you want to restore this message?'; $messages['noldapserver'] = 'Please select an ldap server to search.'; $messages['nosearchname'] = 'Please enter a contact name or email address.'; $messages['notuploadedwarning'] = 'Not all attachments have been uploaded yet. Please wait or cancel the upload.'; diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc index 646d2bcd1..7f5435fa3 100644 --- a/program/steps/mail/compose.inc +++ b/program/steps/mail/compose.inc @@ -110,9 +110,10 @@ $OUTPUT->add_label('nosubject', 'nosenderwarning', 'norecipientwarning', 'nosubj 'nobodywarning', 'notsentwarning', 'notuploadedwarning', 'savingmessage', 'sendingmessage', 'messagesaved', 'converting', 'editorwarning', 'searching', 'uploading', 'uploadingmany', 'fileuploaderror', 'sendmessage', 'savenewresponse', 'responsename', 'responsetext', 'save', - 'savingresponse'); + 'savingresponse', 'restoresavedcomposedata', 'restoremessage', 'delete', 'restore'); $OUTPUT->set_env('compose_id', $COMPOSE['id']); +$OUTPUT->set_env('session_id', session_id()); $OUTPUT->set_pagetitle(rcube_label('compose')); // add config parameters to client script @@ -827,6 +828,9 @@ function rcmail_compose_body($attrib) $msgtype = new html_hiddenfield(array('name' => '_is_html', 'value' => ($isHtml?"1":"0"))); $out .= $msgtype->show(); + $framed = new html_hiddenfield(array('name' => '_framed', 'value' => '1')); + $out .= $framed->show(); + // If desired, set this textarea to be editable by TinyMCE if ($isHtml) { $MESSAGE_BODY = htmlentities($MESSAGE_BODY, ENT_NOQUOTES, RCMAIL_CHARSET); diff --git a/program/steps/mail/sendmail.inc b/program/steps/mail/sendmail.inc index 52b02ecff..ea5eaaed1 100644 --- a/program/steps/mail/sendmail.inc +++ b/program/steps/mail/sendmail.inc @@ -855,6 +855,7 @@ else { $folders[] = $COMPOSE['mailbox']; rcmail_compose_cleanup($COMPOSE_ID); + $OUTPUT->command('remove_compose_data', $COMPOSE_ID); if ($store_folder && !$saved) $OUTPUT->command('sent_successfully', 'error', rcube_label('errorsavingsent'), $folders); diff --git a/skins/larry/ui.js b/skins/larry/ui.js index 660b18ff9..760cc7a03 100644 --- a/skins/larry/ui.js +++ b/skins/larry/ui.js @@ -110,7 +110,11 @@ function rcube_mail_ui() }); } else if (rcmail.env.action == 'compose') { - rcmail.addEventListener('aftertoggle-editor', function(){ window.setTimeout(function(){ layout_composeview() }, 200); }); + rcmail.addEventListener('aftertoggle-editor', function(e){ + window.setTimeout(function(){ layout_composeview() }, 200); + if (e && e.mode) + $("select[name='editorSelector']").val(e.mode); + }); rcmail.addEventListener('aftersend-attachment', show_uploadform); rcmail.addEventListener('add-recipient', function(p){ show_header_row(p.field, true); }); -- cgit v1.2.3