diff --git a/CHANGELOG b/CHANGELOG index 426d3c957..b7f16896c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Added addressbook advanced search - Add popup with basic fields selection for addressbook search - Case-insensitive matching in autocompletion (#1487933) - Added option to force spellchecking before sending a message (#1485458) diff --git a/program/include/rcube_contacts.php b/program/include/rcube_contacts.php index 7c142f545..5e7165edd 100644 --- a/program/include/rcube_contacts.php +++ b/program/include/rcube_contacts.php @@ -232,12 +232,13 @@ class rcube_contacts extends rcube_addressbook /** * Search contacts * - * @param array List of fields to search in - * @param string Search value - * @param boolean True for strict (=), False for partial (LIKE) matching - * @param boolean True if results are requested, False if count only - * @param boolean True to skip the count query (select only) - * @param array List of fields that cannot be empty + * @param mixed $fields The field name of array of field names to search in + * @param mixed $value Search value (or array of values when $fields is array) + * @param boolean $strict True for strict (=), False for partial (LIKE) matching + * @param boolean $select True if results are requested, False if count only + * @param boolean $nocount True to skip the count query (select only) + * @param array $required List of fields that cannot be empty + * * @return object rcube_result_set Contact records and 'count' value */ function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array()) @@ -249,23 +250,42 @@ class rcube_contacts extends rcube_addressbook $where = $and_where = array(); - foreach ($fields as $col) { + foreach ($fields as $idx => $col) { + // direct ID search if ($col == 'ID' || $col == $this->primary_key) { $ids = !is_array($value) ? explode(',', $value) : $value; $ids = $this->db->array2list($ids, 'integer'); $where[] = 'c.' . $this->primary_key.' IN ('.$ids.')'; + continue; } + // fulltext search in all fields else if ($col == '*') { $words = array(); - foreach(explode(" ", self::normalize_string($value)) as $word) + foreach (explode(" ", self::normalize_string($value)) as $word) $words[] = $this->db->ilike('words', '%'.$word.'%'); $where[] = '(' . join(' AND ', $words) . ')'; } - else if ($strict) { - $where[] = $this->db->quoteIdentifier($col).' = '.$this->db->quote($value); - } - else if (in_array($col, $this->table_cols)) { - $where[] = $this->db->ilike($col, '%'.$value.'%'); + else { + $val = is_array($value) ? $value[$idx] : $value; + // table column + if (in_array($col, $this->table_cols)) { + if ($strict) { + $where[] = $this->db->quoteIdentifier($col).' = '.$this->db->quote($val); + } + else { + $where[] = $this->db->ilike($col, '%'.$val.'%'); + } + } + // vCard field + else { + if (in_array($col, $this->fulltext_cols)) { + foreach (explode(" ", self::normalize_string($val)) as $word) + $words[] = $this->db->ilike('words', '%'.$word.'%'); + $where[] = '(' . join(' AND ', $words) . ')'; + } + if (is_array($value)) + $post_search[$col] = $strict ? $val : mb_strtolower($val); + } } } @@ -273,12 +293,65 @@ class rcube_contacts extends rcube_addressbook $and_where[] = $this->db->quoteIdentifier($col).' <> '.$this->db->quote(''); } - if (!empty($where)) - $where = join(' OR ', $where); + if (!empty($where)) { + // use AND operator for advanced searches + $where = join(is_array($value) ? ' AND ' : ' OR ', $where); + } if (!empty($and_where)) $where = ($where ? "($where) AND " : '') . join(' AND ', $and_where); + // Post-searching in vCard data fields + // we will search in all records and then build a where clause for their IDs + if (!empty($post_search)) { + $ids = array(0); + // build key name regexp + $regexp = '/^(' . implode(array_keys($post_search), '|') . ')(:.*?)$/'; + // use initial WHERE clause, to limit records number if possible + if (!empty($where)) + $this->set_search_set($where); + + // count result pages + $cnt = $this->count(); + $pages = ceil($cnt / $this->page_size); + $scnt = count($post_search); + + // get (paged) result + for ($i=0; $i<$pages; $i++) { + $this->list_records(null, $i, true); + while ($row = $this->result->next()) { + $id = $row[$this->primary_key]; + $found = 0; + foreach (preg_grep($regexp, array_keys($row)) as $col) { + $pos = strpos($col, ':'); + $colname = $pos ? substr($col, 0, $pos) : $col; + $search = $post_search[$colname]; + foreach ((array)$row[$col] as $value) { + // composite field, e.g. address + if (is_array($value)) { + $value = implode($value); + } + if (($strict && $value == $search) + || (!$strict && strpos(mb_strtolower($value), $search) !== false) + ) { + $found++; + break; + } + } + } + // all fields match + if ($found == $scnt) { + $ids[] = $id; + } + } + } + + // build WHERE clause + $ids = $this->db->array2list($ids, 'integer'); + $where = 'c.' . $this->primary_key.' IN ('.$ids.')'; + unset($this->cache['count']); + } + if (!empty($where)) { $this->set_search_set($where); if ($select) @@ -287,7 +360,7 @@ class rcube_contacts extends rcube_addressbook $this->result = $this->count(); } - return $this->result; + return $this->result; } diff --git a/program/include/rcube_ldap.php b/program/include/rcube_ldap.php index 7f0ea84fa..0c865a9c7 100644 --- a/program/include/rcube_ldap.php +++ b/program/include/rcube_ldap.php @@ -451,12 +451,13 @@ class rcube_ldap extends rcube_addressbook /** * Search contacts * - * @param array List of fields to search in - * @param string Search value - * @param boolean True for strict, False for partial (fuzzy) matching - * @param boolean True if results are requested, False if count only - * @param boolean (Not used) - * @param array List of fields that cannot be empty + * @param mixed $fields The field name of array of field names to search in + * @param mixed $value Search value (or array of values when $fields is array) + * @param boolean $strict True for strict, False for partial (fuzzy) matching + * @param boolean $select True if results are requested, False if count only + * @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 */ function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array()) @@ -477,8 +478,10 @@ class rcube_ldap extends rcube_addressbook return $result; } - $filter = '(|'; - $wc = !$strict && $this->prop['fuzzy_search'] ? '*' : ''; + // use AND operator for advanced searches + $filter = is_array($value) ? '(&' : '(|'; + $wc = !$strict && $this->prop['fuzzy_search'] ? '*' : ''; + if ($fields == '*') { // search_fields are required for fulltext search @@ -490,15 +493,19 @@ class rcube_ldap extends rcube_addressbook } if (is_array($this->prop['search_fields'])) { - foreach ($this->prop['search_fields'] as $k => $field) + foreach ($this->prop['search_fields'] as $field) { $filter .= "($field=$wc" . $this->_quote_string($value) . "$wc)"; + } } } else { - foreach ((array)$fields as $field) - if ($f = $this->_map_field($field)) - $filter .= "($f=$wc" . $this->_quote_string($value) . "$wc)"; + foreach ((array)$fields as $idx => $field) { + $val = is_array($value) ? $value[$idx] : $value; + if ($f = $this->_map_field($field)) { + $filter .= "($f=$wc" . $this->_quote_string($val) . "$wc)"; + } + } } $filter .= ')'; diff --git a/program/include/rcube_result_set.php b/program/include/rcube_result_set.php index 1739cacff..10361609f 100644 --- a/program/include/rcube_result_set.php +++ b/program/include/rcube_result_set.php @@ -44,27 +44,27 @@ class rcube_result_set { $this->records[] = $rec; } - + function iterate() { return $this->records[$this->current++]; } - + function first() { $this->current = 0; return $this->records[$this->current++]; } - + // alias for iterate() function next() { return $this->iterate(); } - + function seek($i) { $this->current = $i; } - + } diff --git a/program/js/app.js b/program/js/app.js index 405a12fc8..48f058eaf 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -326,11 +326,12 @@ function rcube_webmail() } } - if ((this.env.action == 'add' || this.env.action == 'edit') && this.gui_objects.editform) { + if (this.gui_objects.editform) { this.enable_command('save', true); - this.init_contact_form(); + if (this.env.action == 'add' || this.env.action == 'edit') + this.init_contact_form(); } - else if (this.gui_objects.qsearchbox) { + if (this.gui_objects.qsearchbox) { this.enable_command('search', 'reset-search', 'moveto', true); $(this.gui_objects.qsearchbox).select(); } @@ -338,7 +339,7 @@ function rcube_webmail() if (this.contact_list && this.contact_list.rowcount > 0) this.enable_command('export', true); - this.enable_command('list', 'listgroup', true); + this.enable_command('list', 'listgroup', 'advanced-search', true); break; @@ -587,7 +588,7 @@ function rcube_webmail() // common commands used in multiple tasks case 'show': - if (this.task=='mail') { + if (this.task == 'mail') { var uid = this.get_single_uid(); if (uid && (!this.env.uid || uid != this.env.uid)) { if (this.env.mailbox == this.env.drafts_mailbox) @@ -596,17 +597,17 @@ function rcube_webmail() this.show_message(uid); } } - else if (this.task=='addressbook') { + else if (this.task == 'addressbook') { var cid = props ? props : this.get_single_cid(); - if (cid && !(this.env.action=='show' && cid==this.env.cid)) + if (cid && !(this.env.action == 'show' && cid == this.env.cid)) this.load_contact(cid, 'show'); } break; case 'add': - if (this.task=='addressbook') + if (this.task == 'addressbook') this.load_contact(0, 'add'); - else if (this.task=='settings') { + else if (this.task == 'settings') { this.identity_list.clear_selection(); this.load_identity(0, 'add-identity'); } @@ -625,27 +626,29 @@ function rcube_webmail() break; case 'save': - if (this.gui_objects.editform) { - var input_pagesize = $("input[name='_pagesize']"); - var input_name = $("input[name='_name']"); - var input_email = $("input[name='_email']"); - + var input, form = this.gui_objects.editform; + if (form) { + // adv. search + if (this.env.action == 'search') { + } // user prefs - if (input_pagesize.length && isNaN(parseInt(input_pagesize.val()))) { + else if ((input = $("input[name='_pagesize']", form)) && input.length && isNaN(parseInt(input.val()))) { alert(this.get_label('nopagesizewarning')); - input_pagesize.focus(); + input.focus(); break; } // contacts/identities else { - if (input_name.length && input_name.val() == '') { + if ((input = $("input[name='_name']", form)) &&input.length && input.val() == '') { alert(this.get_label('nonamewarning')); - input_name.focus(); + input.focus(); break; } - else if (this.task == 'settings' && input_email.length && (this.env.identities_level % 2) == 0 && !rcube_check_email(input_email.val())) { + else if (this.task == 'settings' && (this.env.identities_level % 2) == 0 && + (input = $("input[name='_email']", form)) && input.length&& !rcube_check_email(input.val()) + ) { alert(this.get_label('noemailwarning')); - input_email.focus(); + input.focus(); break; } @@ -653,7 +656,7 @@ function rcube_webmail() $('input.placeholder').each(function(){ if (this.value == this._placeholder) this.value = ''; }); } - this.gui_objects.editform.submit(); + form.submit(); } break; @@ -3348,8 +3351,7 @@ function rcube_webmail() if (mods) mods = mods[mbox] ? mods[mbox] : mods['*']; } else if (this.contact_list) { - this.contact_list.clear(true); - this.show_contentframe(false); + this.list_contacts_clear(); } if (mods) { @@ -3715,9 +3717,7 @@ function rcube_webmail() this.list_contacts_remote = function(src, group, page) { // clear message list first - this.contact_list.clear(true); - this.show_contentframe(false); - this.enable_command('delete', 'compose', false); + this.list_contacts_clear(); // send request to server var url = (src ? '_source='+urlencode(src) : '') + (page ? (src?'&':'') + '_page='+page : ''), @@ -3736,6 +3736,13 @@ function rcube_webmail() this.http_request('list', url, lock); }; + this.list_contacts_clear = function() + { + this.contact_list.clear(true); + this.show_contentframe(false); + this.enable_command('delete', 'compose', false); + }; + // load contact record this.load_contact = function(cid, action, framed) { @@ -4077,7 +4084,7 @@ function rcube_webmail() else { var lastelem = $('.ff_'+col), appendcontainer = $('#contactsection'+section+' .contactcontroller'+col); - + if (!appendcontainer.length) appendcontainer = $('