From fcc7f861b170596c6970aecb1ddc87a3567b112f Mon Sep 17 00:00:00 2001
From: thomascube <thomas@roundcube.net>
Date: Sat, 30 Jul 2011 11:32:13 -0400
Subject: [PATCH] Log session validation errors; keep error message when redirecting to login after session error

---
 program/js/app.js |  586 ++++++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 424 insertions(+), 162 deletions(-)

diff --git a/program/js/app.js b/program/js/app.js
index 48f058e..a9ed9ad 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -138,14 +138,6 @@
       return;
     }
 
-    // Enable debug console
-    if (!window.console || !window.console.log) {
-      window.console = new rcube_console();
-    }
-    else {
-      $('#console').hide();
-    }
-
     // find all registered gui containers
     for (var n in this.gui_containers)
       this.gui_containers[n] = $('#'+this.gui_containers[n]);
@@ -164,7 +156,7 @@
     }
 
     // enable general commands
-    this.enable_command('logout', 'mail', 'addressbook', 'settings', 'save-pref', true);
+    this.enable_command('logout', 'mail', 'addressbook', 'settings', 'save-pref', 'undo', true);
 
     if (this.env.permaurl)
       this.enable_command('permaurl', true);
@@ -209,7 +201,7 @@
           $(this.gui_objects.qsearchbox).focusin(function() { rcmail.message_list.blur(); });
         }
 
-        if (this.env.trash_mailbox && this.env.mailbox != this.env.trash_mailbox)
+        if (!this.env.flag_for_deletion && this.env.trash_mailbox && this.env.mailbox != this.env.trash_mailbox)
           this.set_alttext('delete', 'movemessagetotrash');
 
         this.env.message_commands = ['show', 'reply', 'reply-all', 'reply-list', 'forward',
@@ -260,7 +252,10 @@
         }
         // show printing dialog
         else if (this.env.action == 'print' && this.env.uid)
-          window.print();
+          if (bw.safari)
+            window.setTimeout('window.print()', 10);
+          else
+            window.print();
 
         // get unread count for each mailbox
         if (this.gui_objects.mailboxlist) {
@@ -304,14 +299,11 @@
           if (this.gui_objects.qsearchbox) {
             $(this.gui_objects.qsearchbox).focusin(function() { rcmail.contact_list.blur(); });
           }
+
+          this.update_group_commands();
         }
 
         this.set_page_buttons();
-
-        if (this.env.address_sources && this.env.address_sources[this.env.source] && !this.env.address_sources[this.env.source].readonly) {
-          this.enable_command('add', 'import', true);
-          this.enable_command('group-create', this.env.address_sources[this.env.source].groups);
-        }
 
         if (this.env.cid) {
           this.enable_command('show', 'edit', true);
@@ -339,7 +331,12 @@
         if (this.contact_list && this.contact_list.rowcount > 0)
           this.enable_command('export', true);
 
+        this.enable_command('add', 'import', this.env.writable_source);
         this.enable_command('list', 'listgroup', 'advanced-search', true);
+        
+        // load contacts of selected source
+        if (!this.env.action)
+          this.command('list', this.env.source);
         break;
 
 
@@ -413,7 +410,7 @@
 
     // show message
     if (this.pending_message)
-      this.display_message(this.pending_message[0], this.pending_message[1]);
+      this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]);
 
     // map implicit containers
     if (this.gui_objects.folderlist)
@@ -435,6 +432,11 @@
     this.start_keepalive();
   };
 
+  this.log = function(msg)
+  {
+    if (window.console && console.log)
+      console.log(msg);
+  };
 
   /*********************************************************/
   /*********       client command interface        *********/
@@ -528,15 +530,15 @@
 
           this.list_mailbox(props);
 
-          if (this.env.trash_mailbox)
+          if (this.env.trash_mailbox && !this.env.flag_for_deletion)
             this.set_alttext('delete', this.env.mailbox != this.env.trash_mailbox ? 'movemessagetotrash' : 'deletemessage');
         }
