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