source: subversion/trunk/plugins/kolab_addressbook/rcube_kolab_contacts.php @ 4269

Last change on this file since 4269 was 4269, checked in by thomasb, 2 years ago

Complete implementation of rcube_kolab_contacts; add localization texts for proprietary contact fields

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id Date Author Revision
File size: 25.2 KB
Line 
1<?php
2
3
4/**
5 * Backend class for a custom address book
6 *
7 * This part of the Roundcube+Kolab integration and connects the
8 * rcube_addressbook interface with the rcube_kolab wrapper for Kolab_Storage
9 *
10 * @author Thomas Bruederli
11 * @see rcube_addressbook
12 */
13class rcube_kolab_contacts extends rcube_addressbook
14{
15    public $primary_key = 'ID';
16    public $readonly = false;
17    public $groups = true;
18    public $coltypes = array(
19      'name'         => array('limit' => 1),
20      'firstname'    => array('limit' => 1),
21      'surname'      => array('limit' => 1),
22      'middlename'   => array('limit' => 1),
23      'prefix'       => array('limit' => 1),
24      'suffix'       => array('limit' => 1),
25      'nickname'     => array('limit' => 1),
26      'jobtitle'     => array('limit' => 1),
27      'organization' => array('limit' => 1),
28      'department'   => array('limit' => 1),
29      'gender'       => array('limit' => 1),
30      'initials'     => array('type' => 'text', 'size' => 6, 'limit' => 1, 'label' => 'kolab_addressbook.initials'),
31      'email'        => array('subtypes' => null),
32      'phone'        => array(),
33      'im'           => array('limit' => 1),
34      'website'      => array('limit' => 1, 'subtypes' => null),
35      'address'      => array('limit' => 2, 'subtypes' => array('home','business')),
36      'birthday'     => array('limit' => 1),
37      'anniversary'  => array('type' => 'date', 'size' => 12, 'limit' => 1, 'label' => 'kolab_addressbook.anniversary'),
38      // TODO: define more Kolab-specific fields such as: office-location, profession, manager-name, assistant, spouse-name, children, language, latitude, longitude, pgp-publickey, free-busy-url
39      'notes'        => array(),
40    );
41   
42    private $gid;
43    private $imap;
44    private $kolab;
45    private $folder;
46    private $contactstorage;
47    private $liststorage;
48    private $contacts;
49    private $distlists;
50    private $groupmembers;
51    private $id2uid;
52    private $filter;
53    private $result;
54    private $imap_folder = 'INBOX/Contacts';
55    private $gender_map = array(0 => 'male', 1 => 'female');
56    private $phonetypemap = array('home' => 'home1', 'work' => 'business1', 'work2' => 'business2', 'workfax' => 'businessfax');
57    private $addresstypemap = array('work' => 'business');
58    private $fieldmap = array(
59      // kolab       => roundcube
60      'full-name'    => 'name',
61      'given-name'   => 'firstname',
62      'middle-names' => 'middlename',
63      'last-name'    => 'surname',
64      'prefix'       => 'prefix',
65      'suffix'       => 'suffix',
66      'nick-name'    => 'nickname',
67      'organization' => 'organization',
68      'department'   => 'department',
69      'job-title'    => 'jobtitle',
70      'initials'     => 'initials',
71      'birthday'     => 'birthday',
72      'anniversary'  => 'anniversary',
73      'im-address'   => 'im:aim',
74      'web-page'     => 'website',
75      'body'         => 'notes',
76    );
77
78
79    public function __construct($imap_folder = null)
80    {
81        if ($imap_folder)
82            $this->imap_folder = $imap_folder;
83           
84        // extend coltypes configuration
85        $format = rcube_kolab::get_format('contact');
86        $this->coltypes['phone']['subtypes'] = $format->_phone_types;
87        $this->coltypes['address']['subtypes'] = $format->_address_types;
88       
89        // set localized labels for proprietary cols
90        foreach ($this->coltypes as $col => $prop) {
91            if (is_string($prop['label']))
92                $this->coltypes[$col]['label'] = rcube_label($prop['label']);
93        }
94       
95        // fetch objects from the given IMAP folder
96        $this->contactstorage = rcube_kolab::get_storage($this->imap_folder);
97        $this->liststorage = rcube_kolab::get_storage($this->imap_folder, 'distributionlist');
98
99        $this->ready = !PEAR::isError($this->contactstorage) && !PEAR::isError($this->liststorage);
100    }
101
102
103    /**
104     * Getter for the address book name to be displayed
105     *
106     * @return string Name of this address book
107     */
108    public function get_name()
109    {
110        return strtr(preg_replace('!^(INBOX|user)/!i', '', $this->imap_folder), '/', ':');
111    }
112
113
114    /**
115     * Setter for the current group
116     */
117    public function set_group($gid)
118    {
119        $this->gid = $gid;
120    }
121
122
123    /**
124     * Save a search string for future listings
125     *
126     * @param mixed Search params to use in listing method, obtained by get_search_set()
127     */
128    public function set_search_set($filter)
129    {
130        $this->filter = $filter;
131    }
132
133
134    /**
135     * Getter for saved search properties
136     *
137     * @return mixed Search properties used by this class
138     */
139    public function get_search_set()
140    {
141        return $this->filter;
142    }
143
144
145    /**
146     * Reset saved results and search parameters
147     */
148    public function reset()
149    {
150        $this->result = null;
151        $this->filter = null;
152    }
153
154
155    /**
156     * List all active contact groups of this source
157     *
158     * @return array  Indexed list of contact groups, each a hash array
159     */
160    function list_groups($search = null)
161    {
162        $this->_fetch_groups();
163        $groups = array();
164        foreach ((array)$this->distlists as $group)
165            $groups[] = array('ID' => $group['ID'], 'name' => $group['last-name']);
166        return $groups;
167    }
168
169    /**
170     * List the current set of contact records
171     *
172     * @param  array  List of cols to show
173     * @param  int    Only return this number of records, use negative values for tail
174     * @return array  Indexed list of contact records, each a hash array
175     */
176    public function list_records($cols=null, $subset=0)
177    {
178        $this->result = $this->count();
179       
180        // list member of the selected group
181        if ($this->gid) {
182            $seen = array();
183            $this->result->count = 0;
184            foreach ((array)$this->distlists[$this->gid]['member'] as $member) {
185                if ($this->contacts[$member['ID']] && !$seen[$member['ID']]++) {
186                    $this->result->add($this->contacts[$member['ID']]);
187                    $this->result->count++;
188                }
189            }
190        }
191        else {
192            $i = $j = 0;
193            foreach ($this->contacts as $id => $contact) {
194                if ($i++ < $this->result->first)
195                    continue;
196                $this->result->add($contact);
197                if (++$j == $this->page_size)
198                    break;
199            }
200        }
201       
202        return $this->result;
203    }
204
205
206    /**
207     * Search records
208     *
209     * @param array   List of fields to search in
210     * @param string  Search value
211     * @param boolean True if results are requested, False if count only
212     * @return Indexed list of contact records and 'count' value
213     */
214    public function search($fields, $value, $strict=false, $select=true)
215    {
216        // search by ID
217        if ($fields == $this->primary_key) {
218            return $this->get_record($value);
219        }
220       
221        // TODO: currently not implemented
222        return new rcube_result_set(0, ($this->list_page-1) * $this->page_size);
223    }
224
225
226    /**
227     * Count number of available contacts in database
228     *
229     * @return rcube_result_set Result set with values for 'count' and 'first'
230     */
231    public function count()
232    {
233        $this->_fetch_contacts();
234        $this->_fetch_groups();
235        $count = $this->gid ? count($this->distlists[$this->gid]['member']) : count($this->contacts);
236        return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
237    }
238
239
240    /**
241     * Return the last result set
242     *
243     * @return rcube_result_set Current result set or NULL if nothing selected yet
244     */
245    public function get_result()
246    {
247        return $this->result;
248    }
249
250    /**
251     * Get a specific contact record
252     *
253     * @param mixed record identifier(s)
254     * @param boolean True to return record as associative array, otherwise a result set is returned
255     * @return mixed Result object with all record fields or False if not found
256     */
257    public function get_record($id, $assoc=false)
258    {
259        $this->_fetch_contacts();
260        if ($this->contacts[$id]) {
261            $this->result = new rcube_result_set(1);
262            $this->result->add($this->contacts[$id]);
263            return $assoc ? $this->contacts[$id] : $this->result;
264        }
265
266        return false;
267    }
268
269
270    /**
271     * Get group assignments of a specific contact record
272     *
273     * @param mixed Record identifier
274     * @return array List of assigned groups as ID=>Name pairs
275     */
276    public function get_record_groups($id)
277    {
278        $out = array();
279        $this->_fetch_groups();
280       
281        foreach ((array)$this->groupmembers[$id] as $gid) {
282            if ($group = $this->distlists[$gid])
283                $out[$gid] = $group['last-name'];
284        }
285       
286        return $out;
287    }
288
289
290    /**
291     * Create a new contact record
292     *
293     * @param array Assoziative array with save data
294     *  Keys:   Field name with optional section in the form FIELD:SECTION
295     *  Values: Field value. Can be either a string or an array of strings for multiple values
296     * @param boolean True to check for duplicates first
297     * @return mixed The created record ID on success, False on error
298     */
299    public function insert($save_data, $check=false)
300    {
301        if (!is_array($save_data))
302            return false;
303
304        $insert_id = $existing = false;
305
306        // check for existing records by e-mail comparison
307        if ($check) {
308            foreach ($this->get_col_values('email', $save_data, true) as $email) {
309                if (($res = $this->search('email', $email, true, false)) && $res->count) {
310                    $existing = true;
311                    break;
312                }
313            }
314        }
315       
316        if (!$existing) {
317            // generate new Kolab contact item
318            $object = $this->_from_rcube_contact($save_data);
319            $object['uid'] = $this->contactstorage->generateUID();
320
321            $saved = $this->contactstorage->save($object);
322
323            if (PEAR::isError($saved)) {
324                raise_error(array(
325                  'code' => 600, 'type' => 'php',
326                  'file' => __FILE__, 'line' => __LINE__,
327                  'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
328                true, false);
329            }
330            else {
331                $contact = $this->_to_rcube_contact($object);
332                $id = $contact['ID'];
333                $this->contacts[$id] = $contact;
334                $this->id2uid[$id] = $object['uid'];
335                $insert_id = $id;
336            }
337        }
338       
339        return $insert_id;
340    }
341
342
343    /**
344     * Update a specific contact record
345     *
346     * @param mixed Record identifier
347     * @param array Assoziative array with save data
348     *  Keys:   Field name with optional section in the form FIELD:SECTION
349     *  Values: Field value. Can be either a string or an array of strings for multiple values
350     * @return boolean True on success, False on error
351     */
352    public function update($id, $save_data)
353    {
354        $updated = false;
355        $this->_fetch_contacts();
356        if ($this->contacts[$id] && ($uid = $this->id2uid[$id])) {
357            $old = $this->contactstorage->getObject($uid);
358            $object = array_merge($old, $this->_from_rcube_contact($save_data));
359
360            $saved = $this->contactstorage->save($object, $uid);
361            if (PEAR::isError($saved)) {
362                raise_error(array(
363                  'code' => 600, 'type' => 'php',
364                  'file' => __FILE__, 'line' => __LINE__,
365                  'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
366                true, false);
367            }
368            else {
369                $this->contacts[$id] = $this->_to_rcube_contact($object);
370                $updated = true;
371            }
372        }
373       
374        return $updated;
375    }
376
377    /**
378     * Mark one or more contact records as deleted
379     *
380     * @param array  Record identifiers
381     */
382    public function delete($ids)
383    {
384        $this->_fetch_contacts();
385        $this->_fetch_groups();
386       
387        if (!is_array($ids))
388            $ids = explode(',', $ids);
389
390        $count = 0;
391        foreach ($ids as $id) {
392            if ($uid = $this->id2uid[$id]) {
393                $deleted = $this->contactstorage->delete($uid);
394
395                if (PEAR::isError($deleted)) {
396                    raise_error(array(
397                      'code' => 600, 'type' => 'php',
398                      'file' => __FILE__, 'line' => __LINE__,
399                      'message' => "Error deleting a contact object from the Kolab server:" . $deleted->getMessage()),
400                    true, false);
401                }
402                else {
403                    // remove from distribution lists
404                    foreach ((array)$this->groupmembers[$id] as $gid)
405                        $this->remove_from_group($gid, $id);
406                   
407                    // clear internal cache
408                    unset($this->contacts[$id], $this->id2uid[$id], $this->groupmembers[$id]);
409                    $count++;
410                }
411            }
412        }
413       
414        return $count;
415    }
416
417    /**
418     * Remove all records from the database
419     */
420    public function delete_all()
421    {
422        if (!PEAR::isError($this->contactstorage->deleteAll())) {
423            $this->contacts = array();
424            $this->id2uid = array();
425            $this->result = null;
426        }
427    }
428
429   
430    /**
431     * Close connection to source
432     * Called on script shutdown
433     */
434    public function close()
435    {
436        rcube_kolab::shutdown();
437    }
438
439
440    /**
441     * Create a contact group with the given name
442     *
443     * @param string The group name
444     * @return mixed False on error, array with record props in success
445     */
446    function create_group($name)
447    {
448        $this->_fetch_groups();
449        $result = false;
450       
451        $list = array(
452            'uid' => $this->liststorage->generateUID(),
453            'last-name' => $name,
454            'member' => array(),
455        );
456        $saved = $this->liststorage->save($list);
457
458        if (PEAR::isError($saved)) {
459            raise_error(array(
460              'code' => 600, 'type' => 'php',
461              'file' => __FILE__, 'line' => __LINE__,
462              'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
463            true, false);
464            return false;
465        }
466        else {
467            $id = md5($list['uid']);
468            $this->distlists[$record['ID']] = $list;
469            $result = array('id' => $id, 'name' => $name);
470        }
471
472        return $result;
473    }
474
475    /**
476     * Delete the given group and all linked group members
477     *
478     * @param string Group identifier
479     * @return boolean True on success, false if no data was changed
480     */
481    function delete_group($gid)
482    {
483        $this->_fetch_groups();
484        $result = false;
485       
486        if ($list = $this->distlists[$gid])
487            $deleted = $this->liststorage->delete($list['uid']);
488
489        if (PEAR::isError($deleted)) {
490            raise_error(array(
491              'code' => 600, 'type' => 'php',
492              'file' => __FILE__, 'line' => __LINE__,
493              'message' => "Error deleting distribution-list object from the Kolab server:" . $deleted->getMessage()),
494            true, false);
495        }
496        else
497            $result = true;
498       
499        return $result;
500    }
501
502    /**
503     * Rename a specific contact group
504     *
505     * @param string Group identifier
506     * @param string New name to set for this group
507     * @return boolean New name on success, false if no data was changed
508     */
509    function rename_group($gid, $newname)
510    {
511        $this->_fetch_groups();
512        $list = $this->distlists[$gid];
513       
514        if ($newname != $list['last-name']) {
515            $list['last-name'] = $newname;
516            $saved = $this->liststorage->save($list, $list['uid']);
517        }
518
519        if (PEAR::isError($saved)) {
520            raise_error(array(
521              'code' => 600, 'type' => 'php',
522              'file' => __FILE__, 'line' => __LINE__,
523              'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
524            true, false);
525            return false;
526        }
527
528        return $newname;
529    }
530
531    /**
532     * Add the given contact records the a certain group
533     *
534     * @param string  Group identifier
535     * @param array   List of contact identifiers to be added
536     * @return int    Number of contacts added
537     */
538    function add_to_group($gid, $ids)
539    {
540        if (!is_array($ids))
541            $ids = explode(',', $ids);
542
543        $added = 0;
544        $exists = array();
545       
546        $this->_fetch_groups();
547        $this->_fetch_contacts();
548        $list = $this->distlists[$gid];
549
550        foreach ((array)$list['member'] as $i => $member)
551            $exists[] = $member['ID'];
552       
553        // substract existing assignments from list
554        $ids = array_diff($ids, $exists);
555
556        foreach ($ids as $contact_id) {
557            if ($uid = $this->id2uid[$contact_id]) {
558                $contact = $this->contacts[$contact_id];
559                foreach ($this->get_col_values('email', $contact, true) as $email) {
560                    $list['member'][] = array(
561                        'uid' => $uid,
562                        'display-name' => $contact['name'],
563                        'smtp-address' => $email,
564                    );
565                }
566                $this->groupmembers[$contact_id][] = $gid;
567                $added++;
568            }
569        }
570       
571        if ($added)
572            $saved = $this->liststorage->save($list, $list['uid']);
573       
574        if (PEAR::isError($saved)) {
575            raise_error(array(
576              'code' => 600, 'type' => 'php',
577              'file' => __FILE__, 'line' => __LINE__,
578              'message' => "Error saving distribution-list to Kolab server:" . $saved->getMessage()),
579            true, false);
580            $added = false;
581        }
582        else {
583            $this->distlists[$gid] = $list;
584        }
585       
586        return $added;
587    }
588
589    /**
590     * Remove the given contact records from a certain group
591     *
592     * @param string  Group identifier
593     * @param array   List of contact identifiers to be removed
594     * @return int    Number of deleted group members
595     */
596    function remove_from_group($gid, $ids)
597    {
598        if (!is_array($ids))
599            $ids = explode(',', $ids);
600       
601        $this->_fetch_groups();
602        if (!($list = $this->distlists[$gid]))
603            return false;
604
605        $new_member = array();
606        foreach ((array)$list['member'] as $member) {
607            if (!in_array($member['ID'], $ids))
608                $new_member[] = $member;
609        }
610
611        // write distribution list back to server
612        $list['member'] = $new_member;
613        $saved = $this->liststorage->save($list, $list['uid']);
614       
615        if (PEAR::isError($saved)) {
616            raise_error(array(
617              'code' => 600, 'type' => 'php',
618              'file' => __FILE__, 'line' => __LINE__,
619              'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
620            true, false);
621        }
622        else {
623            // remove group assigments in local cache
624            foreach ($ids as $id) {
625                $j = array_search($gid, $this->groupmembers[$id]);
626                unset($this->groupmembers[$id][$j]);
627            }
628            $this->distlists[$gid] = $list;
629            return true;
630        }
631
632        return false;
633    }
634
635
636    /**
637     * Simply fetch all records and store them in private member vars
638     */
639    private function _fetch_contacts()
640    {
641        if (!isset($this->contacts)) {
642            // read contacts
643            $this->contacts = $this->id2uid = array();
644            foreach ((array)$this->contactstorage->getObjects() as $record) {
645                $contact = $this->_to_rcube_contact($record);
646                $id = $contact['ID'];
647                $this->contacts[$id] = $contact;
648                $this->id2uid[$id] = $record['uid'];
649            }
650
651            // TODO: sort data arrays according to desired list sorting
652        }
653    }
654   
655   
656    /**
657     * Read distribution-lists AKA groups from server
658     */
659    private function _fetch_groups()
660    {
661        if (!isset($this->distlists)) {
662            $this->distlists = $this->groupmembers = array();
663            foreach ((array)$this->liststorage->getObjects() as $record) {
664                // FIXME: folders without any distribution-list objects return contacts instead ?!
665                if ($record['__type'] != 'Group')
666                    continue;
667                $record['ID'] = md5($record['uid']);
668                foreach ((array)$record['member'] as $i => $member) {
669                    $mid = md5($member['uid']);
670                    $record['member'][$i]['ID'] = $mid;
671                    $this->groupmembers[$mid][] = $record['ID'];
672                }
673                $this->distlists[$record['ID']] = $record;
674            }
675        }
676    }
677   
678   
679    /**
680     * Map fields from internal Kolab_Format to Roundcube contact format
681     */
682    private function _to_rcube_contact($record)
683    {
684        $out = array(
685          'ID' => md5($record['uid']),
686          'email' => array(),
687          'phone' => array(),
688        );
689       
690        foreach ($this->fieldmap as $kolab => $rcube) {
691          if (strlen($record[$kolab]))
692            $out[$rcube] = $record[$kolab];
693        }
694       
695        if (isset($record['gender']))
696            $out['gender'] = $this->gender_map[$record['gender']];
697
698        foreach ((array)$record['email'] as $i => $email)
699            $out['email'][] = $email['smtp-address'];
700           
701        if (!$record['email'] && $record['emails'])
702            $out['email'] = preg_split('/,\s*/', $record['emails']);
703
704        foreach ((array)$record['phone'] as $i => $phone)
705            $out['phone:'.$phone['type']][] = $phone['number'];
706
707        if (is_array($record['address'])) {
708            foreach ($record['address'] as $i => $adr) {
709                $key = 'address:' . $adr['type'];
710                $out[$key][] = array(
711                    'street' => $adr['street'],
712                    'locality' => $adr['locality'],
713                    'zipcode' => $adr['postal-code'],
714                    'region' => $adr['region'],
715                    'country' => $adr['country'],
716                );
717            }
718        }
719
720        // remove empty fields
721        return array_filter($out);
722    }
723
724    private function _from_rcube_contact($contact)
725    {
726        $object = array();
727
728        foreach (array_flip($this->fieldmap) as $rcube => $kolab) {
729            if (isset($contact[$rcube]))
730                $object[$kolab] = is_array($contact[$rcube]) ? $contact[$rcube][0] : $contact[$rcube];
731            else if ($rcube .= ':home' && isset($contact[$rcube]))
732                $object[$kolab] = is_array($contact[$rcube]) ? $contact[$rcube][0] : $contact[$rcube];
733        }
734
735        // format dates
736        if ($object['birthday'] && ($date = @strtotime($object['birthday'])))
737            $object['birthday'] = date('Y-m-d', $date);
738        if ($object['anniversary'] && ($date = @strtotime($object['anniversary'])))
739            $object['anniversary'] = date('Y-m-d', $date);
740
741        $gendermap = array_flip($this->gender_map);
742        if (isset($contact['gender']))
743            $object['gender'] = $gendermap[$contact['gender']];
744
745        $emails = $this->get_col_values('email', $contact, true);
746        $object['emails'] = join(', ', $emails);
747
748        foreach ($this->get_col_values('phone', $contact) as $type => $values) {
749            if ($this->phonetypemap[$type])
750                $type = $this->phonetypemap[$type];
751            foreach ((array)$values as $phone)
752                $object['phone'][] = array('number' => $phone, 'type' => $type);
753        }
754
755        foreach ($this->get_col_values('address', $contact) as $type => $values) {
756            if ($this->addresstypemap[$type])
757                $type = $this->addresstypemap[$type];
758           
759            $basekey = 'addr-' . $type . '-';
760            foreach ((array)$values as $adr) {
761                // switch type if slot is already taken
762                if (isset($object[$basekey . 'type'])) {
763                    $type = $type == 'home' ? 'business' : 'home';
764                    $basekey = 'addr-' . $type . '-';
765                }
766               
767                if (!isset($object[$basekey . 'type'])) {
768                    $object[$basekey . 'type'] = $type;
769                    $object[$basekey . 'street'] = $adr['street'];
770                    $object[$basekey . 'locality'] = $adr['locality'];
771                    $object[$basekey . 'postal-code'] = $adr['zipcode'];
772                    $object[$basekey . 'region'] = $adr['region'];
773                    $object[$basekey . 'country'] = $adr['country'];
774                }
775                else {
776                    $object['address'][] = array(
777                        'type' => $type,
778                        'street' => $adr['street'],
779                        'locality' => $adr['locality'],
780                        'postal-code' => $adr['zipcode'],
781                        'region' => $adr['region'],
782                        'country' => $adr['country'],
783                    );
784                }
785            }
786        }
787
788        return $object;
789    }
790
791}
Note: See TracBrowser for help on using the repository browser.