summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--index.php14
-rw-r--r--program/include/rcmail.php3
-rw-r--r--program/js/app.js208
-rw-r--r--program/lib/Roundcube/rcube_user.php8
-rw-r--r--program/localization/en_US/labels.inc3
-rw-r--r--program/localization/en_US/messages.inc1
-rw-r--r--program/steps/mail/compose.inc6
-rw-r--r--program/steps/mail/sendmail.inc1
-rw-r--r--skins/larry/ui.js6
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, '<br/>'),
+ 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); });