/** * rcube:utils::tokenize_string() */ function test_tokenize_string() { $test = array('' => array(), 'abc d' => array('abc'), 'abc de' => array('abc', 'de'), 'äàé;êöü-xyz' => array('äàé', 'êöü', 'xyz'), '日期格式' => array('日期格式')); foreach ($test as $input => $output) { $result = rcube_utils::tokenize_string($input); $this->assertSame($output, $result); } }
/** * Search contacts * * @param mixed $fields The field name or array of field names to search in * @param mixed $value Search value (or array of values when $fields is array) * @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, $mode = 0, $select = true, $nocount = false, $required = array()) { if (!is_array($required) && !empty($required)) { $required = array($required); } $where = $and_where = $post_search = array(); $mode = intval($mode); $WS = ' '; $AS = self::SEPARATOR; // direct ID search if ($fields == 'ID' || $fields == $this->primary_key) { $ids = !is_array($value) ? explode(self::SEPARATOR, $value) : $value; $ids = $this->db->array2list($ids, 'integer'); $where[] = 'c.' . $this->primary_key . ' IN (' . $ids . ')'; } else { if (is_array($value)) { foreach ((array) $fields as $idx => $col) { $val = $value[$idx]; if (!strlen($val)) { continue; } // table column if (in_array($col, $this->table_cols)) { switch ($mode) { case 1: // strict $where[] = '(' . $this->db->quote_identifier($col) . ' = ' . $this->db->quote($val) . ' OR ' . $this->db->ilike($col, $val . $AS . '%') . ' OR ' . $this->db->ilike($col, '%' . $AS . $val . $AS . '%') . ' OR ' . $this->db->ilike($col, '%' . $AS . $val) . ')'; break; case 2: // prefix $where[] = '(' . $this->db->ilike($col, $val . '%') . ' OR ' . $this->db->ilike($col, $AS . $val . '%') . ')'; break; default: // partial $where[] = $this->db->ilike($col, '%' . $val . '%'); } } else { if (in_array($col, $this->fulltext_cols)) { $where[] = $this->fulltext_sql_where($val, $mode, 'words'); } $post_search[$col] = mb_strtolower($val); } } } else { if ($fields == '*') { $where[] = $this->fulltext_sql_where($value, $mode, 'words'); } else { // require each word in to be present in one of the fields $words = $mode == 1 ? array($value) : rcube_utils::tokenize_string($value, 1); foreach ($words as $word) { $groups = array(); foreach ((array) $fields as $idx => $col) { $groups[] = $this->fulltext_sql_where($word, $mode, $col); } $where[] = '(' . join(' OR ', $groups) . ')'; } } } } foreach (array_intersect($required, $this->table_cols) as $col) { $and_where[] = $this->db->quote_identifier($col) . ' <> ' . $this->db->quote(''); } $required = array_diff($required, $this->table_cols); if (!empty($where)) { // use AND operator for advanced searches $where = join(" AND ", $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) || !empty($required)) { $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()->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 = array(); if (!empty($post_search)) { 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) { if ($this->compare_search_value($colname, $value, $search, $mode)) { $found[$colname] = true; break 2; } } } } // check if required fields are present if (!empty($required)) { foreach ($required as $req) { $hit = false; foreach ($row as $c => $values) { if ($c === $req || strpos($c, $req . ':') === 0) { if (is_string($row[$c]) && strlen($row[$c]) || !empty($row[$c])) { $hit = true; break; } } } if (!$hit) { continue 2; } } } // all fields match if (count($found) >= $scnt) { $ids[] = $id; } } } // build WHERE clause $ids = $this->db->array2list($ids, 'integer'); $where = 'c.`' . $this->primary_key . '` IN (' . $ids . ')'; // reset counter unset($this->cache['count']); // when we know we have an empty result if ($ids == '0') { $this->set_search_set($where); return $this->result = new rcube_result_set(0, 0); } } if (!empty($where)) { $this->set_search_set($where); if ($select) { $this->list_records(null, 0, $nocount); } else { $this->result = $this->count(); } } return $this->result; }
/** * Compose an LDAP filter string matching all words from the search string * in the given list of attributes. * * @param string $value Search value * @param mixed $attrs List of LDAP attributes to search * @param int $mode Matching mode: * 0 - partial (*abc*), * 1 - strict (=), * 2 - prefix (abc*) * @return string LDAP filter */ public static function fulltext_search_filter($value, $attributes, $mode = 1) { if (empty($attributes)) { $attributes = array('cn'); } $groups = array(); $value = str_replace('*', '', $value); $words = $mode == 0 ? rcube_utils::tokenize_string($value, 1) : array($value); // set wildcards $wp = $ws = ''; if ($mode != 1) { $ws = '*'; $wp = !$mode ? '*' : ''; } // search each word in all listed attributes foreach ($words as $word) { $parts = array(); foreach ($attributes as $attr) { $parts[] = "({$attr}={$wp}" . self::quote_string($word) . "{$ws})"; } $groups[] = '(|' . join('', $parts) . ')'; } return count($groups) > 1 ? '(&' . join('', $groups) . ')' : join('', $groups); }
/** * @param integer Event's new start (unix timestamp) * @param integer Event's new end (unix timestamp) * @param string Search query (optional) * @param boolean Include virtual events (optional) * @param array Additional parameters to query storage * @param array Additional query to filter events * @return array A list of event records */ public function list_events($start, $end, $search = null, $virtual = 1, $query = array(), $filter_query = null) { // convert to DateTime for comparisons // #5190: make the range a little bit wider // to workaround possible timezone differences try { $start = new DateTime('@' . ($start - 12 * 3600)); } catch (Exception $e) { $start = new DateTime('@0'); } try { $end = new DateTime('@' . ($end + 12 * 3600)); } catch (Exception $e) { $end = new DateTime('today +10 years'); } // get email addresses of the current user $user_emails = $this->cal->get_user_emails(); // query Kolab storage $query[] = array('dtstart', '<=', $end); $query[] = array('dtend', '>=', $start); if (is_array($filter_query)) { $query = array_merge($query, $filter_query); } if (!empty($search)) { $search = mb_strtolower($search); $words = rcube_utils::tokenize_string($search, 1); foreach (rcube_utils::normalize_string($search, true) as $word) { $query[] = array('words', 'LIKE', $word); } } else { $words = array(); } // set partstat filter to skip pending and declined invitations if (empty($filter_query) && $this->get_namespace() != 'other') { $partstat_exclude = array('NEEDS-ACTION', 'DECLINED'); } else { $partstat_exclude = array(); } $events = array(); foreach ($this->storage->select($query) as $record) { $event = $this->_to_driver_event($record, !$virtual); // remember seen categories if ($event['categories']) { $cat = is_array($event['categories']) ? $event['categories'][0] : $event['categories']; $this->categories[$cat]++; } // list events in requested time window if ($event['start'] <= $end && $event['end'] >= $start) { unset($event['_attendees']); $add = true; // skip the first instance of a recurring event if listed in exdate if ($virtual && !empty($event['recurrence']['EXDATE'])) { $event_date = $event['start']->format('Ymd'); $exdates = (array) $event['recurrence']['EXDATE']; foreach ($exdates as $exdate) { if ($exdate->format('Ymd') == $event_date) { $add = false; break; } } } // find and merge exception for the first instance if ($virtual && !empty($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS'])) { foreach ($event['recurrence']['EXCEPTIONS'] as $exception) { if ($event['_instance'] == $exception['_instance']) { // clone date objects from main event before adjusting them with exception data if (is_object($event['start'])) { $event['start'] = clone $record['start']; } if (is_object($event['end'])) { $event['end'] = clone $record['end']; } kolab_driver::merge_exception_data($event, $exception); } } } if ($add) { $events[] = $event; } } // resolve recurring events if ($record['recurrence'] && $virtual == 1) { $events = array_merge($events, $this->get_recurring_events($record, $start, $end)); } else { if (is_array($record['exceptions'])) { foreach ($record['exceptions'] as $ex) { $component = $this->_to_driver_event($ex); if ($component['start'] <= $end && $component['end'] >= $start) { $events[] = $component; } } } } } // post-filter all events by fulltext search and partstat values $me = $this; $events = array_filter($events, function ($event) use($words, $partstat_exclude, $user_emails, $me) { // fulltext search if (count($words)) { $hits = 0; foreach ($words as $word) { $hits += $me->fulltext_match($event, $word, false); } if ($hits < count($words)) { return false; } } // partstat filter if (count($partstat_exclude) && is_array($event['attendees'])) { foreach ($event['attendees'] as $attendee) { if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], $partstat_exclude)) { return false; } } } return true; }); // avoid session race conditions that will loose temporary subscriptions $this->cal->rc->session->nowrite = true; return $events; }