-        else if (this.task=='addressbook') {
+        else if (this.task == 'addressbook') {
           if (!this.env.search_request || (props != this.env.source))
             this.reset_qsearch();
 
           this.list_contacts(props);
-          this.enable_command('add', 'import', (this.env.address_sources && !this.env.address_sources[this.env.source].readonly));
+          this.enable_command('add', 'import', this.env.writable_source);
         }
         break;
 
@@ -639,13 +641,17 @@
           }
           // contacts/identities
           else {
-            if ((input = $("input[name='_name']", form)) &&input.length && input.val() == '') {
+            // reload form
+            if (props == 'reload') {
+              form.action += '?_reload=1';
+            }
+            else if ((input = $("input[name='_name']", form)) &&input.length && input.val() == '') {
               alert(this.get_label('nonamewarning'));
               input.focus();
               break;
             }
             else if (this.task == 'settings' && (this.env.identities_level % 2) == 0  &&
-              (input = $("input[name='_email']", form)) && input.length&& !rcube_check_email(input.val())
+              (input = $("input[name='_email']", form)) && input.length && !rcube_check_email(input.val())
             ) {
               alert(this.get_label('noemailwarning'));
               input.focus();
@@ -655,6 +661,10 @@
             // clear empty input fields
             $('input.placeholder').each(function(){ if (this.value == this._placeholder) this.value = ''; });
           }
+
+          // add selected source (on the list)
+          if (parent.rcmail && parent.rcmail.env.source)
+            form.action = this.add_url(form.action, '_orig_source', parent.rcmail.env.source);
 
           form.submit();
         }
@@ -990,8 +1000,14 @@
 
         if (s && this.env.mailbox)
           this.list_mailbox(this.env.mailbox);
-        else if (s && this.task == 'addressbook')
+        else if (s && this.task == 'addressbook') {
+          if (this.env.source == '') {
+            for (var n in this.env.address_sources) break;
+            this.env.source = n;
+            this.env.group = '';
+          }
           this.list_contacts(this.env.source, this.env.group);
+        }
         break;
 
       case 'listgroup':
@@ -1032,6 +1048,10 @@
       case 'identities':
       case 'folders':
         this.goto_url('settings/' + command);
+        break;
+
+      case 'undo':
+        this.http_request('undo', '', this.display_message('', 'loading'));
         break;
 
       // unified command call (command name == function name)
@@ -1284,7 +1304,7 @@
       var toffset = -moffset-boffset;
       var li, div, pos, mouse, check, oldclass,
         layerclass = 'draglayernormal';
-      
+
       if (this.contact_list && this.contact_list.draglayer)
         oldclass = this.contact_list.draglayer.attr('class');
 
@@ -1860,9 +1880,7 @@
     if (action == 'preview' && String(target.location.href).indexOf(url) >= 0)
       this.show_contentframe(true);
     else {
-      if (!this.env.frame_lock) {
-        (this.is_framed() ? parent.rcmail : this).env.frame_lock = this.set_busy(true, 'loading');
-      }
+      this.lock_frame();
       this.location_href(this.env.comm_path+url, target);
 
       // mark as read and change mbox unread counter
@@ -1895,6 +1913,12 @@
 
     if (!show && this.busy)
       this.set_busy(false, null, this.env.frame_lock);
+  };
+
+  this.lock_frame = function()
+  {
+    if (!this.env.frame_lock)
+      (this.is_framed() ? parent.rcmail : this).env.frame_lock = this.set_busy(true, 'loading');
   };
 
   // list a specific page
@@ -2851,19 +2875,28 @@
       input_subject = $("input[name='_subject']"),
       input_message = $("[name='_message']").get(0),
       html_mode = $("input[name='_is_html']").val() == '1',
-      ac_fields = ['cc', 'bcc', 'replyto', 'followupto'];
+      ac_fields = ['cc', 'bcc', 'replyto', 'followupto'],
+      ac_props;
+
+    // configure parallel autocompletion
+    if (this.env.autocomplete_threads > 0) {
+      ac_props = {
+        threads: this.env.autocomplete_threads,
+        sources: this.env.autocomplete_sources,
+      };
+    }
 
     // init live search events
-    this.init_address_input_events(input_to);
+    this.init_address_input_events(input_to, ac_props);
     for (var i in ac_fields) {
-      this.init_address_input_events($("[name='_"+ac_fields[i]+"']"));
+      this.init_address_input_events($("[name='_"+ac_fields[i]+"']"), ac_props);
     }
 
     if (!html_mode) {
       this.set_caret_pos(input_message, this.env.top_posting ? 0 : $(input_message).val().length);
       // add signature according to selected identity
       // if we have HTML editor, signature is added in callback
-      if (input_from.attr('type') == 'select-one' && $("input[name='_draft_saveid']").val() == '') {
+      if (input_from.prop('type') == 'select-one' && $("input[name='_draft_saveid']").val() == '') {
         this.change_identity(input_from[0]);
       }
     }
@@ -2884,9 +2917,9 @@
     this.auto_save_start();
   };
 
-  this.init_address_input_events = function(obj)
+  this.init_address_input_events = function(obj, props)
   {
-    obj[bw.ie || bw.safari || bw.chrome ? 'keydown' : 'keypress'](function(e){ return ref.ksearch_keydown(e, this); })
+    obj[bw.ie || bw.safari || bw.chrome ? 'keydown' : 'keypress'](function(e) { return ref.ksearch_keydown(e, this, props); })
       .attr('autocomplete', 'off');
   };
 
@@ -2902,7 +2935,7 @@
       input_message = $("[name='_message']");
 
     // check sender (if have no identities)
-    if (input_from.attr('type') == 'text' && !rcube_check_email(input_from.val(), true)) {
+    if (input_from.prop('type') == 'text' && !rcube_check_email(input_from.val(), true)) {
       alert(this.get_label('nosenderwarning'));
       input_from.focus();
       return false;
@@ -3256,7 +3289,7 @@
       });
 
       // display upload indicator and cancel button
-      var content = this.get_label('uploading' + (files > 1 ? 'many' : '')),
+      var content = '<span>' + this.get_label('uploading' + (files > 1 ? 'many' : '')) + '</span>',
         ts = frame_name.replace(/^rcmupload/, '');
 
       if (this.env.loadingicon)
@@ -3264,6 +3297,11 @@
       if (this.env.cancelicon)
         content = '<a title="'+this.get_label('cancel')+'" onclick="return rcmail.cancel_attachment_upload(\''+ts+'\', \''+frame_name+'\');" href="#cancelupload"><img src="'+this.env.cancelicon+'" alt="" /></a>'+content;
       this.add2attachment_list(ts, { name:'', html:content, complete:false });
+
+      // upload progress support
+      if (this.env.upload_progress_time) {
+        this.upload_progress_start('upload', ts);
+      }
     }
 
     // set reference to the form object
