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