Thomas Bruederli
2013-10-07 120db629b0645033fd6a477b9f96cc8dad589213
commit | author | age
cc97ea 1 <?php
T 2
3 /*
4  +-----------------------------------------------------------------------+
e019f2 5  | This file is part of the Roundcube Webmail client                     |
79367a 6  | Copyright (C) 2006-2013, The Roundcube Dev Team                       |
7fe381 7  |                                                                       |
T 8  | Licensed under the GNU General Public License version 3 or            |
9  | any later version with exceptions for skins & plugins.                |
10  | See the README file for a full license statement.                     |
cc97ea 11  |                                                                       |
T 12  | PURPOSE:                                                              |
13  |   Interface to the local address book database                        |
14  +-----------------------------------------------------------------------+
15  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
16  +-----------------------------------------------------------------------+
17 */
18
19
20 /**
21  * Abstract skeleton of an address book/repository
22  *
9ab346 23  * @package    Framework
AM 24  * @subpackage Addressbook
cc97ea 25  */
T 26 abstract class rcube_addressbook
27 {
0501b6 28     /** constants for error reporting **/
T 29     const ERROR_READ_ONLY = 1;
30     const ERROR_NO_CONNECTION = 2;
39cafa 31     const ERROR_VALIDATE = 3;
0501b6 32     const ERROR_SAVING = 4;
3e2637 33     const ERROR_SEARCH = 5;
cc90ed 34
0501b6 35     /** public properties (mandatory) */
T 36     public $primary_key;
37     public $groups = false;
79367a 38     public $export_groups = true;
0501b6 39     public $readonly = true;
2d3e2b 40     public $searchonly = false;
7f5a84 41     public $undelete = false;
0501b6 42     public $ready = false;
06670e 43     public $group_id = null;
0501b6 44     public $list_page = 1;
T 45     public $page_size = 10;
438753 46     public $sort_col = 'name';
T 47     public $sort_order = 'ASC';
0501b6 48     public $coltypes = array('name' => array('limit'=>1), 'firstname' => array('limit'=>1), 'surname' => array('limit'=>1), 'email' => array('limit'=>1));
3e3767 49     public $date_cols = array();
cc90ed 50
0501b6 51     protected $error;
cc90ed 52
A 53     /**
54      * Returns addressbook name (e.g. for addressbooks listing)
55      */
56     abstract function get_name();
cc97ea 57
T 58     /**
59      * Save a search string for future listings
60      *
61      * @param mixed Search params to use in listing method, obtained by get_search_set()
62      */
63     abstract function set_search_set($filter);
64
65     /**
66      * Getter for saved search properties
67      *
68      * @return mixed Search properties used by this class
69      */
70     abstract function get_search_set();
71
72     /**
73      * Reset saved results and search parameters
74      */
75     abstract function reset();
76
77     /**
0501b6 78      * Refresh saved search set after data has changed
T 79      *
80      * @return mixed New search set
81      */
82     function refresh_search()
83     {
84         return $this->get_search_set();
85     }
86
87     /**
cc97ea 88      * List the current set of contact records
T 89      *
90      * @param  array  List of cols to show
91      * @param  int    Only return this number of records, use negative values for tail
92      * @return array  Indexed list of contact records, each a hash array
93      */
94     abstract function list_records($cols=null, $subset=0);
a61bbb 95
T 96     /**
cc97ea 97      * Search records
T 98      *
99      * @param array   List of fields to search in
100      * @param string  Search value
f21a04 101      * @param int     Matching mode:
A 102      *                0 - partial (*abc*),
103      *                1 - strict (=),
104      *                2 - prefix (abc*)
cc97ea 105      * @param boolean True if results are requested, False if count only
0501b6 106      * @param boolean True to skip the count query (select only)
T 107      * @param array   List of fields that cannot be empty
108      * @return object rcube_result_set List of contact records and 'count' value
cc97ea 109      */
f21a04 110     abstract function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array());
cc97ea 111
T 112     /**
113      * Count number of available contacts in database
114      *
5c461b 115      * @return rcube_result_set Result set with values for 'count' and 'first'
cc97ea 116      */
T 117     abstract function count();
118
119     /**
120      * Return the last result set
121      *
5c461b 122      * @return rcube_result_set Current result set or NULL if nothing selected yet
cc97ea 123      */
T 124     abstract function get_result();
125
126     /**
127      * Get a specific contact record
128      *
129      * @param mixed record identifier(s)
130      * @param boolean True to return record as associative array, otherwise a result set is returned
b393e5 131      *
cc97ea 132      * @return mixed Result object with all record fields or False if not found
T 133      */
134     abstract function get_record($id, $assoc=false);
0501b6 135
T 136     /**
b3e259 137      * Returns the last error occurred (e.g. when updating/inserting failed)
0501b6 138      *
T 139      * @return array Hash array with the following fields: type, message
140      */
141     function get_error()
142     {
0d2144 143         return $this->error;
0501b6 144     }
cc90ed 145
0501b6 146     /**
T 147      * Setter for errors for internal use
148      *
149      * @param int Error type (one of this class' error constants)
150      * @param string Error message (name of a text label)
151      */
152     protected function set_error($type, $message)
153     {
0d2144 154         $this->error = array('type' => $type, 'message' => $message);
0501b6 155     }
cc97ea 156
T 157     /**
158      * Close connection to source
159      * Called on script shutdown
160      */
161     function close() { }
162
163     /**
164      * Set internal list page
165      *
166      * @param  number  Page number to list
167      * @access public
168      */
169     function set_page($page)
170     {
2eb794 171         $this->list_page = (int)$page;
cc97ea 172     }
T 173
174     /**
175      * Set internal page size
176      *
177      * @param  number  Number of messages to display on one page
178      * @access public
179      */
180     function set_pagesize($size)
181     {
2eb794 182         $this->page_size = (int)$size;
cc97ea 183     }
a61bbb 184
438753 185     /**
T 186      * Set internal sort settings
187      *
188      * @param string $sort_col Sort column
189      * @param string $sort_order Sort order
190      */
191     function set_sort_order($sort_col, $sort_order = null)
192     {
193         if ($sort_col != null && ($this->coltypes[$sort_col] || in_array($sort_col, $this->coltypes))) {
194             $this->sort_col = $sort_col;
195         }
196         if ($sort_order != null) {
197             $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
198         }
199     }
07b95d 200
T 201     /**
202      * Check the given data before saving.
e84818 203      * If input isn't valid, the message to display can be fetched using get_error()
07b95d 204      *
T 205      * @param array Assoziative array with data to save
39cafa 206      * @param boolean Attempt to fix/complete record automatically
07b95d 207      * @return boolean True if input is valid, False if not.
T 208      */
39cafa 209     public function validate(&$save_data, $autofix = false)
07b95d 210     {
996af3 211         $rcube = rcube::get_instance();
0c2596 212
07b95d 213         // check validity of email addresses
T 214         foreach ($this->get_col_values('email', $save_data, true) as $email) {
215             if (strlen($email)) {
1aceb9 216                 if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) {
996af3 217                     $error = $rcube->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email)));
0c2596 218                     $this->set_error(self::ERROR_VALIDATE, $error);
07b95d 219                     return false;
T 220                 }
221             }
222         }
223
224         return true;
225     }
226
a61bbb 227     /**
cc97ea 228      * Create a new contact record
T 229      *
230      * @param array Assoziative array with save data
0501b6 231      *  Keys:   Field name with optional section in the form FIELD:SECTION
T 232      *  Values: Field value. Can be either a string or an array of strings for multiple values
cc97ea 233      * @param boolean True to check for duplicates first
5c461b 234      * @return mixed The created record ID on success, False on error
cc97ea 235      */
T 236     function insert($save_data, $check=false)
237     {
2eb794 238         /* empty for read-only address books */
cc97ea 239     }
T 240
241     /**
0501b6 242      * Create new contact records for every item in the record set
T 243      *
244      * @param object rcube_result_set Recordset to insert
245      * @param boolean True to check for duplicates first
246      * @return array List of created record IDs
247      */
248     function insertMultiple($recset, $check=false)
249     {
250         $ids = array();
251         if (is_object($recset) && is_a($recset, rcube_result_set)) {
252             while ($row = $recset->next()) {
253                 if ($insert = $this->insert($row, $check))
254                     $ids[] = $insert;
255             }
256         }
257         return $ids;
258     }
259
260     /**
cc97ea 261      * Update a specific contact record
T 262      *
263      * @param mixed Record identifier
264      * @param array Assoziative array with save data
0501b6 265      *  Keys:   Field name with optional section in the form FIELD:SECTION
T 266      *  Values: Field value. Can be either a string or an array of strings for multiple values
5c461b 267      * @return boolean True on success, False on error
cc97ea 268      */
T 269     function update($id, $save_cols)
270     {
2eb794 271         /* empty for read-only address books */
cc97ea 272     }
T 273
274     /**
275      * Mark one or more contact records as deleted
276      *
277      * @param array  Record identifiers
63fda8 278      * @param bool   Remove records irreversible (see self::undelete)
cc97ea 279      */
63fda8 280     function delete($ids, $force=true)
cc97ea 281     {
2eb794 282         /* empty for read-only address books */
cc97ea 283     }
T 284
285     /**
7f5a84 286      * Unmark delete flag on contact record(s)
A 287      *
288      * @param array  Record identifiers
289      */
290     function undelete($ids)
291     {
292         /* empty for read-only address books */
293     }
294
295     /**
296      * Mark all records in database as deleted
cc97ea 297      */
T 298     function delete_all()
299     {
2eb794 300         /* empty for read-only address books */
cc97ea 301     }
T 302
04adaa 303     /**
b393e5 304      * Setter for the current group
A 305      * (empty, has to be re-implemented by extending class)
306      */
307     function set_group($gid) { }
308
309     /**
310      * List all active contact groups of this source
311      *
0501b6 312      * @param string  Optional search string to match group name
ec4331 313      * @param int     Matching mode:
AM 314      *                0 - partial (*abc*),
315      *                1 - strict (=),
316      *                2 - prefix (abc*)
317      *
b393e5 318      * @return array  Indexed list of contact groups, each a hash array
A 319      */
ec4331 320     function list_groups($search = null, $mode = 0)
b393e5 321     {
A 322         /* empty for address books don't supporting groups */
323         return array();
324     }
325
326     /**
dc6c4f 327      * Get group properties such as name and email address(es)
T 328      *
329      * @param string Group identifier
330      * @return array Group properties as hash array
331      */
332     function get_group($group_id)
333     {
334         /* empty for address books don't supporting groups */
335         return null;
336     }
337
338     /**
04adaa 339      * Create a contact group with the given name
T 340      *
341      * @param string The group name
5c461b 342      * @return mixed False on error, array with record props in success
04adaa 343      */
T 344     function create_group($name)
345     {
2eb794 346         /* empty for address books don't supporting groups */
A 347         return false;
04adaa 348     }
b393e5 349
04adaa 350     /**
T 351      * Delete the given group and all linked group members
352      *
353      * @param string Group identifier
354      * @return boolean True on success, false if no data was changed
355      */
356     function delete_group($gid)
357     {
2eb794 358         /* empty for address books don't supporting groups */
A 359         return false;
04adaa 360     }
b393e5 361
04adaa 362     /**
T 363      * Rename a specific contact group
364      *
365      * @param string Group identifier
366      * @param string New name to set for this group
360bd3 367      * @param string New group identifier (if changed, otherwise don't set)
04adaa 368      * @return boolean New name on success, false if no data was changed
T 369      */
360bd3 370     function rename_group($gid, $newname, &$newid)
04adaa 371     {
2eb794 372         /* empty for address books don't supporting groups */
A 373         return false;
04adaa 374     }
b393e5 375
04adaa 376     /**
T 377      * Add the given contact records the a certain group
378      *
40d419 379      * @param string       Group identifier
AM 380      * @param array|string List of contact identifiers to be added
381      *
382      * @return int Number of contacts added
04adaa 383      */
T 384     function add_to_group($group_id, $ids)
385     {
2eb794 386         /* empty for address books don't supporting groups */
A 387         return 0;
04adaa 388     }
b393e5 389
04adaa 390     /**
T 391      * Remove the given contact records from a certain group
392      *
40d419 393      * @param string       Group identifier
AM 394      * @param array|string List of contact identifiers to be removed
395      *
396      * @return int Number of deleted group members
04adaa 397      */
T 398     function remove_from_group($group_id, $ids)
399     {
2eb794 400         /* empty for address books don't supporting groups */
A 401         return 0;
04adaa 402     }
b393e5 403
A 404     /**
405      * Get group assignments of a specific contact record
406      *
407      * @param mixed Record identifier
408      *
409      * @return array List of assigned groups as ID=>Name pairs
410      * @since 0.5-beta
411      */
412     function get_record_groups($id)
413     {
414         /* empty for address books don't supporting groups */
415         return array();
416     }
0501b6 417
T 418     /**
419      * Utility function to return all values of a certain data column
420      * either as flat list or grouped by subtype
421      *
422      * @param string Col name
423      * @param array  Record data array as used for saving
424      * @param boolean True to return one array with all values, False for hash array with values grouped by type
425      * @return array List of column values
426      */
ad003c 427     public static function get_col_values($col, $data, $flat = false)
0501b6 428     {
T 429         $out = array();
88fb56 430         foreach ((array)$data as $c => $values) {
bed577 431             if ($c === $col || strpos($c, $col.':') === 0) {
0501b6 432                 if ($flat) {
T 433                     $out = array_merge($out, (array)$values);
434                 }
435                 else {
3725cf 436                     list(, $type) = explode(':', $c);
0501b6 437                     $out[$type] = array_merge((array)$out[$type], (array)$values);
T 438                 }
439             }
440         }
cc90ed 441
f368b0 442         // remove duplicates
AM 443         if ($flat && !empty($out)) {
444             $out = array_unique($out);
445         }
446
0501b6 447         return $out;
T 448     }
3e2637 449
T 450     /**
451      * Normalize the given string for fulltext search.
452      * Currently only optimized for Latin-1 characters; to be extended
453      *
454      * @param string Input string (UTF-8)
455      * @return string Normalized string
ceb5b5 456      * @deprecated since 0.9-beta
3e2637 457      */
T 458     protected static function normalize_string($str)
459     {
062963 460         return rcube_utils::normalize_string($str);
3e2637 461     }
e84818 462
T 463     /**
464      * Compose a valid display name from the given structured contact data
465      *
466      * @param array  Hash array with contact data as key-value pairs
f9a967 467      * @param bool   Don't attempt to extract components from the email address
71e8cc 468      *
e84818 469      * @return string Display name
T 470      */
f9a967 471     public static function compose_display_name($contact, $full_email = false)
e84818 472     {
bfc307 473         $contact = rcube::get_instance()->plugins->exec_hook('contact_displayname', $contact);
e84818 474         $fn = $contact['name'];
T 475
f9a967 476         if (!$fn)  // default display name composition according to vcard standard
6898b4 477             $fn = trim(join(' ', array_filter(array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']))));
e84818 478
T 479         // use email address part for name
ad003c 480         $email = self::get_col_values('email', $contact, true);
AM 481         $email = $email[0];
71e8cc 482
e84818 483         if ($email && (empty($fn) || $fn == $email)) {
f9a967 484             // return full email
T 485             if ($full_email)
71e8cc 486                 return $email;
A 487
e84818 488             list($emailname) = explode('@', $email);
T 489             if (preg_match('/(.*)[\.\-\_](.*)/', $emailname, $match))
490                 $fn = trim(ucfirst($match[1]).' '.ucfirst($match[2]));
491             else
492                 $fn = ucfirst($emailname);
493         }
494
495         return $fn;
496     }
497
f9a967 498     /**
T 499      * Compose the name to display in the contacts list for the given contact record.
500      * This respects the settings parameter how to list conacts.
501      *
502      * @param array  Hash array with contact data as key-value pairs
503      * @return string List name
504      */
505     public static function compose_list_name($contact)
506     {
507         static $compose_mode;
508
509         if (!isset($compose_mode))  // cache this
bfc307 510             $compose_mode = rcube::get_instance()->config->get('addressbook_name_listing', 0);
f9a967 511
T 512         if ($compose_mode == 3)
513             $fn = join(' ', array($contact['surname'] . ',', $contact['firstname'], $contact['middlename']));
514         else if ($compose_mode == 2)
515             $fn = join(' ', array($contact['surname'], $contact['firstname'], $contact['middlename']));
516         else if ($compose_mode == 1)
517             $fn = join(' ', array($contact['firstname'], $contact['middlename'], $contact['surname']));
518         else
519             $fn = !empty($contact['name']) ? $contact['name'] : join(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']));
520
521         $fn = trim($fn, ', ');
522
523         // fallback to display name
524         if (empty($fn) && $contact['name'])
525             $fn = $contact['name'];
526
527         // fallback to email address
ad003c 528         if (empty($fn) && ($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
AM 529             return $email[0];
530         }
f9a967 531
T 532         return $fn;
533     }
534
83f707 535     /**
13dc9f 536      * Create a unique key for sorting contacts
TB 537      */
538     public static function compose_contact_key($contact, $sort_col)
539     {
c027ba 540         $key = $contact[$sort_col] . ':' . $contact['sourceid'];
13dc9f 541
TB 542         // add email to a key to not skip contacts with the same name (#1488375)
ad003c 543         if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
AM 544             $key .= ':' . implode(':', (array)$email);
39a82a 545         }
13dc9f 546
39a82a 547         return $key;
13dc9f 548     }
TB 549
550     /**
83f707 551      * Compare search value with contact data
AM 552      *
553      * @param string       $colname Data name
554      * @param string|array $value   Data value
555      * @param string       $search  Search value
556      * @param int          $mode    Search mode
557      *
558      * @return bool Comparision result
559      */
560     protected function compare_search_value($colname, $value, $search, $mode)
561     {
562         // The value is a date string, for date we'll
563         // use only strict comparison (mode = 1)
564         // @TODO: partial search, e.g. match only day and month
b5767d 565         if (in_array($colname, $this->date_cols)) {
52830e 566             return (($value = rcube_utils::anytodatetime($value))
TB 567                 && ($search = rcube_utils::anytodatetime($search))
568                 && $value->format('Ymd') == $search->format('Ymd'));
83f707 569         }
AM 570
571         // composite field, e.g. address
572         foreach ((array)$value as $val) {
573             $val = mb_strtolower($val);
574             switch ($mode) {
575             case 1:
576                 $got = ($val == $search);
577                 break;
578
579             case 2:
580                 $got = ($search == substr($val, 0, strlen($search)));
581                 break;
582
583             default:
584                 $got = (strpos($val, $search) !== false);
585             }
586
587             if ($got) {
588                 return true;
589             }
590         }
591
592         return false;
593     }
594
cc97ea 595 }
b393e5 596