@@ -3326,6 +3364,25 @@
     this.remove_from_attachment_list(name);
     $("iframe[name='"+frame_name+"']").remove();
     return false;
+  };
+
+  this.upload_progress_start = function(action, name)
+  {
+    window.setTimeout(function() { rcmail.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);
   };
 
   // send remote request to add a new contact
@@ -3397,14 +3454,14 @@
   /*********************************************************/
 
   // handler for keyboard events on address-fields
-  this.ksearch_keydown = function(e, obj)
+  this.ksearch_keydown = function(e, obj, props)
   {
     if (this.ksearch_timer)
       clearTimeout(this.ksearch_timer);
 
-    var highlight;
-    var key = rcube_event.get_keycode(e);
-    var mod = rcube_event.get_modifier(e);
+    var highlight,
+      key = rcube_event.get_keycode(e),
+      mod = rcube_event.get_modifier(e);
 
     switch (key) {
       case 38:  // key up
@@ -3427,8 +3484,8 @@
         if (mod == SHIFT_KEY)
           break;
 
-     case 13:  // enter
-        if (this.ksearch_selected===null || !this.ksearch_input || !this.ksearch_value)
+      case 13:  // enter
+        if (this.ksearch_selected === null || !this.ksearch_value)
           break;
 
         // insert selected address and hide ksearch pane
@@ -3448,7 +3505,7 @@
     }
 
     // start timer
-    this.ksearch_timer = window.setTimeout(function(){ ref.ksearch_get_results(); }, 200);
+    this.ksearch_timer = window.setTimeout(function(){ ref.ksearch_get_results(props); }, 200);
     this.ksearch_input = obj;
 
     return true;
@@ -3476,21 +3533,25 @@
     var inp_value = this.ksearch_input.value,
       cpos = this.get_caret_pos(this.ksearch_input),
       p = inp_value.lastIndexOf(this.ksearch_value, cpos),
+      trigger = false,
       insert = '',
-
       // replace search string with full address
       pre = inp_value.substring(0, p),
       end = inp_value.substring(p+this.ksearch_value.length, inp_value.length);
+
+    this.ksearch_destroy();
 
     // insert all members of a group
     if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].id) {
       insert += this.env.contacts[id].name + ', ';
       this.group2expand = $.extend({}, this.env.contacts[id]);
       this.group2expand.input = this.ksearch_input;
-      this.http_request('group-expand', '_source='+urlencode(this.env.contacts[id].source)+'&_gid='+urlencode(this.env.contacts[id].id), false);
+      this.http_request('mail/group-expand', '_source='+urlencode(this.env.contacts[id].source)+'&_gid='+urlencode(this.env.contacts[id].id), false);
     }
-    else if (typeof this.env.contacts[id] === 'string')
+    else if (typeof this.env.contacts[id] === 'string') {
       insert = this.env.contacts[id] + ', ';
+      trigger = true;
+    }
 
     this.ksearch_input.value = pre + insert + end;
 
@@ -3498,18 +3559,22 @@
     cpos = p+insert.length;
     if (this.ksearch_input.setSelectionRange)
       this.ksearch_input.setSelectionRange(cpos, cpos);
+
+    if (trigger)
+      this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert });
   };
 
   this.replace_group_recipients = function(id, recipients)
   {
     if (this.group2expand && this.group2expand.id == id) {
       this.group2expand.input.value = this.group2expand.input.value.replace(this.group2expand.name, recipients);
+      this.triggerEvent('autocomplete_insert', { field:this.group2expand.input, insert:recipients });
       this.group2expand = null;
     }
   };
 
   // address search processor
-  this.ksearch_get_results = function()
+  this.ksearch_get_results = function(props)
   {
     var inp_value = this.ksearch_input ? this.ksearch_input.value : null;
 
@@ -3554,59 +3619,102 @@
     if (old_value && old_value.length && this.env.contacts && !this.env.contacts.length && q.indexOf(old_value) == 0)
       return;
 
-    var lock = this.display_message(this.get_label('searching'), 'loading');
-    this.http_post('autocomplete', '_search='+urlencode(q), lock);
+    this.ksearch_destroy();
+
+    var i, lock, source, xhr, reqid = new Date().getTime(),
+      threads = props && props.threads ? props.threads : 1,
+      sources = props && props.sources ? props.sources : [],
+      action = props && props.action ? props.action : 'mail/autocomplete';
+
+    this.ksearch_data = {id: reqid, sources: sources.slice(), action: action, locks: [], requests: []};
+
+    for (i=0; i<threads; i++) {
+      source = this.ksearch_data.sources.shift();
+      if (threads > 1 && source === null)
+        break;
+
+      lock = this.display_message(this.get_label('searching'), 'loading');
+      xhr = this.http_post(action, '_search='+urlencode(q)+'&_id='+reqid
+        + (source ? '&_source='+urlencode(source) : ''), lock);
+
+      this.ksearch_data.locks.push(lock);
+      this.ksearch_data.requests.push(xhr);
+    }
   };
 
