diff --git a/CHANGELOG b/CHANGELOG index 8bc0fb631..3d26cea2c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Searching in both contacts and groups when LDAP addressbook with group_filters option is used - Fix vulnerability in handling of mail()'s 5th argument - Fix To: header encoding in mail sent with mail() method (#5475) - Fix flickering of header topline in min-mode (#5426) diff --git a/program/js/app.js b/program/js/app.js index 43f186c84..82e47498d 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -1266,21 +1266,37 @@ function rcube_webmail() break; case 'pushgroup': - // add group ID to stack - this.env.address_group_stack.push(props.id); + // add group ID and current search to stack + var group = { + id: props.id, + search_request: this.env.search_request, + page: this.env.current_page, + search: this.env.search_request && this.gui_objects.qsearchbox ? this.gui_objects.qsearchbox.value : null + }; + + this.env.address_group_stack.push(group); if (obj && event) rcube_event.cancel(event); case 'listgroup': this.reset_qsearch(); - this.list_contacts(props.source, props.id); + this.list_contacts(props.source, props.id, 1, group); break; case 'popgroup': - if (this.env.address_group_stack.length > 1) { - this.env.address_group_stack.pop(); + if (this.env.address_group_stack.length) { + var old = 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]); + + if (old.search_request) { + // this code is executed when going back to the search result + if (old.search && this.gui_objects.qsearchbox) + $(this.gui_objects.qsearchbox).val(old.search); + this.env.search_request = old.search_request; + this.list_contacts_remote(null, null, this.env.current_page = old.page); + } + else + this.list_contacts(props.source, this.env.address_group_stack[this.env.address_group_stack.length-1].id); } break; @@ -5453,9 +5469,9 @@ function rcube_webmail() return false; }; - this.list_contacts = function(src, group, page) + this.list_contacts = function(src, group, page, search) { - var win, folder, url = {}, + var win, folder, index = -1, url = {}, refresh = src === undefined && group === undefined && page === undefined, target = window; @@ -5465,9 +5481,6 @@ function rcube_webmail() if (refresh) group = this.env.group; - if (page && this.current_page == page && src == this.env.source && group == this.env.group) - return false; - if (src != this.env.source) { page = this.env.current_page = 1; this.reset_qsearch(); @@ -5484,21 +5497,26 @@ function rcube_webmail() 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); + $.each(this.env.address_group_stack, function(i, v) { + if (ref.env.group == v.id) { + index = i; + return false; + } + }); + + this.env.address_group_stack = index < 0 ? [] : 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); + if (!search) search = {}; + search.id = this.env.group; + this.env.address_group_stack.push(search); // mark the first group on the stack as selected in the directory list - folder = 'G'+src+this.env.address_group_stack[0]; + folder = 'G'+src+this.env.address_group_stack[0].id; } else if (this.gui_objects.addresslist_title) { - $(this.gui_objects.addresslist_title).html(this.get_label('contacts')); + $(this.gui_objects.addresslist_title).text(this.get_label('contacts')); } if (!this.env.search_id) @@ -5571,7 +5589,9 @@ function rcube_webmail() 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) { + if (this.env.address_group_stack.length > 1 + || (this.env.address_group_stack.length == 1 && this.env.address_group_stack[0].search_request) + ) { $('...') .attr('title', this.get_label('uponelevel')) .addClass('poplink') @@ -5580,10 +5600,11 @@ function rcube_webmail() boxtitle.append(' » '); } - boxtitle.append($('').text(prop.name)); + boxtitle.append($('').text(prop ? prop.name : this.get_label('contacts'))); } - this.triggerEvent('groupupdate', prop); + if (prop) + this.triggerEvent('groupupdate', prop); }; // load contact record diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php index c84339990..f0f9e4a16 100644 --- a/program/lib/Roundcube/rcube_ldap.php +++ b/program/lib/Roundcube/rcube_ldap.php @@ -567,30 +567,15 @@ class rcube_ldap extends rcube_addressbook $this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size); } else { - $prop = $this->group_id ? $this->group_data : $this->prop; - $base_dn = $this->group_id ? $prop['base_dn'] : $this->base_dn; - - // use global search filter - if (!empty($this->filter)) - $prop['filter'] = $this->filter; - // exec LDAP search if no result resource is stored - if ($this->ready && !$this->ldap_result) - $this->ldap_result = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], $this->prop['attributes'], $prop); + if ($this->ready && $this->ldap_result === null) { + $this->ldap_result = $this->extended_search(); + } // count contacts for this user $this->result = $this->count(); - // we have a search result resource - if ($this->ldap_result && $this->result->count > 0) { - // sorting still on the ldap server - if ($this->sort_col && $prop['scope'] !== 'base' && !$this->ldap->vlv_active) - $this->ldap_result->sort($this->sort_col); - - // get all entries from the ldap server - $entries = $this->ldap_result->entries(); - } - + $entries = $this->ldap_result; } // end else // start and end of the page @@ -750,7 +735,7 @@ class rcube_ldap extends rcube_addressbook * @param boolean $nocount (Not used) * @param array $required List of fields that cannot be empty * - * @return array Indexed list of contact records and 'count' value + * @return rcube_result_set List of contact records */ function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array()) { @@ -881,7 +866,9 @@ class rcube_ldap extends rcube_addressbook } // set filter string and execute search - $this->set_search_set($filter); + // @FIXME: we need a better way to detect/define when groups are allowed in the result + $prefix = empty($required) ? 'e:' : ''; + $this->set_search_set($prefix . $filter); if ($select) $this->list_records(); @@ -899,24 +886,97 @@ class rcube_ldap extends rcube_addressbook function count() { $count = 0; - if ($this->ldap_result) { - $count = $this->ldap_result->count(); + if (!empty($this->ldap_result)) { + $count = $this->ldap_result['count']; } else if ($this->group_id && $this->group_data['dn']) { $count = count($this->list_group_members($this->group_data['dn'], true)); } // We have a connection but no result set, attempt to get one. else if ($this->ready) { - $prop = $this->group_id ? $this->group_data : $this->prop; - $base_dn = $this->group_id ? $this->group_base_dn : $this->base_dn; + $count = $this->extended_search(true); + } + + return new rcube_result_set($count, ($this->list_page-1) * $this->page_size); + } + + /** + * Wrapper on LDAP searches with group_filters support, which + * allows searching for contacts AND groups. + * + * @param bool $count Return count instead of the records + * + * @return int|array Count of records or the result array (with 'count' item) + */ + protected function extended_search($count = false) + { + $prop = $this->group_id ? $this->group_data : $this->prop; + $base_dn = $this->group_id ? $this->groups_base_dn : $this->base_dn; + $attrs = $count ? array('dn') : $this->prop['attributes']; + $entries = array(); + + // Use global search filter + if ($filter = $this->filter) { + if ($filter[0] == 'e' && $filter[1] == ':') { + $filter = substr($filter, 2); + $is_extended_search = !$this->group_id; + } - if (!empty($this->filter)) { // Use global search filter - $prop['filter'] = $this->filter; + $prop['filter'] = $filter; + + // add general filter to query + if (!empty($this->prop['filter'])) { + $prop['filter'] = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $prop['filter'] . ')'; } - $count = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], array('dn'), $prop, true); } - return new rcube_result_set($count, ($this->list_page-1) * $this->page_size); + $result = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], $attrs, $prop, $count); + + // we have a search result resource, get all entries + if (!$count && $result && $result->count() > 0) { + $result = $result->entries(); + unset($result['count']); + } + + // search for groups + if ($is_extended_search + && is_array($this->prop['group_filters']) + && !empty($this->prop['groups']['filter']) + ) { + $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['groups']['filter']) . ')' . $filter . ')'; + + // for groups we may use cn instead of displayname... + if ($this->prop['fieldmap']['name'] != $this->prop['groups']['name_attr']) { + $filter = str_replace(strtolower($this->prop['fieldmap']['name']) . '=', $this->prop['groups']['name_attr'] . '=', $filter); + } + + $name_attr = $this->prop['groups']['name_attr']; + $email_attr = $this->prop['groups']['email_attr'] ?: 'mail'; + $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr)); + + $res = $this->ldap->search($this->groups_base_dn, $filter, $this->prop['groups']['scope'], $attrs, $prop, $count); + + if ($count && $res) { + $result += $res; + } + else if (!$count && $res && $res->count()) { + $res = $res->entries(); + unset($res['count']); + $result = array_merge($result, $res); + } + } + + if (!$count && $result) { + // sorting + if ($this->sort_col && $prop['scope'] !== 'base' && !$this->ldap->vlv_active) { + usort($result, array($this, '_entry_sort_cmp')); + } + + $result['count'] = count($result); + $this->result_entries = $result; + } + + return $result; } /** diff --git a/program/steps/addressbook/list.inc b/program/steps/addressbook/list.inc index 1918f917b..9841088ff 100644 --- a/program/steps/addressbook/list.inc +++ b/program/steps/addressbook/list.inc @@ -57,11 +57,13 @@ else { } if ($CONTACTS->group_id) { - $OUTPUT->command('set_group_prop', array('ID' => $CONTACTS->group_id) - + array_intersect_key((array)$CONTACTS->get_group($CONTACTS->group_id), array('name'=>1,'email'=>1))); + $group_data = array('ID' => $CONTACTS->group_id) + + array_intersect_key((array)$CONTACTS->get_group($CONTACTS->group_id), array('name'=>1,'email'=>1)); } } +$OUTPUT->command('set_group_prop', $group_data); + // update message count display $OUTPUT->set_env('pagecount', ceil($result->count / $PAGE_SIZE)); $OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result)); diff --git a/program/steps/addressbook/search.inc b/program/steps/addressbook/search.inc index 36c372f86..6a938cef8 100644 --- a/program/steps/addressbook/search.inc +++ b/program/steps/addressbook/search.inc @@ -230,13 +230,15 @@ function rcmail_contact_search() } // update message count display - $OUTPUT->command('set_env', 'search_request', $search_request); - $OUTPUT->command('set_env', 'pagecount', ceil($result->count / $PAGE_SIZE)); + $OUTPUT->set_env('search_request', $search_request); + $OUTPUT->set_env('pagecount', ceil($result->count / $PAGE_SIZE)); $OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result)); // Re-set current source - $OUTPUT->command('set_env', 'search_id', $sid); - $OUTPUT->command('set_env', 'source', ''); - $OUTPUT->command('set_env', 'group', ''); + $OUTPUT->set_env('search_id', $sid); + $OUTPUT->set_env('source', ''); + $OUTPUT->set_env('group', ''); + // Re-set list header + $OUTPUT->command('set_group_prop', null); if (!$sid) { // unselect currently selected directory/group