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 }