-  this.ksearch_query_results = function(results, search)
+  this.ksearch_query_results = function(results, search, reqid)
   {
     // ignore this outdated search response
-    if (this.ksearch_value && search != this.ksearch_value)
+    if (this.ksearch_input && this.ksearch_value && search != this.ksearch_value)
       return;
 
-    this.env.contacts = results ? results : [];
-    this.ksearch_display_results(this.env.contacts);
-  };
-
-  this.ksearch_display_results = function (a_results)
-  {
     // display search results
-    if (a_results.length && this.ksearch_input && this.ksearch_value) {
-      var p, ul, li, text, s_val = this.ksearch_value;
+    var p, ul, li, text, init, s_val = this.ksearch_value,
+      maxlen = this.env.autocomplete_max ? this.env.autocomplete_max : 15;
 
-      // create results pane if not present
-      if (!this.ksearch_pane) {
-        ul = $('<ul>');
-        this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane').css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);
-        this.ksearch_pane.__ul = ul[0];
-      }
+    // create results pane if not present
+    if (!this.ksearch_pane) {
+      ul = $('<ul>');
+      this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane')
+        .css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);
+      this.ksearch_pane.__ul = ul[0];
+    }
 
-      // remove all search results
-      ul = this.ksearch_pane.__ul;
+    ul = this.ksearch_pane.__ul;
+
+    // remove all search results or add to existing list if parallel search
+    if (reqid && this.ksearch_pane.data('reqid') == reqid) {
+      maxlen -= ul.childNodes.length;
+    }
+    else {
+      this.ksearch_pane.data('reqid', reqid);
+      init = 1;
+      // reset content
       ul.innerHTML = '';
+      this.env.contacts = [];
+      // move the results pane right under the input box
+      var pos = $(this.ksearch_input).offset();
+      this.ksearch_pane.css({ left:pos.left+'px', top:(pos.top + this.ksearch_input.offsetHeight)+'px', display: 'none'});
+    }
 
-      // add each result line to list
-      for (i=0; i < a_results.length; i++) {
-        text = typeof a_results[i] === 'object' ? a_results[i].name : a_results[i];
+    // add each result line to list
+    if (results && results.length) {
+      for (i=0; i < results.length && maxlen > 0; i++) {
+        text = typeof results[i] === 'object' ? results[i].name : results[i];
         li = document.createElement('LI');
         li.innerHTML = text.replace(new RegExp('('+RegExp.escape(s_val)+')', 'ig'), '##$1%%').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/##([^%]+)%%/g, '<b>$1</b>');
         li.onmouseover = function(){ ref.ksearch_select(this); };
         li.onmouseup = function(){ ref.ksearch_click(this) };
         li._rcm_id = i;
         ul.appendChild(li);
+        maxlen -= 1;
       }
-
-      // select the first
-      $(ul.firstChild).attr('id', 'rcmksearchSelected').addClass('selected');
-      this.ksearch_selected = 0;
-
-      // move the results pane right under the input box and make it visible
-      var pos = $(this.ksearch_input).offset();
-      this.ksearch_pane.css({ left:pos.left+'px', top:(pos.top + this.ksearch_input.offsetHeight)+'px' }).show();
     }
-    // hide results pane
-    else
-      this.ksearch_hide();
+
+    if (ul.childNodes.length) {
+      this.ksearch_pane.show();
+      // select the first
+      if (!this.env.contacts.length) {
+        $('li:first', ul).attr('id', 'rcmksearchSelected').addClass('selected');
+        this.ksearch_selected = 0;
+      }
+    }
+
+    if (results && results.length)
+      this.env.contacts = this.env.contacts.concat(results);
+
+    // run next parallel search
+    if (maxlen > 0 && this.ksearch_data.id == reqid && this.ksearch_data.sources.length) {
+      var lock, xhr, props = this.ksearch_data, source = props.sources.shift();
+      if (source) {
+        lock = this.display_message(this.get_label('searching'), 'loading');
+        xhr = this.http_post(props.action, '_search='+urlencode(s_val)+'&_id='+reqid
+          +'&_source='+urlencode(source), lock);
+
+        this.ksearch_data.locks.push(lock);
+        this.ksearch_data.requests.push(xhr);
+      }
+    }
   };
 
   this.ksearch_click = function(node)
@@ -3623,20 +3731,36 @@
     if (this.ksearch_timer)
       clearTimeout(this.ksearch_timer);
 
-    this.ksearch_value = '';
     this.ksearch_input = null;
     this.ksearch_hide();
   };
 
-
   this.ksearch_hide = function()
   {
     this.ksearch_selected = null;
+    this.ksearch_value = '';
 
     if (this.ksearch_pane)
       this.ksearch_pane.hide();
-   };
 
+    this.ksearch_destroy();
+  };
+
+  // Aborts pending autocomplete requests
+  this.ksearch_destroy = function()
+  {
+    var i, len, ac = this.ksearch_data;
+
+    if (!ac)
+      return;
+
+    for (i=0, len=ac.locks.length; i<len; i++) {
+      this.hide_message(ac.locks[i]); // hide loading message
+      ac.requests[i].abort(); // abort ajax request
+    }
+
+    this.ksearch_data = null;
+  }
 
   /*********************************************************/
   /*********         address book methods          *********/
@@ -3653,15 +3777,34 @@
     if (this.preview_timer)
       clearTimeout(this.preview_timer);
 
-    var id, frame, ref = this;
+    var n, id, sid, ref = this, writable = false,
+      source = this.env.source ? this.env.address_sources[this.env.source] : null;
+
     if (id = list.get_single_selection())
       this.preview_timer = window.setTimeout(function(){ ref.load_contact(id, 'show'); }, 200);
     else if (this.env.contentframe)
       this.show_contentframe(false);
 
