1<?php
2/**
3 * Copyright 2000-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file LICENSE for license information (ASL).  If you did
6 * did not receive this file, see http://www.horde.org/licenses/apache.
7 *
8 * @category  Horde
9 * @copyright 2000-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/apache ASL
11 * @package   Turba
12 */
13
14/**
15 * Provides a common abstracted interface to the various directory search
16 * drivers.  It includes functions for searching, adding, removing, and
17 * modifying directory entries.
18 *
19 * @author    Chuck Hagenbuch <chuck@horde.org>
20 * @author    Jon Parise <jon@csh.rit.edu>
21 * @category  Horde
22 * @copyright 2000-2017 Horde LLC
23 * @license   http://www.horde.org/licenses/apache ASL
24 * @package   Turba
25 */
26class Turba_Driver implements Countable
27{
28    /**
29     * The symbolic title of this source.
30     *
31     * @var string
32     */
33    public $title;
34
35    /**
36     * Hash describing the mapping between Turba attributes and
37     * driver-specific fields.
38     *
39     * @var array
40     */
41    public $map = array();
42
43    /**
44     * Hash with all tabs and their fields.
45     *
46     * @var array
47     */
48    public $tabs = array();
49
50    /**
51     * List of all fields that can be accessed in the backend (excludes
52     * composite attributes, etc.).
53     *
54     * @var array
55     */
56    public $fields = array();
57
58    /**
59     * Array of fields that must match exactly.
60     *
61     * @var array
62     */
63    public $strict = array();
64
65    /**
66     * Array of fields to search "approximately" (@see
67     * config/backends.php).
68     *
69     * @var array
70     */
71    public $approximate = array();
72
73    /**
74     * The name of a field to store contact list names in if not the default.
75     *
76     * @var string
77     */
78    public $listNameField = null;
79
80    /**
81     * The name of a field to use as an alternative to the name field if that
82     * one is empty.
83     *
84     * @var string
85     */
86    public $alternativeName = null;
87
88    /**
89     * The internal name of this source.
90     *
91     * @var string
92     */
93    protected $_name;
94
95    /**
96     * Hash holding the driver's additional parameters.
97     *
98     * @var array
99     */
100    protected $_params = array();
101
102    /**
103     * What can this backend do?
104     *
105     * @var array
106     */
107    protected $_capabilities = array();
108
109    /**
110     * Any additional options passed to Turba_Object constructors.
111     *
112     * @var array
113     */
114    protected $_objectOptions = array();
115
116    /**
117     * Number of contacts in this source.
118     *
119     * @var integer
120     */
121    protected $_count = null;
122
123    /**
124     * Hold the value for the owner of this address book.
125     *
126     * @var string
127     */
128    protected $_contact_owner = '';
129
130    /**
131     * Mapping of Turba attributes to ActiveSync fields.
132     *
133     * @var array
134     */
135    static protected $_asMap = array(
136        'name' => 'fileas',
137        'lastname' => 'lastname',
138        'firstname' => 'firstname',
139        'middlenames' => 'middlename',
140        'alias' => 'nickname',
141        'nickname' => 'nickname',
142        'namePrefix' => 'title',
143        'nameSuffix' => 'suffix',
144        'homeStreet' => 'homestreet',
145        'homeCity' => 'homecity',
146        'homeProvince' => 'homestate',
147        'homePostalCode' => 'homepostalcode',
148        'homeCountryFree' => 'homecountry',
149        'otherStreet' => 'otherstreet',
150        'otherCity' => 'othercity',
151        'otherProvince' => 'otherstate',
152        'otherPostalCode' => 'otherpostalcode',
153        'otherCountryFree' => 'othercountry',
154        'workStreet' => 'businessstreet',
155        'workCity' => 'businesscity',
156        'workProvince' => 'businessstate',
157        'workPostalCode' => 'businesspostalcode',
158        'workCountryFree' => 'businesscountry',
159        'title' => 'jobtitle',
160        'company' => 'companyname',
161        'department' => 'department',
162        'office' => 'officelocation',
163        'spouse' => 'spouse',
164        'website' => 'webpage',
165        'assistant' => 'assistantname',
166        'manager' => 'managername',
167        'yomifirstname' => 'yomifirstname',
168        'yomilastname' => 'yomilastname',
169        'imaddress' => 'imaddress',
170        'imaddress2' => 'imaddress2',
171        'imaddress3' => 'imaddress3',
172        'homePhone' => 'homephonenumber',
173        'homePhone2' => 'home2phonenumber',
174        'workPhone' => 'businessphonenumber',
175        'workPhone2' => 'business2phonenumber',
176        'fax' => 'businessfaxnumber',
177        'homeFax' => 'homefaxnumber',
178        'pager' => 'pagernumber',
179        'cellPhone' => 'mobilephonenumber',
180        'carPhone' => 'carphonenumber',
181        'assistPhone' => 'assistnamephonenumber',
182        'companyPhone' => 'companymainphone',
183        'radioPhone' => 'radiophonenumber'
184    );
185
186    /**
187     * Constructs a new Turba_Driver object.
188     *
189     * @param string $name   Source name
190     * @param array $params  Hash containing additional configuration
191     *                       parameters.
192     */
193    public function __construct($name = '', array $params = array())
194    {
195        $this->_name = $name;
196        $this->_params = $params;
197    }
198
199    /**
200     * Returns the current driver's additional parameters.
201     *
202     * @return array  Hash containing the driver's additional parameters.
203     */
204    public function getParams()
205    {
206        return $this->_params;
207    }
208
209    /**
210     * Checks if this backend has a certain capability.
211     *
212     * @param string $capability  The capability to check for.
213     *
214     * @return boolean  Supported or not.
215     */
216    public function hasCapability($capability)
217    {
218        return !empty($this->_capabilities[$capability]);
219    }
220
221    /**
222     * Returns the attributes that are blob types.
223     *
224     * @return array  List of blob attributes in the array keys.
225     */
226    public function getBlobs()
227    {
228        global $attributes;
229
230        $blobs = array();
231        foreach (array_keys($this->fields) as $attribute) {
232            if (isset($attributes[$attribute]) &&
233                $attributes[$attribute]['type'] == 'image') {
234                $blobs[$attribute] = true;
235            }
236        }
237
238        return $blobs;
239    }
240
241    /**
242     *  Returns the attributes that represent dates.
243     *
244     * @return array List of date attributes in the array keys.
245     * @since 4.2.0
246     */
247    public function getDateFields()
248    {
249        global $attributes;
250
251        $dates = array();
252        foreach (array_keys($this->fields) as $attribute) {
253            if (isset($attributes[$attribute]) &&
254                $attributes[$attribute]['type'] == 'monthdayyear') {
255                $dates[$attribute] = '0000-00-00';
256            }
257        }
258
259        return $dates;
260    }
261
262    /**
263     * Translates the keys of the first hash from the generalized Turba
264     * attributes to the driver-specific fields. The translation is based on
265     * the contents of $this->map.
266     *
267     * @param array $hash  Hash using Turba keys.
268     *
269     * @return array  Translated version of $hash.
270     */
271    public function toDriverKeys(array $hash)
272    {
273        if (!empty($hash['name']) &&
274            !empty($this->listNameField) &&
275            !empty($hash['__type']) &&
276            is_array($this->map['name']) &&
277            ($hash['__type'] == 'Group')) {
278            $hash[$this->listNameField] = $hash['name'];
279            unset($hash['name']);
280        }
281
282        // Add composite fields to $hash if at least one field part exists
283        // and the composite field will be saved to storage.
284        // Otherwise composite fields won't be computed during an import.
285        foreach ($this->map as $key => $val) {
286            if (!is_array($val) ||
287                empty($this->map[$key]['attribute']) ||
288                array_key_exists($key, $hash)) {
289                continue;
290            }
291
292            foreach ($this->map[$key]['fields'] as $mapfields) {
293                if (isset($hash[$mapfields])) {
294                    // Add composite field
295                    $hash[$key] = null;
296                    break;
297                }
298            }
299        }
300
301        $fields = array();
302        foreach ($hash as $key => $val) {
303            if (!isset($this->map[$key])) {
304                continue;
305            }
306            if (!is_array($this->map[$key])) {
307                $fields[$this->map[$key]] = $val;
308            } elseif (!empty($this->map[$key]['attribute'])) {
309                $fieldarray = array();
310                foreach ($this->map[$key]['fields'] as $mapfields) {
311                    $fieldarray[] = isset($hash[$mapfields])
312                        ? $hash[$mapfields]
313                        : '';
314                }
315                $fields[$this->map[$key]['attribute']] = Turba::formatCompositeField($this->map[$key]['format'], $fieldarray);
316            } else {
317                // If 'parse' is not specified, use 'format' and 'fields'.
318                if (!isset($this->map[$key]['parse'])) {
319                    $this->map[$key]['parse'] = array(
320                        array(
321                            'format' => $this->map[$key]['format'],
322                            'fields' => $this->map[$key]['fields']
323                        )
324                    );
325                }
326                foreach ($this->map[$key]['parse'] as $parse) {
327                    $splitval = sscanf($val, $parse['format']);
328                    $count = 0;
329                    $tmp_fields = array();
330                    foreach ($parse['fields'] as $mapfield) {
331                        if (isset($hash[$mapfield])) {
332                            // If the compositing fields are set
333                            // individually, then don't set them at all.
334                            break 2;
335                        }
336                        $tmp_fields[$this->map[$mapfield]] = $splitval[$count++];
337                    }
338                    // Exit if we found the best match.
339                    if ($splitval[$count - 1] !== null) {
340                        break;
341                    }
342                }
343                $fields = array_merge($fields, $tmp_fields);
344            }
345        }
346
347        return $fields;
348    }
349
350    /**
351     * Takes a hash of Turba key => search value and return a (possibly
352     * nested) array, using backend attribute names, that can be turned into a
353     * search by the driver. The translation is based on the contents of
354     * $this->map, and includes nested OR searches for composite fields.
355     *
356     * @param array  $criteria      Hash of criteria using Turba keys.
357     * @param string $search_type   OR search or AND search?
358     * @param array  $strict        Fields that must be matched exactly.
359     * @param boolean $match_begin  Whether to match only at beginning of
360     *                              words.
361     * @param array $custom_strict  Custom set of fields that are to matched
362     *                              exactly, but are glued using $search_type
363     *                              and 'AND' together with $strict fields.
364     *                              Allows an 'OR' search pm a custom set of
365     *                              $strict fields.
366     *
367     * @return array  An array of search criteria.
368     */
369    public function makeSearch($criteria, $search_type, array $strict,
370                               $match_begin = false, array $custom_strict = array())
371    {
372        $search = $search_terms = $subsearch = $strict_search = array();
373        $glue = $temp = '';
374        $lastChar = '\"';
375        $blobs = $this->getBlobs();
376
377        foreach ($criteria as $key => $val) {
378            if (!isset($this->map[$key])) {
379                continue;
380            }
381            if (is_array($this->map[$key])) {
382                /* Composite field, break out the search terms. */
383                $parts = explode(' ', $val);
384                if (count($parts) > 1) {
385                    /* Only parse if there was more than 1 search term and
386                     * 'AND' the cumulative subsearches. */
387                    for ($i = 0; $i < count($parts); ++$i) {
388                        $term = $parts[$i];
389                        $firstChar = substr($term, 0, 1);
390                        if ($firstChar == '"') {
391                            $temp = substr($term, 1, strlen($term) - 1);
392                            $done = false;
393                            while (!$done && $i < count($parts) - 1) {
394                                $lastChar = substr($parts[$i + 1], -1);
395                                if ($lastChar == '"') {
396                                    $temp .= ' ' . substr($parts[$i + 1], 0, -1);
397                                    $done = true;
398                                } else {
399                                    $temp .= ' ' . $parts[$i + 1];
400                                }
401                                ++$i;
402                            }
403                            $search_terms[] = $temp;
404                        } else {
405                            $search_terms[] = $term;
406                        }
407                    }
408                    $glue = 'AND';
409                } else {
410                    /* If only one search term, use original input and
411                       'OR' the searces since we're only looking for 1
412                       term in any of the composite fields. */
413                    $search_terms[0] = $val;
414                    $glue = 'OR';
415                }
416
417                foreach ($this->map[$key]['fields'] as $field) {
418                    if (!empty($blobs[$field])) {
419                        continue;
420                    }
421                    $field = $this->toDriver($field);
422                    if (!empty($strict[$field])) {
423                        /* For strict matches, use the original search
424                         * vals. */
425                        $strict_search[] = array(
426                            'field' => $field,
427                            'op' => '=',
428                            'test' => $val,
429                        );
430                    } elseif (!empty($custom_strict[$field])) {
431                        $search[] = array(
432                            'field' => $field,
433                            'op' => '=',
434                            'test' => $val,
435                        );
436                    } else {
437                        /* Create a subsearch for each individual search
438                         * term. */
439                        if (count($search_terms) > 1) {
440                            /* Build the 'OR' search for each search term
441                             * on this field. */
442                            $atomsearch = array();
443                            for ($i = 0; $i < count($search_terms); ++$i) {
444                                $atomsearch[] = array(
445                                    'field' => $field,
446                                    'op' => 'LIKE',
447                                    'test' => $search_terms[$i],
448                                    'begin' => $match_begin,
449                                    'approximate' => !empty($this->approximate[$field]),
450                                );
451                            }
452                            $atomsearch[] = array(
453                                'field' => $field,
454                                'op' => '=',
455                                'test' => '',
456                                'begin' => $match_begin,
457                                'approximate' => !empty($this->approximate[$field])
458                            );
459
460                            $subsearch[] = array('OR' => $atomsearch);
461                            unset($atomsearch);
462                            $glue = 'AND';
463                        } else {
464                            /* $parts may have more than one element, but
465                             * if they are all quoted we will only have 1
466                             * $subsearch. */
467                            $subsearch[] = array(
468                                'field' => $field,
469                                'op' => 'LIKE',
470                                'test' => $search_terms[0],
471                                'begin' => $match_begin,
472                                'approximate' => !empty($this->approximate[$field]),
473                            );
474                            $glue = 'OR';
475                        }
476                    }
477                }
478                if (count($subsearch)) {
479                    $search[] = array($glue => $subsearch);
480                }
481            } else {
482                /* Not a composite field. */
483                if (!empty($blobs[$key])) {
484                    continue;
485                }
486                if (!empty($strict[$this->map[$key]])) {
487                    $strict_search[] = array(
488                        'field' => $this->map[$key],
489                        'op' => '=',
490                        'test' => $val,
491                    );
492                } elseif (!empty($custom_strict[$this->map[$key]])) {
493                    $search[] = array(
494                        'field' => $this->map[$key],
495                        'op' => '=',
496                        'test' => $val,
497                    );
498                } else {
499                    $search[] = array(
500                        'field' => $this->map[$key],
501                        'op' => 'LIKE',
502                        'test' => $val,
503                        'begin' => $match_begin,
504                        'approximate' => !empty($this->approximate[$this->map[$key]]),
505                    );
506                }
507            }
508        }
509
510        if (count($strict_search) && count($search)) {
511            return array(
512                'AND' => array(
513                    $search_type => $strict_search,
514                    array(
515                        $search_type => $search
516                    )
517                )
518            );
519        } elseif (count($strict_search)) {
520            return array(
521                $search_type => $strict_search
522            );
523        } elseif (count($search)) {
524            return array(
525                $search_type => $search
526            );
527        }
528
529        return array();
530    }
531
532    /**
533     * Translates a single Turba attribute to the driver-specific
534     * counterpart. The translation is based on the contents of
535     * $this->map. This ignores composite fields.
536     *
537     * @param string $attribute  The Turba attribute to translate.
538     *
539     * @return string  The driver name for this attribute.
540     */
541    public function toDriver($attribute)
542    {
543        if (!isset($this->map[$attribute])) {
544            return null;
545        }
546
547        return is_array($this->map[$attribute])
548            ? $this->map[$attribute]['fields']
549            : $this->map[$attribute];
550    }
551
552    /**
553     * Translates a hash from being keyed on driver-specific fields to being
554     * keyed on the generalized Turba attributes. The translation is based on
555     * the contents of $this->map.
556     *
557     * @param array $entry  A hash using driver-specific keys.
558     *
559     * @return array  Translated version of $entry.
560     */
561    public function toTurbaKeys(array $entry)
562    {
563        $new_entry = array();
564        foreach ($this->map as $key => $val) {
565            if (!is_array($val)) {
566                $new_entry[$key] = (isset($entry[$val]) && (!empty($entry[$val]) || (is_string($entry[$val]) && strlen($entry[$val]))))
567                    ? trim($entry[$val])
568                    : null;
569            }
570        }
571
572        return $new_entry;
573    }
574
575    /**
576     * Searches the source based on the provided criteria.
577     *
578     * @todo Allow $criteria to contain the comparison operator (<, =, >,
579     *       'like') and modify the drivers accordingly.
580     *
581     * @param array $search_criteria  Hash containing the search criteria.
582     * @param string $sort_order      The requested sort order which is passed
583     *                                to Turba_List::sort().
584     * @param string $search_type     Do an AND or an OR search (defaults to
585     *                                AND).
586     * @param array $return_fields    A list of fields to return; defaults to
587     *                                all fields.
588     * @param array $custom_strict    A list of fields that must match exactly.
589     * @param boolean $match_begin    Whether to match only at beginning of
590     *                                words.
591     * @param boolean $count_only   Only return the count of matching entries,
592     *                              not the entries themselves.
593     *
594     * @return mixed Turba_List|integer  The sorted, filtered list of search
595     *                                   results or the number of matching
596     *                                   entries (if $count_only is true).
597     * @throws Turba_Exception
598     */
599    public function search(array $search_criteria, $sort_order = null,
600                           $search_type = 'AND', array $return_fields = array(),
601                           array $custom_strict = array(), $match_begin = false,
602                           $count_only = false)
603    {
604        /* Add any fields that must match exactly for this source to the
605         * $strict_fields array. */
606        $strict_fields = $custom_strict_fields = array();
607        foreach ($this->strict as $strict_field) {
608            $strict_fields[$strict_field] = true;
609        }
610
611        /* Differentiate between provided $custom_strict fields - which honor
612         * the $search_type and $strict fields which are not
613         * explicitly requested as part of this search, and as such, are not
614         * constrained by the requested $search_type. */
615        foreach ($custom_strict as $strict_field) {
616            if (isset($this->map[$strict_field])) {
617                $custom_strict_fields[$this->map[$strict_field]] = true;
618            }
619        }
620
621        /* Translate the Turba attributes to driver-specific attributes. */
622        $fields = $this->makeSearch($search_criteria, $search_type,
623                                    $strict_fields, $match_begin, $custom_strict_fields);
624
625        /* If we are not using Horde_Share, enforce the requirement that the
626         * current user must be the owner of the addressbook. */
627        if (isset($this->map['__owner'])) {
628            $fields = array(
629                'AND' => array(
630                    $fields,
631                    array(
632                        'field' => $this->toDriver('__owner'),
633                        'op' => '=',
634                        'test' => $this->getContactOwner()
635                    )
636                )
637            );
638        }
639
640        if (in_array('email', $return_fields) &&
641            !in_array('emails', $return_fields)) {
642            $return_fields[] = 'emails';
643        }
644        if (count($return_fields)) {
645            $default_fields = array('__key', '__type', '__owner', '__members', 'name');
646            if ($this->alternativeName) {
647                $default_fields[] = $this->alternativeName;
648            }
649            $return_fields_pre = array_unique(array_merge($default_fields, $return_fields));
650            $return_fields = array();
651            foreach ($return_fields_pre as $field) {
652                $result = $this->toDriver($field);
653                if (is_array($result)) {
654                    foreach ($result as $composite_field) {
655                        $composite_result = $this->toDriver($composite_field);
656                        if ($composite_result) {
657                            $return_fields[] = $composite_result;
658                        }
659                    }
660                } elseif ($result) {
661                    $return_fields[] = $result;
662                }
663            }
664        } else {
665            /* Need to force the array to be re-keyed for the (fringe) case
666             * where we might have 1 DB field mapped to 2 or more Turba
667             * fields */
668            $return_fields = array_values(
669                array_unique(array_values($this->fields)));
670        }
671
672        /* Retrieve the search results from the driver. */
673        $objects = $this->_search($fields, $return_fields, $this->toDriverKeys($this->getBlobs()), $count_only);
674        if ($count_only) {
675            return $objects;
676        }
677        return $this->_toTurbaObjects($objects, $sort_order);
678    }
679
680    /**
681     * Searches the current address book for duplicate entries.
682     *
683     * Duplicates are determined by comparing email and name or last name and
684     * first name values.
685     *
686     * @return array  A hash with the following format:
687     * <code>
688     * array('name' => array('John Doe' => Turba_List, ...), ...)
689     * </code>
690     * @throws Turba_Exception
691     */
692    public function searchDuplicates()
693    {
694        return array();
695    }
696
697    /**
698     * Takes an array of object hashes and returns a Turba_List
699     * containing the correct Turba_Objects
700     *
701     * @param array $objects     An array of object hashes (keyed to backend).
702     * @param array $sort_order  Array of hashes describing sort fields.  Each
703     *                           hash has the following fields:
704     * <pre>
705     * ascending - (boolean) Indicating sort direction.
706     * field - (string) Sort field.
707     * </pre>
708     *
709     * @return Turba_List  A list object.
710     */
711    protected function _toTurbaObjects(array $objects, array $sort_order = null)
712    {
713        $list = new Turba_List();
714
715        foreach ($objects as $object) {
716            /* Translate the driver-specific fields in the result back to the
717             * more generalized common Turba attributes using the map. */
718            $object = $this->toTurbaKeys($object);
719
720            $done = false;
721            if (!empty($object['__type']) &&
722                ucwords($object['__type']) != 'Object') {
723                $class = 'Turba_Object_' . ucwords($object['__type']);
724                if (class_exists($class)) {
725                    $list->insert(new $class($this, $object, $this->_objectOptions));
726                    $done = true;
727                }
728            }
729            if (!$done) {
730                $list->insert(new Turba_Object($this, $object, $this->_objectOptions));
731            }
732        }
733
734        $list->sort($sort_order);
735
736        /* Return the filtered (sorted) results. */
737        return $list;
738    }
739
740    /**
741     * Returns a list of birthday or anniversary hashes from this source for a
742     * certain period.
743     *
744     * @param Horde_Date $start  The start date of the valid period.
745     * @param Horde_Date $end    The end date of the valid period.
746     * @param string $category   The timeObjects category to return.
747     *
748     * @return array  A list of timeObject hashes.
749     * @throws Turba Exception
750     */
751    public function listTimeObjects(Horde_Date $start, Horde_Date $end, $category)
752    {
753        try {
754            $res = $this->getTimeObjectTurbaList($start, $end, $category);
755        } catch (Turba_Exception $e) {
756            /* Try the default implementation before returning an error */
757            $res = $this->_getTimeObjectTurbaListFallback($start, $end, $category);
758        }
759
760        $t_objects = array();
761        while ($ob = $res->next()) {
762            $t_object = $ob->getValue($category);
763            if (empty($t_object)) {
764                continue;
765            }
766
767            try {
768                $t_object = new Horde_Date($t_object);
769            } catch (Horde_Date_Exception $e) {
770                continue;
771            }
772
773            if ($t_object->compareDate($end) > 0) {
774                continue;
775            }
776
777            $t_object_end = new Horde_Date($t_object);
778            ++$t_object_end->mday;
779            $key = $ob->getValue('__key');
780
781            // Calculate the age of the time object
782            if ($start->year == $end->year ||
783                $end->year == 9999) {
784                $age = $start->year - $t_object->year;
785            } elseif ($t_object->month <= $end->month) {
786                // t_object must be in later year
787                $age = $end->year - $t_object->year;
788            } else {
789                // t_object must be in earlier year
790                $age = $start->year - $t_object->year;
791            }
792
793            $title = sprintf(_("%d. %s of %s"),
794                             $age,
795                             $GLOBALS['attributes'][$category]['label'],
796                             $ob->getValue('name'));
797
798            $t_objects[] = array(
799                'id' => $key,
800                'title' => $title,
801                'start' => sprintf('%d-%02d-%02dT00:00:00',
802                                   $t_object->year,
803                                   $t_object->month,
804                                   $t_object->mday),
805                'end' => sprintf('%d-%02d-%02dT00:00:00',
806                                 $t_object_end->year,
807                                 $t_object_end->month,
808                                 $t_object_end->mday),
809                'recurrence' => array('type' => Horde_Date_Recurrence::RECUR_YEARLY_DATE,
810                                      'interval' => 1),
811                'params' => array('source' => $this->_name, 'key' => $key),
812                'link' => Horde::url('contact.php', true)->add(array('source' => $this->_name, 'key' => $key))->setRaw(true)
813            );
814        }
815
816        return $t_objects;
817    }
818
819    /**
820     * Default implementation for obtaining a Turba_List to get TimeObjects
821     * out of.
822     *
823     * @param Horde_Date $start  The starting date.
824     * @param Horde_Date $end    The ending date.
825     * @param string $field      The address book field containing the
826     *                           timeObject information (birthday,
827     *                           anniversary).
828     *
829     * @return Turba_List  A list of objects.
830     * @throws Turba_Exception
831     */
832    public function getTimeObjectTurbaList(Horde_Date $start, Horde_Date $end, $field)
833    {
834        return $this->_getTimeObjectTurbaListFallback($start, $end, $field);
835    }
836
837    /**
838     * Default implementation for obtaining a Turba_List to get TimeObjects
839     * out of.
840     *
841     * @param Horde_Date $start  The starting date.
842     * @param Horde_Date $end    The ending date.
843     * @param string $field      The address book field containing the
844     *                           timeObject information (birthday,
845     *                           anniversary).
846     *
847     * @return Turba_List  A list of objects.
848     * @throws Turba_Exception
849     */
850    protected function _getTimeObjectTurbaListFallback(Horde_Date $start, Horde_Date $end, $field)
851    {
852        return $this->search(array(), null, 'AND', array('name', $field));
853    }
854
855    /**
856     * Retrieves a set of objects from the source.
857     *
858     * @param array $objectIds  The unique ids of the objects to retrieve.
859     *
860     * @return array  The array of retrieved objects (Turba_Objects).
861     * @throws Turba_Exception
862     * @throws Horde_Exception_NotFound
863     */
864    public function getObjects(array $objectIds)
865    {
866        $objects = $this->_read($this->map['__key'], $objectIds,
867                                $this->getContactOwner(),
868                                array_values($this->fields),
869                                $this->toDriverKeys($this->getBlobs()),
870                                $this->toDriverKeys($this->getDateFields()));
871        if (!is_array($objects)) {
872            throw new Horde_Exception_NotFound();
873        }
874
875        $results = array();
876        foreach ($objects as $object) {
877            $object = $this->toTurbaKeys($object);
878            $done = false;
879            if (!empty($object['__type']) &&
880                ucwords($object['__type']) != 'Object') {
881                $class = 'Turba_Object_' . ucwords($object['__type']);
882                if (class_exists($class)) {
883                    $results[] = new $class($this, $object, $this->_objectOptions);
884                    $done = true;
885                }
886            }
887            if (!$done) {
888                $results[] = new Turba_Object($this, $object, $this->_objectOptions);
889            }
890        }
891
892        return $results;
893    }
894
895    /**
896     * Retrieves one object from the source.
897     *
898     * @param string $objectId  The unique id of the object to retrieve.
899     *
900     * @return Turba_Object  The retrieved object.
901     * @throws Turba_Exception
902     * @throws Horde_Exception_NotFound
903     */
904    public function getObject($objectId)
905    {
906        $result = $this->getObjects(array($objectId));
907
908        if (empty($result[0])) {
909            throw new Horde_Exception_NotFound();
910        }
911
912        $result = $result[0];
913        if (!isset($this->map['__owner'])) {
914            $result->attributes['__owner'] = $this->getContactOwner();
915        }
916
917        return $result;
918    }
919
920    /**
921     * Adds a new entry to the contact source.
922     *
923     * @param array $attributes  The attributes of the new object to add.
924     *
925     * @return string  The new __key value on success.
926     * @throws Turba_Exception
927     */
928    public function add(array $attributes)
929    {
930        /* Only set __type and __owner if they are not already set. */
931        if (!isset($attributes['__type'])) {
932            $attributes['__type'] = 'Object';
933        }
934        if (isset($this->map['__owner']) && !isset($attributes['__owner'])) {
935            $attributes['__owner'] = $this->getContactOwner();
936        }
937
938        if (!isset($attributes['__uid'])) {
939            $attributes['__uid'] = $this->_makeUid();
940        }
941
942        $key = $attributes['__key'] = $this->_makeKey($this->toDriverKeys($attributes));
943        $uid = $attributes['__uid'];
944
945        /* Remember any tags, since toDriverKeys will remove them.
946           (They are not stored in the Turba backend so have no mapping). */
947        $tags = isset($attributes['__tags'])
948            ? $attributes['__tags']
949            : false;
950
951        $attributes = $this->toDriverKeys($attributes);
952
953        $this->_add($attributes, $this->toDriverKeys($this->getBlobs()), $this->toDriverKeys($this->getDateFields()));
954
955        /* Add tags. */
956        if ($tags !== false) {
957            $GLOBALS['injector']->getInstance('Turba_Tagger')->tag(
958                $uid,
959                $tags,
960                $GLOBALS['registry']->getAuth(),
961                'contact'
962            );
963        }
964
965        /* Log the creation of this item in the history log. */
966        try {
967            $GLOBALS['injector']->getInstance('Horde_History')
968                ->log('turba:' . $this->getName() . ':' . $uid,
969                      array('action' => 'add'), true);
970        } catch (Exception $e) {
971            Horde::log($e, 'ERR');
972        }
973
974        return $key;
975    }
976
977    /**
978     * Returns ability of the backend to add new contacts.
979     *
980     * @return boolean  Can backend add?
981     */
982    public function canAdd()
983    {
984        return $this->_canAdd();
985    }
986
987    /**
988     * Returns ability of the backend to add new contacts.
989     *
990     * @return boolean  Can backend add?
991     */
992    protected function _canAdd()
993    {
994        return false;
995    }
996
997    /**
998     * Deletes the specified entry from the contact source.
999     *
1000     * @param string $object_id      The ID of the object to delete.
1001     * @param  boolean $remove_tags  Remove tags if true.
1002     *
1003     * @throws Turba_Exception
1004     * @throws Horde_Exception_NotFound
1005     */
1006    public function delete($object_id, $remove_tags = true)
1007    {
1008        $object = $this->getObject($object_id);
1009
1010        if (!$object->hasPermission(Horde_Perms::DELETE)) {
1011            throw new Turba_Exception(_("Permission denied"));
1012        }
1013
1014        $this->_delete($this->toDriver('__key'), $object_id);
1015
1016        $own_contact = $GLOBALS['prefs']->getValue('own_contact');
1017        if (!empty($own_contact)) {
1018            @list(,$id) = explode(';', $own_contact);
1019            if ($id == $object_id) {
1020                $GLOBALS['prefs']->setValue('own_contact', '');
1021            }
1022        }
1023
1024        /* Log the deletion of this item in the history log. */
1025        if ($object->getValue('__uid')) {
1026            try {
1027                $GLOBALS['injector']->getInstance('Horde_History')->log($object->getGuid(),
1028                                                array('action' => 'delete'),
1029                                                true);
1030            } catch (Exception $e) {
1031                Horde::log($e, 'ERR');
1032            }
1033        }
1034
1035        /* Remove any CalDAV mappings. */
1036        try {
1037            $davStorage = $GLOBALS['injector']
1038                ->getInstance('Horde_Dav_Storage');
1039            try {
1040                $davStorage
1041                    ->deleteInternalObjectId($object_id, $this->_name);
1042            } catch (Horde_Exception $e) {
1043                Horde::log($e);
1044            }
1045        } catch (Horde_Exception $e) {
1046        }
1047
1048        /* Remove tags */
1049        if ($remove_tags) {
1050            $GLOBALS['injector']->getInstance('Turba_Tagger')
1051                ->replaceTags($object->getValue('__uid'), array(), $this->getContactOwner(), 'contact');
1052
1053            /* Might have tags disabled, hence no Content_* objects autoloadable. */
1054            try {
1055                /* Tell content we removed the object */
1056                $GLOBALS['injector']->getInstance('Content_Objects_Manager')
1057                    ->delete(array($object->getValue('__uid')), 'contact');
1058            } catch (Horde_Exception $e) {}
1059        }
1060    }
1061
1062    /**
1063     * Deletes all contacts from an address book.
1064     *
1065     * @param string $sourceName  The identifier of the address book to
1066     *                            delete.  If omitted, will clear the current
1067     *                            user's 'default' address book for this
1068     *                            source type.
1069     *
1070     * @throws Turba_Exception
1071     */
1072    public function deleteAll($sourceName = null)
1073    {
1074        if (!$this->hasCapability('delete_all')) {
1075            throw new Turba_Exception('Not supported');
1076        }
1077
1078        $ids = $this->_deleteAll($sourceName);
1079
1080        // Update Horde_History and Tagger
1081        $history = $GLOBALS['injector']->getInstance('Horde_History');
1082        try {
1083            foreach ($ids as $uid) {
1084                // This is slightly hackish, but it saves us from having to
1085                // create and save an array of Turba_Objects before we delete
1086                // them, just to be able to calculate this using
1087                // Turba_Object#getGuid
1088                $guid = 'turba:' . $this->getName() . ':' . $uid;
1089                $history->log($guid, array('action' => 'delete'), true);
1090
1091                // Remove tags.
1092                $GLOBALS['injector']->getInstance('Turba_Tagger')
1093                    ->replaceTags($uid, array(), $this->getContactOwner(), 'contact');
1094
1095                /* Tell content we removed the object */
1096               $GLOBALS['injector']->getInstance('Content_Objects_Manager')
1097                    ->delete(array($uid), 'contact');
1098            }
1099        } catch (Exception $e) {
1100            Horde::log($e, 'ERR');
1101        }
1102    }
1103
1104    /**
1105     * Deletes all contacts from an address book.
1106     *
1107     * @param string $sourceName  The source to remove all contacts from.
1108     *
1109     * @return array  An array of UIDs that have been deleted.
1110     * @throws Turba_Exception
1111     */
1112    protected function _deleteAll()
1113    {
1114        return array();
1115    }
1116
1117    /**
1118     * Modifies an existing entry in the contact source.
1119     *
1120     * @param Turba_Object $object  The object to update.
1121     *
1122     * @return string  The object id, possibly updated.
1123     * @throws Turba_Exception
1124     */
1125    public function save(Turba_Object $object)
1126    {
1127        $object_id = $this->_save($object);
1128
1129        if ($uid = $object->getValue('__uid')) {
1130            /* Update tags. */
1131            if (!is_null($tags = $object->getValue('__tags'))) {
1132                $GLOBALS['injector']->getInstance('Turba_Tagger')->replaceTags(
1133                    $uid,
1134                    $tags,
1135                    $this->getContactOwner(),
1136                    'contact'
1137                );
1138            }
1139
1140            /* Log the modification of this item in the history log. */
1141            try {
1142                $GLOBALS['injector']->getInstance('Horde_History')->log($object->getGuid(),
1143                                                array('action' => 'modify'),
1144                                                true);
1145            } catch (Exception $e) {
1146                Horde::log($e, 'ERR');
1147            }
1148        }
1149
1150        return $object_id;
1151    }
1152
1153    /**
1154     * Returns the criteria available for this source except '__key'.
1155     *
1156     * @return array  An array containing the criteria.
1157     */
1158    public function getCriteria()
1159    {
1160        return array_diff_key($this->map, array('__key' => true));
1161    }
1162
1163    /**
1164     * Returns all non-composite fields for this source. Useful for importing
1165     * and exporting data, etc.
1166     *
1167     * @return array  The field list.
1168     */
1169    public function getFields()
1170    {
1171        return array_flip($this->fields);
1172    }
1173
1174    /**
1175     * Exports a given Turba_Object as an iCalendar vCard.
1176     *
1177     * @param Turba_Object $object  Turba_Object.
1178     * @param string $version       The vcard version to produce.
1179     * @param array $fields         Hash of field names and
1180     *                              Horde_SyncMl_Property properties with the
1181     *                              requested fields.
1182     * @param boolean $skipEmpty    Whether to skip empty fields.
1183     *
1184     * @return Horde_Icalendar_Vcard  A vcard object.
1185     */
1186    public function tovCard(Turba_Object $object, $version = '2.1',
1187                            array $fields = null, $skipEmpty = false)
1188    {
1189        global $injector;
1190
1191        $hash = $object->getAttributes();
1192        $attributes = array_keys($this->map);
1193        $vcard = new Horde_Icalendar_Vcard($version);
1194        $formattedname = false;
1195        $charset = ($version == '2.1')
1196            ? array('CHARSET' => 'UTF-8')
1197            : array();
1198
1199        $hooks = $injector->getInstance('Horde_Core_Hooks');
1200        $decode_hook = $hooks->hookExists('decode_attribute', 'turba');
1201
1202        // Tags are stored externally to Turba, so they don't appear in the
1203        // source map.
1204        $attributes[] = '__tags';
1205
1206        foreach ($attributes as $key) {
1207            $val = $object->getValue($key);
1208            if ($skipEmpty && !is_array($val) && !strlen($val)) {
1209                continue;
1210            }
1211            if ($decode_hook) {
1212                try {
1213                    $val = $hooks->callHook(
1214                        'decode_attribute',
1215                        'turba',
1216                        array($key, $val, $object)
1217                    );
1218                } catch (Turba_Exception $e) {}
1219            }
1220            switch ($key) {
1221            case '__uid':
1222                $vcard->setAttribute('UID', $val);
1223                break;
1224
1225            case 'name':
1226                if ($fields && !isset($fields['FN'])) {
1227                    break;
1228                }
1229                $vcard->setAttribute('FN', $val, Horde_Mime::is8bit($val) ? $charset : array());
1230                $formattedname = true;
1231                break;
1232
1233            case 'nickname':
1234            case 'alias':
1235                $params = Horde_Mime::is8bit($val) ? $charset : array();
1236                if (!$fields || isset($fields['NICKNAME'])) {
1237                    $vcard->setAttribute('NICKNAME', $val, $params);
1238                }
1239                if (!$fields || isset($fields['X-EPOCSECONDNAME'])) {
1240                    $vcard->setAttribute('X-EPOCSECONDNAME', $val, $params);
1241                }
1242                break;
1243
1244            case 'homeAddress':
1245                if ($fields &&
1246                    (!isset($fields['LABEL']) ||
1247                     (isset($fields['LABEL']->Params['TYPE']) &&
1248                      !$this->_hasValEnum($fields['LABEL']->Params['TYPE']->ValEnum, 'HOME')))) {
1249                    break;
1250                }
1251                if ($version == '2.1') {
1252                    $vcard->setAttribute('LABEL', $val, array('HOME' => null));
1253                } else {
1254                    $vcard->setAttribute('LABEL', $val, array('TYPE' => 'HOME'));
1255                }
1256                break;
1257
1258            case 'workAddress':
1259                if ($fields &&
1260                    (!isset($fields['LABEL']) ||
1261                     (isset($fields['LABEL']->Params['TYPE']) &&
1262                      !$this->_hasValEnum($fields['LABEL']->Params['TYPE']->ValEnum, 'WORK')))) {
1263                    break;
1264                }
1265                if ($version == '2.1') {
1266                    $vcard->setAttribute('LABEL', $val, array('WORK' => null));
1267                } else {
1268                    $vcard->setAttribute('LABEL', $val, array('TYPE' => 'WORK'));
1269                }
1270                break;
1271
1272            case 'otherAddress':
1273                if ($fields && !isset($fields['LABEL'])) {
1274                    break;
1275                }
1276                $vcard->setAttribute('LABEL', $val);
1277                break;
1278
1279            case 'phone':
1280                if ($fields && !isset($fields['TEL'])) {
1281                    break;
1282                }
1283                $vcard->setAttribute('TEL', $val);
1284                break;
1285
1286            case 'homePhone':
1287                if ($fields &&
1288                    (!isset($fields['TEL']) ||
1289                     (isset($fields['TEL']->Params['TYPE']) &&
1290                      !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'HOME')))) {
1291                    break;
1292                }
1293                if ($version == '2.1') {
1294                    $vcard->setAttribute('TEL', $val, array('HOME' => null, 'VOICE' => null));
1295                } else {
1296                    $vcard->setAttribute('TEL', $val, array('TYPE' => array('HOME', 'VOICE')));
1297                }
1298                break;
1299
1300            case 'workPhone':
1301                if ($fields &&
1302                    (!isset($fields['TEL']) ||
1303                     (isset($fields['TEL']->Params['TYPE']) &&
1304                      !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'WORK')))) {
1305                    break;
1306                }
1307                if ($version == '2.1') {
1308                    $vcard->setAttribute('TEL', $val, array('WORK' => null, 'VOICE' => null));
1309                } else {
1310                    $vcard->setAttribute('TEL', $val, array('TYPE' => array('WORK', 'VOICE')));
1311                }
1312                break;
1313
1314            case 'cellPhone':
1315                if ($fields &&
1316                    (!isset($fields['TEL']) ||
1317                     (isset($fields['TEL']->Params['TYPE']) &&
1318                      !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'CELL')))) {
1319                    break;
1320                }
1321                if ($version == '2.1') {
1322                    $vcard->setAttribute('TEL', $val, array('CELL' => null, 'VOICE' => null));
1323                } else {
1324                    $vcard->setAttribute('TEL', $val, array('TYPE' => array('CELL', 'VOICE')));
1325                }
1326                break;
1327
1328            case 'homeCellPhone':
1329                $parameters = array();
1330                if ($fields) {
1331                    if (!isset($fields['TEL'])) {
1332                        break;
1333                    }
1334                    if (!isset($fields['TEL']->Params['TYPE']) ||
1335                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'CELL')) {
1336                        if ($version == '2.1') {
1337                            $parameters['CELL'] = null;
1338                            $parameters['VOICE'] = null;
1339                        } else {
1340                            $parameters['TYPE'] = array('CELL', 'VOICE');
1341                        }
1342                    }
1343                    if (!isset($fields['TEL']->Params['TYPE']) ||
1344                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'HOME')) {
1345                        if ($version == '2.1') {
1346                            $parameters['HOME'] = null;
1347                            $parameters['VOICE'] = null;
1348                        } else {
1349                            $parameters['TYPE'] = array('HOME', 'VOICE');
1350                        }
1351                    }
1352                    if (empty($parameters)) {
1353                        break;
1354                    }
1355                } else {
1356                    if ($version == '2.1') {
1357                        $parameters = array('CELL' => null, 'HOME' => null, 'VOICE' => null);
1358                    } else {
1359                        $parameters = array('TYPE' => array('CELL', 'HOME', 'VOICE'));
1360                    }
1361                }
1362                $vcard->setAttribute('TEL', $val, $parameters);
1363                break;
1364
1365            case 'workCellPhone':
1366                $parameters = array();
1367                if ($fields) {
1368                    if (!isset($fields['TEL'])) {
1369                        break;
1370                    }
1371                    if (!isset($fields['TEL']->Params['TYPE']) ||
1372                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'CELL')) {
1373                        if ($version == '2.1') {
1374                            $parameters['CELL'] = null;
1375                            $parameters['VOICE'] = null;
1376                        } else {
1377                            $parameters['TYPE'] = array('CELL', 'VOICE');
1378                        }
1379                    }
1380                    if (!isset($fields['TEL']->Params['TYPE']) ||
1381                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'WORK')) {
1382                        if ($version == '2.1') {
1383                            $parameters['WORK'] = null;
1384                            $parameters['VOICE'] = null;
1385                        } else {
1386                            $parameters['TYPE'] = array('WORK', 'VOICE');
1387                        }
1388                    }
1389                    if (empty($parameters)) {
1390                        break;
1391                    }
1392                } else {
1393                    if ($version == '2.1') {
1394                        $parameters = array('CELL' => null, 'WORK' => null, 'VOICE' => null);
1395                    } else {
1396                        $parameters = array('TYPE' => array('CELL', 'WORK', 'VOICE'));
1397                    }
1398                }
1399                $vcard->setAttribute('TEL', $val, $parameters);
1400                break;
1401
1402            case 'videoCall':
1403                if ($fields &&
1404                    (!isset($fields['TEL']) ||
1405                     (isset($fields['TEL']->Params['TYPE']) &&
1406                      !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'VIDEO')))) {
1407                    break;
1408                }
1409                if ($version == '2.1') {
1410                    $vcard->setAttribute('TEL', $val, array('VIDEO' => null));
1411                } else {
1412                    $vcard->setAttribute('TEL', $val, array('TYPE' => 'VIDEO'));
1413                }
1414                break;
1415
1416            case 'homeVideoCall':
1417                $parameters = array();
1418                if ($fields) {
1419                    if (!isset($fields['TEL'])) {
1420                        break;
1421                    }
1422                    if (!isset($fields['TEL']->Params['TYPE']) ||
1423                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'VIDEO')) {
1424                        if ($version == '2.1') {
1425                            $parameters['VIDEO'] = null;
1426                        } else {
1427                            $parameters['TYPE'] = 'VIDEO';
1428                        }
1429                    }
1430                    if (!isset($fields['TEL']->Params['TYPE']) ||
1431                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'HOME')) {
1432                        if ($version == '2.1') {
1433                            $parameters['HOME'] = null;
1434                        } else {
1435                            $parameters['TYPE'] = 'HOME';
1436                        }
1437                    }
1438                    if (empty($parameters)) {
1439                        break;
1440                    }
1441                } else {
1442                    if ($version == '2.1') {
1443                        $parameters = array('VIDEO' => null, 'HOME' => null);
1444                    } else {
1445                        $parameters = array('TYPE' => array('VIDEO', 'HOME'));
1446                    }
1447                }
1448                $vcard->setAttribute('TEL', $val, $parameters);
1449                break;
1450
1451            case 'workVideoCall':
1452                $parameters = array();
1453                if ($fields) {
1454                    if (!isset($fields['TEL'])) {
1455                        break;
1456                    }
1457                    if (!isset($fields['TEL']->Params['TYPE']) ||
1458                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'VIDEO')) {
1459                        if ($version == '2.1') {
1460                            $parameters['VIDEO'] = null;
1461                        } else {
1462                            $parameters['TYPE'] = 'VIDEO';
1463                        }
1464                    }
1465                    if (!isset($fields['TEL']->Params['TYPE']) ||
1466                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'WORK')) {
1467                        if ($version == '2.1') {
1468                            $parameters['WORK'] = null;
1469                        } else {
1470                            $parameters['TYPE'] = 'WORK';
1471                        }
1472                    }
1473                    if (empty($parameters)) {
1474                        break;
1475                    }
1476                } else {
1477                    if ($version == '2.1') {
1478                        $parameters = array('VIDEO' => null, 'WORK' => null);
1479                    } else {
1480                        $parameters = array('TYPE' => array('VIDEO', 'WORK'));
1481                    }
1482                }
1483                $vcard->setAttribute('TEL', $val, $parameters);
1484                break;
1485
1486            case 'sip':
1487                if ($fields && !isset($fields['X-SIP'])) {
1488                    break;
1489                }
1490                $vcard->setAttribute('X-SIP', $val);
1491                break;
1492            case 'ptt':
1493                if ($fields &&
1494                    (!isset($fields['X-SIP']) ||
1495                     (isset($fields['X-SIP']->Params['TYPE']) &&
1496                      !$this->_hasValEnum($fields['X-SIP']->Params['TYPE']->ValEnum, 'POC')))) {
1497                    break;
1498                }
1499                if ($version == '2.1') {
1500                    $vcard->setAttribute('X-SIP', $val, array('POC' => null));
1501                } else {
1502                    $vcard->setAttribute('X-SIP', $val, array('TYPE' => 'POC'));
1503                }
1504                break;
1505
1506            case 'voip':
1507                if ($fields &&
1508                    (!isset($fields['X-SIP']) ||
1509                     (isset($fields['X-SIP']->Params['TYPE']) &&
1510                      !$this->_hasValEnum($fields['X-SIP']->Params['TYPE']->ValEnum, 'VOIP')))) {
1511                    break;
1512                }
1513                if ($version == '2.1') {
1514                    $vcard->setAttribute('X-SIP', $val, array('VOIP' => null));
1515                } else {
1516                    $vcard->setAttribute('X-SIP', $val, array('TYPE' => 'VOIP'));
1517                }
1518                break;
1519
1520            case 'shareView':
1521                if ($fields &&
1522                    (!isset($fields['X-SIP']) ||
1523                     (isset($fields['X-SIP']->Params['TYPE']) &&
1524                      !$this->_hasValEnum($fields['X-SIP']->Params['TYPE']->ValEnum, 'SWIS')))) {
1525                    break;
1526                }
1527                if ($version == '2.1') {
1528                    $vcard->setAttribute('X-SIP', $val, array('SWIS' => null));
1529                } else {
1530                    $vcard->setAttribute('X-SIP', $val, array('TYPE' => 'SWIS'));
1531                }
1532                break;
1533
1534            case 'imaddress':
1535                if ($fields && !isset($fields['X-WV-ID'])) {
1536                    break;
1537                }
1538                $vcard->setAttribute('X-WV-ID', $val);
1539                break;
1540
1541            case 'fax':
1542                if ($fields &&
1543                    (!isset($fields['TEL']) ||
1544                     (isset($fields['TEL']->Params['TYPE']) &&
1545                      !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'FAX')))) {
1546                    break;
1547                }
1548                if ($version == '2.1') {
1549                    $vcard->setAttribute('TEL', $val, array('FAX' => null));
1550                } else {
1551                    $vcard->setAttribute('TEL', $val, array('TYPE' => 'FAX'));
1552                }
1553                break;
1554
1555            case 'homeFax':
1556                $parameters = array();
1557                if ($fields) {
1558                    if (!isset($fields['TEL'])) {
1559                        break;
1560                    }
1561                    if (!isset($fields['TEL']->Params['TYPE']) ||
1562                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'FAX')) {
1563                        if ($version == '2.1') {
1564                            $parameters['FAX'] = null;
1565                        } else {
1566                            $parameters['TYPE'] = 'FAX';
1567                        }
1568                    }
1569                    if (!isset($fields['TEL']->Params['TYPE']) ||
1570                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'HOME')) {
1571                        if ($version == '2.1') {
1572                            $parameters['HOME'] = null;
1573                        } else {
1574                            $parameters['TYPE'] = 'HOME';
1575                        }
1576                    }
1577                    if (empty($parameters)) {
1578                        break;
1579                    }
1580                } else {
1581                    if ($version == '2.1') {
1582                        $parameters = array('FAX' => null, 'HOME' => null);
1583                    } else {
1584                        $parameters = array('TYPE' => array('FAX', 'HOME'));
1585                    }
1586                }
1587                $vcard->setAttribute('TEL', $val, $parameters);
1588                break;
1589
1590            case 'workFax':
1591                $parameters = array();
1592                if ($fields) {
1593                    if (!isset($fields['TEL'])) {
1594                        break;
1595                    }
1596                    if (!isset($fields['TEL']->Params['TYPE']) ||
1597                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'FAX')) {
1598                        if ($version == '2.1') {
1599                            $parameters['FAX'] = null;
1600                        } else {
1601                            $parameters['TYPE'] = 'FAX';
1602                        }
1603                    }
1604                    if (!isset($fields['TEL']->Params['TYPE']) ||
1605                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'WORK')) {
1606                        if ($version == '2.1') {
1607                            $parameters['WORK'] = null;
1608                        } else {
1609                            $parameters['TYPE'] = 'WORK';
1610                        }
1611                    }
1612                    if (empty($parameters)) {
1613                        break;
1614                    }
1615                } else {
1616                    if ($version == '2.1') {
1617                        $parameters = array('FAX' => null, 'WORK' => null);
1618                    } else {
1619                        $parameters = array('TYPE' => array('FAX', 'WORK'));
1620                    }
1621                }
1622                $vcard->setAttribute('TEL', $val, $parameters);
1623                if ($version == '2.1') {
1624                    $vcard->setAttribute('TEL', $val, array('FAX' => null, 'WORK' => null));
1625                } else {
1626                    $vcard->setAttribute('TEL', $val, array('TYPE' => array('FAX', 'WORK')));
1627                }
1628                break;
1629
1630            case 'pager':
1631                if ($fields &&
1632                    (!isset($fields['TEL']) ||
1633                     (isset($fields['TEL']->Params['TYPE']) &&
1634                      !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'PAGER')))) {
1635                    break;
1636                }
1637                if ($version == '2.1') {
1638                    $vcard->setAttribute('TEL', $val, array('PAGER' => null));
1639                } else {
1640                    $vcard->setAttribute('TEL', $val, array('TYPE' => 'PAGER'));
1641                }
1642                break;
1643
1644            case 'email':
1645                if ($fields && !isset($fields['EMAIL'])) {
1646                    break;
1647                }
1648                if ($version == '2.1') {
1649                    $vcard->setAttribute(
1650                        'EMAIL',
1651                        Horde_Icalendar_Vcard::getBareEmail($val),
1652                        array('INTERNET' => null));
1653                } else {
1654                    $vcard->setAttribute(
1655                        'EMAIL',
1656                        Horde_Icalendar_Vcard::getBareEmail($val),
1657                        array('TYPE' => 'INTERNET'));
1658                }
1659                break;
1660
1661            case 'homeEmail':
1662                if ($fields &&
1663                    (!isset($fields['EMAIL']) ||
1664                     (isset($fields['EMAIL']->Params['TYPE']) &&
1665                      !$this->_hasValEnum($fields['EMAIL']->Params['TYPE']->ValEnum, 'HOME')))) {
1666                    break;
1667                }
1668                if ($version == '2.1') {
1669                    $vcard->setAttribute('EMAIL',
1670                                         Horde_Icalendar_Vcard::getBareEmail($val),
1671                                         array('HOME' => null));
1672                } else {
1673                    $vcard->setAttribute('EMAIL',
1674                                         Horde_Icalendar_Vcard::getBareEmail($val),
1675                                         array('TYPE' => 'HOME'));
1676                }
1677                break;
1678
1679            case 'workEmail':
1680                if ($fields &&
1681                    (!isset($fields['EMAIL']) ||
1682                     (isset($fields['EMAIL']->Params['TYPE']) &&
1683                      !$this->_hasValEnum($fields['EMAIL']->Params['TYPE']->ValEnum, 'WORK')))) {
1684                    break;
1685                }
1686                if ($version == '2.1') {
1687                    $vcard->setAttribute('EMAIL',
1688                                         Horde_Icalendar_Vcard::getBareEmail($val),
1689                                         array('WORK' => null));
1690                } else {
1691                    $vcard->setAttribute('EMAIL',
1692                                         Horde_Icalendar_Vcard::getBareEmail($val),
1693                                         array('TYPE' => 'WORK'));
1694                }
1695                break;
1696
1697            case 'emails':
1698                if ($fields && !isset($fields['EMAIL'])) {
1699                    break;
1700                }
1701                $emails = explode(',', $val);
1702                foreach ($emails as $email) {
1703                    $vcard->setAttribute('EMAIL', Horde_Icalendar_Vcard::getBareEmail($email));
1704                }
1705                break;
1706
1707            case 'title':
1708                if ($fields && !isset($fields['TITLE'])) {
1709                    break;
1710                }
1711                $vcard->setAttribute('TITLE', $val, Horde_Mime::is8bit($val) ? $charset : array());
1712                break;
1713
1714            case 'role':
1715                if ($fields && !isset($fields['ROLE'])) {
1716                    break;
1717                }
1718                $vcard->setAttribute('ROLE', $val, Horde_Mime::is8bit($val) ? $charset : array());
1719                break;
1720
1721            case 'notes':
1722                if ($fields && !isset($fields['NOTE'])) {
1723                    break;
1724                }
1725                $vcard->setAttribute('NOTE', $val, Horde_Mime::is8bit($val) ? $charset : array());
1726                break;
1727
1728            case '__tags':
1729                $val = $injector->getInstance('Turba_Tagger')->split($val);
1730            case 'businessCategory':
1731                // No CATEGORIES in vCard 2.1
1732                if ($version == '2.1' ||
1733                    ($fields && !isset($fields['CATEGORIES']))) {
1734                    break;
1735                }
1736                $vcard->setAttribute('CATEGORIES', null, array(), true, $val);
1737                break;
1738
1739            case 'anniversary':
1740                if (!$fields || isset($fields['X-ANNIVERSARY'])) {
1741                    $vcard->setAttribute('X-ANNIVERSARY', $val);
1742                }
1743                break;
1744
1745            case 'spouse':
1746                if (!$fields || isset($fields['X-SPOUSE'])) {
1747                    $vcard->setAttribute('X-SPOUSE', $val);
1748                }
1749                break;
1750
1751            case 'children':
1752                if (!$fields || isset($fields['X-CHILDREN'])) {
1753                    $vcard->setAttribute('X-CHILDREN', $val);
1754                }
1755                break;
1756
1757            case 'website':
1758                if ($fields && !isset($fields['URL'])) {
1759                    break;
1760                }
1761                $vcard->setAttribute('URL', $val);
1762                break;
1763
1764            case 'homeWebsite':
1765                if ($fields &&
1766                    (!isset($fields['URL']) ||
1767                     (isset($fields['URL']->Params['TYPE']) &&
1768                      !$this->_hasValEnum($fields['URL']->Params['TYPE']->ValEnum, 'HOME')))) {
1769                    break;
1770                }
1771                if ($version == '2.1') {
1772                    $vcard->setAttribute('URL', $val, array('HOME' => null));
1773                } else {
1774                    $vcard->setAttribute('URL', $val, array('TYPE' => 'HOME'));
1775                }
1776                break;
1777
1778            case 'workWebsite':
1779                if ($fields &&
1780                    (!isset($fields['URL']) ||
1781                     (isset($fields['URL']->Params['TYPE']) &&
1782                      !$this->_hasValEnum($fields['URL']->Params['TYPE']->ValEnum, 'WORK')))) {
1783                    break;
1784                }
1785                if ($version == '2.1') {
1786                    $vcard->setAttribute('URL', $val, array('WORK' => null));
1787                } else {
1788                    $vcard->setAttribute('URL', $val, array('TYPE' => 'WORK'));
1789                }
1790                break;
1791
1792            case 'freebusyUrl':
1793                if ($version == '2.1' ||
1794                    ($fields && !isset($fields['FBURL']))) {
1795                    break;
1796                }
1797                $vcard->setAttribute('FBURL', $val);
1798                break;
1799
1800            case 'birthday':
1801                if ($fields && !isset($fields['BDAY'])) {
1802                    break;
1803                }
1804                $vcard->setAttribute('BDAY', $val);
1805                break;
1806
1807            case 'timezone':
1808                if ($fields && !isset($fields['TZ'])) {
1809                    break;
1810                }
1811                $vcard->setAttribute('TZ', $val, array('VALUE' => 'text'));
1812                break;
1813
1814            case 'latitude':
1815                if ($fields && !isset($fields['GEO'])) {
1816                    break;
1817                }
1818                if (isset($hash['longitude'])) {
1819                    $vcard->setAttribute('GEO',
1820                                         array('latitude' => $val,
1821                                               'longitude' => $hash['longitude']));
1822                }
1823                break;
1824
1825            case 'homeLatitude':
1826                if ($fields &&
1827                    (!isset($fields['GEO']) ||
1828                     (isset($fields['GEO']->Params['TYPE']) &&
1829                      !$this->_hasValEnum($fields['GEO']->Params['TYPE']->ValEnum, 'HOME')))) {
1830                    break;
1831                }
1832                if (isset($hash['homeLongitude'])) {
1833                    if ($version == '2.1') {
1834                        $vcard->setAttribute('GEO',
1835                                             array('latitude' => $val,
1836                                                   'longitude' => $hash['homeLongitude']),
1837                                             array('HOME' => null));
1838                   } else {
1839                        $vcard->setAttribute('GEO',
1840                                             array('latitude' => $val,
1841                                                   'longitude' => $hash['homeLongitude']),
1842                                             array('TYPE' => 'HOME'));
1843                   }
1844                }
1845                break;
1846
1847            case 'workLatitude':
1848                if ($fields &&
1849                    (!isset($fields['GEO']) ||
1850                     (isset($fields['GEO']->Params['TYPE']) &&
1851                      !$this->_hasValEnum($fields['GEO']->Params['TYPE']->ValEnum, 'HOME')))) {
1852                    break;
1853                }
1854                if (isset($hash['workLongitude'])) {
1855                    if ($version == '2.1') {
1856                        $vcard->setAttribute('GEO',
1857                                             array('latitude' => $val,
1858                                                   'longitude' => $hash['workLongitude']),
1859                                             array('WORK' => null));
1860                   } else {
1861                        $vcard->setAttribute('GEO',
1862                                             array('latitude' => $val,
1863                                                   'longitude' => $hash['workLongitude']),
1864                                             array('TYPE' => 'WORK'));
1865                   }
1866                }
1867                break;
1868
1869            case 'photo':
1870            case 'logo':
1871                $name = Horde_String::upper($key);
1872                $params = array();
1873                if (strlen($hash[$key])) {
1874                    $params['ENCODING'] = 'b';
1875                }
1876                if (isset($hash[$key . 'type'])) {
1877                    $params['TYPE'] = $hash[$key . 'type'];
1878                }
1879                if ($fields &&
1880                    (!isset($fields[$name]) ||
1881                     (isset($params['TYPE']) &&
1882                      isset($fields[$name]->Params['TYPE']) &&
1883                      !$this->_hasValEnum($fields[$name]->Params['TYPE']->ValEnum, $params['TYPE'])))) {
1884                    break;
1885                }
1886                $vcard->setAttribute($name,
1887                                     base64_encode($hash[$key]),
1888                                     $params);
1889                break;
1890            }
1891        }
1892
1893        // No explicit firstname/lastname in data source: we have to guess.
1894        if (!isset($hash['lastname']) && isset($hash['name'])) {
1895            $this->_guessName($hash);
1896        }
1897
1898        $a = array(
1899            Horde_Icalendar_Vcard::N_FAMILY => isset($hash['lastname']) ? $hash['lastname'] : '',
1900            Horde_Icalendar_Vcard::N_GIVEN  => isset($hash['firstname']) ? $hash['firstname'] : '',
1901            Horde_Icalendar_Vcard::N_ADDL   => isset($hash['middlenames']) ? $hash['middlenames'] : '',
1902            Horde_Icalendar_Vcard::N_PREFIX => isset($hash['namePrefix']) ? $hash['namePrefix'] : '',
1903            Horde_Icalendar_Vcard::N_SUFFIX => isset($hash['nameSuffix']) ? $hash['nameSuffix'] : '',
1904        );
1905        $val = implode(';', $a);
1906        if (!$fields || isset($fields['N'])) {
1907            $vcard->setAttribute('N', $val, Horde_Mime::is8bit($val) ? $charset : array(), false, $a);
1908        }
1909
1910        if (!$formattedname && (!$fields || isset($fields['FN']))) {
1911            if ($object->getValue('name')) {
1912                $val = $object->getValue('name');
1913            } elseif (!empty($this->alternativeName) &&
1914                isset($hash[$this->alternativeName])) {
1915                $val = $hash[$this->alternativeName];
1916            } else {
1917                $val = '';
1918            }
1919            $vcard->setAttribute('FN', $val, Horde_Mime::is8bit($val) ? $charset : array());
1920        }
1921
1922        $org = array();
1923        if (!empty($hash['company']) ||
1924            (!$skipEmpty && array_key_exists('company', $hash))) {
1925            $org[] = $hash['company'];
1926        }
1927        if (!empty($hash['department']) ||
1928            (!$skipEmpty && array_key_exists('department', $hash))) {
1929            $org[] = $hash['department'];
1930        }
1931        if (count($org) && (!$fields || isset($fields['ORG']))) {
1932            $val = implode(';', $org);
1933            $vcard->setAttribute('ORG', $val, Horde_Mime::is8bit($val) ? $charset : array(), false, $org);
1934        }
1935
1936        if ((!$fields || isset($fields['ADR'])) &&
1937            (!empty($hash['commonAddress']) ||
1938             !empty($hash['commonStreet']) ||
1939             !empty($hash['commonPOBox']) ||
1940             !empty($hash['commonExtended']) ||
1941             !empty($hash['commonCity']) ||
1942             !empty($hash['commonProvince']) ||
1943             !empty($hash['commonPostalCode']) ||
1944             !empty($hash['commonCountry']) ||
1945             (!$skipEmpty &&
1946              (array_key_exists('commonAddress', $hash) ||
1947               array_key_exists('commonStreet', $hash) ||
1948               array_key_exists('commonPOBox', $hash) ||
1949               array_key_exists('commonExtended', $hash) ||
1950               array_key_exists('commonCity', $hash) ||
1951               array_key_exists('commonProvince', $hash) ||
1952               array_key_exists('commonPostalCode', $hash) ||
1953               array_key_exists('commonCountry', $hash))))) {
1954            /* We can't know if this particular Turba source uses a single
1955             * address field or multiple for
1956             * street/city/province/postcode/country. Try to deal with
1957             * both. */
1958            if (isset($hash['commonAddress']) &&
1959                !isset($hash['commonStreet'])) {
1960                $hash['commonStreet'] = $hash['commonAddress'];
1961            }
1962            $a = array(
1963                Horde_Icalendar_Vcard::ADR_POB      => isset($hash['commonPOBox'])
1964                    ? $hash['commonPOBox'] : '',
1965                Horde_Icalendar_Vcard::ADR_EXTEND   => isset($hash['commonExtended'])
1966                    ? $hash['commonExtended'] : '',
1967                Horde_Icalendar_Vcard::ADR_STREET   => isset($hash['commonStreet'])
1968                    ? $hash['commonStreet'] : '',
1969                Horde_Icalendar_Vcard::ADR_LOCALITY => isset($hash['commonCity'])
1970                    ? $hash['commonCity'] : '',
1971                Horde_Icalendar_Vcard::ADR_REGION   => isset($hash['commonProvince'])
1972                    ? $hash['commonProvince'] : '',
1973                Horde_Icalendar_Vcard::ADR_POSTCODE => isset($hash['commonPostalCode'])
1974                    ? $hash['commonPostalCode'] : '',
1975                Horde_Icalendar_Vcard::ADR_COUNTRY  => isset($hash['commonCountry'])
1976                    ? Horde_Nls::getCountryISO($hash['commonCountry']) : '',
1977            );
1978
1979            $val = implode(';', $a);
1980            if ($version == '2.1') {
1981                $params = array();
1982                if (Horde_Mime::is8bit($val)) {
1983                    $params['CHARSET'] = 'UTF-8';
1984                }
1985            } else {
1986                $params = array('TYPE' => '');
1987            }
1988            $vcard->setAttribute('ADR', $val, $params, true, $a);
1989        }
1990
1991        if ((!$fields ||
1992             (isset($fields['ADR']) &&
1993              (!isset($fields['ADR']->Params['TYPE']) ||
1994               $this->_hasValEnum($fields['ADR']->Params['TYPE']->ValEnum, 'HOME')))) &&
1995            (!empty($hash['homeAddress']) ||
1996             !empty($hash['homeStreet']) ||
1997             !empty($hash['homePOBox']) ||
1998             !empty($hash['homeExtended']) ||
1999             !empty($hash['homeCity']) ||
2000             !empty($hash['homeProvince']) ||
2001             !empty($hash['homePostalCode']) ||
2002             !empty($hash['homeCountry']) ||
2003             (!$skipEmpty &&
2004              (array_key_exists('homeAddress', $hash) ||
2005               array_key_exists('homeStreet', $hash) ||
2006               array_key_exists('homePOBox', $hash) ||
2007               array_key_exists('homeExtended', $hash) ||
2008               array_key_exists('homeCity', $hash) ||
2009               array_key_exists('homeProvince', $hash) ||
2010               array_key_exists('homePostalCode', $hash) ||
2011               array_key_exists('homeCountry', $hash))))) {
2012            if (isset($hash['homeAddress']) && !isset($hash['homeStreet'])) {
2013                $hash['homeStreet'] = $hash['homeAddress'];
2014            }
2015            $a = array(
2016                Horde_Icalendar_Vcard::ADR_POB      => isset($hash['homePOBox'])
2017                    ? $hash['homePOBox'] : '',
2018                Horde_Icalendar_Vcard::ADR_EXTEND   => isset($hash['homeExtended'])
2019                    ? $hash['homeExtended'] : '',
2020                Horde_Icalendar_Vcard::ADR_STREET   => isset($hash['homeStreet'])
2021                    ? $hash['homeStreet'] : '',
2022                Horde_Icalendar_Vcard::ADR_LOCALITY => isset($hash['homeCity'])
2023                    ? $hash['homeCity'] : '',
2024                Horde_Icalendar_Vcard::ADR_REGION   => isset($hash['homeProvince'])
2025                    ? $hash['homeProvince'] : '',
2026                Horde_Icalendar_Vcard::ADR_POSTCODE => isset($hash['homePostalCode'])
2027                    ? $hash['homePostalCode'] : '',
2028                Horde_Icalendar_Vcard::ADR_COUNTRY  => isset($hash['homeCountry'])
2029                    ? Horde_Nls::getCountryISO($hash['homeCountry']) : '',
2030            );
2031
2032            $val = implode(';', $a);
2033            if ($version == '2.1') {
2034                $params = array('HOME' => null);
2035                if (Horde_Mime::is8bit($val)) {
2036                    $params['CHARSET'] = 'UTF-8';
2037                }
2038            } else {
2039                $params = array('TYPE' => 'HOME');
2040            }
2041            $vcard->setAttribute('ADR', $val, $params, true, $a);
2042        }
2043
2044        if ((!$fields ||
2045             (isset($fields['ADR']) &&
2046              (!isset($fields['ADR']->Params['TYPE']) ||
2047               $this->_hasValEnum($fields['ADR']->Params['TYPE']->ValEnum, 'WORK')))) &&
2048            (!empty($hash['workAddress']) ||
2049             !empty($hash['workStreet']) ||
2050             !empty($hash['workPOBox']) ||
2051             !empty($hash['workExtended']) ||
2052             !empty($hash['workCity']) ||
2053             !empty($hash['workProvince']) ||
2054             !empty($hash['workPostalCode']) ||
2055             !empty($hash['workCountry']) ||
2056             (!$skipEmpty &&
2057              (array_key_exists('workAddress', $hash) ||
2058               array_key_exists('workStreet', $hash) ||
2059               array_key_exists('workPOBox', $hash) ||
2060               array_key_exists('workExtended', $hash) ||
2061               array_key_exists('workCity', $hash) ||
2062               array_key_exists('workProvince', $hash) ||
2063               array_key_exists('workPostalCode', $hash) ||
2064               array_key_exists('workCountry', $hash))))) {
2065            if (isset($hash['workAddress']) && !isset($hash['workStreet'])) {
2066                $hash['workStreet'] = $hash['workAddress'];
2067            }
2068            $a = array(
2069                Horde_Icalendar_Vcard::ADR_POB      => isset($hash['workPOBox'])
2070                    ? $hash['workPOBox'] : '',
2071                Horde_Icalendar_Vcard::ADR_EXTEND   => isset($hash['workExtended'])
2072                    ? $hash['workExtended'] : '',
2073                Horde_Icalendar_Vcard::ADR_STREET   => isset($hash['workStreet'])
2074                    ? $hash['workStreet'] : '',
2075                Horde_Icalendar_Vcard::ADR_LOCALITY => isset($hash['workCity'])
2076                    ? $hash['workCity'] : '',
2077                Horde_Icalendar_Vcard::ADR_REGION   => isset($hash['workProvince'])
2078                    ? $hash['workProvince'] : '',
2079                Horde_Icalendar_Vcard::ADR_POSTCODE => isset($hash['workPostalCode'])
2080                    ? $hash['workPostalCode'] : '',
2081                Horde_Icalendar_Vcard::ADR_COUNTRY  => isset($hash['workCountry'])
2082                    ? Horde_Nls::getCountryISO($hash['workCountry']) : '',
2083            );
2084
2085            $val = implode(';', $a);
2086            if ($version == '2.1') {
2087                $params = array('WORK' => null);
2088                if (Horde_Mime::is8bit($val)) {
2089                    $params['CHARSET'] = 'UTF-8';
2090                }
2091            } else {
2092                $params = array('TYPE' => 'WORK');
2093            }
2094            $vcard->setAttribute('ADR', $val, $params, true, $a);
2095        }
2096
2097        return $vcard;
2098    }
2099
2100    /**
2101     * Returns whether a ValEnum entry from a DevInf object contains a certain
2102     * type.
2103     *
2104     * @param array $valEnum  A ValEnum hash.
2105     * @param string $type    A requested attribute type.
2106     *
2107     * @return boolean  True if $type exists in $valEnum.
2108     */
2109    protected function _hasValEnum($valEnum, $type)
2110    {
2111        foreach (array_keys($valEnum) as $key) {
2112            if (in_array($type, explode(',', $key))) {
2113                return true;
2114            }
2115        }
2116        return false;
2117    }
2118
2119    /**
2120     * Function to convert a Horde_Icalendar_Vcard object into a Turba
2121     * Object Hash with Turba attributes suitable as a parameter for add().
2122     *
2123     * @see add()
2124     *
2125     * @param Horde_Icalendar_Vcard $vcard  The Horde_Icalendar_Vcard object
2126     *                                      to parse.
2127     *
2128     * @return array  A Turba attribute hash.
2129     */
2130    public function toHash(Horde_Icalendar_Vcard $vcard)
2131    {
2132        global $attributes;
2133
2134        $hash = array();
2135        $attr = $vcard->getAllAttributes();
2136
2137        foreach ($attr as $item) {
2138            switch ($item['name']) {
2139            case 'UID':
2140                $hash['__uid'] = $item['value'];
2141                break;
2142
2143            case 'FN':
2144                $hash['name'] = $item['value'];
2145                break;
2146
2147            case 'N':
2148                $name = $item['values'];
2149                if (!empty($name[Horde_Icalendar_Vcard::N_FAMILY])) {
2150                    $hash['lastname'] = $name[Horde_Icalendar_Vcard::N_FAMILY];
2151                }
2152                if (!empty($name[Horde_Icalendar_Vcard::N_GIVEN])) {
2153                    $hash['firstname'] = $name[Horde_Icalendar_Vcard::N_GIVEN];
2154                }
2155                if (!empty($name[Horde_Icalendar_Vcard::N_ADDL])) {
2156                    $hash['middlenames'] = $name[Horde_Icalendar_Vcard::N_ADDL];
2157                }
2158                if (!empty($name[Horde_Icalendar_Vcard::N_PREFIX])) {
2159                    $hash['namePrefix'] = $name[Horde_Icalendar_Vcard::N_PREFIX];
2160                }
2161                if (!empty($name[Horde_Icalendar_Vcard::N_SUFFIX])) {
2162                    $hash['nameSuffix'] = $name[Horde_Icalendar_Vcard::N_SUFFIX];
2163                }
2164                break;
2165
2166            case 'NICKNAME':
2167            case 'X-EPOCSECONDNAME':
2168                $hash['nickname'] = $item['value'];
2169                $hash['alias'] = $item['value'];
2170                break;
2171
2172            // We use LABEL but also support ADR.
2173            case 'LABEL':
2174                if (isset($item['params']['HOME']) && !isset($hash['homeAddress'])) {
2175                    $hash['homeAddress'] = $item['value'];
2176                } elseif (isset($item['params']['WORK']) && !isset($hash['workAddress'])) {
2177                    $hash['workAddress'] = $item['value'];
2178                } elseif (!isset($hash['commonAddress'])) {
2179                    $hash['commonAddress'] = $item['value'];
2180                }
2181                break;
2182
2183            case 'ADR':
2184                if (isset($item['params']['TYPE'])) {
2185                    if (!is_array($item['params']['TYPE'])) {
2186                        $item['params']['TYPE'] = array($item['params']['TYPE']);
2187                    }
2188                } else {
2189                    $item['params']['TYPE'] = array();
2190                    if (isset($item['params']['WORK'])) {
2191                        $item['params']['TYPE'][] = 'WORK';
2192                    }
2193                    if (isset($item['params']['HOME'])) {
2194                        $item['params']['TYPE'][] = 'HOME';
2195                    }
2196                    if (count($item['params']['TYPE']) == 0) {
2197                        $item['params']['TYPE'][] = 'COMMON';
2198                    }
2199                }
2200
2201                $address = $item['values'];
2202                foreach ($item['params']['TYPE'] as $adr) {
2203                    switch (Horde_String::upper($adr)) {
2204                    case 'HOME':
2205                        $prefix = 'home';
2206                        break;
2207
2208                    case 'WORK':
2209                        $prefix = 'work';
2210                        break;
2211
2212                    default:
2213                        $prefix = 'common';
2214                    }
2215
2216                    if (isset($hash[$prefix . 'Address'])) {
2217                        continue;
2218                    }
2219
2220                    $hash[$prefix . 'Address'] = '';
2221
2222                    if (!empty($address[Horde_Icalendar_Vcard::ADR_STREET])) {
2223                        $hash[$prefix . 'Street'] = $address[Horde_Icalendar_Vcard::ADR_STREET];
2224                        $hash[$prefix . 'Address'] .= $hash[$prefix . 'Street'] . "\n";
2225                    }
2226                    if (!empty($address[Horde_Icalendar_Vcard::ADR_EXTEND])) {
2227                        $hash[$prefix . 'Extended'] = $address[Horde_Icalendar_Vcard::ADR_EXTEND];
2228                        $hash[$prefix . 'Address'] .= $hash[$prefix . 'Extended'] . "\n";
2229                    }
2230                    if (!empty($address[Horde_Icalendar_Vcard::ADR_POB])) {
2231                        $hash[$prefix . 'POBox'] = $address[Horde_Icalendar_Vcard::ADR_POB];
2232                        $hash[$prefix . 'Address'] .= $hash[$prefix . 'POBox'] . "\n";
2233                    }
2234                    if (!empty($address[Horde_Icalendar_Vcard::ADR_LOCALITY])) {
2235                        $hash[$prefix . 'City'] = $address[Horde_Icalendar_Vcard::ADR_LOCALITY];
2236                        $hash[$prefix . 'Address'] .= $hash[$prefix . 'City'];
2237                    }
2238                    if (!empty($address[Horde_Icalendar_Vcard::ADR_REGION])) {
2239                        $hash[$prefix . 'Province'] = $address[Horde_Icalendar_Vcard::ADR_REGION];
2240                        $hash[$prefix . 'Address'] .= ', ' . $hash[$prefix . 'Province'];
2241                    }
2242                    if (!empty($address[Horde_Icalendar_Vcard::ADR_POSTCODE])) {
2243                        $hash[$prefix . 'PostalCode'] = $address[Horde_Icalendar_Vcard::ADR_POSTCODE];
2244                        $hash[$prefix . 'Address'] .= ' ' . $hash[$prefix . 'PostalCode'];
2245                    }
2246                    if (!empty($address[Horde_Icalendar_Vcard::ADR_COUNTRY])) {
2247                        include 'Horde/Nls/Countries.php';
2248                        $country = array_search($address[Horde_Icalendar_Vcard::ADR_COUNTRY], $countries);
2249                        if ($country === false) {
2250                            $country = $address[Horde_Icalendar_Vcard::ADR_COUNTRY];
2251                        }
2252                        $hash[$prefix . 'Country'] = $country;
2253                        $hash[$prefix . 'Address'] .= "\n" . $address[Horde_Icalendar_Vcard::ADR_COUNTRY];
2254                    }
2255
2256                    $hash[$prefix . 'Address'] = trim($hash[$prefix . 'Address']);
2257                }
2258                break;
2259
2260            case 'TZ':
2261                // We only support textual timezones.
2262                if (!isset($item['params']['VALUE']) ||
2263                    Horde_String::lower($item['params']['VALUE']) != 'text') {
2264                    break;
2265                }
2266                $timezones = explode(';', $item['value']);
2267                $available_timezones = Horde_Nls::getTimezones();
2268                foreach ($timezones as $timezone) {
2269                    $timezone = trim($timezone);
2270                    if (isset($available_timezones[$timezone])) {
2271                        $hash['timezone'] = $timezone;
2272                        break 2;
2273                    }
2274                }
2275                break;
2276
2277            case 'GEO':
2278                if (isset($item['params']['HOME'])) {
2279                    $hash['homeLatitude'] = $item['value']['latitude'];
2280                    $hash['homeLongitude'] = $item['value']['longitude'];
2281                } elseif (isset($item['params']['WORK'])) {
2282                    $hash['workLatitude'] = $item['value']['latitude'];
2283                    $hash['workLongitude'] = $item['value']['longitude'];
2284                } else {
2285                    $hash['latitude'] = $item['value']['latitude'];
2286                    $hash['longitude'] = $item['value']['longitude'];
2287                }
2288                break;
2289
2290            case 'TEL':
2291                if (isset($item['params']['FAX'])) {
2292                    if (isset($item['params']['WORK']) &&
2293                        !isset($hash['workFax'])) {
2294                        $hash['workFax'] = $item['value'];
2295                    } elseif (isset($item['params']['HOME']) &&
2296                              !isset($hash['homeFax'])) {
2297                        $hash['homeFax'] = $item['value'];
2298                    } elseif (!isset($hash['fax'])) {
2299                        $hash['fax'] = $item['value'];
2300                    }
2301                } elseif (isset($item['params']['PAGER']) &&
2302                          !isset($hash['pager'])) {
2303                    $hash['pager'] = $item['value'];
2304                } elseif (isset($item['params']['TYPE'])) {
2305                    if (!is_array($item['params']['TYPE'])) {
2306                        $item['params']['TYPE'] = array($item['params']['TYPE']);
2307                    }
2308                    foreach ($item['params']['TYPE'] as &$type) {
2309                        $type = Horde_String::upper($type);
2310                    }
2311                    // For vCard 3.0.
2312                    if (in_array('CELL', $item['params']['TYPE'])) {
2313                        if (in_array('HOME', $item['params']['TYPE']) &&
2314                            !isset($hash['homeCellPhone'])) {
2315                            $hash['homeCellPhone'] = $item['value'];
2316                        } elseif (in_array('WORK', $item['params']['TYPE']) &&
2317                                  !isset($hash['workCellPhone'])) {
2318                            $hash['workCellPhone'] = $item['value'];
2319                        } elseif (!isset($hash['cellPhone'])) {
2320                            $hash['cellPhone'] = $item['value'];
2321                        }
2322                    } elseif (in_array('FAX', $item['params']['TYPE'])) {
2323                        if (in_array('HOME', $item['params']['TYPE']) &&
2324                            !isset($hash['homeFax'])) {
2325                            $hash['homeFax'] = $item['value'];
2326                        } elseif (in_array('WORK', $item['params']['TYPE']) &&
2327                                  !isset($hash['workFax'])) {
2328                            $hash['workFax'] = $item['value'];
2329                        } elseif (!isset($hash['fax'])) {
2330                            $hash['fax'] = $item['value'];
2331                        }
2332                    } elseif (in_array('VIDEO', $item['params']['TYPE'])) {
2333                        if (in_array('HOME', $item['params']['TYPE']) &&
2334                            !isset($hash['homeVideoCall'])) {
2335                            $hash['homeVideoCall'] = $item['value'];
2336                        } elseif (in_array('WORK', $item['params']['TYPE']) &&
2337                                  !isset($hash['workVideoCall'])) {
2338                            $hash['workVideoCall'] = $item['value'];
2339                        } elseif (!isset($hash['videoCall'])) {
2340                            $hash['videoCall'] = $item['value'];
2341                        }
2342                    } elseif (in_array('PAGER', $item['params']['TYPE']) &&
2343                              !isset($hash['pager'])) {
2344                        $hash['pager'] = $item['value'];
2345                    } elseif (in_array('WORK', $item['params']['TYPE']) &&
2346                              !isset($hash['workPhone'])) {
2347                        $hash['workPhone'] = $item['value'];
2348                    } elseif (in_array('HOME', $item['params']['TYPE']) &&
2349                              !isset($hash['homePhone'])) {
2350                        $hash['homePhone'] = $item['value'];
2351                    } elseif (!isset($hash['phone'])) {
2352                        $hash['phone'] = $item['value'];
2353                    }
2354                } elseif (isset($item['params']['CELL'])) {
2355                    if (isset($item['params']['WORK']) &&
2356                        !isset($hash['workCellPhone'])) {
2357                        $hash['workCellPhone'] = $item['value'];
2358                    } elseif (isset($item['params']['HOME']) &&
2359                              !isset($hash['homeCellPhone'])) {
2360                        $hash['homeCellPhone'] = $item['value'];
2361                    } elseif (!isset($hash['cellPhone'])) {
2362                        $hash['cellPhone'] = $item['value'];
2363                    }
2364                } elseif (isset($item['params']['VIDEO'])) {
2365                    if (isset($item['params']['WORK']) &&
2366                        !isset($hash['workVideoCall'])) {
2367                        $hash['workVideoCall'] = $item['value'];
2368                    } elseif (isset($item['params']['HOME']) &&
2369                              !isset($hash['homeVideoCall'])) {
2370                        $hash['homeVideoCall'] = $item['value'];
2371                    } elseif (!isset($hash['videoCall'])) {
2372                        $hash['videoCall'] = $item['value'];
2373                    }
2374                } else {
2375                    if (isset($item['params']['WORK']) &&
2376                        !isset($hash['workPhone'])) {
2377                        $hash['workPhone'] = $item['value'];
2378                    } elseif (isset($item['params']['HOME']) &&
2379                              !isset($hash['homePhone'])) {
2380                        $hash['homePhone'] = $item['value'];
2381                    } else {
2382                        $hash['phone'] = $item['value'];
2383                    }
2384                }
2385                break;
2386
2387            case 'EMAIL':
2388                $email_set = false;
2389                if (isset($item['params']['HOME']) &&
2390                    !empty($this->map['homeEmail']) &&
2391                    (!isset($hash['homeEmail']) ||
2392                     isset($item['params']['PREF']))) {
2393                    $e = Horde_Icalendar_Vcard::getBareEmail($item['value']);
2394                    $hash['homeEmail'] = $e ? $e : '';
2395                    $email_set = true;
2396                } elseif (isset($item['params']['WORK']) &&
2397                          !empty($this->map['workEmail']) &&
2398                          (!isset($hash['workEmail']) ||
2399                           isset($item['params']['PREF']))) {
2400                    $e = Horde_Icalendar_Vcard::getBareEmail($item['value']);
2401                    $hash['workEmail'] = $e ? $e : '';
2402                    $email_set = true;
2403                } elseif (isset($item['params']['TYPE'])) {
2404                    if (!is_array($item['params']['TYPE'])) {
2405                        $item['params']['TYPE'] = array($item['params']['TYPE']);
2406                    }
2407                    foreach ($item['params']['TYPE'] as &$type) {
2408                        $type = Horde_String::upper($type);
2409                    }
2410                    if (in_array('HOME', $item['params']['TYPE']) &&
2411                        !empty($this->map['homeEmail']) &&
2412                        (!isset($hash['homeEmail']) ||
2413                         in_array('PREF', $item['params']['TYPE']))) {
2414                        $e = Horde_Icalendar_Vcard::getBareEmail($item['value']);
2415                        $hash['homeEmail'] = $e ? $e : '';
2416                        $email_set = true;
2417                    } elseif (in_array('WORK', $item['params']['TYPE']) &&
2418                              !empty($this->map['workEmail']) &&
2419                              (!isset($hash['workEmail']) ||
2420                         in_array('PREF', $item['params']['TYPE']))) {
2421                        $e = Horde_Icalendar_Vcard::getBareEmail($item['value']);
2422                        $hash['workEmail'] = $e ? $e : '';
2423                        $email_set = true;
2424                    }
2425                }
2426
2427                if (!$email_set &&
2428                    (!isset($hash['email']) ||
2429                     isset($item['params']['PREF']) ||
2430                     (!empty($item['params']['TYPE']) && is_array($item['params']['TYPE']) && in_array('PREF', $item['params']['TYPE'])))) {
2431                    $e = Horde_Icalendar_Vcard::getBareEmail($item['value']);
2432                    $hash['email'] = $e ? $e : '';
2433                }
2434
2435                if (!isset($hash['emails'])) {
2436                    $hash['emails'] = '';
2437                }
2438                if ($e = Horde_Icalendar_Vcard::getBareEmail($item['value'])) {
2439                    if (strlen($hash['emails'])) {
2440                        $hash['emails'] .= ',';
2441                    }
2442                    $hash['emails'] .= $e;
2443                }
2444                break;
2445
2446            case 'TITLE':
2447                $hash['title'] = $item['value'];
2448                break;
2449
2450            case 'ROLE':
2451                $hash['role'] = $item['value'];
2452                break;
2453
2454            case 'ORG':
2455                // The VCARD 2.1 specification requires the presence of two
2456                // SEMI-COLON separated fields: Organizational Name and
2457                // Organizational Unit. Additional fields are optional.
2458                $hash['company'] = !empty($item['values'][0]) ? $item['values'][0] : '';
2459                $hash['department'] = !empty($item['values'][1]) ? $item['values'][1] : '';
2460                break;
2461
2462            case 'NOTE':
2463                $hash['notes'] = $item['value'];
2464                break;
2465
2466            case 'CATEGORIES':
2467                $hash['businessCategory'] = $item['value'];
2468                $hash['__tags'] = $item['values'];
2469                break;
2470
2471            case 'URL':
2472                if (isset($item['params']['HOME']) &&
2473                    !isset($hash['homeWebsite'])) {
2474                    $hash['homeWebsite'] = $item['value'];
2475                } elseif (isset($item['params']['WORK']) &&
2476                          !isset($hash['workWebsite'])) {
2477                    $hash['workWebsite'] = $item['value'];
2478                } elseif (!isset($hash['website'])) {
2479                    $hash['website'] = $item['value'];
2480                }
2481                break;
2482
2483            case 'BDAY':
2484                if (empty($item['value'])) {
2485                    $hash['birthday'] = null;
2486                } else {
2487                    $hash['birthday'] = $item['value']['year'] . '-' . $item['value']['month'] . '-' .  $item['value']['mday'];
2488                }
2489                break;
2490
2491            case 'PHOTO':
2492            case 'LOGO':
2493                if (isset($item['params']['VALUE']) &&
2494                    Horde_String::lower($item['params']['VALUE']) == 'uri') {
2495                    // No support for URIs yet.
2496                    break;
2497                }
2498                if (!isset($item['params']['ENCODING']) ||
2499                    (Horde_String::lower($item['params']['ENCODING']) != 'b' &&
2500                     Horde_String::upper($item['params']['ENCODING']) != 'BASE64')) {
2501                    // Invalid property.
2502                    break;
2503                }
2504                $type = Horde_String::lower($item['name']);
2505                $hash[$type] = base64_decode($item['value']);
2506                if (isset($item['params']['TYPE'])) {
2507                    $hash[$type . 'type'] = $item['params']['TYPE'];
2508                }
2509                break;
2510
2511            case 'X-SIP':
2512                if (isset($item['params']['POC']) &&
2513                    !isset($hash['ptt'])) {
2514                    $hash['ptt'] = $item['value'];
2515                } elseif (isset($item['params']['VOIP']) &&
2516                          !isset($hash['voip'])) {
2517                    $hash['voip'] = $item['value'];
2518                } elseif (isset($item['params']['SWIS']) &&
2519                          !isset($hash['shareView'])) {
2520                    $hash['shareView'] = $item['value'];
2521                } elseif (!isset($hash['sip'])) {
2522                    $hash['sip'] = $item['value'];
2523                }
2524                break;
2525
2526            case 'X-WV-ID':
2527                $hash['imaddress'] = $item['value'];
2528                break;
2529
2530            case 'X-ANNIVERSARY':
2531                if (empty($item['value'])) {
2532                    $hash['anniversary'] = null;
2533                } else {
2534                    $hash['anniversary'] = $item['value']['year'] . '-' . $item['value']['month'] . '-' . $item['value']['mday'];
2535                }
2536                break;
2537
2538            case 'X-CHILDREN':
2539                $hash['children'] = $item['value'];
2540                break;
2541
2542            case 'X-SPOUSE':
2543                $hash['spouse'] = $item['value'];
2544                break;
2545            }
2546        }
2547
2548        /* Ensure we have a valid name field. */
2549        $hash = $this->_parseName($hash);
2550
2551        return $hash;
2552    }
2553
2554    /**
2555     * Convert the contact to an ActiveSync contact message
2556     *
2557     * @param Turba_Object $object  The turba object to convert
2558     * @param array $options        Options:
2559     *   - protocolversion: (float)  The EAS version to support
2560     *                      DEFAULT: 2.5
2561     *   - bodyprefs: (array)  A BODYPREFERENCE array.
2562     *                DEFAULT: none (No body prefs enforced).
2563     *   - truncation: (integer)  Truncate event body to this length
2564     *                 DEFAULT: none (No truncation).
2565     *   - device: (Horde_ActiveSync_Device) The device object.
2566     *
2567     * @return Horde_ActiveSync_Message_Contact
2568     */
2569    public function toASContact(Turba_Object $object, array $options = array())
2570    {
2571        global $injector;
2572
2573        $message = new Horde_ActiveSync_Message_Contact(array(
2574            'logger' => $injector->getInstance('Horde_Log_Logger'),
2575            'protocolversion' => $options['protocolversion'],
2576            'device' => !empty($options['device']) ? $options['device'] : null
2577        ));
2578        $hash = $object->getAttributes();
2579        if (!isset($hash['lastname']) && isset($hash['name'])) {
2580            $this->_guessName($hash);
2581        }
2582
2583        // Ensure we have at least a good guess as to separate address fields.
2584        // Not ideal, but EAS does not have a single "address" field so we must
2585        // map "common" to either home or work. I choose home since
2586        // work/non-personal installs will be more likely to have separated
2587        // address fields.
2588        if (!empty($hash['commonAddress'])) {
2589            if (!isset($hash['commonStreet'])) {
2590                $hash['commonStreet'] = $hash['commonHome'];
2591            }
2592            foreach (array('Address', 'Street', 'POBox', 'Extended', 'City', 'Province', 'PostalCode', 'Country') as $field) {
2593                $hash['home' . $field] = $hash['common' . $field];
2594            }
2595        } else {
2596            if (isset($hash['homeAddress']) && !isset($hash['homeStreet'])) {
2597                $hash['homeStreet'] = $hash['homeAddress'];
2598            }
2599            if (isset($hash['workAddress']) && !isset($hash['workStreet'])) {
2600                $hash['workStreet'] = $hash['workAddress'];
2601            }
2602        }
2603
2604        $hooks = $injector->getInstance('Horde_Core_Hooks');
2605        $decode_hook = $hooks->hookExists('decode_attribute', 'turba');
2606
2607        foreach ($hash as $field => $value) {
2608            if ($decode_hook) {
2609                try {
2610                    $value = $hooks->callHook(
2611                        'decode_attribute',
2612                        'turba',
2613                        array($field, $value, $object)
2614                    );
2615                } catch (Turba_Exception $e) {
2616                    Horde::log($e);
2617                }
2618            }
2619            if (isset(self::$_asMap[$field])) {
2620                try {
2621                    $message->{self::$_asMap[$field]} = $value;
2622                } catch (InvalidArgumentException $e) {
2623                }
2624                continue;
2625            }
2626
2627            switch ($field) {
2628            case 'photo':
2629                $message->picture = base64_encode($value);
2630                break;
2631
2632            case 'homeCountry':
2633                $message->homecountry = !empty($hash['homeCountryFree'])
2634                    ? $hash['homeCountryFree']
2635                    : !empty($hash['homeCountry'])
2636                        ? Horde_Nls::getCountryISO($hash['homeCountry'])
2637                        : null;
2638                break;
2639
2640            case 'otherCountry':
2641                $message->othercountry = !empty($hash['otherCountryFree'])
2642                    ? $hash['otherCountryFree']
2643                    : !empty($hash['otherCountry'])
2644                        ? Horde_Nls::getCountryISO($hash['otherCountry'])
2645                        : null;
2646                break;
2647
2648            case 'workCountry':
2649                $message->businesscountry = !empty($hash['workCountryFree'])
2650                    ? $hash['workCountryFree']
2651                    : !empty($hash['workCountry'])
2652                        ? Horde_Nls::getCountryISO($hash['workCountry'])
2653                        : null;
2654                break;
2655
2656            case 'email':
2657                $message->email1address = $value;
2658                break;
2659
2660            case 'homeEmail':
2661                $message->email2address = $value;
2662                break;
2663
2664            case 'workEmail':
2665                $message->email3address = $value;
2666                break;
2667
2668            case 'emails':
2669                $address = 1;
2670                foreach (explode(',', $value) as $email) {
2671                    while ($address <= 3 &&
2672                           $message->{'email' . $address . 'address'}) {
2673                        $address++;
2674                    }
2675                    if ($address > 3) {
2676                        break;
2677                    }
2678                    $message->{'email' . $address . 'address'} = $email;
2679                    $address++;
2680                }
2681                break;
2682
2683            case 'children':
2684                // Children FROM horde are a simple string value. Even though EAS
2685                // uses an array stucture to pass them, we pass as a single
2686                // string since we can't assure what delimter the user will
2687                // use and (at least in some languages) a comma can be used
2688                // within a full name.
2689                $message->children = array($value);
2690                break;
2691
2692            case 'notes':
2693                if (strlen($value) && $options['protocolversion'] > Horde_ActiveSync::VERSION_TWOFIVE) {
2694                    $bp = $options['bodyprefs'];
2695                    $note = new Horde_ActiveSync_Message_AirSyncBaseBody();
2696                    // No HTML supported in Turba's notes. Always use plaintext.
2697                    $note->type = Horde_ActiveSync::BODYPREF_TYPE_PLAIN;
2698                    if (isset($bp[Horde_ActiveSync::BODYPREF_TYPE_PLAIN]['truncationsize']) &&
2699                        Horde_String::length($value) > $bp[Horde_ActiveSync::BODYPREF_TYPE_PLAIN]['truncationsize']) {
2700                            $note->data = Horde_String::substr($value, 0, $bp[Horde_ActiveSync::BODYPREF_TYPE_PLAIN]['truncationsize']);
2701                            $note->truncated = 1;
2702                    } else {
2703                        $note->data = $value;
2704                    }
2705                    $note->estimateddatasize = Horde_String::length($value);
2706                    $message->airsyncbasebody = $note;
2707                } elseif (strlen($value)) {
2708                    // EAS 2.5
2709                    $message->body = $value;
2710                    $message->bodysize = strlen($message->body);
2711                    $message->bodytruncated = 0;
2712                }
2713                break;
2714
2715            case 'birthday':
2716            case 'anniversary':
2717                if (!empty($value) && $value != '0000-00-00') {
2718                    try {
2719                        $date = new Horde_Date($value);
2720                    } catch (Horde_Date_Exception $e) {
2721                        $message->$field = null;
2722                    }
2723                    // Some sanity checking to make sure the date was
2724                    // successfully parsed.
2725                    if ($date->month != 0) {
2726                        $message->$field = $date;
2727                    } else {
2728                        $message->$field = null;
2729                    }
2730                } else {
2731                    $message->$field = null;
2732                }
2733                break;
2734            }
2735        }
2736
2737        /* Get tags. */
2738        $message->categories = $injector->getInstance('Turba_Tagger')
2739            ->split($object->getValue('__tags'));
2740
2741        if (empty($this->fileas)) {
2742            $message->fileas = Turba::formatName($object);
2743        }
2744
2745        return $message;
2746    }
2747
2748    /**
2749     * Convert an ActiveSync contact message into a hash suitable for
2750     * importing via self::add().
2751     *
2752     * @param Horde_ActiveSync_Message_Contact $message  The contact message
2753     *                                                   object.
2754     *
2755     * @return array  A contact hash.
2756     */
2757    public function fromASContact(Horde_ActiveSync_Message_Contact $message)
2758    {
2759        $hash = array();
2760
2761        foreach (self::$_asMap as $turbaField => $asField) {
2762            if (!$message->isGhosted($asField)) {
2763                try {
2764                    $hash[$turbaField] = $message->{$asField};
2765                } catch (InvalidArgumentException $e) {
2766                }
2767            }
2768        }
2769
2770        // Try our best to get a name attribute;
2771        $hash = $this->_parseName($hash);
2772
2773        /* Requires special handling */
2774
2775        try {
2776            if ($message->getProtocolVersion() >= Horde_ActiveSync::VERSION_TWELVE) {
2777                if (!empty($message->airsyncbasebody)) {
2778                    $hash['notes'] = $message->airsyncbasebody->data;
2779                }
2780            } else {
2781                $hash['notes'] = $message->body;
2782            }
2783        } catch (InvalidArgumentException $e) {}
2784
2785        // picture ($message->picture *should* already be base64 encdoed)
2786        if (!$message->isGhosted('picture')) {
2787            $hash['photo'] = base64_decode($message->picture);
2788        }
2789
2790        /* Email addresses */
2791        $hash['emails'] = array();
2792        if (!$message->isGhosted('email1address')) {
2793            $e = Horde_Icalendar_Vcard::getBareEmail($message->email1address);
2794            $hash['emails'][] = $hash['email'] = $e ? $e : '';
2795
2796        }
2797        if (!$message->isGhosted('email2address')) {
2798            $e = Horde_Icalendar_Vcard::getBareEmail($message->email2address);
2799            $hash['emails'][] = $hash['homeEmail'] = $e ? $e : '';
2800        }
2801        if (!$message->isGhosted('email3address')) {
2802            $e = Horde_Icalendar_Vcard::getBareEmail($message->email3address);
2803            $hash['emails'][] = $hash['workEmail'] = $e ? $e : '';
2804        }
2805        $hash['emails'] = implode(',', $hash['emails']);
2806
2807        /* Categories */
2808        if (!$message->isGhosted('categories') && empty($message->categories)) {
2809            $hash['__tags'] = array();
2810        } elseif (is_array($message->categories) &&
2811                  count($message->categories)) {
2812            $hash['__tags'] = $message->categories;
2813        }
2814
2815        /* Children */
2816        if (is_array($message->children) && count($message->children)) {
2817            // We use a comma as incoming delimiter as it's the most
2818            // common even though it might be used withing a name string.
2819            $hash['children'] = implode(', ', $message->children);
2820        } elseif (!$message->isGhosted('children')) {
2821            $hash['children'] = '';
2822        }
2823
2824        /* Birthday and Anniversary */
2825        if (!empty($message->birthday)) {
2826            $bday = new Horde_Date($message->birthday);
2827            $bday->setTimezone(date_default_timezone_get());
2828            $hash['birthday'] = $bday->format('Y-m-d');
2829        } elseif (!$message->isGhosted('birthday')) {
2830            $hash['birthday'] = '';
2831        }
2832        if (!empty($message->anniversary)) {
2833            $anniversary = new Horde_Date($message->anniversary);
2834            $anniversary->setTimezone(date_default_timezone_get());
2835            $hash['anniversary'] = $anniversary->format('Y-m-d');
2836        } elseif (!$message->isGhosted('anniversary')) {
2837            $hash['anniversary'] = '';
2838        }
2839
2840        /* Countries */
2841        include 'Horde/Nls/Countries.php';
2842        if (!empty($message->homecountry)) {
2843            if (!empty($this->map['homeCountryFree'])) {
2844                $hash['homeCountryFree'] = $message->homecountry;
2845            } else {
2846                $country = array_search($message->homecountry, $countries);
2847                if ($country === false) {
2848                    $country = $message->homecountry;
2849                }
2850                $hash['homeCountry'] = $country;
2851            }
2852        } elseif (!$message->isGhosted('homecountry')) {
2853            $hash['homeCountry'] = '';
2854        }
2855
2856        if (!empty($message->businesscountry)) {
2857            if (!empty($this->map['workCountryFree'])) {
2858                $hash['workCountryFree'] = $message->businesscountry;
2859            } else {
2860                $country = array_search($message->businesscountry, $countries);
2861                if ($country === false) {
2862                    $country = $message->businesscountry;
2863                }
2864                $hash['workCountry'] = $country;
2865            }
2866        } elseif (!$message->isGhosted('businesscountry')) {
2867            $hash['workCountry'] = '';
2868        }
2869
2870        if (!empty($message->othercountry)) {
2871            if (!empty($this->map['otherCountryFree'])) {
2872                $hash['otherCountryFree'] = $message->othercountry;
2873            } else {
2874                $country = array_search($message->othercountry, $countries);
2875                if ($country === false) {
2876                    $country = $message->othercountry;
2877                }
2878                $hash['otherCountry'] = $country;
2879            }
2880        } elseif (!$message->isGhosted('othercountry')) {
2881            $hash['otherCountry'] = '';
2882        }
2883
2884        return $hash;
2885    }
2886
2887    /**
2888     * Checks $hash for the presence of a 'name' attribute. If not found,
2889     * attempt to build one from other available values.
2890     *
2891     * @param  array $hash  A hash of turba attributes.
2892     *
2893     * @return array  Hash of Turba attributes, with the 'name' attribute
2894     *                populated.
2895     */
2896    protected function _parseName(array $hash)
2897    {
2898        if (empty($hash['name'])) {
2899            /* If name is a composite field, it won't be present in the
2900             * $this->fields array, so check for that as well. */
2901            if (isset($this->map['name']) &&
2902                is_array($this->map['name']) &&
2903                !empty($this->map['name']['attribute'])) {
2904                $fieldarray = array();
2905                foreach ($this->map['name']['fields'] as $mapfields) {
2906                    $fieldarray[] = isset($hash[$mapfields]) ?
2907                        $hash[$mapfields] : '';
2908                }
2909                $hash['name'] = Turba::formatCompositeField($this->map['name']['format'], $fieldarray);
2910            } else {
2911                $hash['name'] = isset($hash['firstname']) ? $hash['firstname'] : '';
2912                if (!empty($hash['lastname'])) {
2913                    $hash['name'] .= ' ' . $hash['lastname'];
2914                }
2915                $hash['name'] = trim($hash['name']);
2916            }
2917        }
2918
2919        return $hash;
2920    }
2921
2922    /**
2923     * Checks if the current user has the requested permissions on this
2924     * address book.
2925     *
2926     * @param integer $perm  The permission to check for.
2927     *
2928     * @return boolean  True if the user has permission, otherwise false.
2929     */
2930    public function hasPermission($perm)
2931    {
2932        $perms = $GLOBALS['injector']->getInstance('Horde_Perms');
2933        return $perms->exists('turba:sources:' . $this->_name)
2934            ? $perms->hasPermission('turba:sources:' . $this->_name, $GLOBALS['registry']->getAuth(), $perm)
2935            // Assume we have permissions if they're not explicitly set.
2936            : true;
2937    }
2938
2939    /**
2940     * Return the name of this address book.
2941     * (This is the key into the cfgSources array)
2942     *
2943     * @string Address book name
2944     */
2945    public function getName()
2946    {
2947        return $this->_name;
2948    }
2949
2950    /**
2951     * Return the owner to use when searching or creating contacts in
2952     * this address book.
2953     *
2954     * @return string  Contact owner.
2955     */
2956    public function getContactOwner()
2957    {
2958        return empty($this->_contact_owner)
2959            ? $this->_getContactOwner()
2960            : $this->_contact_owner;
2961    }
2962
2963    /**
2964     * Override the contactOwner setting for this driver.
2965     *
2966     * @param string $owner  The contact owner.
2967     */
2968    public function setContactOwner($owner)
2969    {
2970        $this->_contact_owner = $owner;
2971    }
2972
2973    /**
2974     * Override the name setting for this driver.
2975     *
2976     * @param string $name  The source name. This is the key into the
2977     *                      $cfgSources array.
2978     */
2979    public function setSourceName($name)
2980    {
2981        $this->_name = $name;
2982    }
2983
2984    /**
2985     * Return the owner to use when searching or creating contacts in
2986     * this address book.
2987     *
2988     * @return string  Contact owner.
2989     */
2990    protected function _getContactOwner()
2991    {
2992        return $GLOBALS['registry']->getAuth();
2993    }
2994
2995    /**
2996     * Creates a new Horde_Share for this source type.
2997     *
2998     * @param string $share_name  The share name
2999     * @param array  $params      The params for the share.
3000     *
3001     * @return Horde_Share  The share object.
3002     */
3003    public function createShare($share_name, array $params)
3004    {
3005        // If the raw address book name is not set, use the share name
3006        if (empty($params['params']['name'])) {
3007            $params['params']['name'] = $share_name;
3008        }
3009
3010        return Turba::createShare($share_name, $params);
3011    }
3012
3013    /**
3014     * Runs any actions after setting a new default tasklist.
3015     *
3016     * @param string $share  The default share ID.
3017     */
3018    public function setDefaultShare($share)
3019    {
3020    }
3021
3022    /**
3023     * Creates an object key for a new object.
3024     *
3025     * @param array $attributes  The attributes (in driver keys) of the
3026     *                           object being added.
3027     *
3028     * @return string  A unique ID for the new object.
3029     */
3030    protected function _makeKey(array $attributes)
3031    {
3032        return hash('md5', mt_rand());
3033    }
3034
3035    /**
3036     * Creates an object UID for a new object.
3037     *
3038     * @return string  A unique ID for the new object.
3039     */
3040    protected function _makeUid()
3041    {
3042        return strval(new Horde_Support_Guid());
3043    }
3044
3045    /**
3046     * Initialize the driver.
3047     *
3048     * @throws Turba_Exception
3049     */
3050    protected function _init()
3051    {
3052    }
3053
3054    /**
3055     * Searches the address book with the given criteria and returns a
3056     * filtered list of results. If the criteria parameter is an empty array,
3057     * all records will be returned.
3058     *
3059     * @param array $criteria       Array containing the search criteria.
3060     * @param array $fields         List of fields to return.
3061     * @param array $blobFields     Array of fields containing binary data.
3062     * @param boolean $count_only   Only return the count of matching entries,
3063     *                              not the entries themselves.
3064     *
3065     * @return array  Hash containing the search results.
3066     * @throws Turba_Exception
3067     */
3068    protected function _search(array $criteria, array $fields, array $blobFields = array(), $count_only = false)
3069    {
3070        throw new Turba_Exception(_("Searching is not available."));
3071    }
3072
3073    /**
3074     * Reads the given data from the address book and returns the results.
3075     *
3076     * @param string $key        The primary key field to use.
3077     * @param mixed $ids         The ids of the contacts to load.
3078     * @param string $owner      Only return contacts owned by this user.
3079     * @param array $fields      List of fields to return.
3080     * @param array $blobFields  Array of fields containing binary data.
3081     * @param array $dateFields  Array of fields containing date data.
3082     *                           @since 4.2.0
3083     *
3084     * @return array  Hash containing the search results.
3085     * @throws Turba_Exception
3086     */
3087    protected function _read($key, $ids, $owner, array $fields,
3088                             array $blobFields = array(),
3089                             array $dateFields = array())
3090    {
3091        throw new Turba_Exception(_("Reading contacts is not available."));
3092    }
3093
3094    /**
3095     * Adds the specified contact to the addressbook.
3096     *
3097     * @param array $attributes  The attribute values of the contact.
3098     * @param array $blob_fields  Fields that represent binary data.
3099     * @param array $date_fields  Fields that represent dates. @since 4.2.0
3100     *
3101     * @throws Turba_Exception
3102     */
3103    protected function _add(array $attributes, array $blob_fields = array(), array $date_fields = array())
3104    {
3105        throw new Turba_Exception(_("Adding contacts is not available."));
3106    }
3107
3108    /**
3109     * Deletes the specified contact from the addressbook.
3110     *
3111     * @param string $object_key TODO
3112     * @param string $object_id  TODO
3113     *
3114     * @throws Turba_Exception
3115     */
3116    protected function _delete($object_key, $object_id)
3117    {
3118        throw new Turba_Exception(_("Deleting contacts is not available."));
3119    }
3120
3121    /**
3122     * Saves the specified object in the SQL database.
3123     *
3124     * @param Turba_Object $object  The object to save
3125     *
3126     * @return string  The object id, possibly updated.
3127     * @throws Turba_Exception
3128     */
3129    protected function _save(Turba_Object $object)
3130    {
3131        throw new Turba_Exception(_("Saving contacts is not available."));
3132    }
3133
3134    /**
3135     * Remove all entries owned by the specified user.
3136     *
3137     * @param string $user  The user's data to remove.
3138     *
3139     * @throws Turba_Exception
3140     */
3141    public function removeUserData($user)
3142    {
3143        throw new Turba_Exception_NotSupported(_("Removing user data is not supported in the current address book storage driver."));
3144    }
3145
3146    /**
3147     * Check if the passed in share is the default share for this source.
3148     *
3149     * @param Horde_Share_Object $share  The share object.
3150     * @param array $srcconfig           The cfgSource entry for the share.
3151     *
3152     * @return boolean TODO
3153     */
3154    public function checkDefaultShare(Horde_Share_Object $share, array $srcconfig)
3155    {
3156        $params = @unserialize($share->get('params'));
3157        if (!isset($params['default'])) {
3158            $params['default'] = ($params['name'] == $GLOBALS['registry']->getAuth());
3159            $share->set('params', serialize($params));
3160            $share->save();
3161        }
3162
3163        return $params['default'];
3164    }
3165
3166    /* Countable methods. */
3167
3168    /**
3169     * Returns the number of contacts of the current user in this address book.
3170     *
3171     * @return integer  The number of contacts that the user owns.
3172     * @throws Turba_Exception
3173     */
3174    public function count()
3175    {
3176        if (is_null($this->_count)) {
3177            $this->_count = count(
3178                $this->_search(array('AND' => array(
3179                                   array('field' => $this->toDriver('__owner'),
3180                                         'op' => '=',
3181                                         'test' => $this->getContactOwner()))),
3182                               array($this->toDriver('__key')))
3183            );
3184        }
3185
3186        return $this->_count;
3187    }
3188
3189    /**
3190     * Helper function for guessing name parts from a single name string.
3191     *
3192     * @param array $hash  The attributes array.
3193     */
3194    protected function _guessName(&$hash)
3195    {
3196        if (($pos = strpos($hash['name'], ',')) !== false) {
3197            // Assume Last, First
3198            $hash['lastname'] = Horde_String::substr($hash['name'], 0, $pos);
3199            $hash['firstname'] = trim(Horde_String::substr($hash['name'], $pos + 1));
3200        } elseif (($pos = Horde_String::rpos($hash['name'], ' ')) !== false) {
3201            // Assume everything after last space as lastname
3202            $hash['lastname'] = trim(Horde_String::substr($hash['name'], $pos + 1));
3203            $hash['firstname'] = Horde_String::substr($hash['name'], 0, $pos);
3204        } else {
3205            $hash['lastname'] = $hash['name'];
3206            $hash['firstname'] = '';
3207        }
3208    }
3209
3210}
3211