From 037af6890fe6fdb84a08d3c86083e847c90ec0ad Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <alec@alec.pl> Date: Tue, 22 Oct 2013 08:17:26 -0400 Subject: [PATCH] Fix vulnerability in handling _session argument of utils/save-prefs (#1489382) --- program/js/app.js | 465 ++++++++++++++++++++++++++++++++------------------------- 1 files changed, 261 insertions(+), 204 deletions(-) diff --git a/program/js/app.js b/program/js/app.js index 3232835..77ec9d9 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -4,7 +4,7 @@ | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2013, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | + | Copyright (C) 2011-2013, Kolab Systems AG | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -178,11 +178,6 @@ parent.rcmail.env.frame_lock = null; } - // Makes that reference to document.activeElement do not throw - // "unspecified error" in IE9 (#1489008) - if (this.env.framed && bw.ie) - document.documentElement.focus(); - // enable general commands this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref', 'compose', 'undo', 'about', 'switch-task', true); @@ -256,7 +251,8 @@ } } else if (this.env.action == 'compose') { - this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'search', 'reset-search', 'extwin']; + this.env.address_group_stack = []; + this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'pushgroup', 'search', 'reset-search', 'extwin']; if (this.env.drafts_mailbox) this.env.compose_commands.push('savedraft') @@ -317,17 +313,19 @@ } // detect browser capabilities - if (!this.is_framed()) + if (!this.is_framed() && !this.env.extwin) this.browser_capabilities_check(); break; case 'addressbook': + this.env.address_group_stack = []; + if (this.gui_objects.folderlist) this.env.contactfolders = $.extend($.extend({}, this.env.address_sources), this.env.contactgroups); this.enable_command('add', 'import', this.env.writable_source); - this.enable_command('list', 'listgroup', 'listsearch', 'advanced-search', true); + this.enable_command('list', 'listgroup', 'pushgroup', 'popgroup', 'listsearch', 'advanced-search', true); if (this.gui_objects.contactslist) { this.contact_list = new rcube_list_widget(this.gui_objects.contactslist, @@ -393,7 +391,7 @@ } else if (this.env.action == 'edit-folder' && this.gui_objects.editform) { this.enable_command('save', 'folder-size', true); - parent.rcmail.env.messagecount = this.env.messagecount; + parent.rcmail.env.exists = this.env.messagecount; parent.rcmail.enable_command('purge', this.env.messagecount); $("input[type='text']").first().select(); } @@ -464,8 +462,21 @@ this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]); // map implicit containers - if (this.gui_objects.folderlist) + if (this.gui_objects.folderlist) { this.gui_containers.foldertray = $(this.gui_objects.folderlist); + + // init treelist widget + if (window.rcube_treelist_widget) { + this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, { + id_prefix: 'rcmli', + id_encode: this.html_identifier_encode, + id_decode: this.html_identifier_decode, + check_droptarget: function(node){ return !node.virtual && ref.check_droptarget(node.id) } + }); + this.treelist.addEventListener('collapse', function(node){ ref.folder_collapsed(node) }); + this.treelist.addEventListener('expand', function(node){ ref.folder_collapsed(node) }); + } + } // 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)) { @@ -588,11 +599,11 @@ var prevstate = this.env.compose_extwin; $("input[name='_action']", this.gui_objects.messageform).val('compose'); this.gui_objects.messageform.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 }); - this.gui_objects.messageform.target = this.open_window('', 1100, 900); + this.gui_objects.messageform.target = this.open_window('', 1100); this.gui_objects.messageform.submit(); } else { - this.open_window(this.env.permaurl, 900, 900); + this.open_window(this.env.permaurl, 900); } break; @@ -761,7 +772,7 @@ case 'moveto': if (this.task == 'mail') this.move_messages(props); - else if (this.task == 'addressbook' && this.drag_active) + else if (this.task == 'addressbook') this.copy_contact(null, props); break; @@ -949,8 +960,8 @@ // Reset the auto-save timer clearTimeout(this.save_timer); - // compose form did not change - if (this.cmp_hash == this.compose_field_hash()) { + // compose form did not change (and draft wasn't saved already) + if (this.env.draft_id && this.cmp_hash == this.compose_field_hash()) { this.auto_save_start(); break; } @@ -1072,9 +1083,23 @@ } break; + case 'pushgroup': + // add group ID to stack + this.env.address_group_stack.push(props.id); + if (obj && event) + rcube_event.cancel(event); + case 'listgroup': this.reset_qsearch(); this.list_contacts(props.source, props.id); + break; + + case 'popgroup': + if (this.env.address_group_stack.length > 1) { + this.env.address_group_stack.pop(); + this.reset_qsearch(); + this.list_contacts(props.source, this.env.address_group_stack[this.env.address_group_stack.length-1]); + } break; case 'import': @@ -1262,11 +1287,12 @@ this.html_identifier = function(str, encode) { - str = String(str); - if (encode) - return Base64.encode(str).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_'); - else - return str.replace(this.identifier_expr, '_'); + return encode ? this.html_identifier_encode(str) : String(str).replace(this.identifier_expr, '_'); + }; + + this.html_identifier_encode = function(str) + { + return Base64.encode(String(str)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_'); }; this.html_identifier_decode = function(str) @@ -1319,29 +1345,9 @@ if (this.preview_read_timer) clearTimeout(this.preview_read_timer); - // save folderlist and folders location/sizes for droptarget calculation in drag_move() - if (this.gui_objects.folderlist && model) { - this.initialBodyScrollTop = bw.ie ? 0 : window.pageYOffset; - this.initialListScrollTop = this.gui_objects.folderlist.parentNode.scrollTop; - - var k, li, height, - list = $(this.gui_objects.folderlist); - pos = list.offset(); - - this.env.folderlist_coords = { x1:pos.left, y1:pos.top, x2:pos.left + list.width(), y2:pos.top + list.height() }; - - this.env.folder_coords = []; - for (k in model) { - if (li = this.get_folder_li(k)) { - // only visible folders - if (height = li.firstChild.offsetHeight) { - pos = $(li.firstChild).offset(); - this.env.folder_coords[k] = { x1:pos.left, y1:pos.top, - x2:pos.left + li.firstChild.offsetWidth, y2:pos.top + height, on:0 }; - } - } - } - } + // prepare treelist widget for dragging interactions + if (this.treelist) + this.treelist.drag_start(); }; this.drag_end = function(e) @@ -1349,87 +1355,28 @@ this.drag_active = false; this.env.last_folder_target = null; - if (this.folder_auto_timer) { - clearTimeout(this.folder_auto_timer); - this.folder_auto_timer = null; - this.folder_auto_expand = null; - } - - // over the folders - if (this.gui_objects.folderlist && this.env.folder_coords) { - for (var k in this.env.folder_coords) { - if (this.env.folder_coords[k].on) - $(this.get_folder_li(k)).removeClass('droptarget'); - } - } + if (this.treelist) + this.treelist.drag_end(); }; this.drag_move = function(e) { - if (this.gui_objects.folderlist && this.env.folder_coords) { - var k, li, div, check, oldclass, + if (this.gui_objects.folderlist) { + var drag_target, oldclass, layerclass = 'draglayernormal', - mouse = rcube_event.get_mouse_pos(e), - pos = this.env.folderlist_coords, - // offsets to compensate for scrolling while dragging a message - boffset = bw.ie ? -document.documentElement.scrollTop : this.initialBodyScrollTop, - moffset = this.initialListScrollTop-this.gui_objects.folderlist.parentNode.scrollTop; + mouse = rcube_event.get_mouse_pos(e); if (this.contact_list && this.contact_list.draglayer) oldclass = this.contact_list.draglayer.attr('class'); - mouse.y += -moffset-boffset; - - // if mouse pointer is outside of folderlist - if (mouse.x < pos.x1 || mouse.x >= pos.x2 || mouse.y < pos.y1 || mouse.y >= pos.y2) { - if (this.env.last_folder_target) { - $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget'); - this.env.folder_coords[this.env.last_folder_target].on = 0; - this.env.last_folder_target = null; - } - if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer) - this.contact_list.draglayer.attr('class', layerclass); - return; + // mouse intersects a valid drop target on the treelist + if (this.treelist && (drag_target = this.treelist.intersects(mouse, true))) { + this.env.last_folder_target = drag_target; + layerclass = 'draglayer' + (this.check_droptarget(drag_target) > 1 ? 'copy' : 'normal'); } - - // over the folders - for (k in this.env.folder_coords) { - pos = this.env.folder_coords[k]; - if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.y >= pos.y1 && mouse.y < pos.y2) { - if (check = this.check_droptarget(k)) { - li = this.get_folder_li(k); - div = $(li.getElementsByTagName('div')[0]); - - // if the folder is collapsed, expand it after 1sec and restart the drag & drop process. - if (div.hasClass('collapsed')) { - if (this.folder_auto_timer) - clearTimeout(this.folder_auto_timer); - - this.folder_auto_expand = this.env.mailboxes[k].id; - this.folder_auto_timer = setTimeout(function() { - rcmail.command('collapse-folder', rcmail.folder_auto_expand); - rcmail.drag_start(null); - }, 1000); - } - else if (this.folder_auto_timer) { - clearTimeout(this.folder_auto_timer); - this.folder_auto_timer = null; - this.folder_auto_expand = null; - } - - $(li).addClass('droptarget'); - this.env.folder_coords[k].on = 1; - this.env.last_folder_target = k; - layerclass = 'draglayer' + (check > 1 ? 'copy' : 'normal'); - } - // Clear target, otherwise drag end will trigger move into last valid droptarget - else - this.env.last_folder_target = null; - } - else if (pos.on) { - $(this.get_folder_li(k)).removeClass('droptarget'); - this.env.folder_coords[k].on = 0; - } + else { + // Clear target, otherwise drag end will trigger move into last valid droptarget + this.env.last_folder_target = null; } if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer) @@ -1439,40 +1386,33 @@ this.collapse_folder = function(name) { - var li = this.get_folder_li(name, '', true), - div = $('div:first', li), - ul = $('ul:first', li); + if (this.treelist) + this.treelist.toggle(name); + }; - if (div.hasClass('collapsed')) { - ul.show(); - div.removeClass('collapsed').addClass('expanded'); - var reg = new RegExp('&'+urlencode(name)+'&'); - this.env.collapsed_folders = this.env.collapsed_folders.replace(reg, ''); - } - else if (div.hasClass('expanded')) { - ul.hide(); - div.removeClass('expanded').addClass('collapsed'); - this.env.collapsed_folders = this.env.collapsed_folders+'&'+urlencode(name)+'&'; + this.folder_collapsed = function(node) + { + var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders'; + + if (node.collapsed) { + this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&'; // select the folder if one of its childs is currently selected // don't select if it's virtual (#1488346) - if (this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !$(li).hasClass('virtual')) + if (this.env.mailbox && this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !node.virtual) this.command('list', name); } - else - return; - - // Work around a bug in IE6 and IE7, see #1485309 - if (bw.ie6 || bw.ie7) { - var siblings = li.nextSibling ? li.nextSibling.getElementsByTagName('ul') : null; - if (siblings && siblings.length && (li = siblings[0]) && li.style && li.style.display != 'none') { - li.style.display = 'none'; - li.style.display = ''; - } + else { + var reg = new RegExp('&'+urlencode(node.id)+'&'); + this.env[prefname] = this.env[prefname].replace(reg, ''); } - this.command('save-pref', { name: 'collapsed_folders', value: this.env.collapsed_folders }); - this.set_unread_count_display(name, false); + if (!this.drag_active) { + this.command('save-pref', { name: prefname, value: this.env[prefname] }); + + if (this.env.unread_counts) + this.set_unread_count_display(node.id, false); + } }; this.doc_mouse_up = function(e) @@ -1497,9 +1437,9 @@ if (this.drag_active && model && this.env.last_folder_target) { var target = model[this.env.last_folder_target]; - $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget'); this.env.last_folder_target = null; list.draglayer.hide(); + this.drag_end(e); if (!this.drag_menu(e, target)) this.command('moveto', target); @@ -1557,7 +1497,7 @@ // start timer for message preview (wait for double click) if (selected && this.env.contentframe && !list.multi_selecting && !this.dummy_select) - this.preview_timer = setTimeout(function(){ ref.msglist_get_preview(); }, 200); + this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, this.dblclick_time); else if (this.env.contentframe) this.show_contentframe(false); }; @@ -1573,12 +1513,13 @@ var win = this.get_frame_window(this.env.contentframe); - if (win && win.location.href.indexOf(this.env.blankpage)>=0) { + if (win && win.location.href.indexOf(this.env.blankpage) >= 0) { if (this.preview_timer) clearTimeout(this.preview_timer); if (this.preview_read_timer) clearTimeout(this.preview_read_timer); - this.preview_timer = setTimeout(function(){ ref.msglist_get_preview(); }, 200); + + this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, this.dblclick_time); } }; @@ -1586,11 +1527,11 @@ { if (this.preview_timer) clearTimeout(this.preview_timer); - if (this.preview_read_timer) clearTimeout(this.preview_read_timer); var uid = list.get_single_selection(); + if (uid && this.env.mailbox == this.env.drafts_mailbox) this.open_compose_step({ _draft_uid: uid, _mbox: this.env.mailbox }); else if (uid) @@ -1678,14 +1619,18 @@ return 0; }; - this.open_window = function(url, width, height) + this.open_window = function(url, width) { - var w = Math.min(width, screen.width - 10), - h = Math.min(height, screen.height - 100), - l = (screen.width - w) / 2 + (screen.left || 0), - t = Math.max(0, (screen.height - h) / 2 + (screen.top || 0) - 20), + var win = this.is_framed() ? parent.window : window, + page = $(win), + page_width = page.width(), + page_height = bw.mz ? $('body', win).height() : page.height(), + w = Math.min(width, page_width), + h = page_height, // always use same height + l = (win.screenLeft || win.screenX) + 20, + t = (win.screenTop || win.screenY) + 20, wname = 'rcmextwin' + new Date().getTime(), - extwin = window.open(url + '&_extwin=1', wname, + extwin = window.open(url + (url.match(/\?/) ? '&' : '?') + '_extwin=1', wname, 'width='+w+',height='+h+',top='+t+',left='+l+',resizable=yes,toolbar=no,status=no,location=no'); // write loading... message to empty windows @@ -1695,8 +1640,6 @@ // focus window, delayed to bring to front window.setTimeout(function() { extwin.focus(); }, 10); - // position window with setTimeout for Chrome (#1488931) - window.setTimeout(function() { extwin.moveTo(l,t); }, bw.chrome ? 100 : 10); return wname; }; @@ -1735,6 +1678,14 @@ if (!row.depth && row.has_children && (expando = document.getElementById('rcmexpando'+row.uid))) { row.expando = expando; expando.onmousedown = function(e) { return self.expand_message_row(e, uid); }; + if (bw.touch) { + expando.addEventListener('touchend', function(e) { + if (e.changedTouches.length == 1) { + self.expand_message_row(e, uid); + return rcube_event.cancel(e); + } + }, false); + } } this.triggerEvent('insertrow', { uid:uid, row:row }); @@ -1894,7 +1845,8 @@ else html = cols[c]; - col.innerHTML = html; + if (html) + col.innerHTML = html; row.appendChild(col); } @@ -1997,7 +1949,7 @@ } else { if (!preview && this.env.message_extwin && !this.env.extwin) - this.open_window(this.env.comm_path+url, 1000, 1200); + this.open_window(this.env.comm_path+url, 1000); else this.location_href(this.env.comm_path+url, target, true); @@ -3023,11 +2975,12 @@ // open new compose window if (this.env.compose_extwin && !this.env.extwin) { - this.open_window(url, 1150, 900); + this.open_window(url, 1150); } else { this.redirect(url); - window.resizeTo(Math.max(1150, $(window).width()), Math.max(900, $(window).height())); + if (this.env.extwin) + window.resizeTo(Math.max(1150, $(window).width()), $(window).height()+24); } }; @@ -3069,7 +3022,7 @@ 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.prop('type') == 'select-one' && !this.env.opened_extwin) { + if (input_from.prop('type') == 'select-one') { this.change_identity(input_from[0]); } } @@ -3130,12 +3083,18 @@ this.compose_recipient_select = function(list) { - this.enable_command('add-recipient', list.selection.length > 0); + var id, n, recipients = 0; + for (n=0; n < list.selection.length; n++) { + id = list.selection[n]; + if (this.env.contactdata[id]) + recipients++; + } + this.enable_command('add-recipient', recipients); }; this.compose_add_recipient = function(field) { - var recipients = [], input = $('#_'+field); + var recipients = [], input = $('#_'+field), delim = this.env.recipients_delimiter; if (this.contact_list && this.contact_list.selection.length) { for (var id, n=0; n < this.contact_list.selection.length; n++) { @@ -3154,8 +3113,10 @@ } if (recipients.length && input.length) { - var oldval = input.val(); - input.val((oldval ? oldval + this.env.recipients_delimiter : '') + recipients.join(this.env.recipients_delimiter)); + var oldval = input.val(), rx = new RegExp(RegExp.escape(delim) + '\\s*$'); + if (oldval && !rx.test(oldval)) + oldval += delim + ' '; + input.val(oldval + recipients.join(delim + ' ') + delim + ' '); this.triggerEvent('add-recipient', { field:field, recipients:recipients }); } }; @@ -3345,6 +3306,15 @@ this.set_draft_id = function(id) { + var rc; + + if (!this.env.draft_id && id && (rc = this.opener())) { + // refresh the drafts folder in opener window + if (rc.env.task == 'mail' && rc.env.action == '' && rc.env.mailbox == this.env.drafts_mailbox) + rc.command('checkmail'); + } + + this.env.draft_id = id; $("input[name='_draft_saveid']").val(id); }; @@ -3389,6 +3359,15 @@ if (!show_sig) show_sig = this.env.show_sig; + // first function execution + if (!this.env.identities_initialized) { + this.env.identities_initialized = true; + if (this.env.show_sig_later) + this.env.show_sig = true; + if (this.env.opened_extwin) + return; + } + var cursor_pos, p = -1, id = obj.options[obj.selectedIndex].value, input_message = $("[name='_message']"), @@ -3410,7 +3389,7 @@ sig = this.env.signatures[sig].text; sig = sig.replace(/\r\n/g, '\n'); - p = this.env.sig_above ? message.indexOf(sig) : message.lastIndexOf(sig); + p = this.env.top_posting ? message.indexOf(sig) : message.lastIndexOf(sig); if (p >= 0) message = message.substring(0, p) + message.substring(p+sig.length, message.length); } @@ -3419,7 +3398,7 @@ sig = this.env.signatures[id].text; sig = sig.replace(/\r\n/g, '\n'); - if (this.env.sig_above) { + if (this.env.top_posting) { if (p >= 0) { // in place of removed signature message = message.substring(0, p) + sig + message.substring(p, message.length); cursor_pos = p - 1; @@ -3463,7 +3442,7 @@ sigElem = doc.createElement('div'); sigElem.setAttribute('id', '_rc_sig'); - if (this.env.sig_above) { + if (this.env.top_posting) { // if no existing sig and top posting then insert at caret pos editor.getWin().focus(); // correct focus in IE & Chrome @@ -3720,15 +3699,19 @@ this.env.search_id = null; }; - this.sent_successfully = function(type, msg) + this.sent_successfully = function(type, msg, target) { this.display_message(msg, type); if (this.env.extwin) { - var opener_rc = this.opener(); + var rc = this.opener(); this.lock_form(this.gui_objects.messageform); - if (opener_rc) - opener_rc.display_message(msg, type); + if (rc) { + rc.display_message(msg, type); + // refresh the folder where sent message was saved + if (target && rc.env.task == 'mail' && rc.env.action == '' && rc.env.mailbox == target) + rc.command('checkmail'); + } setTimeout(function(){ window.close() }, 1000); } else { @@ -4100,42 +4083,49 @@ if (this.preview_timer) clearTimeout(this.preview_timer); - var n, id, sid, ref = this, writable = false, + var n, id, sid, contact, ref = this, writable = false, source = this.env.source ? this.env.address_sources[this.env.source] : null; + // we don't have dblclick handler here, so use 200 instead of this.dblclick_time if (id = list.get_single_selection()) this.preview_timer = setTimeout(function(){ ref.load_contact(id, 'show'); }, 200); else if (this.env.contentframe) this.show_contentframe(false); if (list.selection.length) { + list.draggable = false; + // no source = search result, we'll need to detect if any of // selected contacts are in writable addressbook to enable edit/delete // we'll also need to know sources used in selection for copy // and group-addmember operations (drag&drop) this.env.selection_sources = []; - if (!source) { - for (n in list.selection) { + + if (source) + this.env.selection_sources.push(this.env.source); + + for (n in list.selection) { + contact = list.data[list.selection[n]]; + if (!source) { sid = String(list.selection[n]).replace(/^[^-]+-/, ''); if (sid && this.env.address_sources[sid]) { - writable = writable || !this.env.address_sources[sid].readonly; + writable = writable || (!this.env.address_sources[sid].readonly && !contact.readonly); this.env.selection_sources.push(sid); } } - this.env.selection_sources = $.unique(this.env.selection_sources); + else + writable = writable || (!source.readonly && !contact.readonly); } - else { - this.env.selection_sources.push(this.env.source); - writable = !source.readonly; - } + + this.env.selection_sources = $.unique(this.env.selection_sources); } // if a group is currently selected, and there is at least one contact selected // thend we can enable the group-remove-selected command - this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0); + this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0 && writable); this.enable_command('compose', this.env.group || list.selection.length > 0); this.enable_command('edit', id && writable); - this.enable_command('delete', list.selection.length && writable); + this.enable_command('delete', list.selection.length > 0 && writable); return false; }; @@ -4163,10 +4153,28 @@ else if (!this.env.search_request) folder = group ? 'G'+src+group : src; - this.select_folder(folder); - this.env.source = src; this.env.group = group; + + // truncate groups listing stack + var index = $.inArray(this.env.group, this.env.address_group_stack); + if (index < 0) + this.env.address_group_stack = []; + else + this.env.address_group_stack = this.env.address_group_stack.slice(0,index); + + // make sure the current group is on top of the stack + if (this.env.group) { + this.env.address_group_stack.push(this.env.group); + + // mark the first group on the stack as selected in the directory list + folder = 'G'+src+this.env.address_group_stack[0]; + } + else if (this.gui_objects.addresslist_title) { + $(this.gui_objects.addresslist_title).html(this.get_label('contacts')); + } + + this.select_folder(folder, '', true); // load contacts remotely if (this.gui_objects.contactslist) { @@ -4222,16 +4230,38 @@ this.list_contacts_clear = function() { + this.contact_list.data = {}; this.contact_list.clear(true); this.show_contentframe(false); this.enable_command('delete', false); this.enable_command('compose', this.env.group ? true : false); }; + this.set_group_prop = function(prop) + { + if (this.gui_objects.addresslist_title) { + var boxtitle = $(this.gui_objects.addresslist_title).html(''); // clear contents + + // add link to pop back to parent group + if (this.env.address_group_stack.length > 1) { + $('<a href="#list">...</a>') + .addClass('poplink') + .appendTo(boxtitle) + .click(function(e){ return ref.command('popgroup','',this); }); + boxtitle.append(' » '); + } + + boxtitle.append($('<span>'+prop.name+'</span>')); + } + + this.triggerEvent('groupupdate', prop); + }; + // load contact record this.load_contact = function(cid, action, framed) { - var win, url = {}, target = window; + var win, url = {}, target = window, + rec = this.contact_list ? this.contact_list.data[cid] : null; if (win = this.get_frame_window(this.env.contentframe)) { url._framed = 1; @@ -4242,7 +4272,9 @@ if (!cid) { // unselect selected row(s) this.contact_list.clear_selection(); - this.enable_command('delete', 'compose', false); + + this.enable_command('compose', rec && rec.email); + this.enable_command('delete', rec && rec._type != 'group'); } } else if (framed) @@ -4353,7 +4385,7 @@ }; // update a contact record in the list - this.update_contact_row = function(cid, cols_arr, newcid, source) + this.update_contact_row = function(cid, cols_arr, newcid, source, data) { var c, row, list = this.contact_list; @@ -4380,11 +4412,13 @@ list.selection[0] = newcid; row.style.display = ''; } + + list.data[cid] = data; } }; // add row to contacts list - this.add_contact_row = function(cid, cols, classes) + this.add_contact_row = function(cid, cols, classes, data) { if (!this.gui_objects.contactslist) return false; @@ -4402,10 +4436,13 @@ for (c in cols) { col = document.createElement('td'); col.className = String(c).toLowerCase(); - col.innerHTML = cols[c]; + if (cols[c]) + col.innerHTML = cols[c]; row.appendChild(col); } + // store data in list member + list.data[cid] = data; list.insert_row(row); this.enable_command('export', list.rowcount > 0); @@ -4469,7 +4506,7 @@ this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); }); this.env.group_renaming = true; - var link, li = this.get_folder_li(this.env.source+this.env.group, 'rcmliG'); + var link, li = this.get_folder_li('G'+this.env.source+this.env.group,'',true); if (li && (link = li.firstChild)) { $(link).hide().before(this.name_input); } @@ -4490,7 +4527,7 @@ this.remove_group_item = function(prop) { var li, key = 'G'+prop.source+prop.id; - if ((li = this.get_folder_li(key))) { + if ((li = this.get_folder_li(key,'',true))) { this.triggerEvent('group_delete', { source:prop.source, id:prop.id, li:li }); li.parentNode.removeChild(li); @@ -4512,8 +4549,22 @@ this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); }); this.name_input_li = $('<li>').addClass(type).append(this.name_input); - var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : this.get_folder_li(this.env.source); - this.name_input_li.insertAfter(li); + var ul, li; + + // find list (UL) element + if (type == 'contactsearch') + ul = this.gui_objects.folderlist; + else + ul = $('ul.groups', this.get_folder_li(this.env.source,'',true)); + + // append to the list + li = $('li:last', ul); + if (li.length) + this.name_input_li.insertAfter(li); + else { + this.name_input_li.appendTo(ul); + ul.show(); // make sure the list is visible + } } this.name_input.select().focus(); @@ -4570,11 +4621,13 @@ this.reset_add_input = function() { if (this.name_input) { + var li = this.name_input.parent(); if (this.env.group_renaming) { - var li = this.name_input.parent(); li.children().last().show(); this.env.group_renaming = false; } + else if ($('li', li.parent()).length == 1) + li.parent().hide(); this.name_input.remove(); @@ -4613,7 +4666,7 @@ this.reset_add_input(); var key = 'G'+prop.source+prop.id, - li = this.get_folder_li(key), + li = this.get_folder_li(key,'',true), link; // group ID has changed, replace link node and identifiers @@ -4652,8 +4705,8 @@ this.add_contact_group_row = function(prop, li, reloc) { var row, name = prop.name.toUpperCase(), - sibling = this.get_folder_li(prop.source), - prefix = 'rcmliG' + this.html_identifier(prop.source); + sibling = this.get_folder_li(prop.source,'',true), + prefix = 'rcmli' + this.html_identifier('G'+prop.source, true); // When renaming groups, we need to remove it from DOM and insert it in the proper place if (reloc) { @@ -4799,6 +4852,9 @@ if (++colprop.count == colprop.limit && colprop.limit) $(menu).children('option[value="'+col+'"]').prop('disabled', true); } + + if (contact._type != 'group') + list.draggable = true; } } }; @@ -4902,12 +4958,12 @@ .attr('rel', id) .click(function() { return rcmail.command('listsearch', id, this); }) .html(name), - li = $('<li>').attr({id: 'rcmli' + this.html_identifier(key), 'class': 'contactsearch'}) + li = $('<li>').attr({ id:'rcmli' + this.html_identifier(key,true), 'class':'contactsearch' }) .append(link), prop = {name:name, id:id, li:li[0]}; this.add_saved_search_row(prop, li); - this.select_folder('S'+id); + this.select_folder(key,'',true); this.enable_command('search-delete', true); this.env.search_id = id; @@ -4961,7 +5017,7 @@ this.remove_search_item = function(id) { var li, key = 'S'+id; - if ((li = this.get_folder_li(key))) { + if ((li = this.get_folder_li(key,'',true))) { this.triggerEvent('search_delete', { id:id, li:li }); li.parentNode.removeChild(li); @@ -4983,7 +5039,7 @@ } this.reset_qsearch(); - this.select_folder('S'+id); + this.select_folder('S'+id, '', true); // reset vars this.env.current_page = 1; @@ -5848,14 +5904,14 @@ for (c=0, len=repl.length; c < len; c++) { cell = document.createElement('td'); - cell.innerHTML = repl[c].html; + cell.innerHTML = repl[c].html || ''; if (repl[c].id) cell.id = repl[c].id; if (repl[c].className) cell.className = repl[c].className; tr.appendChild(cell); } th.appendChild(tr); thead.parentNode.replaceChild(th, thead); - thead = th; + list.thead = thead = th; } for (n=0, len=this.env.coltypes.length; n<len; n++) { @@ -6476,9 +6532,10 @@ url: ref.url(ref.env.filedrop.action||'upload', { _id:ref.env.compose_id||ref.env.cid||'', _uploadid:ts, _remote:1 }), contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary, processData: false, + timeout: 0, // disable default timeout set in ajaxSetup() data: formdata || multipart, headers: {'X-Roundcube-Request': ref.env.request_token}, - beforeSend: function(xhr, s) { if (!formdata && xhr.sendAsBinary) xhr.send = xhr.sendAsBinary; }, + 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'); } }); @@ -6518,7 +6575,7 @@ multipart += '; filename="' + (f.name_bin || file.name) + '"' + crlf; multipart += 'Content-Length: ' + file.size + crlf; multipart += 'Content-Type: ' + file.type + crlf + crlf; - multipart += e.target.result + crlf; + multipart += reader.result + crlf; multipart += dashdash + boundary + crlf; if (j == last) // we're done, submit the data -- Gitblit v1.9.1