+    // no source = search result, we'll need to detect if any of
+    // selected contacts are in writable addressbook to enable edit/delete
+    if (list.selection.length) {
+      if (!source) {
+        for (n in list.selection) {
+          sid = String(list.selection[n]).replace(/^[^-]+-/, '');
+          if (sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly) {
+            writable = true;
+            break;
+          }
+        }
+      }
+      else {
+        writable = !source.readonly;
+      }
+    }
+
     this.enable_command('compose', list.selection.length > 0);
-    this.enable_command('edit', (id && this.env.address_sources && !this.env.address_sources[this.env.source].readonly) ? true : false);
-    this.enable_command('delete', list.selection.length && this.env.address_sources && !this.env.address_sources[this.env.source].readonly);
+    this.enable_command('edit', id && writable);
+    this.enable_command('delete', list.selection.length && writable);
 
     return false;
   };
@@ -3752,6 +3895,13 @@
       add_url = '&_framed=1';
       target = window.frames[this.env.contentframe];
       this.show_contentframe(true);
+
+      // load dummy content
+      if (!cid) {
+        // unselect selected row(s)
+        this.contact_list.clear_selection();
+        this.enable_command('delete', 'compose', false);
+      }
     }
     else if (framed)
       return false;
@@ -3760,8 +3910,10 @@
       if (this.env.group)
         add_url += '&_gid='+urlencode(this.env.group);
 
-      this.set_busy(true);
-      this.location_href(this.env.comm_path+'&_action='+action+'&_source='+urlencode(this.env.source)+'&_cid='+urlencode(cid) + add_url, target);
+      this.lock_frame();
+      this.location_href(this.env.comm_path+'&_action='+action
+        +'&_source='+urlencode(this.env.source)
+        +'&_cid='+urlencode(cid) + add_url, target);
     }
     return true;
   };
@@ -3799,12 +3951,12 @@
     if (!(selection.length || this.env.cid) || !confirm(this.get_label('deletecontactconfirm')))
       return;
 
-    var id, a_cids = [], qs = '';
+    var id, n, a_cids = [], qs = '';
 
     if (this.env.cid)
       a_cids.push(this.env.cid);
     else {
-      for (var n=0; n<selection.length; n++) {
+      for (n=0; n<selection.length; n++) {
         id = selection[n];
         a_cids.push(id);
         this.contact_list.remove_row(id, (n == selection.length-1));
@@ -3819,7 +3971,7 @@
       qs += '&_gid='+urlencode(this.env.group);
 
     // also send search request to get the right records from the next page
-    if (this.env.search_request) 
+    if (this.env.search_request)
       qs += '&_search='+this.env.search_request;
 
     // send request to server
@@ -3906,7 +4058,7 @@
 
   this.group_create = function()
   {
-    if (!this.gui_objects.folderlist || !this.env.address_sources[this.env.source].groups)
+    if (!this.gui_objects.folderlist)
       return;
 
     if (!this.name_input) {
@@ -4013,17 +4165,16 @@
     this.reset_add_input();
 
     prop.type = 'group';
-    var key = 'G'+prop.source+prop.id;
-    this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
+    var key = 'G'+prop.source+prop.id,
+      link = $('<a>').attr('href', '#')
+        .attr('rel', prop.source+':'+prop.id)
+        .click(function() { return rcmail.command('listgroup', prop, this); })
+        .html(prop.name),
+      li = $('<li>').attr({id: 'rcmli'+key.replace(this.identifier_expr, '_'), 'class': 'contactgroup'})
+        .append(link);
 
-    var link = $('<a>').attr('href', '#')
-      .attr('rel', prop.source+':'+prop.id)
-      .bind('click', function() { return rcmail.command('listgroup', prop, this);})
-      .html(prop.name);
-    var li = $('<li>').attr('id', 'rcmli'+key.replace(this.identifier_expr, '_'))
-      .addClass('contactgroup')
-      .append(link)
-      .insertAfter(this.get_folder_li(prop.source));
+    this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
+    this.add_contact_group_row(prop, li);
 
     this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:li[0] });
   };
@@ -4039,19 +4190,23 @@
 
     // group ID has changed, replace link node and identifiers
     if (li && prop.newid) {
-      var newkey = 'G'+prop.source+prop.newid;
+      var newkey = 'G'+prop.source+prop.newid,
+        newprop = $.extend({}, prop);;
+
       li.id = String('rcmli'+newkey).replace(this.identifier_expr, '_');
       this.env.contactfolders[newkey] = this.env.contactfolders[key];
       this.env.contactfolders[newkey].id = prop.newid;
       this.env.group = prop.newid;
 
-      var newprop = $.extend({}, prop);
+      delete this.env.contactfolders[key];
+      delete this.env.contactgroups[key];
+
       newprop.id = prop.newid;
       newprop.type = 'group';
 
       link = $('<a>').attr('href', '#')
         .attr('rel', prop.source+':'+prop.newid)
-        .bind('click', function() { return rcmail.command('listgroup', newprop, this);})
+        .click(function() { return rcmail.command('listgroup', newprop, this); })
         .html(prop.name);
       $(li).children().replaceWith(link);
     }
@@ -4060,7 +4215,41 @@
       link.innerHTML = prop.name;
 
     this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
+    this.add_contact_group_row(prop, $(li), true);
+
     this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:li[0], newid:prop.newid });
