- Add option to define matching method for addressbook search (#1486564, #1487907)
| | |
| | | CHANGELOG Roundcube Webmail |
| | | =========================== |
| | | |
| | | - Add option to define matching method for addressbook search (#1486564, #1487907) |
| | | - Make email recipients separator configurable |
| | | - Fix so folders with \Noinferiors attribute aren't listed in parent selector |
| | | - Fix handling of curly brackets in URLs (#1488168) |
| | |
| | | // available placeholders: {street}, {locality}, {zipcode}, {country}, {region} |
| | | $rcmail_config['address_template'] = '{street}<br/>{locality} {zipcode}<br/>{country} {region}'; |
| | | |
| | | // Matching mode for addressbook search (including autocompletion) |
| | | // 0 - partial (*abc*), default |
| | | // 1 - strict (abc) |
| | | // 2 - prefix (abc*) |
| | | // Note: For LDAP sources fuzzy_search must be enabled to use 'partial' or 'prefix' mode |
| | | $rcmail_config['addressbook_search_mode'] = 0; |
| | | |
| | | // ---------------------------------- |
| | | // USER PREFERENCES |
| | | // ---------------------------------- |
| | |
| | | * |
| | | * @param array List of fields to search in |
| | | * @param string Search value |
| | | * @param int Matching mode: |
| | | * 0 - partial (*abc*), |
| | | * 1 - strict (=), |
| | | * 2 - prefix (abc*) |
| | | * @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 |
| | | * @return object rcube_result_set List of contact records and 'count' value |
| | | */ |
| | | abstract function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array()); |
| | | abstract function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array()); |
| | | |
| | | /** |
| | | * Count number of available contacts in database |
| | |
| | | * |
| | | * @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 int $mode Matching mode: |
| | | * 0 - partial (*abc*), |
| | | * 1 - strict (=), |
| | | * 2 - prefix (abc*) |
| | | * @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()) |
| | | function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array()) |
| | | { |
| | | if (!is_array($fields)) |
| | | $fields = array($fields); |
| | |
| | | $required = array($required); |
| | | |
| | | $where = $and_where = array(); |
| | | $mode = intval($mode); |
| | | |
| | | foreach ($fields as $idx => $col) { |
| | | // direct ID search |
| | |
| | | // 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) { |
| | | switch ($mode) { |
| | | case 1: // strict |
| | | $words[] = '(' . $this->db->ilike('words', $word.' %') |
| | | . ' OR ' . $this->db->ilike('words', '% '.$word.' %') |
| | | . ' OR ' . $this->db->ilike('words', '% '.$word) . ')'; |
| | | break; |
| | | case 2: // prefix |
| | | $words[] = '(' . $this->db->ilike('words', $word.'%') |
| | | . ' OR ' . $this->db->ilike('words', '% '.$word.'%') . ')'; |
| | | break; |
| | | default: // partial |
| | | $words[] = $this->db->ilike('words', '%'.$word.'%'); |
| | | } |
| | | } |
| | | $where[] = '(' . join(' AND ', $words) . ')'; |
| | | } |
| | | else { |
| | | $val = is_array($value) ? $value[$idx] : $value; |
| | | // table column |
| | | if (in_array($col, $this->table_cols)) { |
| | | if ($strict) { |
| | | switch ($mode) { |
| | | case 1: // strict |
| | | $where[] = $this->db->quoteIdentifier($col).' = '.$this->db->quote($val); |
| | | } |
| | | else { |
| | | break; |
| | | case 2: // prefix |
| | | $where[] = $this->db->ilike($col, $val.'%'); |
| | | break; |
| | | default: // partial |
| | | $where[] = $this->db->ilike($col, '%'.$val.'%'); |
| | | } |
| | | } |
| | | // vCard field |
| | | else { |
| | | if (in_array($col, $this->fulltext_cols)) { |
| | | foreach (explode(" ", self::normalize_string($val)) as $word) |
| | | foreach (explode(" ", self::normalize_string($val)) as $word) { |
| | | switch ($mode) { |
| | | case 1: // strict |
| | | $words[] = '(' . $this->db->ilike('words', $word.' %') |
| | | . ' OR ' . $this->db->ilike('words', '% '.$word.' %') |
| | | . ' OR ' . $this->db->ilike('words', '% '.$word) . ')'; |
| | | break; |
| | | case 2: // prefix |
| | | $words[] = '(' . $this->db->ilike('words', $word.'%') |
| | | . ' OR ' . $this->db->ilike('words', ' '.$word.'%') . ')'; |
| | | break; |
| | | default: // partial |
| | | $words[] = $this->db->ilike('words', '%'.$word.'%'); |
| | | } |
| | | } |
| | | $where[] = '(' . join(' AND ', $words) . ')'; |
| | | } |
| | | if (is_array($value)) |
| | |
| | | $search = $post_search[$colname]; |
| | | foreach ((array)$row[$col] as $value) { |
| | | // composite field, e.g. address |
| | | if (is_array($value)) { |
| | | $value = implode($value); |
| | | } |
| | | $value = mb_strtolower($value); |
| | | if (($strict && $value == $search) || (!$strict && strpos($value, $search) !== false)) { |
| | | $found[$colname] = true; |
| | | foreach ((array)$value as $val) { |
| | | $val = mb_strtolower($val); |
| | | switch ($mode) { |
| | | case 1: |
| | | $got = ($val == $search); |
| | | break; |
| | | case 2: |
| | | $got = ($search == substr($val, 0, strlen($search))); |
| | | break; |
| | | default: |
| | | $got = (strpos($val, $search) !== false); |
| | | break; |
| | | } |
| | | |
| | | if ($got) { |
| | | $found[$colname] = true; |
| | | break 2; |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | * |
| | | * @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 int $mode Matching mode: |
| | | * 0 - partial (*abc*), |
| | | * 1 - strict (=), |
| | | * 2 - prefix (abc*) |
| | | * @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()) |
| | | function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array()) |
| | | { |
| | | $mode = intval($mode); |
| | | |
| | | // special treatment for ID-based search |
| | | if ($fields == 'ID' || $fields == $this->primary_key) |
| | | { |
| | |
| | | array_values($this->fieldmap), 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']); |
| | | |
| | | // get all entries of this page and post-filter those that really match the query |
| | | $search = mb_strtolower($value); |
| | | $this->result = new rcube_result_set(0); |
| | | $entries = ldap_get_entries($this->conn, $this->ldap_result); |
| | | |
| | | for ($i = 0; $i < $entries['count']; $i++) { |
| | | $rec = $this->_ldap2result($entries[$i]); |
| | | if (stripos($rec['name'] . $rec['email'], $value) !== false) { |
| | | foreach (array('email', 'name') as $f) { |
| | | $val = mb_strtolower($rec[$f]); |
| | | switch ($mode) { |
| | | case 1: |
| | | $got = ($val == $search); |
| | | break; |
| | | case 2: |
| | | $got = ($search == substr($val, 0, strlen($search))); |
| | | break; |
| | | default: |
| | | $got = (strpos($val, $search) !== false); |
| | | break; |
| | | } |
| | | |
| | | if ($got) { |
| | | $this->result->add($rec); |
| | | $this->result->count++; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | // use AND operator for advanced searches |
| | | $filter = is_array($value) ? '(&' : '(|'; |
| | | $wc = !$strict && $this->prop['fuzzy_search'] ? '*' : ''; |
| | | // set wildcards |
| | | $wp = $ws = ''; |
| | | if (!empty($this->prop['fuzzy_search']) && $mode != 1) { |
| | | $ws = '*'; |
| | | if (!$mode) { |
| | | $wp = '*'; |
| | | } |
| | | } |
| | | |
| | | if ($fields == '*') |
| | | { |
| | |
| | | if (is_array($this->prop['search_fields'])) |
| | | { |
| | | foreach ($this->prop['search_fields'] as $field) { |
| | | $filter .= "($field=$wc" . $this->_quote_string($value) . "$wc)"; |
| | | $filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)"; |
| | | } |
| | | } |
| | | } |
| | |
| | | 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 .= "($f=$wp" . $this->_quote_string($val) . "$ws)"; |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | $groups = array(); |
| | | if ($search) { |
| | | $search = strtolower($search); |
| | | $search = mb_strtolower($search); |
| | | foreach ($group_cache as $group) { |
| | | if (strstr(strtolower($group['name']), $search)) |
| | | if (strpos(mb_strtolower($group['name']), $search) !== false) |
| | | $groups[] = $group; |
| | | } |
| | | } |
| | |
| | | $groups[$group_id]['email'][] = $ldap_data[$i][$email_attr][$j]; |
| | | } |
| | | |
| | | $group_sortnames[] = strtolower($ldap_data[$i][$sort_attr][0]); |
| | | $group_sortnames[] = mb_strtolower($ldap_data[$i][$sort_attr][0]); |
| | | } |
| | | |
| | | // recursive call can exit here |
| | |
| | | // Check if contact exists, if so, we'll need it's ID |
| | | // Note: Some addressbooks allows empty email address field |
| | | if (!empty($a_record['email'])) |
| | | $result = $TARGET->search('email', $a_record['email'], true, true, true); |
| | | $result = $TARGET->search('email', $a_record['email'], 1, true, true); |
| | | else if (!empty($a_record['name'])) |
| | | $result = $TARGET->search('name', $a_record['name'], true, true, true); |
| | | $result = $TARGET->search('name', $a_record['name'], 1, true, true); |
| | | else |
| | | $result = new rcube_result_set(); |
| | | |
| | |
| | | |
| | | if (!$replace && $email) { |
| | | // compare e-mail address |
| | | $existing = $CONTACTS->search('email', $email, false, false); |
| | | $existing = $CONTACTS->search('email', $email, 1, false); |
| | | if (!$existing->count && $vcard->displayname) { // compare display name |
| | | $existing = $CONTACTS->search('name', $vcard->displayname, false, false); |
| | | $existing = $CONTACTS->search('name', $vcard->displayname, 1, false); |
| | | } |
| | | if ($existing->count) { |
| | | $IMPORT_STATS->skipped++; |
| | |
| | | { |
| | | $CONTACTS->set_page(1); |
| | | $CONTACTS->set_pagesize(count($cid) + 2); // +2 to skip counting query |
| | | $recipients = $CONTACTS->search($CONTACTS->primary_key, $cid, false, true, true, 'email'); |
| | | $recipients = $CONTACTS->search($CONTACTS->primary_key, $cid, 0, true, true, 'email'); |
| | | } |
| | | } |
| | | |
| | |
| | | // show notice if existing contacts with same e-mail are found |
| | | $existing = false; |
| | | foreach ($CONTACTS->get_col_values('email', $a_record, true) as $email) { |
| | | if ($email && ($res = $CONTACTS->search('email', $email, false, false, true)) && $res->count) { |
| | | if ($email && ($res = $CONTACTS->search('email', $email, 1, false, true)) && $res->count) { |
| | | $OUTPUT->show_message('contactexists', 'notice', null, false); |
| | | break; |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | // Values matching mode |
| | | $mode = (int) $RCMAIL->config->get('addressbook_search_mode'); |
| | | |
| | | // get sources list |
| | | $sources = $RCMAIL->get_address_sources(); |
| | | $search_set = array(); |
| | |
| | | $source->set_pagesize(9999); |
| | | |
| | | // get contacts count |
| | | $result = $source->search($fields, $search, false, false); |
| | | $result = $source->search($fields, $search, $mode, false); |
| | | |
| | | if (!$result->count) { |
| | | continue; |
| | |
| | | } |
| | | |
| | | // check for existing contacts |
| | | $existing = $CONTACTS->search('email', $contact['email'], true, false); |
| | | $existing = $CONTACTS->search('email', $contact['email'], 1, false); |
| | | |
| | | if ($done = $existing->count) |
| | | $OUTPUT->show_message('contactexists', 'warning'); |
| | |
| | | |
| | | |
| | | $MAXNUM = (int)$RCMAIL->config->get('autocomplete_max', 15); |
| | | $mode = (int) $RCMAIL->config->get('addressbook_search_mode'); |
| | | $search = get_input_value('_search', RCUBE_INPUT_GPC, true); |
| | | $source = get_input_value('_source', RCUBE_INPUT_GPC); |
| | | $sid = get_input_value('_id', RCUBE_INPUT_GPC); |
| | |
| | | $abook = $RCMAIL->get_address_book($id); |
| | | $abook->set_pagesize($MAXNUM); |
| | | |
| | | if ($result = $abook->search(array('email','name'), $search, false, true, true, 'email')) { |
| | | if ($result = $abook->search(array('email','name'), $search, $mode, true, true, 'email')) { |
| | | while ($sql_arr = $result->iterate()) { |
| | | // Contact can have more than one e-mail address |
| | | $email_arr = (array)$abook->get_col_values('email', $sql_arr, true); |
| | |
| | | } |
| | | |
| | | // also list matching contact groups |
| | | if ($abook->groups) { |
| | | if ($abook->groups && count($contacts) < $MAXNUM) { |
| | | foreach ($abook->list_groups($search) as $group) { |
| | | $abook->reset(); |
| | | $abook->set_group($group['ID']); |