From 66d4ef6e31789b1e1c8a5b5c38cc94dd744e8d68 Mon Sep 17 00:00:00 2001 From: Thomas B <thomas@roundcube.net> Date: Sun, 10 Jan 2016 10:02:51 -0500 Subject: [PATCH] Merge pull request #316 from namesco/prevent-blank-contactnames --- program/lib/Roundcube/rcube_addressbook.php | 281 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 files changed, 221 insertions(+), 60 deletions(-) diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php index a8f274a..b4ce03b 100644 --- a/program/lib/Roundcube/rcube_addressbook.php +++ b/program/lib/Roundcube/rcube_addressbook.php @@ -1,9 +1,9 @@ <?php -/* +/** +-----------------------------------------------------------------------+ | This file is part of the Roundcube Webmail client | - | Copyright (C) 2006-2012, The Roundcube Dev Team | + | Copyright (C) 2006-2013, The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -16,7 +16,6 @@ +-----------------------------------------------------------------------+ */ - /** * Abstract skeleton of an address book/repository * @@ -26,25 +25,32 @@ abstract class rcube_addressbook { /** constants for error reporting **/ - const ERROR_READ_ONLY = 1; + const ERROR_READ_ONLY = 1; const ERROR_NO_CONNECTION = 2; - const ERROR_VALIDATE = 3; - const ERROR_SAVING = 4; - const ERROR_SEARCH = 5; + const ERROR_VALIDATE = 3; + const ERROR_SAVING = 4; + const ERROR_SEARCH = 5; /** public properties (mandatory) */ public $primary_key; - public $groups = false; - public $readonly = true; - public $searchonly = false; - public $undelete = false; - public $ready = false; - public $group_id = null; - public $list_page = 1; - public $page_size = 10; - public $sort_col = 'name'; - public $sort_order = 'ASC'; - public $coltypes = array('name' => array('limit'=>1), 'firstname' => array('limit'=>1), 'surname' => array('limit'=>1), 'email' => array('limit'=>1)); + public $groups = false; + public $export_groups = true; + public $readonly = true; + public $searchonly = false; + public $undelete = false; + public $ready = false; + public $group_id = null; + public $list_page = 1; + public $page_size = 10; + public $sort_col = 'name'; + public $sort_order = 'ASC'; + public $date_cols = array(); + public $coltypes = array( + 'name' => array('limit'=>1), + 'firstname' => array('limit'=>1), + 'surname' => array('limit'=>1), + 'email' => array('limit'=>1) + ); protected $error; @@ -132,24 +138,24 @@ abstract function get_record($id, $assoc=false); /** - * Returns the last error occured (e.g. when updating/inserting failed) + * Returns the last error occurred (e.g. when updating/inserting failed) * * @return array Hash array with the following fields: type, message */ function get_error() { - return $this->error; + return $this->error; } /** * Setter for errors for internal use * - * @param int Error type (one of this class' error constants) + * @param int Error type (one of this class' error constants) * @param string Error message (name of a text label) */ protected function set_error($type, $message) { - $this->error = array('type' => $type, 'message' => $message); + $this->error = array('type' => $type, 'message' => $message); } /** @@ -161,8 +167,7 @@ /** * Set internal list page * - * @param number Page number to list - * @access public + * @param number Page number to list */ function set_page($page) { @@ -172,8 +177,7 @@ /** * Set internal page size * - * @param number Number of messages to display on one page - * @access public + * @param number Number of messages to display on one page */ function set_pagesize($size) { @@ -200,13 +204,15 @@ * Check the given data before saving. * If input isn't valid, the message to display can be fetched using get_error() * - * @param array Assoziative array with data to save + * @param array Assoziative array with data to save * @param boolean Attempt to fix/complete record automatically + * * @return boolean True if input is valid, False if not. */ public function validate(&$save_data, $autofix = false) { $rcube = rcube::get_instance(); + $valid = true; // check validity of email addresses foreach ($this->get_col_values('email', $save_data, true) as $email) { @@ -214,14 +220,29 @@ if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) { $error = $rcube->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email))); $this->set_error(self::ERROR_VALIDATE, $error); - return false; + $valid = false; + break; } } } - return true; - } + // allow plugins to do contact validation and auto-fixing + $plugin = $rcube->plugins->exec_hook('contact_validate', array( + 'record' => $save_data, + 'autofix' => $autofix, + 'valid' => $valid, + )); + if ($valid && !$plugin['valid']) { + $this->set_error(self::ERROR_VALIDATE, $plugin['error']); + } + + if (is_array($plugin['record'])) { + $save_data = $plugin['record']; + } + + return $plugin['valid']; + } /** * Create a new contact record @@ -263,7 +284,8 @@ * @param array Assoziative array with save data * Keys: Field name with optional section in the form FIELD:SECTION * Values: Field value. Can be either a string or an array of strings for multiple values - * @return boolean True on success, False on error + * + * @return mixed On success if ID has been changed returns ID, otherwise True, False on error */ function update($id, $save_cols) { @@ -273,10 +295,10 @@ /** * Mark one or more contact records as deleted * - * @param array Record identifiers - * @param bool Remove records irreversible (see self::undelete) + * @param array Record identifiers + * @param bool Remove records irreversible (see self::undelete) */ - function delete($ids, $force=true) + function delete($ids, $force = true) { /* empty for read-only address books */ } @@ -284,7 +306,7 @@ /** * Unmark delete flag on contact record(s) * - * @param array Record identifiers + * @param array Record identifiers */ function undelete($ids) { @@ -293,8 +315,10 @@ /** * Mark all records in database as deleted + * + * @param bool $with_groups Remove also groups */ - function delete_all() + function delete_all($with_groups = false) { /* empty for read-only address books */ } @@ -309,9 +333,14 @@ * List all active contact groups of this source * * @param string Optional search string to match group name + * @param int Matching mode: + * 0 - partial (*abc*), + * 1 - strict (=), + * 2 - prefix (abc*) + * * @return array Indexed list of contact groups, each a hash array */ - function list_groups($search = null) + function list_groups($search = null, $mode = 0) { /* empty for address books don't supporting groups */ return array(); @@ -370,9 +399,10 @@ /** * Add the given contact records the a certain group * - * @param string Group identifier - * @param array List of contact identifiers to be added - * @return int Number of contacts added + * @param string Group identifier + * @param array|string List of contact identifiers to be added + * + * @return int Number of contacts added */ function add_to_group($group_id, $ids) { @@ -383,9 +413,10 @@ /** * Remove the given contact records from a certain group * - * @param string Group identifier - * @param array List of contact identifiers to be removed - * @return int Number of deleted group members + * @param string Group identifier + * @param array|string List of contact identifiers to be removed + * + * @return int Number of deleted group members */ function remove_from_group($group_id, $ids) { @@ -407,7 +438,6 @@ return array(); } - /** * Utility function to return all values of a certain data column * either as flat list or grouped by subtype @@ -417,7 +447,7 @@ * @param boolean True to return one array with all values, False for hash array with values grouped by type * @return array List of column values */ - function get_col_values($col, $data, $flat = false) + public static function get_col_values($col, $data, $flat = false) { $out = array(); foreach ((array)$data as $c => $values) { @@ -426,7 +456,7 @@ $out = array_merge($out, (array)$values); } else { - list($f, $type) = explode(':', $c); + list(, $type) = explode(':', $c); $out[$type] = array_merge((array)$out[$type], (array)$values); } } @@ -439,7 +469,6 @@ return $out; } - /** * Normalize the given string for fulltext search. @@ -471,7 +500,8 @@ $fn = trim(join(' ', array_filter(array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix'])))); // use email address part for name - $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email']; + $email = self::get_col_values('email', $contact, true); + $email = $email[0]; if ($email && (empty($fn) || $fn == $email)) { // return full email @@ -487,7 +517,6 @@ return $fn; } - /** * Compose the name to display in the contacts list for the given contact record. @@ -509,22 +538,154 @@ $fn = join(' ', array($contact['surname'], $contact['firstname'], $contact['middlename'])); else if ($compose_mode == 1) $fn = join(' ', array($contact['firstname'], $contact['middlename'], $contact['surname'])); - else - $fn = !empty($contact['name']) ? $contact['name'] : join(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix'])); + else if ($compose_mode == 0) + $fn = $contact['name'] ?: join(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix'])); + else { + $plugin = rcube::get_instance()->plugins->exec_hook('contact_listname', array('contact' => $contact)); + $fn = $plugin['fn']; + } $fn = trim($fn, ', '); - // fallback to display name - if (empty($fn) && $contact['name']) - $fn = $contact['name']; - - // fallback to email address - $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email']; - if (empty($fn) && $email) - return $email; + // fallbacks... + if ($fn === '') { + // ... display name + if (!empty(trim($contact['name']))) { + $fn = $contact['name']; + } + // ... organization + else if (!empty($contact['organization'])) { + $fn = $contact['organization']; + } + // ... email address + else if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) { + $fn = $email[0]; + } + } return $fn; } -} + /** + * Build contact display name for autocomplete listing + * + * @param array Hash array with contact data as key-value pairs + * @param string Optional email address + * @param string Optional name (self::compose_list_name() result) + * @param string Optional template to use (defaults to the 'contact_search_name' config option) + * + * @return string Display name + */ + public static function compose_search_name($contact, $email = null, $name = null, $templ = null) + { + static $template; + if (empty($templ) && !isset($template)) { // cache this + $template = rcube::get_instance()->config->get('contact_search_name'); + if (empty($template)) { + $template = '{name} <{email}>'; + } + } + + $result = $templ ?: $template; + + if (preg_match_all('/\{[a-z]+\}/', $result, $matches)) { + foreach ($matches[0] as $key) { + $key = trim($key, '{}'); + $value = ''; + + switch ($key) { + case 'name': + $value = $name ?: self::compose_list_name($contact); + + // If name(s) are undefined compose_list_name() may return an email address + // here we prevent from returning the same name and email + if ($name === $email && strpos($result, '{email}') !== false) { + $value = ''; + } + + break; + + case 'email': + $value = $email; + break; + } + + if (empty($value)) { + $value = strpos($key, ':') ? $contact[$key] : self::get_col_values($key, $contact, true); + if (is_array($value)) { + $value = $value[0]; + } + } + + $result = str_replace('{' . $key . '}', $value, $result); + } + } + + $result = preg_replace('/\s+/', ' ', $result); + $result = preg_replace('/\s*(<>|\(\)|\[\])/', '', $result); + $result = trim($result, '/ '); + + return $result; + } + + /** + * Create a unique key for sorting contacts + */ + public static function compose_contact_key($contact, $sort_col) + { + $key = $contact[$sort_col] . ':' . $contact['sourceid']; + + // add email to a key to not skip contacts with the same name (#1488375) + if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) { + $key .= ':' . implode(':', (array)$email); + } + + return $key; + } + + /** + * Compare search value with contact data + * + * @param string $colname Data name + * @param string|array $value Data value + * @param string $search Search value + * @param int $mode Search mode + * + * @return bool Comparision result + */ + protected function compare_search_value($colname, $value, $search, $mode) + { + // The value is a date string, for date we'll + // use only strict comparison (mode = 1) + // @TODO: partial search, e.g. match only day and month + if (in_array($colname, $this->date_cols)) { + return (($value = rcube_utils::anytodatetime($value)) + && ($search = rcube_utils::anytodatetime($search)) + && $value->format('Ymd') == $search->format('Ymd')); + } + + // composite field, e.g. address + 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); + } + + if ($got) { + return true; + } + } + + return false; + } +} -- Gitblit v1.9.1