+  };
+
+  // add contact group row to the list, with sorting
+  this.add_contact_group_row = function(prop, li, reloc)
+  {
+    var row, name = prop.name.toUpperCase(),
+      sibling = this.get_folder_li(prop.source),
+      prefix = 'rcmliG'+(prop.source).replace(this.identifier_expr, '_');
+
+    // When renaming groups, we need to remove it from DOM and insert it in the proper place
+    if (reloc) {
+      row = li.clone(true);
+      li.remove();
+    }
+    else
+      row = li;
+
+    $('li[id^="'+prefix+'"]', this.gui_objects.folderlist).each(function(i, elem) {
+      if (name >= $(this).text().toUpperCase())
+        sibling = elem;
+      else
+        return false;
+    });
+
+    row.insertAfter(sibling);
+  };
+
+  this.update_group_commands = function()
+  {
+    var source = this.env.source != '' ? this.env.address_sources[this.env.source] : null;
+    this.enable_command('group-create', (source && source.groups && !source.readonly));
+    this.enable_command('group-rename', 'group-delete', (source && source.groups && this.env.group && !source.readonly));
   };
 
   this.init_edit_field = function(col, elem)
@@ -4109,14 +4298,27 @@
           this.init_edit_field(col, input);
         }
         else if (colprop.type == 'composite') {
-          var childcol, cp, first;
-          for (childcol in colprop.childs) {
+          var childcol, cp, first, templ, cols = [], suffices = [];
+          // read template for composite field order
+          if ((templ = this.env[col+'_template'])) {
+            for (var j=0; j < templ.length; j++) {
+              cols.push(templ[j][1]);
+              suffices.push(templ[j][2]);
+            }
+          }
+          else {  // list fields according to appearance in colprop
+            for (childcol in colprop.childs)
+              cols.push(childcol);
+          }
+
+          for (var i=0; i < cols.length; i++) {
+            childcol = cols[i];
             cp = colprop.childs[childcol];
             input = $('<input>')
               .addClass('ff_'+childcol)
-              .attr({type: 'text', name: '_'+childcol+name_suffix, size: cp.size})
+              .attr({ type: 'text', name: '_'+childcol+name_suffix, size: cp.size })
               .appendTo(cell);
-            cell.append(" ");
+            cell.append(suffices[i] || " ");
             this.init_edit_field(childcol, input);
             if (!first) first = input;
           }
@@ -4230,14 +4432,21 @@
       target = window.frames[this.env.contentframe];
       this.contact_list.clear_selection();
     }
-    else if (framed)
-      return false;
 
-    this.location_href(this.env.comm_path+'&_action=search'+add_url
-      +'&_source='+urlencode(this.env.source)
-      +(this.env.group ? '&_gid='+urlencode(this.env.group) : ''), target);
+    this.lock_frame();
+    this.location_href(this.env.comm_path+'&_action=search'+add_url, target);
 
     return true;
+  };
+
+  // unselect directory/group
+  this.unselect_directory = function()
+  {
+    if (this.env.address_sources.length > 1 || this.env.group != '') {
+      this.select_folder('', (this.env.group ? 'G'+this.env.source+this.env.group : this.env.source));
+      this.env.group = '';
+      this.env.source = '';
+    }
   };
 
 
@@ -4258,6 +4467,7 @@
         add_url = '&_framed=1';
         target = window.frames[this.env.contentframe];
       }
+      this.lock_frame();
       this.location_href(this.env.comm_path+'&_action=edit-prefs&_section='+id+add_url, target);
     }
 
@@ -4327,6 +4537,10 @@
       row.obj.onmouseout = function() { p.unfocus_subscription(row.id); };
     };
     this.subscription_list.init();
+    $('#mailboxroot')
+      .mouseover(function(){ p.focus_subscription(this.id); })
+      .mouseout(function(){ p.unfocus_subscription(this.id); })
+      .mouseup(function(){ if (p.drag_active) p.subscription_move_folder(); });
   };
 
   this.focus_subscription = function(id)
@@ -4345,6 +4559,10 @@
           this.set_env('dstfolder', folder);
           $(row).addClass('droptarget');
         }
+      }
+      else if (id == 'mailboxroot') {
+        this.set_env('dstfolder', '');
+        $(row).addClass('droptarget');
       }
       else if (this.env.mailbox.match(new RegExp(delim))) {
         this.set_env('dstfolder', this.env.delimiter);
@@ -4390,11 +4608,13 @@
         (this.env.dstfolder != this.env.mailbox.replace(reg, ''))
     ) {
       reg = new RegExp('[^'+delim+']*['+delim+']', 'g');
-      var lock = this.set_busy(true, 'foldermoving'),
-        basename = this.env.mailbox.replace(reg, ''),
+      var basename = this.env.mailbox.replace(reg, ''),
         newname = this.env.dstfolder==this.env.delimiter ? basename : this.env.dstfolder+this.env.delimiter+basename;
 
-      this.http_post('rename-folder', '_folder_oldname='+urlencode(this.env.mailbox)+'&_folder_newname='+urlencode(newname), lock);
+      if (newname != this.env.mailbox) {
+        this.http_post('rename-folder', '_folder_oldname='+urlencode(this.env.mailbox)+'&_folder_newname='+urlencode(newname), this.set_busy(true, 'foldermoving'));
+        this.subscription_list.draglayer.hide();
+      }
     }
     this.drag_active = false;
     this.unfocus_subscription(this.get_folder_row_id(this.env.dstfolder));
@@ -4419,14 +4639,14 @@
   };
 
   // Add folder row to the table and initialize it
