diff --git a/CHANGELOG b/CHANGELOG
index 3395b42a42f..25d734fb728 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
- Update TinyMCE to version 4.3.13 (#5309)
- Enigma: Delete user keys when using deluser.sh script
- Enigma: Fix redundant list-secret-keys/list-public-keys calls on signing/encryption
diff --git a/program/js/app.js b/program/js/app.js
index 1252539c94f..d67723bd350 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -1278,21 +1278,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;
@@ -5501,9 +5517,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;
@@ -5513,9 +5529,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();
@@ -5532,21 +5545,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)
@@ -5619,7 +5637,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')
@@ -5628,10 +5648,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 b08ee7b9dac..2ddb0022214 100644
--- a/program/lib/Roundcube/rcube_ldap.php
+++ b/program/lib/Roundcube/rcube_ldap.php
@@ -567,30 +567,15 @@ function list_records($cols=null, $subset=0)
$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
@@ -753,7 +738,7 @@ function _entry_sort_cmp($a, $b)
* @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())
{
@@ -879,12 +864,10 @@ function search($fields, $value, $mode=0, $select=true, $nocount=false, $require
// avoid double-wildcard if $value is empty
$filter = preg_replace('/\*+/', '*', $filter);
- // add general filter to query
- if (!empty($this->prop['filter']))
- $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
-
// 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();
@@ -902,24 +885,98 @@ function search($fields, $value, $mode=0, $select=true, $nocount=false, $require
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);
+ }
- if (!empty($this->filter)) { // Use global search filter
- $prop['filter'] = $this->filter;
+ 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;
+ }
+
+ $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'])
+ && $this->groups_base_dn != $base_dn
+ ) {
+ $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 1918f917b0c..9841088ffa6 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 afd11f89a98..cba1eec080f 100644
--- a/program/steps/addressbook/search.inc
+++ b/program/steps/addressbook/search.inc
@@ -236,6 +236,8 @@ function rcmail_contact_search()
$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