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