-  this.add_folder_row = function (name, display_name, protected, subscribed, skip_init)
+  this.add_folder_row = function (name, display_name, is_protected, subscribed, skip_init, class_name)
   {
     if (!this.gui_objects.subscriptionlist)
       return false;
 
-    var row, n, i, tmp, folders, len, list = [], slist = [],
+    var row, n, i, tmp, folders, rowid, list = [], slist = [],
       tbody = this.gui_objects.subscriptionlist.tBodies[0],
-      refrow = $('tr', tbody).get(0),
+      refrow = $('tr', tbody).get(1),
       id = 'rcmrow'+((new Date).getTime());
 
     if (!refrow) {
@@ -4436,21 +4656,27 @@
     }
 
     // clone a table row if there are existing rows
-    row    = $(refrow).clone(true);
+    row = $(refrow).clone(true);
+
+    // set ID, reset css class
     row.attr('id', id);
+    row.attr('class', class_name);
 
     // set folder name
     row.find('td:first').html(display_name);
 
     // update subscription checkbox
     $('input[name="_subscribed[]"]', row).val(name)
-      .prop({checked: subscribed ? true : false, disabled: protected ? true : false});
+      .prop({checked: subscribed ? true : false, disabled: is_protected ? true : false});
 
     // add to folder/row-ID map
     this.env.subscriptionrows[id] = [name, display_name, 0];
 
     // sort folders, to find a place where to insert the row
-    folders = this.env.subscriptionrows;
+    folders = [];
+    $.each(this.env.subscriptionrows, function(k,v){ folders.push(v) });
+    folders.sort(function(a,b){ return a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0) });
+
     for (n in folders) {
       // protected folder
       if (folders[n][2]) {
@@ -4466,19 +4692,22 @@
         tmp = null;
       }
     }
-    list.sort();
-    // make sure protected folders (and their subs) are on top
-    list = slist.concat(list);
+
+    // check if subfolder of a protected folder
+    for (n=0; n<slist.length; n++) {
+      if (name.indexOf(slist[n]+this.env.delimiter) == 0)
+        rowid = this.get_folder_row_id(slist[n]);
+    }
 
     // find folder position after sorting
-    for (n=0, len=list.length; n<len; n++) {
-      if (list[n] == name)
-        break;
+    for (n=0; !rowid && n<list.length; n++) {
+      if (n && list[n] == name)
+        rowid = this.get_folder_row_id(list[n-1]);
     }
 
     // add row to the table
-    if (n && n < len)
-      $('#'+this.get_folder_row_id(list[n-1])).after(row);
+    if (rowid)
+      $('#'+rowid).after(row);
     else
       row.appendTo(tbody);
 
@@ -4495,7 +4724,7 @@
   };
 
   // replace an existing table row with a new folder line (with subfolders)
-  this.replace_folder_row = function(oldfolder, newfolder, display_name, protected)
+  this.replace_folder_row = function(oldfolder, newfolder, display_name, is_protected, class_name)
   {
     if (!this.gui_objects.subscriptionlist)
       return false;
@@ -4511,7 +4740,7 @@
 
     // replace an existing table row
     this._remove_folder_row(id);
-    row = $(this.add_folder_row(newfolder, display_name, protected, subscribed, true));
+    row = $(this.add_folder_row(newfolder, display_name, is_protected, subscribed, true, class_name));
 
     // detect tree depth change
     if (len = list.length) {
@@ -4640,9 +4869,7 @@
       this.show_contentframe(true);
     }
     else {
-      if (!this.env.frame_lock) {
-        (parent.rcmail ? parent.rcmail : this).env.frame_lock = this.set_busy(true, 'loading');
-      }
+      this.lock_frame();
       this.location_href(this.env.comm_path+url, target);
     }
   };
@@ -4859,7 +5086,7 @@
     if (elem._placeholder && (!$elem.val() || $elem.val() == elem._placeholder))
       $elem.addClass('placeholder').attr('spellcheck', false).val(elem._placeholder);
   };
-  
+
   // write to the document/window title
   this.set_pagetitle = function(title)
   {
@@ -4868,27 +5095,29 @@
   };
 
   // display a system message, list of types in common.css (below #message definition)
-  this.display_message = function(msg, type)
+  this.display_message = function(msg, type, timeout)
   {
     // pass command to parent window
     if (this.is_framed())
-      return parent.rcmail.display_message(msg, type);
+      return parent.rcmail.display_message(msg, type, timeout);
 
     if (!this.gui_objects.message) {
       // save message in order to display after page loaded
       if (type != 'loading')
-        this.pending_message = new Array(msg, type);
+        this.pending_message = new Array(msg, type, timeout);
       return false;
     }
 
     type = type ? type : 'notice';
 
     var ref = this,
-      key = msg,
+      key = String(msg).replace(this.identifier_expr, '_'),
       date = new Date(),
-      id = type + date.getTime(),
+      id = type + date.getTime();
+
+    if (!timeout)
       timeout = this.message_time * (type == 'error' || type == 'warning' ? 2 : 1);
-      
+
     if (type == 'loading') {
       key = 'loading';
       timeout = this.env.request_timeout * 1000;
@@ -4924,7 +5153,8 @@
       obj.click(function() { return ref.hide_message(obj); });
     }
 
-    window.setTimeout(function() { ref.hide_message(id, type == 'loading'); }, timeout);
+    if (timeout > 0)
+      window.setTimeout(function() { ref.hide_message(id, type == 'loading'); }, timeout);
     return id;
   };
 
