<?php /** +-----------------------------------------------------------------------+ | This file is part of the Roundcube Webmail client | | | | Copyright (C) The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | | See the README file for a full license statement. | | | | PURPOSE: | | Interface to the collected addresses database | +-----------------------------------------------------------------------+ | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ /** * Collected addresses database * * @package Framework * @subpackage Addressbook */ class rcube_addresses extends rcube_contacts { protected $db_name = 'collected_addresses'; protected $type = 0; protected $table_cols = ['name', 'email']; protected $fulltext_cols = ['name']; // public properties public $primary_key = 'address_id'; public $readonly = true; public $groups = false; public $undelete = false; public $deletable = true; public $coltypes = ['name', 'email']; public $date_cols = []; /** * Object constructor * * @param object $dbconn Instance of the rcube_db class * @param int $user User-ID * @param int $type Type of the address (1 - recipient, 2 - trusted sender) */ public function __construct($dbconn, $user, $type) { $this->db = $dbconn; $this->user_id = $user; $this->type = $type; $this->ready = $this->db && !$this->db->is_error(); } /** * Returns addressbook name * * @return string */ public function get_name() { if ($this->type == self::TYPE_RECIPIENT) { return rcube::get_instance()->gettext('collectedrecipients'); } if ($this->type == self::TYPE_TRUSTED_SENDER) { return rcube::get_instance()->gettext('trustedsenders'); } return ''; } /** * List the current set of contact records * * @param array $cols List of cols to show, Null means all * @param int $subset Only return this number of records, use negative values for tail * @param bool $nocount True to skip the count query (select only) * * @return array Indexed list of contact records, each a hash array */ public function list_records($cols = null, $subset = 0, $nocount = false) { if ($nocount || $this->list_page <= 1) { // create dummy result, we don't need a count now $this->result = new rcube_result_set(); } else { // count all records $this->result = $this->count(); } $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first; $length = $subset != 0 ? abs($subset) : $this->page_size; $sql_result = $this->db->limitquery( "SELECT * FROM " . $this->db->table_name($this->db_name, true) . " WHERE `user_id` = ? AND `type` = ?" . ($this->filter ? " AND ".$this->filter : "") . " ORDER BY `name` " . $this->sort_order . ", `email` " . $this->sort_order, $start_row, $length, $this->user_id, $this->type ); while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) { $sql_arr['ID'] = $sql_arr[$this->primary_key]; $this->result->add($sql_arr); } $cnt = count($this->result->records); // update counter if ($nocount) { $this->result->count = $cnt; } else if ($this->list_page <= 1) { if ($cnt < $this->page_size && $subset == 0) { $this->result->count = $cnt; } else if (isset($this->cache['count'])) { $this->result->count = $this->cache['count']; } else { $this->result->count = $this->_count(); } } return $this->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 Search mode. Sum of rcube_addressbook::SEARCH_* * @param bool $select True if results are requested, False if count only * @param bool $nocount True to skip the count query (select only) * @param array $required List of fields that cannot be empty * * @return rcube_result_set Contact records and 'count' value */ public function search($fields, $value, $mode = 0, $select = true, $nocount = false, $required = []) { if (!is_array($required) && !empty($required)) { $required = [$required]; } $where = $post_search = []; $mode = intval($mode); // 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[] = $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 ($col == 'email' && ($mode & rcube_addressbook::SEARCH_STRICT)) { $where[] = $this->db->ilike($col, $val); } else if (in_array($col, $this->table_cols)) { $where[] = $this->fulltext_sql_where($val, $mode, $col); } else { $where[] = '1 = 0'; // unsupported column } } } else { // fulltext search in all fields if ($fields == '*') { $fields = ['name', 'email']; } // require each word in to be present in one of the fields $words = ($mode & rcube_addressbook::SEARCH_STRICT) ? [$value] : rcube_utils::tokenize_string($value, 1); foreach ($words as $word) { $groups = []; foreach ((array) $fields as $idx => $col) { if ($col == 'email' && ($mode & rcube_addressbook::SEARCH_STRICT)) { $groups[] = $this->db->ilike($col, $word); } else if (in_array($col, $this->table_cols)) { $groups[] = $this->fulltext_sql_where($word, $mode, $col); } } $where[] = '(' . implode(' OR ', $groups) . ')'; } } foreach (array_intersect($required, $this->table_cols) as $col) { $where[] = $this->db->quote_identifier($col) . ' <> ' . $this->db->quote(''); } if (!empty($where)) { // use AND operator for advanced searches $where = implode(' AND ', $where); $this->set_search_set($where); if ($select) { $this->list_records(null, 0, $nocount); } else { $this->result = $this->count(); } } else { $this->result = new rcube_result_set(); } return $this->result; } /** * Count number of available contacts in database * * @return int Contacts count */ protected function _count() { // count contacts for this user $sql_result = $this->db->query( "SELECT COUNT(`address_id`) AS cnt" . " FROM " . $this->db->table_name($this->db_name, true) . " WHERE `user_id` = ? AND `type` = ?" . ($this->filter ? " AND (" . $this->filter . ")" : ""), $this->user_id, $this->type ); $sql_arr = $this->db->fetch_assoc($sql_result); $this->cache['count'] = (int) $sql_arr['cnt']; return $this->cache['count']; } /** * Get a specific contact record * * @param mixed $id Record identifier(s) * @param bool $assoc Enables returning associative array * * @return rcube_result_set|array Result object with all record fields */ function get_record($id, $assoc = false) { // return cached result if ($this->result && ($first = $this->result->first()) && $first[$this->primary_key] == $id) { return $assoc ? $first : $this->result; } $this->db->query( "SELECT * FROM " . $this->db->table_name($this->db_name, true) . " WHERE `address_id` = ? AND `user_id` = ?", $id, $this->user_id ); $this->result = null; if ($record = $this->db->fetch_assoc()) { $record['ID'] = $record['address_id']; $this->result = new rcube_result_set(1); $this->result->add($record); } return $assoc && !empty($record) ? $record : $this->result; } /** * Check the given data before saving. * If input not valid, the message to display can be fetched using get_error() * * @param array &$save_data Associative array with data to save * @param bool $autofix Try to fix/complete record automatically * * @return bool True if input is valid, False if not. */ public function validate(&$save_data, $autofix = false) { $email = array_filter($this->get_col_values('email', $save_data, true)); // require email if (empty($email) || count($email) > 1) { $this->set_error(self::ERROR_VALIDATE, 'noemailwarning'); return false; } $email = $email[0]; // check validity of the email address if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) { $rcube = rcube::get_instance(); $error = $rcube->gettext(['name' => 'emailformaterror', 'vars' => ['email' => $email]]); $this->set_error(self::ERROR_VALIDATE, $error); return false; } return true; } /** * Create a new contact record * * @param array $save_data Associative array with save data * @param bool $check Enables validity checks * * @return int|bool The created record ID on success, False on error */ function insert($save_data, $check = false) { if (!is_array($save_data)) { return false; } if ($check && ($existing = $this->search('email', $save_data['email'], false, false))) { if ($existing->count) { return false; } } $this->cache = null; $this->db->query( "INSERT INTO " . $this->db->table_name($this->db_name, true) . " (`user_id`, `changed`, `type`, `name`, `email`)" . " VALUES (?, " . $this->db->now() . ", ?, ?, ?)", $this->user_id, $this->type, $save_data['name'], $save_data['email'] ); return $this->db->insert_id($this->db_name); } /** * Update a specific contact record * * @param mixed $id Record identifier * @param array $save_cols Associative array with save data * * @return bool True on success, False on error */ function update($id, $save_cols) { return false; } /** * Delete one or more contact records * * @param array $ids Record identifiers * @param bool $force Remove record(s) irreversible (unsupported) * * @return int|false Number of removed records */ function delete($ids, $force = true) { if (!is_array($ids)) { $ids = explode(self::SEPARATOR, $ids); } $ids = $this->db->array2list($ids, 'integer'); // flag record as deleted (always) $this->db->query( "DELETE FROM " . $this->db->table_name($this->db_name, true) . " WHERE `user_id` = ? AND `type` = ? AND `address_id` IN ($ids)", $this->user_id, $this->type ); $this->cache = null; return $this->db->affected_rows(); } /** * Remove all records from the database * * @param bool $with_groups Remove also groups * * @return int Number of removed records */ function delete_all($with_groups = false) { $this->db->query("DELETE FROM " . $this->db->table_name($this->db_name, true) . " WHERE `user_id` = ? AND `type` = ?", $this->user_id, $this->type ); $this->cache = null; return $this->db->affected_rows(); } }