Takika
2013-10-21 03713dbf5d45ee978b07586c4af1f924b204b5f1
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();
49b8e5 212         $valid = true;
0c2596 213
07b95d 214         // check validity of email addresses
T 215         foreach ($this->get_col_values('email', $save_data, true) as $email) {
216             if (strlen($email)) {
1aceb9 217                 if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) {
996af3 218                     $error = $rcube->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email)));
0c2596 219                     $this->set_error(self::ERROR_VALIDATE, $error);
49b8e5 220                     $valid = false;
TB 221                     break;
07b95d 222                 }
T 223             }
224         }
225
49b8e5 226         // allow plugins to do contact validation and auto-fixing
TB 227         $plugin = $rcube->plugins->exec_hook('contact_validate', array(
228             'record'  => $save_data,
229             'autofix' => $autofix,
230             'valid'   => $valid,
231         ));
232
d29f78 233         if ($valid && !$plugin['valid']) {
TB 234             $this->set_error(self::ERROR_VALIDATE, $plugin['error']);
235         }
236
49b8e5 237         if (is_array($plugin['record'])) {
TB 238             $save_data = $plugin['record'];
239         }
240
241         return $plugin['valid'];
07b95d 242     }
T 243
a61bbb 244     /**
cc97ea 245      * Create a new contact record
T 246      *
247      * @param array Assoziative array with save data
0501b6 248      *  Keys:   Field name with optional section in the form FIELD:SECTION
T 249      *  Values: Field value. Can be either a string or an array of strings for multiple values
cc97ea 250      * @param boolean True to check for duplicates first
5c461b 251      * @return mixed The created record ID on success, False on error
cc97ea 252      */
T 253     function insert($save_data, $check=false)
254     {
2eb794 255         /* empty for read-only address books */
cc97ea 256     }
T 257
258     /**
0501b6 259      * Create new contact records for every item in the record set
T 260      *
261      * @param object rcube_result_set Recordset to insert
262      * @param boolean True to check for duplicates first
263      * @return array List of created record IDs
264      */
265     function insertMultiple($recset, $check=false)
266     {
267         $ids = array();
268         if (is_object($recset) && is_a($recset, rcube_result_set)) {
269             while ($row = $recset->next()) {
270                 if ($insert = $this->insert($row, $check))
271                     $ids[] = $insert;
272             }
273         }
274         return $ids;
275     }
276
277     /**
cc97ea 278      * Update a specific contact record
T 279      *
280      * @param mixed Record identifier
281      * @param array Assoziative array with save data
0501b6 282      *  Keys:   Field name with optional section in the form FIELD:SECTION
T 283      *  Values: Field value. Can be either a string or an array of strings for multiple values
5c461b 284      * @return boolean True on success, False on error
cc97ea 285      */
T 286     function update($id, $save_cols)
287     {
2eb794 288         /* empty for read-only address books */
cc97ea 289     }
T 290
291     /**
292      * Mark one or more contact records as deleted
293      *
294      * @param array  Record identifiers
63fda8 295      * @param bool   Remove records irreversible (see self::undelete)
cc97ea 296      */
63fda8 297     function delete($ids, $force=true)
cc97ea 298     {
2eb794 299         /* empty for read-only address books */
cc97ea 300     }
T 301
302     /**
7f5a84 303      * Unmark delete flag on contact record(s)
A 304      *
305      * @param array  Record identifiers
306      */
307     function undelete($ids)
308     {
309         /* empty for read-only address books */
310     }
311
312     /**
313      * Mark all records in database as deleted
cc97ea 314      */
T 315     function delete_all()
316     {
2eb794 317         /* empty for read-only address books */
cc97ea 318     }
T 319
04adaa 320     /**
b393e5 321      * Setter for the current group
A 322      * (empty, has to be re-implemented by extending class)
323      */
324     function set_group($gid) { }
325
326     /**
327      * List all active contact groups of this source
328      *
0501b6 329      * @param string  Optional search string to match group name
ec4331 330      * @param int     Matching mode:
AM 331      *                0 - partial (*abc*),
332      *                1 - strict (=),
333      *                2 - prefix (abc*)
334      *
b393e5 335      * @return array  Indexed list of contact groups, each a hash array
A 336      */
ec4331 337     function list_groups($search = null, $mode = 0)
b393e5 338     {
A 339         /* empty for address books don't supporting groups */
340         return array();
341     }
342
343     /**
dc6c4f 344      * Get group properties such as name and email address(es)
T 345      *
346      * @param string Group identifier
347      * @return array Group properties as hash array
348      */
349     function get_group($group_id)
350     {
351         /* empty for address books don't supporting groups */
352         return null;
353     }
354
355     /**
04adaa 356      * Create a contact group with the given name
T 357      *
358      * @param string The group name
5c461b 359      * @return mixed False on error, array with record props in success
04adaa 360      */
T 361     function create_group($name)
362     {
2eb794 363         /* empty for address books don't supporting groups */
A 364         return false;
04adaa 365     }
b393e5 366
04adaa 367     /**
T 368      * Delete the given group and all linked group members
369      *
370      * @param string Group identifier
371      * @return boolean True on success, false if no data was changed
372      */
373     function delete_group($gid)
374     {
2eb794 375         /* empty for address books don't supporting groups */
A 376         return false;
04adaa 377     }
b393e5 378
04adaa 379     /**
T 380      * Rename a specific contact group
381      *
382      * @param string Group identifier
383      * @param string New name to set for this group
360bd3 384      * @param string New group identifier (if changed, otherwise don't set)
04adaa 385      * @return boolean New name on success, false if no data was changed
T 386      */
360bd3 387     function rename_group($gid, $newname, &$newid)
04adaa 388     {
2eb794 389         /* empty for address books don't supporting groups */
A 390         return false;
04adaa 391     }
b393e5 392
04adaa 393     /**
T 394      * Add the given contact records the a certain group
395      *
40d419 396      * @param string       Group identifier
AM 397      * @param array|string List of contact identifiers to be added
398      *
399      * @return int Number of contacts added
04adaa 400      */
T 401     function add_to_group($group_id, $ids)
402     {
2eb794 403         /* empty for address books don't supporting groups */
A 404         return 0;
04adaa 405     }
b393e5 406
04adaa 407     /**
T 408      * Remove the given contact records from a certain group
409      *
40d419 410      * @param string       Group identifier
AM 411      * @param array|string List of contact identifiers to be removed
412      *
413      * @return int Number of deleted group members
04adaa 414      */
T 415     function remove_from_group($group_id, $ids)
416     {
2eb794 417         /* empty for address books don't supporting groups */
A 418         return 0;
04adaa 419     }
b393e5 420
A 421     /**
422      * Get group assignments of a specific contact record
423      *
424      * @param mixed Record identifier
425      *
426      * @return array List of assigned groups as ID=>Name pairs
427      * @since 0.5-beta
428      */
429     function get_record_groups($id)
430     {
431         /* empty for address books don't supporting groups */
432         return array();
433     }
0501b6 434
T 435     /**
436      * Utility function to return all values of a certain data column
437      * either as flat list or grouped by subtype
438      *
439      * @param string Col name
440      * @param array  Record data array as used for saving
441      * @param boolean True to return one array with all values, False for hash array with values grouped by type
442      * @return array List of column values
443      */
ad003c 444     public static function get_col_values($col, $data, $flat = false)
0501b6 445     {
T 446         $out = array();
88fb56 447         foreach ((array)$data as $c => $values) {
bed577 448             if ($c === $col || strpos($c, $col.':') === 0) {
0501b6 449                 if ($flat) {
T 450                     $out = array_merge($out, (array)$values);
451                 }
452                 else {
3725cf 453                     list(, $type) = explode(':', $c);
0501b6 454                     $out[$type] = array_merge((array)$out[$type], (array)$values);
T 455                 }
456             }
457         }
cc90ed 458
f368b0 459         // remove duplicates
AM 460         if ($flat && !empty($out)) {
461             $out = array_unique($out);
462         }
463
0501b6 464         return $out;
T 465     }
3e2637 466
T 467     /**
468      * Normalize the given string for fulltext search.
469      * Currently only optimized for Latin-1 characters; to be extended
470      *
471      * @param string Input string (UTF-8)
472      * @return string Normalized string
ceb5b5 473      * @deprecated since 0.9-beta
3e2637 474      */
T 475     protected static function normalize_string($str)
476     {
062963 477         return rcube_utils::normalize_string($str);
3e2637 478     }
e84818 479
T 480     /**
481      * Compose a valid display name from the given structured contact data
482      *
483      * @param array  Hash array with contact data as key-value pairs
f9a967 484      * @param bool   Don't attempt to extract components from the email address
71e8cc 485      *
e84818 486      * @return string Display name
T 487      */
f9a967 488     public static function compose_display_name($contact, $full_email = false)
e84818 489     {
bfc307 490         $contact = rcube::get_instance()->plugins->exec_hook('contact_displayname', $contact);
e84818 491         $fn = $contact['name'];
T 492
f9a967 493         if (!$fn)  // default display name composition according to vcard standard
6898b4 494             $fn = trim(join(' ', array_filter(array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']))));
e84818 495
T 496         // use email address part for name
ad003c 497         $email = self::get_col_values('email', $contact, true);
AM 498         $email = $email[0];
71e8cc 499
e84818 500         if ($email && (empty($fn) || $fn == $email)) {
f9a967 501             // return full email
T 502             if ($full_email)
71e8cc 503                 return $email;
A 504
e84818 505             list($emailname) = explode('@', $email);
T 506             if (preg_match('/(.*)[\.\-\_](.*)/', $emailname, $match))
507                 $fn = trim(ucfirst($match[1]).' '.ucfirst($match[2]));
508             else
509                 $fn = ucfirst($emailname);
510         }
511
512         return $fn;
513     }
514
f9a967 515     /**
T 516      * Compose the name to display in the contacts list for the given contact record.
517      * This respects the settings parameter how to list conacts.
518      *
519      * @param array  Hash array with contact data as key-value pairs
520      * @return string List name
521      */
522     public static function compose_list_name($contact)
523     {
524         static $compose_mode;
525
526         if (!isset($compose_mode))  // cache this
bfc307 527             $compose_mode = rcube::get_instance()->config->get('addressbook_name_listing', 0);
f9a967 528
T 529         if ($compose_mode == 3)
530             $fn = join(' ', array($contact['surname'] . ',', $contact['firstname'], $contact['middlename']));
531         else if ($compose_mode == 2)
532             $fn = join(' ', array($contact['surname'], $contact['firstname'], $contact['middlename']));
533         else if ($compose_mode == 1)
534             $fn = join(' ', array($contact['firstname'], $contact['middlename'], $contact['surname']));
03713d 535         else if ($compose_mode == 0)
f9a967 536             $fn = !empty($contact['name']) ? $contact['name'] : join(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']));
03713d 537         else {
T 538             $plugin = rcube::get_instance()->plugins->exec_hook('contact_listname', array('contact' => $contact));
539             $fn     = $plugin['fn'];
540         }
f9a967 541
T 542         $fn = trim($fn, ', ');
543
544         // fallback to display name
545         if (empty($fn) && $contact['name'])
546             $fn = $contact['name'];
547
548         // fallback to email address
ad003c 549         if (empty($fn) && ($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
AM 550             return $email[0];
551         }
f9a967 552
T 553         return $fn;
554     }
555
83f707 556     /**
13dc9f 557      * Create a unique key for sorting contacts
TB 558      */
559     public static function compose_contact_key($contact, $sort_col)
560     {
c027ba 561         $key = $contact[$sort_col] . ':' . $contact['sourceid'];
13dc9f 562
TB 563         // add email to a key to not skip contacts with the same name (#1488375)
ad003c 564         if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
AM 565             $key .= ':' . implode(':', (array)$email);
39a82a 566         }
13dc9f 567
39a82a 568         return $key;
13dc9f 569     }
TB 570
571     /**
83f707 572      * Compare search value with contact data
AM 573      *
574      * @param string       $colname Data name
575      * @param string|array $value   Data value
576      * @param string       $search  Search value
577      * @param int          $mode    Search mode
578      *
579      * @return bool Comparision result
580      */
581     protected function compare_search_value($colname, $value, $search, $mode)
582     {
583         // The value is a date string, for date we'll
584         // use only strict comparison (mode = 1)
585         // @TODO: partial search, e.g. match only day and month
b5767d 586         if (in_array($colname, $this->date_cols)) {
52830e 587             return (($value = rcube_utils::anytodatetime($value))
TB 588                 && ($search = rcube_utils::anytodatetime($search))
589                 && $value->format('Ymd') == $search->format('Ymd'));
83f707 590         }
AM 591
592         // composite field, e.g. address
593         foreach ((array)$value as $val) {
594             $val = mb_strtolower($val);
595             switch ($mode) {
596             case 1:
597                 $got = ($val == $search);
598                 break;
599
600             case 2:
601                 $got = ($search == substr($val, 0, strlen($search)));
602                 break;
603
604             default:
605                 $got = (strpos($val, $search) !== false);
606             }
607
608             if ($got) {
609                 return true;
610             }
611         }
612
613         return false;
614     }
615
cc97ea 616 }
b393e5 617