@@ -5282,11 +5512,11 @@
       url = '?_task=utils&_action=html2text',
       lock = this.set_busy(true, 'converting');
 
-    console.log('HTTP POST: ' + url);
+    this.log('HTTP POST: ' + url);
 
     $.ajax({ type: 'POST', url: url, data: htmlText, contentType: 'application/octet-stream',
       error: function(o, status, err) { rcmail.http_error(o, status, err, lock); },
-      success: function(data) { rcmail.set_busy(false, null, lock); $(document.getElementById(id)).val(data); console.log(data); }
+      success: function(data) { rcmail.set_busy(false, null, lock); $(document.getElementById(id)).val(data); rcmail.log(data); }
     });
   };
 
@@ -5301,7 +5531,7 @@
   /********************************************************/
   /*********        remote request methods        *********/
   /********************************************************/
-  
+
   // compose a valid url with the given parameters
   this.url = function(action, query)
   {
@@ -5379,8 +5609,9 @@
     url += '&_remote=1';
 
     // send request
-    console.log('HTTP GET: ' + url);
-    $.ajax({
+    this.log('HTTP GET: ' + url);
+
+    return $.ajax({
       type: 'GET', url: url, data: { _unlock:(lock?lock:0) }, dataType: 'json',
       success: function(data){ ref.http_response(data); },
       error: function(o, status, err) { rcmail.http_error(o, status, err, lock); }
@@ -5410,8 +5641,9 @@
     }
 
     // send request
-    console.log('HTTP POST: ' + url);
-    $.ajax({
+    this.log('HTTP POST: ' + url);
+
+    return $.ajax({
       type: 'POST', url: url, data: postdata, dataType: 'json',
       success: function(data){ ref.http_response(data); },
       error: function(o, status, err) { rcmail.http_error(o, status, err, lock); }
@@ -5443,7 +5675,7 @@
 
     // if we get javascript code from server -> execute it
     if (response.exec) {
-      console.log(response.exec);
+      this.log(response.exec);
       eval(response.exec);
     }
 
@@ -5457,9 +5689,20 @@
     switch (response.action) {
       case 'delete':
         if (this.task == 'addressbook') {
-          var uid = this.contact_list.get_selection();
+          var sid, uid = this.contact_list.get_selection(), writable = false;
+
+          if (uid && this.contact_list.rows[uid]) {
+            // search results, get source ID from record ID
+            if (this.env.source == '') {
+              sid = String(uid).replace(/^[^-]+-/, '');
+              writable = sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly;
+            }
+            else {
+              writable = !this.env.address_sources[this.env.source].readonly;
+            }
+          }
           this.enable_command('compose', (uid && this.contact_list.rows[uid]));
-          this.enable_command('delete', 'edit', (uid && this.contact_list.rows[uid] && this.env.address_sources && !this.env.address_sources[this.env.source].readonly));
+          this.enable_command('delete', 'edit', writable);
           this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
         }
 
@@ -5508,10 +5751,7 @@
           this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
 
           if (response.action == 'list' || response.action == 'search') {
-            this.enable_command('group-create',
-              (this.env.address_sources[this.env.source].groups && !this.env.address_sources[this.env.source].readonly));
-            this.enable_command('group-rename', 'group-delete',
-              (this.env.address_sources[this.env.source].groups && this.env.group && !this.env.address_sources[this.env.source].readonly));
+            this.update_group_commands();
             this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
           }
         }
@@ -5543,6 +5783,19 @@
     var ts = new Date().getTime(),
       frame_name = 'rcmupload'+ts;
 
+    // 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);
+    }
+
     // have to do it this way for IE
     // otherwise the form will be posted to a new window
     if (document.all) {
@@ -5564,12 +5817,13 @@
 
     form.target = frame_name;
     form.action = this.url(action, { _id:this.env.compose_id||'', _uploadid:ts });
+    form.setAttribute('method', 'POST');
     form.setAttribute('enctype', 'multipart/form-data');
     form.submit();
 
     return frame_name;
   };
-  
+
   // starts interval for keep-alive/check-recent signal
   this.start_keepalive = function()
   {
@@ -5579,7 +5833,14 @@
     if (this.env.keep_alive && !this.env.framed && this.task == 'mail' && this.gui_objects.mailboxlist)
       this._int = setInterval(function(){ ref.check_for_recent(false); }, this.env.keep_alive * 1000);
     else if (this.env.keep_alive && !this.env.framed && this.task != 'login' && this.env.action != 'print')
-      this._int = setInterval(function(){ ref.http_request('keep-alive'); }, this.env.keep_alive * 1000);
+      this._int = setInterval(function(){ ref.keep_alive(); }, this.env.keep_alive * 1000);
+  };
+
+  // sends keep-alive signal
+  this.keep_alive = function()
+  {
+    if (!this.busy)
+      this.http_request('keep-alive');
   };
 
   // sends request to check for recent messages
@@ -5680,11 +5941,12 @@
 
       if (elm.type == 'hidden')
         continue;
-
       // remember which elem was disabled before lock
       if (lock && elm.disabled)
         this.disabled_form_elements.push(elm);
-      else if (lock || $.inArray(elm, this.disabled_form_elements)<0)
+      // check this.disabled_form_elements before inArray() as a workaround for FF5 bug
+      // http://bugs.jquery.com/ticket/9873
+      else if (lock || (this.disabled_form_elements && $.inArray(elm, this.disabled_form_elements)<0))
         elm.disabled = lock;
     }
   };

--
Gitblit v1.9.1