<?php
/**
 * Copyright 2000-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file LICENSE for license information (ASL).  If you did
 * did not receive this file, see http://www.horde.org/licenses/apache.
 *
 * @category  Horde
 * @copyright 2000-2017 Horde LLC
 * @license   http://www.horde.org/licenses/apache ASL
 * @package   Turba
 */

/**
 * Provides a common abstracted interface to the various directory search
 * drivers.  It includes functions for searching, adding, removing, and
 * modifying directory entries.
 *
 * @author    Chuck Hagenbuch <chuck@horde.org>
 * @author    Jon Parise <jon@csh.rit.edu>
 * @category  Horde
 * @copyright 2000-2017 Horde LLC
 * @license   http://www.horde.org/licenses/apache ASL
 * @package   Turba
 */
class Turba_Driver implements Countable
{
    /**
     * The symbolic title of this source.
     *
     * @var string
     */
    public $title;

    /**
     * Hash describing the mapping between Turba attributes and
     * driver-specific fields.
     *
     * @var array
     */
    public $map = array();

    /**
     * Hash with all tabs and their fields.
     *
     * @var array
     */
    public $tabs = array();

    /**
     * List of all fields that can be accessed in the backend (excludes
     * composite attributes, etc.).
     *
     * @var array
     */
    public $fields = array();

    /**
     * Array of fields that must match exactly.
     *
     * @var array
     */
    public $strict = array();

    /**
     * Array of fields to search "approximately" (@see
     * config/backends.php).
     *
     * @var array
     */
    public $approximate = array();

    /**
     * The name of a field to store contact list names in if not the default.
     *
     * @var string
     */
    public $listNameField = null;

    /**
     * The name of a field to use as an alternative to the name field if that
     * one is empty.
     *
     * @var string
     */
    public $alternativeName = null;

    /**
     * The internal name of this source.
     *
     * @var string
     */
    protected $_name;

    /**
     * Hash holding the driver's additional parameters.
     *
     * @var array
     */
    protected $_params = array();

    /**
     * What can this backend do?
     *
     * @var array
     */
    protected $_capabilities = array();

    /**
     * Any additional options passed to Turba_Object constructors.
     *
     * @var array
     */
    protected $_objectOptions = array();

    /**
     * Number of contacts in this source.
     *
     * @var integer
     */
    protected $_count = null;

    /**
     * Hold the value for the owner of this address book.
     *
     * @var string
     */
    protected $_contact_owner = '';

    /**
     * Mapping of Turba attributes to ActiveSync fields.
     *
     * @var array
     */
    static protected $_asMap = array(
        'name' => 'fileas',
        'lastname' => 'lastname',
        'firstname' => 'firstname',
        'middlenames' => 'middlename',
        'alias' => 'nickname',
        'nickname' => 'nickname',
        'namePrefix' => 'title',
        'nameSuffix' => 'suffix',
        'homeStreet' => 'homestreet',
        'homeCity' => 'homecity',
        'homeProvince' => 'homestate',
        'homePostalCode' => 'homepostalcode',
        'homeCountryFree' => 'homecountry',
        'otherStreet' => 'otherstreet',
        'otherCity' => 'othercity',
        'otherProvince' => 'otherstate',
        'otherPostalCode' => 'otherpostalcode',
        'otherCountryFree' => 'othercountry',
        'workStreet' => 'businessstreet',
        'workCity' => 'businesscity',
        'workProvince' => 'businessstate',
        'workPostalCode' => 'businesspostalcode',
        'workCountryFree' => 'businesscountry',
        'title' => 'jobtitle',
        'company' => 'companyname',
        'department' => 'department',
        'office' => 'officelocation',
        'spouse' => 'spouse',
        'website' => 'webpage',
        'assistant' => 'assistantname',
        'manager' => 'managername',
        'yomifirstname' => 'yomifirstname',
        'yomilastname' => 'yomilastname',
        'imaddress' => 'imaddress',
        'imaddress2' => 'imaddress2',
        'imaddress3' => 'imaddress3',
        'homePhone' => 'homephonenumber',
        'homePhone2' => 'home2phonenumber',
        'workPhone' => 'businessphonenumber',
        'workPhone2' => 'business2phonenumber',
        'fax' => 'businessfaxnumber',
        'homeFax' => 'homefaxnumber',
        'pager' => 'pagernumber',
        'cellPhone' => 'mobilephonenumber',
        'carPhone' => 'carphonenumber',
        'assistPhone' => 'assistnamephonenumber',
        'companyPhone' => 'companymainphone',
        'radioPhone' => 'radiophonenumber'
    );

    /**
     * Constructs a new Turba_Driver object.
     *
     * @param string $name   Source name
     * @param array $params  Hash containing additional configuration
     *                       parameters.
     */
    public function __construct($name = '', array $params = array())
    {
        $this->_name = $name;
        $this->_params = $params;
    }

    /**
     * Returns the current driver's additional parameters.
     *
     * @return array  Hash containing the driver's additional parameters.
     */
    public function getParams()
    {
        return $this->_params;
    }

    /**
     * Checks if this backend has a certain capability.
     *
     * @param string $capability  The capability to check for.
     *
     * @return boolean  Supported or not.
     */
    public function hasCapability($capability)
    {
        return !empty($this->_capabilities[$capability]);
    }

    /**
     * Returns the attributes that are blob types.
     *
     * @return array  List of blob attributes in the array keys.
     */
    public function getBlobs()
    {
        global $attributes;

        $blobs = array();
        foreach (array_keys($this->fields) as $attribute) {
            if (isset($attributes[$attribute]) &&
                $attributes[$attribute]['type'] == 'image') {
                $blobs[$attribute] = true;
            }
        }

        return $blobs;
    }

    /**
     *  Returns the attributes that represent dates.
     *
     * @return array List of date attributes in the array keys.
     * @since 4.2.0
     */
    public function getDateFields()
    {
        global $attributes;

        $dates = array();
        foreach (array_keys($this->fields) as $attribute) {
            if (isset($attributes[$attribute]) &&
                $attributes[$attribute]['type'] == 'monthdayyear') {
                $dates[$attribute] = '0000-00-00';
            }
        }

        return $dates;
    }

    /**
     * Translates the keys of the first hash from the generalized Turba
     * attributes to the driver-specific fields. The translation is based on
     * the contents of $this->map.
     *
     * @param array $hash  Hash using Turba keys.
     *
     * @return array  Translated version of $hash.
     */
    public function toDriverKeys(array $hash)
    {
        if (!empty($hash['name']) &&
            !empty($this->listNameField) &&
            !empty($hash['__type']) &&
            is_array($this->map['name']) &&
            ($hash['__type'] == 'Group')) {
            $hash[$this->listNameField] = $hash['name'];
            unset($hash['name']);
        }

        // Add composite fields to $hash if at least one field part exists
        // and the composite field will be saved to storage.
        // Otherwise composite fields won't be computed during an import.
        foreach ($this->map as $key => $val) {
            if (!is_array($val) ||
                empty($this->map[$key]['attribute']) ||
                array_key_exists($key, $hash)) {
                continue;
            }

            foreach ($this->map[$key]['fields'] as $mapfields) {
                if (isset($hash[$mapfields])) {
                    // Add composite field
                    $hash[$key] = null;
                    break;
                }
            }
        }

        $fields = array();
        foreach ($hash as $key => $val) {
            if (!isset($this->map[$key])) {
                continue;
            }
            if (!is_array($this->map[$key])) {
                $fields[$this->map[$key]] = $val;
            } elseif (!empty($this->map[$key]['attribute'])) {
                $fieldarray = array();
                foreach ($this->map[$key]['fields'] as $mapfields) {
                    $fieldarray[] = isset($hash[$mapfields])
                        ? $hash[$mapfields]
                        : '';
                }
                $fields[$this->map[$key]['attribute']] = Turba::formatCompositeField($this->map[$key]['format'], $fieldarray);
            } else {
                // If 'parse' is not specified, use 'format' and 'fields'.
                if (!isset($this->map[$key]['parse'])) {
                    $this->map[$key]['parse'] = array(
                        array(
                            'format' => $this->map[$key]['format'],
                            'fields' => $this->map[$key]['fields']
                        )
                    );
                }
                foreach ($this->map[$key]['parse'] as $parse) {
                    $splitval = sscanf($val, $parse['format']);
                    $count = 0;
                    $tmp_fields = array();
                    foreach ($parse['fields'] as $mapfield) {
                        if (isset($hash[$mapfield])) {
                            // If the compositing fields are set
                            // individually, then don't set them at all.
                            break 2;
                        }
                        $tmp_fields[$this->map[$mapfield]] = $splitval[$count++];
                    }
                    // Exit if we found the best match.
                    if ($splitval[$count - 1] !== null) {
                        break;
                    }
                }
                $fields = array_merge($fields, $tmp_fields);
            }
        }

        return $fields;
    }

    /**
     * Takes a hash of Turba key => search value and return a (possibly
     * nested) array, using backend attribute names, that can be turned into a
     * search by the driver. The translation is based on the contents of
     * $this->map, and includes nested OR searches for composite fields.
     *
     * @param array  $criteria      Hash of criteria using Turba keys.
     * @param string $search_type   OR search or AND search?
     * @param array  $strict        Fields that must be matched exactly.
     * @param boolean $match_begin  Whether to match only at beginning of
     *                              words.
     * @param array $custom_strict  Custom set of fields that are to matched
     *                              exactly, but are glued using $search_type
     *                              and 'AND' together with $strict fields.
     *                              Allows an 'OR' search pm a custom set of
     *                              $strict fields.
     *
     * @return array  An array of search criteria.
     */
    public function makeSearch($criteria, $search_type, array $strict,
                               $match_begin = false, array $custom_strict = array())
    {
        $search = $search_terms = $subsearch = $strict_search = array();
        $glue = $temp = '';
        $lastChar = '\"';
        $blobs = $this->getBlobs();

        foreach ($criteria as $key => $val) {
            if (!isset($this->map[$key])) {
                continue;
            }
            if (is_array($this->map[$key])) {
                /* Composite field, break out the search terms. */
                $parts = explode(' ', $val);
                if (count($parts) > 1) {
                    /* Only parse if there was more than 1 search term and
                     * 'AND' the cumulative subsearches. */
                    for ($i = 0; $i < count($parts); ++$i) {
                        $term = $parts[$i];
                        $firstChar = substr($term, 0, 1);
                        if ($firstChar == '"') {
                            $temp = substr($term, 1, strlen($term) - 1);
                            $done = false;
                            while (!$done && $i < count($parts) - 1) {
                                $lastChar = substr($parts[$i + 1], -1);
                                if ($lastChar == '"') {
                                    $temp .= ' ' . substr($parts[$i + 1], 0, -1);
                                    $done = true;
                                } else {
                                    $temp .= ' ' . $parts[$i + 1];
                                }
                                ++$i;
                            }
                            $search_terms[] = $temp;
                        } else {
                            $search_terms[] = $term;
                        }
                    }
                    $glue = 'AND';
                } else {
                    /* If only one search term, use original input and
                       'OR' the searces since we're only looking for 1
                       term in any of the composite fields. */
                    $search_terms[0] = $val;
                    $glue = 'OR';
                }

                foreach ($this->map[$key]['fields'] as $field) {
                    if (!empty($blobs[$field])) {
                        continue;
                    }
                    $field = $this->toDriver($field);
                    if (!empty($strict[$field])) {
                        /* For strict matches, use the original search
                         * vals. */
                        $strict_search[] = array(
                            'field' => $field,
                            'op' => '=',
                            'test' => $val,
                        );
                    } elseif (!empty($custom_strict[$field])) {
                        $search[] = array(
                            'field' => $field,
                            'op' => '=',
                            'test' => $val,
                        );
                    } else {
                        /* Create a subsearch for each individual search
                         * term. */
                        if (count($search_terms) > 1) {
                            /* Build the 'OR' search for each search term
                             * on this field. */
                            $atomsearch = array();
                            for ($i = 0; $i < count($search_terms); ++$i) {
                                $atomsearch[] = array(
                                    'field' => $field,
                                    'op' => 'LIKE',
                                    'test' => $search_terms[$i],
                                    'begin' => $match_begin,
                                    'approximate' => !empty($this->approximate[$field]),
                                );
                            }
                            $atomsearch[] = array(
                                'field' => $field,
                                'op' => '=',
                                'test' => '',
                                'begin' => $match_begin,
                                'approximate' => !empty($this->approximate[$field])
                            );

                            $subsearch[] = array('OR' => $atomsearch);
                            unset($atomsearch);
                            $glue = 'AND';
                        } else {
                            /* $parts may have more than one element, but
                             * if they are all quoted we will only have 1
                             * $subsearch. */
                            $subsearch[] = array(
                                'field' => $field,
                                'op' => 'LIKE',
                                'test' => $search_terms[0],
                                'begin' => $match_begin,
                                'approximate' => !empty($this->approximate[$field]),
                            );
                            $glue = 'OR';
                        }
                    }
                }
                if (count($subsearch)) {
                    $search[] = array($glue => $subsearch);
                }
            } else {
                /* Not a composite field. */
                if (!empty($blobs[$key])) {
                    continue;
                }
                if (!empty($strict[$this->map[$key]])) {
                    $strict_search[] = array(
                        'field' => $this->map[$key],
                        'op' => '=',
                        'test' => $val,
                    );
                } elseif (!empty($custom_strict[$this->map[$key]])) {
                    $search[] = array(
                        'field' => $this->map[$key],
                        'op' => '=',
                        'test' => $val,
                    );
                } else {
                    $search[] = array(
                        'field' => $this->map[$key],
                        'op' => 'LIKE',
                        'test' => $val,
                        'begin' => $match_begin,
                        'approximate' => !empty($this->approximate[$this->map[$key]]),
                    );
                }
            }
        }

        if (count($strict_search) && count($search)) {
            return array(
                'AND' => array(
                    $search_type => $strict_search,
                    array(
                        $search_type => $search
                    )
                )
            );
        } elseif (count($strict_search)) {
            return array(
                $search_type => $strict_search
            );
        } elseif (count($search)) {
            return array(
                $search_type => $search
            );
        }

        return array();
    }

    /**
     * Translates a single Turba attribute to the driver-specific
     * counterpart. The translation is based on the contents of
     * $this->map. This ignores composite fields.
     *
     * @param string $attribute  The Turba attribute to translate.
     *
     * @return string  The driver name for this attribute.
     */
    public function toDriver($attribute)
    {
        if (!isset($this->map[$attribute])) {
            return null;
        }

        return is_array($this->map[$attribute])
            ? $this->map[$attribute]['fields']
            : $this->map[$attribute];
    }

    /**
     * Translates a hash from being keyed on driver-specific fields to being
     * keyed on the generalized Turba attributes. The translation is based on
     * the contents of $this->map.
     *
     * @param array $entry  A hash using driver-specific keys.
     *
     * @return array  Translated version of $entry.
     */
    public function toTurbaKeys(array $entry)
    {
        $new_entry = array();
        foreach ($this->map as $key => $val) {
            if (!is_array($val)) {
                $new_entry[$key] = (isset($entry[$val]) && (!empty($entry[$val]) || (is_string($entry[$val]) && strlen($entry[$val]))))
                    ? trim($entry[$val])
                    : null;
            }
        }

        return $new_entry;
    }

    /**
     * Searches the source based on the provided criteria.
     *
     * @todo Allow $criteria to contain the comparison operator (<, =, >,
     *       'like') and modify the drivers accordingly.
     *
     * @param array $search_criteria  Hash containing the search criteria.
     * @param string $sort_order      The requested sort order which is passed
     *                                to Turba_List::sort().
     * @param string $search_type     Do an AND or an OR search (defaults to
     *                                AND).
     * @param array $return_fields    A list of fields to return; defaults to
     *                                all fields.
     * @param array $custom_strict    A list of fields that must match exactly.
     * @param boolean $match_begin    Whether to match only at beginning of
     *                                words.
     * @param boolean $count_only   Only return the count of matching entries,
     *                              not the entries themselves.
     *
     * @return mixed Turba_List|integer  The sorted, filtered list of search
     *                                   results or the number of matching
     *                                   entries (if $count_only is true).
     * @throws Turba_Exception
     */
    public function search(array $search_criteria, $sort_order = null,
                           $search_type = 'AND', array $return_fields = array(),
                           array $custom_strict = array(), $match_begin = false,
                           $count_only = false)
    {
        /* Add any fields that must match exactly for this source to the
         * $strict_fields array. */
        $strict_fields = $custom_strict_fields = array();
        foreach ($this->strict as $strict_field) {
            $strict_fields[$strict_field] = true;
        }

        /* Differentiate between provided $custom_strict fields - which honor
         * the $search_type and $strict fields which are not
         * explicitly requested as part of this search, and as such, are not
         * constrained by the requested $search_type. */
        foreach ($custom_strict as $strict_field) {
            if (isset($this->map[$strict_field])) {
                $custom_strict_fields[$this->map[$strict_field]] = true;
            }
        }

        /* Translate the Turba attributes to driver-specific attributes. */
        $fields = $this->makeSearch($search_criteria, $search_type,
                                    $strict_fields, $match_begin, $custom_strict_fields);

        /* If we are not using Horde_Share, enforce the requirement that the
         * current user must be the owner of the addressbook. */
        if (isset($this->map['__owner'])) {
            $fields = array(
                'AND' => array(
                    $fields,
                    array(
                        'field' => $this->toDriver('__owner'),
                        'op' => '=',
                        'test' => $this->getContactOwner()
                    )
                )
            );
        }

        if (in_array('email', $return_fields) &&
            !in_array('emails', $return_fields)) {
            $return_fields[] = 'emails';
        }
        if (count($return_fields)) {
            $default_fields = array('__key', '__type', '__owner', '__members', 'name');
            if ($this->alternativeName) {
                $default_fields[] = $this->alternativeName;
            }
            $return_fields_pre = array_unique(array_merge($default_fields, $return_fields));
            $return_fields = array();
            foreach ($return_fields_pre as $field) {
                $result = $this->toDriver($field);
                if (is_array($result)) {
                    foreach ($result as $composite_field) {
                        $composite_result = $this->toDriver($composite_field);
                        if ($composite_result) {
                            $return_fields[] = $composite_result;
                        }
                    }
                } elseif ($result) {
                    $return_fields[] = $result;
                }
            }
        } else {
            /* Need to force the array to be re-keyed for the (fringe) case
             * where we might have 1 DB field mapped to 2 or more Turba
             * fields */
            $return_fields = array_values(
                array_unique(array_values($this->fields)));
        }

        /* Retrieve the search results from the driver. */
        $objects = $this->_search($fields, $return_fields, $this->toDriverKeys($this->getBlobs()), $count_only);
        if ($count_only) {
            return $objects;
        }
        return $this->_toTurbaObjects($objects, $sort_order);
    }

    /**
     * Searches the current address book for duplicate entries.
     *
     * Duplicates are determined by comparing email and name or last name and
     * first name values.
     *
     * @return array  A hash with the following format:
     * <code>
     * array('name' => array('John Doe' => Turba_List, ...), ...)
     * </code>
     * @throws Turba_Exception
     */
    public function searchDuplicates()
    {
        return array();
    }

    /**
     * Takes an array of object hashes and returns a Turba_List
     * containing the correct Turba_Objects
     *
     * @param array $objects     An array of object hashes (keyed to backend).
     * @param array $sort_order  Array of hashes describing sort fields.  Each
     *                           hash has the following fields:
     * <pre>
     * ascending - (boolean) Indicating sort direction.
     * field - (string) Sort field.
     * </pre>
     *
     * @return Turba_List  A list object.
     */
    protected function _toTurbaObjects(array $objects, array $sort_order = null)
    {
        $list = new Turba_List();

        foreach ($objects as $object) {
            /* Translate the driver-specific fields in the result back to the
             * more generalized common Turba attributes using the map. */
            $object = $this->toTurbaKeys($object);

            $done = false;
            if (!empty($object['__type']) &&
                ucwords($object['__type']) != 'Object') {
                $class = 'Turba_Object_' . ucwords($object['__type']);
                if (class_exists($class)) {
                    $list->insert(new $class($this, $object, $this->_objectOptions));
                    $done = true;
                }
            }
            if (!$done) {
                $list->insert(new Turba_Object($this, $object, $this->_objectOptions));
            }
        }

        $list->sort($sort_order);

        /* Return the filtered (sorted) results. */
        return $list;
    }

    /**
     * Returns a list of birthday or anniversary hashes from this source for a
     * certain period.
     *
     * @param Horde_Date $start  The start date of the valid period.
     * @param Horde_Date $end    The end date of the valid period.
     * @param string $category   The timeObjects category to return.
     *
     * @return array  A list of timeObject hashes.
     * @throws Turba Exception
     */
    public function listTimeObjects(Horde_Date $start, Horde_Date $end, $category)
    {
        try {
            $res = $this->getTimeObjectTurbaList($start, $end, $category);
        } catch (Turba_Exception $e) {
            /* Try the default implementation before returning an error */
            $res = $this->_getTimeObjectTurbaListFallback($start, $end, $category);
        }

        $t_objects = array();
        while ($ob = $res->next()) {
            $t_object = $ob->getValue($category);
            if (empty($t_object)) {
                continue;
            }

            try {
                $t_object = new Horde_Date($t_object);
            } catch (Horde_Date_Exception $e) {
                continue;
            }

            if ($t_object->compareDate($end) > 0) {
                continue;
            }

            $t_object_end = new Horde_Date($t_object);
            ++$t_object_end->mday;
            $key = $ob->getValue('__key');

            // Calculate the age of the time object
            if ($start->year == $end->year ||
                $end->year == 9999) {
                $age = $start->year - $t_object->year;
            } elseif ($t_object->month <= $end->month) {
                // t_object must be in later year
                $age = $end->year - $t_object->year;
            } else {
                // t_object must be in earlier year
                $age = $start->year - $t_object->year;
            }

            $title = sprintf(_("%d. %s of %s"),
                             $age,
                             $GLOBALS['attributes'][$category]['label'],
                             $ob->getValue('name'));

            $t_objects[] = array(
                'id' => $key,
                'title' => $title,
                'start' => sprintf('%d-%02d-%02dT00:00:00',
                                   $t_object->year,
                                   $t_object->month,
                                   $t_object->mday),
                'end' => sprintf('%d-%02d-%02dT00:00:00',
                                 $t_object_end->year,
                                 $t_object_end->month,
                                 $t_object_end->mday),
                'recurrence' => array('type' => Horde_Date_Recurrence::RECUR_YEARLY_DATE,
                                      'interval' => 1),
                'params' => array('source' => $this->_name, 'key' => $key),
                'link' => Horde::url('contact.php', true)->add(array('source' => $this->_name, 'key' => $key))->setRaw(true)
            );
        }

        return $t_objects;
    }

    /**
     * Default implementation for obtaining a Turba_List to get TimeObjects
     * out of.
     *
     * @param Horde_Date $start  The starting date.
     * @param Horde_Date $end    The ending date.
     * @param string $field      The address book field containing the
     *                           timeObject information (birthday,
     *                           anniversary).
     *
     * @return Turba_List  A list of objects.
     * @throws Turba_Exception
     */
    public function getTimeObjectTurbaList(Horde_Date $start, Horde_Date $end, $field)
    {
        return $this->_getTimeObjectTurbaListFallback($start, $end, $field);
    }

    /**
     * Default implementation for obtaining a Turba_List to get TimeObjects
     * out of.
     *
     * @param Horde_Date $start  The starting date.
     * @param Horde_Date $end    The ending date.
     * @param string $field      The address book field containing the
     *                           timeObject information (birthday,
     *                           anniversary).
     *
     * @return Turba_List  A list of objects.
     * @throws Turba_Exception
     */
    protected function _getTimeObjectTurbaListFallback(Horde_Date $start, Horde_Date $end, $field)
    {
        return $this->search(array(), null, 'AND', array('name', $field));
    }

    /**
     * Retrieves a set of objects from the source.
     *
     * @param array $objectIds  The unique ids of the objects to retrieve.
     *
     * @return array  The array of retrieved objects (Turba_Objects).
     * @throws Turba_Exception
     * @throws Horde_Exception_NotFound
     */
    public function getObjects(array $objectIds)
    {
        $objects = $this->_read($this->map['__key'], $objectIds,
                                $this->getContactOwner(),
                                array_values($this->fields),
                                $this->toDriverKeys($this->getBlobs()),
                                $this->toDriverKeys($this->getDateFields()));
        if (!is_array($objects)) {
            throw new Horde_Exception_NotFound();
        }

        $results = array();
        foreach ($objects as $object) {
            $object = $this->toTurbaKeys($object);
            $done = false;
            if (!empty($object['__type']) &&
                ucwords($object['__type']) != 'Object') {
                $class = 'Turba_Object_' . ucwords($object['__type']);
                if (class_exists($class)) {
                    $results[] = new $class($this, $object, $this->_objectOptions);
                    $done = true;
                }
            }
            if (!$done) {
                $results[] = new Turba_Object($this, $object, $this->_objectOptions);
            }
        }

        return $results;
    }

    /**
     * Retrieves one object from the source.
     *
     * @param string $objectId  The unique id of the object to retrieve.
     *
     * @return Turba_Object  The retrieved object.
     * @throws Turba_Exception
     * @throws Horde_Exception_NotFound
     */
    public function getObject($objectId)
    {
        $result = $this->getObjects(array($objectId));

        if (empty($result[0])) {
            throw new Horde_Exception_NotFound();
        }

        $result = $result[0];
        if (!isset($this->map['__owner'])) {
            $result->attributes['__owner'] = $this->getContactOwner();
        }

        return $result;
    }

    /**
     * Adds a new entry to the contact source.
     *
     * @param array $attributes  The attributes of the new object to add.
     *
     * @return string  The new __key value on success.
     * @throws Turba_Exception
     */
    public function add(array $attributes)
    {
        /* Only set __type and __owner if they are not already set. */
        if (!isset($attributes['__type'])) {
            $attributes['__type'] = 'Object';
        }
        if (isset($this->map['__owner']) && !isset($attributes['__owner'])) {
            $attributes['__owner'] = $this->getContactOwner();
        }

        if (!isset($attributes['__uid'])) {
            $attributes['__uid'] = $this->_makeUid();
        }

        $key = $attributes['__key'] = $this->_makeKey($this->toDriverKeys($attributes));
        $uid = $attributes['__uid'];

        /* Remember any tags, since toDriverKeys will remove them.
           (They are not stored in the Turba backend so have no mapping). */
        $tags = isset($attributes['__tags'])
            ? $attributes['__tags']
            : false;

        $attributes = $this->toDriverKeys($attributes);

        $this->_add($attributes, $this->toDriverKeys($this->getBlobs()), $this->toDriverKeys($this->getDateFields()));

        /* Add tags. */
        if ($tags !== false) {
            $GLOBALS['injector']->getInstance('Turba_Tagger')->tag(
                $uid,
                $tags,
                $GLOBALS['registry']->getAuth(),
                'contact'
            );
        }

        /* Log the creation of this item in the history log. */
        try {
            $GLOBALS['injector']->getInstance('Horde_History')
                ->log('turba:' . $this->getName() . ':' . $uid,
                      array('action' => 'add'), true);
        } catch (Exception $e) {
            Horde::log($e, 'ERR');
        }

        return $key;
    }

    /**
     * Returns ability of the backend to add new contacts.
     *
     * @return boolean  Can backend add?
     */
    public function canAdd()
    {
        return $this->_canAdd();
    }

    /**
     * Returns ability of the backend to add new contacts.
     *
     * @return boolean  Can backend add?
     */
    protected function _canAdd()
    {
        return false;
    }

    /**
     * Deletes the specified entry from the contact source.
     *
     * @param string $object_id      The ID of the object to delete.
     * @param  boolean $remove_tags  Remove tags if true.
     *
     * @throws Turba_Exception
     * @throws Horde_Exception_NotFound
     */
    public function delete($object_id, $remove_tags = true)
    {
        $object = $this->getObject($object_id);

        if (!$object->hasPermission(Horde_Perms::DELETE)) {
            throw new Turba_Exception(_("Permission denied"));
        }

        $this->_delete($this->toDriver('__key'), $object_id);

        $own_contact = $GLOBALS['prefs']->getValue('own_contact');
        if (!empty($own_contact)) {
            @list(,$id) = explode(';', $own_contact);
            if ($id == $object_id) {
                $GLOBALS['prefs']->setValue('own_contact', '');
            }
        }

        /* Log the deletion of this item in the history log. */
        if ($object->getValue('__uid')) {
            try {
                $GLOBALS['injector']->getInstance('Horde_History')->log($object->getGuid(),
                                                array('action' => 'delete'),
                                                true);
            } catch (Exception $e) {
                Horde::log($e, 'ERR');
            }
        }

        /* Remove any CalDAV mappings. */
        try {
            $davStorage = $GLOBALS['injector']
                ->getInstance('Horde_Dav_Storage');
            try {
                $davStorage
                    ->deleteInternalObjectId($object_id, $this->_name);
            } catch (Horde_Exception $e) {
                Horde::log($e);
            }
        } catch (Horde_Exception $e) {
        }

        /* Remove tags */
        if ($remove_tags) {
            $GLOBALS['injector']->getInstance('Turba_Tagger')
                ->replaceTags($object->getValue('__uid'), array(), $this->getContactOwner(), 'contact');

            /* Might have tags disabled, hence no Content_* objects autoloadable. */
            try {
                /* Tell content we removed the object */
                $GLOBALS['injector']->getInstance('Content_Objects_Manager')
                    ->delete(array($object->getValue('__uid')), 'contact');
            } catch (Horde_Exception $e) {}
        }
    }

    /**
     * Deletes all contacts from an address book.
     *
     * @param string $sourceName  The identifier of the address book to
     *                            delete.  If omitted, will clear the current
     *                            user's 'default' address book for this
     *                            source type.
     *
     * @throws Turba_Exception
     */
    public function deleteAll($sourceName = null)
    {
        if (!$this->hasCapability('delete_all')) {
            throw new Turba_Exception('Not supported');
        }

        $ids = $this->_deleteAll($sourceName);

        // Update Horde_History and Tagger
        $history = $GLOBALS['injector']->getInstance('Horde_History');
        try {
            foreach ($ids as $uid) {
                // This is slightly hackish, but it saves us from having to
                // create and save an array of Turba_Objects before we delete
                // them, just to be able to calculate this using
                // Turba_Object#getGuid
                $guid = 'turba:' . $this->getName() . ':' . $uid;
                $history->log($guid, array('action' => 'delete'), true);

                // Remove tags.
                $GLOBALS['injector']->getInstance('Turba_Tagger')
                    ->replaceTags($uid, array(), $this->getContactOwner(), 'contact');

                /* Tell content we removed the object */
               $GLOBALS['injector']->getInstance('Content_Objects_Manager')
                    ->delete(array($uid), 'contact');
            }
        } catch (Exception $e) {
            Horde::log($e, 'ERR');
        }
    }

    /**
     * Deletes all contacts from an address book.
     *
     * @param string $sourceName  The source to remove all contacts from.
     *
     * @return array  An array of UIDs that have been deleted.
     * @throws Turba_Exception
     */
    protected function _deleteAll()
    {
        return array();
    }

    /**
     * Modifies an existing entry in the contact source.
     *
     * @param Turba_Object $object  The object to update.
     *
     * @return string  The object id, possibly updated.
     * @throws Turba_Exception
     */
    public function save(Turba_Object $object)
    {
        $object_id = $this->_save($object);

        if ($uid = $object->getValue('__uid')) {
            /* Update tags. */
            if (!is_null($tags = $object->getValue('__tags'))) {
                $GLOBALS['injector']->getInstance('Turba_Tagger')->replaceTags(
                    $uid,
                    $tags,
                    $this->getContactOwner(),
                    'contact'
                );
            }

            /* Log the modification of this item in the history log. */
            try {
                $GLOBALS['injector']->getInstance('Horde_History')->log($object->getGuid(),
                                                array('action' => 'modify'),
                                                true);
            } catch (Exception $e) {
                Horde::log($e, 'ERR');
            }
        }

        return $object_id;
    }

    /**
     * Returns the criteria available for this source except '__key'.
     *
     * @return array  An array containing the criteria.
     */
    public function getCriteria()
    {
        return array_diff_key($this->map, array('__key' => true));
    }

    /**
     * Returns all non-composite fields for this source. Useful for importing
     * and exporting data, etc.
     *
     * @return array  The field list.
     */
    public function getFields()
    {
        return array_flip($this->fields);
    }

    /**
     * Exports a given Turba_Object as an iCalendar vCard.
     *
     * @param Turba_Object $object  Turba_Object.
     * @param string $version       The vcard version to produce.
     * @param array $fields         Hash of field names and
     *                              Horde_SyncMl_Property properties with the
     *                              requested fields.
     * @param boolean $skipEmpty    Whether to skip empty fields.
     *
     * @return Horde_Icalendar_Vcard  A vcard object.
     */
    public function tovCard(Turba_Object $object, $version = '2.1',
                            array $fields = null, $skipEmpty = false)
    {
        global $injector;

        $hash = $object->getAttributes();
        $attributes = array_keys($this->map);
        $vcard = new Horde_Icalendar_Vcard($version);
        $formattedname = false;
        $charset = ($version == '2.1')
            ? array('CHARSET' => 'UTF-8')
            : array();

        $hooks = $injector->getInstance('Horde_Core_Hooks');
        $decode_hook = $hooks->hookExists('decode_attribute', 'turba');

        // Tags are stored externally to Turba, so they don't appear in the
        // source map.
        $attributes[] = '__tags';

        foreach ($attributes as $key) {
            $val = $object->getValue($key);
            if ($skipEmpty && !is_array($val) && !strlen($val)) {
                continue;
            }
            if ($decode_hook) {
                try {
                    $val = $hooks->callHook(
                        'decode_attribute',
                        'turba',
                        array($key, $val, $object)
                    );
                } catch (Turba_Exception $e) {}
            }
            switch ($key) {
            case '__uid':
                $vcard->setAttribute('UID', $val);
                break;

            case 'name':
                if ($fields && !isset($fields['FN'])) {
                    break;
                }
                $vcard->setAttribute('FN', $val, Horde_Mime::is8bit($val) ? $charset : array());
                $formattedname = true;
                break;

            case 'nickname':
            case 'alias':
                $params = Horde_Mime::is8bit($val) ? $charset : array();
                if (!$fields || isset($fields['NICKNAME'])) {
                    $vcard->setAttribute('NICKNAME', $val, $params);
                }
                if (!$fields || isset($fields['X-EPOCSECONDNAME'])) {
                    $vcard->setAttribute('X-EPOCSECONDNAME', $val, $params);
                }
                break;

            case 'homeAddress':
                if ($fields &&
                    (!isset($fields['LABEL']) ||
                     (isset($fields['LABEL']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['LABEL']->Params['TYPE']->ValEnum, 'HOME')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('LABEL', $val, array('HOME' => null));
                } else {
                    $vcard->setAttribute('LABEL', $val, array('TYPE' => 'HOME'));
                }
                break;

            case 'workAddress':
                if ($fields &&
                    (!isset($fields['LABEL']) ||
                     (isset($fields['LABEL']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['LABEL']->Params['TYPE']->ValEnum, 'WORK')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('LABEL', $val, array('WORK' => null));
                } else {
                    $vcard->setAttribute('LABEL', $val, array('TYPE' => 'WORK'));
                }
                break;

            case 'otherAddress':
                if ($fields && !isset($fields['LABEL'])) {
                    break;
                }
                $vcard->setAttribute('LABEL', $val);
                break;

            case 'phone':
                if ($fields && !isset($fields['TEL'])) {
                    break;
                }
                $vcard->setAttribute('TEL', $val);
                break;

            case 'homePhone':
                if ($fields &&
                    (!isset($fields['TEL']) ||
                     (isset($fields['TEL']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'HOME')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('TEL', $val, array('HOME' => null, 'VOICE' => null));
                } else {
                    $vcard->setAttribute('TEL', $val, array('TYPE' => array('HOME', 'VOICE')));
                }
                break;

            case 'workPhone':
                if ($fields &&
                    (!isset($fields['TEL']) ||
                     (isset($fields['TEL']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'WORK')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('TEL', $val, array('WORK' => null, 'VOICE' => null));
                } else {
                    $vcard->setAttribute('TEL', $val, array('TYPE' => array('WORK', 'VOICE')));
                }
                break;

            case 'cellPhone':
                if ($fields &&
                    (!isset($fields['TEL']) ||
                     (isset($fields['TEL']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'CELL')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('TEL', $val, array('CELL' => null, 'VOICE' => null));
                } else {
                    $vcard->setAttribute('TEL', $val, array('TYPE' => array('CELL', 'VOICE')));
                }
                break;

            case 'homeCellPhone':
                $parameters = array();
                if ($fields) {
                    if (!isset($fields['TEL'])) {
                        break;
                    }
                    if (!isset($fields['TEL']->Params['TYPE']) ||
                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'CELL')) {
                        if ($version == '2.1') {
                            $parameters['CELL'] = null;
                            $parameters['VOICE'] = null;
                        } else {
                            $parameters['TYPE'] = array('CELL', 'VOICE');
                        }
                    }
                    if (!isset($fields['TEL']->Params['TYPE']) ||
                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'HOME')) {
                        if ($version == '2.1') {
                            $parameters['HOME'] = null;
                            $parameters['VOICE'] = null;
                        } else {
                            $parameters['TYPE'] = array('HOME', 'VOICE');
                        }
                    }
                    if (empty($parameters)) {
                        break;
                    }
                } else {
                    if ($version == '2.1') {
                        $parameters = array('CELL' => null, 'HOME' => null, 'VOICE' => null);
                    } else {
                        $parameters = array('TYPE' => array('CELL', 'HOME', 'VOICE'));
                    }
                }
                $vcard->setAttribute('TEL', $val, $parameters);
                break;

            case 'workCellPhone':
                $parameters = array();
                if ($fields) {
                    if (!isset($fields['TEL'])) {
                        break;
                    }
                    if (!isset($fields['TEL']->Params['TYPE']) ||
                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'CELL')) {
                        if ($version == '2.1') {
                            $parameters['CELL'] = null;
                            $parameters['VOICE'] = null;
                        } else {
                            $parameters['TYPE'] = array('CELL', 'VOICE');
                        }
                    }
                    if (!isset($fields['TEL']->Params['TYPE']) ||
                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'WORK')) {
                        if ($version == '2.1') {
                            $parameters['WORK'] = null;
                            $parameters['VOICE'] = null;
                        } else {
                            $parameters['TYPE'] = array('WORK', 'VOICE');
                        }
                    }
                    if (empty($parameters)) {
                        break;
                    }
                } else {
                    if ($version == '2.1') {
                        $parameters = array('CELL' => null, 'WORK' => null, 'VOICE' => null);
                    } else {
                        $parameters = array('TYPE' => array('CELL', 'WORK', 'VOICE'));
                    }
                }
                $vcard->setAttribute('TEL', $val, $parameters);
                break;

            case 'videoCall':
                if ($fields &&
                    (!isset($fields['TEL']) ||
                     (isset($fields['TEL']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'VIDEO')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('TEL', $val, array('VIDEO' => null));
                } else {
                    $vcard->setAttribute('TEL', $val, array('TYPE' => 'VIDEO'));
                }
                break;

            case 'homeVideoCall':
                $parameters = array();
                if ($fields) {
                    if (!isset($fields['TEL'])) {
                        break;
                    }
                    if (!isset($fields['TEL']->Params['TYPE']) ||
                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'VIDEO')) {
                        if ($version == '2.1') {
                            $parameters['VIDEO'] = null;
                        } else {
                            $parameters['TYPE'] = 'VIDEO';
                        }
                    }
                    if (!isset($fields['TEL']->Params['TYPE']) ||
                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'HOME')) {
                        if ($version == '2.1') {
                            $parameters['HOME'] = null;
                        } else {
                            $parameters['TYPE'] = 'HOME';
                        }
                    }
                    if (empty($parameters)) {
                        break;
                    }
                } else {
                    if ($version == '2.1') {
                        $parameters = array('VIDEO' => null, 'HOME' => null);
                    } else {
                        $parameters = array('TYPE' => array('VIDEO', 'HOME'));
                    }
                }
                $vcard->setAttribute('TEL', $val, $parameters);
                break;

            case 'workVideoCall':
                $parameters = array();
                if ($fields) {
                    if (!isset($fields['TEL'])) {
                        break;
                    }
                    if (!isset($fields['TEL']->Params['TYPE']) ||
                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'VIDEO')) {
                        if ($version == '2.1') {
                            $parameters['VIDEO'] = null;
                        } else {
                            $parameters['TYPE'] = 'VIDEO';
                        }
                    }
                    if (!isset($fields['TEL']->Params['TYPE']) ||
                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'WORK')) {
                        if ($version == '2.1') {
                            $parameters['WORK'] = null;
                        } else {
                            $parameters['TYPE'] = 'WORK';
                        }
                    }
                    if (empty($parameters)) {
                        break;
                    }
                } else {
                    if ($version == '2.1') {
                        $parameters = array('VIDEO' => null, 'WORK' => null);
                    } else {
                        $parameters = array('TYPE' => array('VIDEO', 'WORK'));
                    }
                }
                $vcard->setAttribute('TEL', $val, $parameters);
                break;

            case 'sip':
                if ($fields && !isset($fields['X-SIP'])) {
                    break;
                }
                $vcard->setAttribute('X-SIP', $val);
                break;
            case 'ptt':
                if ($fields &&
                    (!isset($fields['X-SIP']) ||
                     (isset($fields['X-SIP']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['X-SIP']->Params['TYPE']->ValEnum, 'POC')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('X-SIP', $val, array('POC' => null));
                } else {
                    $vcard->setAttribute('X-SIP', $val, array('TYPE' => 'POC'));
                }
                break;

            case 'voip':
                if ($fields &&
                    (!isset($fields['X-SIP']) ||
                     (isset($fields['X-SIP']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['X-SIP']->Params['TYPE']->ValEnum, 'VOIP')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('X-SIP', $val, array('VOIP' => null));
                } else {
                    $vcard->setAttribute('X-SIP', $val, array('TYPE' => 'VOIP'));
                }
                break;

            case 'shareView':
                if ($fields &&
                    (!isset($fields['X-SIP']) ||
                     (isset($fields['X-SIP']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['X-SIP']->Params['TYPE']->ValEnum, 'SWIS')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('X-SIP', $val, array('SWIS' => null));
                } else {
                    $vcard->setAttribute('X-SIP', $val, array('TYPE' => 'SWIS'));
                }
                break;

            case 'imaddress':
                if ($fields && !isset($fields['X-WV-ID'])) {
                    break;
                }
                $vcard->setAttribute('X-WV-ID', $val);
                break;

            case 'fax':
                if ($fields &&
                    (!isset($fields['TEL']) ||
                     (isset($fields['TEL']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'FAX')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('TEL', $val, array('FAX' => null));
                } else {
                    $vcard->setAttribute('TEL', $val, array('TYPE' => 'FAX'));
                }
                break;

            case 'homeFax':
                $parameters = array();
                if ($fields) {
                    if (!isset($fields['TEL'])) {
                        break;
                    }
                    if (!isset($fields['TEL']->Params['TYPE']) ||
                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'FAX')) {
                        if ($version == '2.1') {
                            $parameters['FAX'] = null;
                        } else {
                            $parameters['TYPE'] = 'FAX';
                        }
                    }
                    if (!isset($fields['TEL']->Params['TYPE']) ||
                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'HOME')) {
                        if ($version == '2.1') {
                            $parameters['HOME'] = null;
                        } else {
                            $parameters['TYPE'] = 'HOME';
                        }
                    }
                    if (empty($parameters)) {
                        break;
                    }
                } else {
                    if ($version == '2.1') {
                        $parameters = array('FAX' => null, 'HOME' => null);
                    } else {
                        $parameters = array('TYPE' => array('FAX', 'HOME'));
                    }
                }
                $vcard->setAttribute('TEL', $val, $parameters);
                break;

            case 'workFax':
                $parameters = array();
                if ($fields) {
                    if (!isset($fields['TEL'])) {
                        break;
                    }
                    if (!isset($fields['TEL']->Params['TYPE']) ||
                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'FAX')) {
                        if ($version == '2.1') {
                            $parameters['FAX'] = null;
                        } else {
                            $parameters['TYPE'] = 'FAX';
                        }
                    }
                    if (!isset($fields['TEL']->Params['TYPE']) ||
                        $this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'WORK')) {
                        if ($version == '2.1') {
                            $parameters['WORK'] = null;
                        } else {
                            $parameters['TYPE'] = 'WORK';
                        }
                    }
                    if (empty($parameters)) {
                        break;
                    }
                } else {
                    if ($version == '2.1') {
                        $parameters = array('FAX' => null, 'WORK' => null);
                    } else {
                        $parameters = array('TYPE' => array('FAX', 'WORK'));
                    }
                }
                $vcard->setAttribute('TEL', $val, $parameters);
                if ($version == '2.1') {
                    $vcard->setAttribute('TEL', $val, array('FAX' => null, 'WORK' => null));
                } else {
                    $vcard->setAttribute('TEL', $val, array('TYPE' => array('FAX', 'WORK')));
                }
                break;

            case 'pager':
                if ($fields &&
                    (!isset($fields['TEL']) ||
                     (isset($fields['TEL']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['TEL']->Params['TYPE']->ValEnum, 'PAGER')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('TEL', $val, array('PAGER' => null));
                } else {
                    $vcard->setAttribute('TEL', $val, array('TYPE' => 'PAGER'));
                }
                break;

            case 'email':
                if ($fields && !isset($fields['EMAIL'])) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute(
                        'EMAIL',
                        Horde_Icalendar_Vcard::getBareEmail($val),
                        array('INTERNET' => null));
                } else {
                    $vcard->setAttribute(
                        'EMAIL',
                        Horde_Icalendar_Vcard::getBareEmail($val),
                        array('TYPE' => 'INTERNET'));
                }
                break;

            case 'homeEmail':
                if ($fields &&
                    (!isset($fields['EMAIL']) ||
                     (isset($fields['EMAIL']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['EMAIL']->Params['TYPE']->ValEnum, 'HOME')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('EMAIL',
                                         Horde_Icalendar_Vcard::getBareEmail($val),
                                         array('HOME' => null));
                } else {
                    $vcard->setAttribute('EMAIL',
                                         Horde_Icalendar_Vcard::getBareEmail($val),
                                         array('TYPE' => 'HOME'));
                }
                break;

            case 'workEmail':
                if ($fields &&
                    (!isset($fields['EMAIL']) ||
                     (isset($fields['EMAIL']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['EMAIL']->Params['TYPE']->ValEnum, 'WORK')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('EMAIL',
                                         Horde_Icalendar_Vcard::getBareEmail($val),
                                         array('WORK' => null));
                } else {
                    $vcard->setAttribute('EMAIL',
                                         Horde_Icalendar_Vcard::getBareEmail($val),
                                         array('TYPE' => 'WORK'));
                }
                break;

            case 'emails':
                if ($fields && !isset($fields['EMAIL'])) {
                    break;
                }
                $emails = explode(',', $val);
                foreach ($emails as $email) {
                    $vcard->setAttribute('EMAIL', Horde_Icalendar_Vcard::getBareEmail($email));
                }
                break;

            case 'title':
                if ($fields && !isset($fields['TITLE'])) {
                    break;
                }
                $vcard->setAttribute('TITLE', $val, Horde_Mime::is8bit($val) ? $charset : array());
                break;

            case 'role':
                if ($fields && !isset($fields['ROLE'])) {
                    break;
                }
                $vcard->setAttribute('ROLE', $val, Horde_Mime::is8bit($val) ? $charset : array());
                break;

            case 'notes':
                if ($fields && !isset($fields['NOTE'])) {
                    break;
                }
                $vcard->setAttribute('NOTE', $val, Horde_Mime::is8bit($val) ? $charset : array());
                break;

            case '__tags':
                $val = $injector->getInstance('Turba_Tagger')->split($val);
            case 'businessCategory':
                // No CATEGORIES in vCard 2.1
                if ($version == '2.1' ||
                    ($fields && !isset($fields['CATEGORIES']))) {
                    break;
                }
                $vcard->setAttribute('CATEGORIES', null, array(), true, $val);
                break;

            case 'anniversary':
                if (!$fields || isset($fields['X-ANNIVERSARY'])) {
                    $vcard->setAttribute('X-ANNIVERSARY', $val);
                }
                break;

            case 'spouse':
                if (!$fields || isset($fields['X-SPOUSE'])) {
                    $vcard->setAttribute('X-SPOUSE', $val);
                }
                break;

            case 'children':
                if (!$fields || isset($fields['X-CHILDREN'])) {
                    $vcard->setAttribute('X-CHILDREN', $val);
                }
                break;

            case 'website':
                if ($fields && !isset($fields['URL'])) {
                    break;
                }
                $vcard->setAttribute('URL', $val);
                break;

            case 'homeWebsite':
                if ($fields &&
                    (!isset($fields['URL']) ||
                     (isset($fields['URL']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['URL']->Params['TYPE']->ValEnum, 'HOME')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('URL', $val, array('HOME' => null));
                } else {
                    $vcard->setAttribute('URL', $val, array('TYPE' => 'HOME'));
                }
                break;

            case 'workWebsite':
                if ($fields &&
                    (!isset($fields['URL']) ||
                     (isset($fields['URL']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['URL']->Params['TYPE']->ValEnum, 'WORK')))) {
                    break;
                }
                if ($version == '2.1') {
                    $vcard->setAttribute('URL', $val, array('WORK' => null));
                } else {
                    $vcard->setAttribute('URL', $val, array('TYPE' => 'WORK'));
                }
                break;

            case 'freebusyUrl':
                if ($version == '2.1' ||
                    ($fields && !isset($fields['FBURL']))) {
                    break;
                }
                $vcard->setAttribute('FBURL', $val);
                break;

            case 'birthday':
                if ($fields && !isset($fields['BDAY'])) {
                    break;
                }
                $vcard->setAttribute('BDAY', $val);
                break;

            case 'timezone':
                if ($fields && !isset($fields['TZ'])) {
                    break;
                }
                $vcard->setAttribute('TZ', $val, array('VALUE' => 'text'));
                break;

            case 'latitude':
                if ($fields && !isset($fields['GEO'])) {
                    break;
                }
                if (isset($hash['longitude'])) {
                    $vcard->setAttribute('GEO',
                                         array('latitude' => $val,
                                               'longitude' => $hash['longitude']));
                }
                break;

            case 'homeLatitude':
                if ($fields &&
                    (!isset($fields['GEO']) ||
                     (isset($fields['GEO']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['GEO']->Params['TYPE']->ValEnum, 'HOME')))) {
                    break;
                }
                if (isset($hash['homeLongitude'])) {
                    if ($version == '2.1') {
                        $vcard->setAttribute('GEO',
                                             array('latitude' => $val,
                                                   'longitude' => $hash['homeLongitude']),
                                             array('HOME' => null));
                   } else {
                        $vcard->setAttribute('GEO',
                                             array('latitude' => $val,
                                                   'longitude' => $hash['homeLongitude']),
                                             array('TYPE' => 'HOME'));
                   }
                }
                break;

            case 'workLatitude':
                if ($fields &&
                    (!isset($fields['GEO']) ||
                     (isset($fields['GEO']->Params['TYPE']) &&
                      !$this->_hasValEnum($fields['GEO']->Params['TYPE']->ValEnum, 'HOME')))) {
                    break;
                }
                if (isset($hash['workLongitude'])) {
                    if ($version == '2.1') {
                        $vcard->setAttribute('GEO',
                                             array('latitude' => $val,
                                                   'longitude' => $hash['workLongitude']),
                                             array('WORK' => null));
                   } else {
                        $vcard->setAttribute('GEO',
                                             array('latitude' => $val,
                                                   'longitude' => $hash['workLongitude']),
                                             array('TYPE' => 'WORK'));
                   }
                }
                break;

            case 'photo':
            case 'logo':
                $name = Horde_String::upper($key);
                $params = array();
                if (strlen($hash[$key])) {
                    $params['ENCODING'] = 'b';
                }
                if (isset($hash[$key . 'type'])) {
                    $params['TYPE'] = $hash[$key . 'type'];
                }
                if ($fields &&
                    (!isset($fields[$name]) ||
                     (isset($params['TYPE']) &&
                      isset($fields[$name]->Params['TYPE']) &&
                      !$this->_hasValEnum($fields[$name]->Params['TYPE']->ValEnum, $params['TYPE'])))) {
                    break;
                }
                $vcard->setAttribute($name,
                                     base64_encode($hash[$key]),
                                     $params);
                break;
            }
        }

        // No explicit firstname/lastname in data source: we have to guess.
        if (!isset($hash['lastname']) && isset($hash['name'])) {
            $this->_guessName($hash);
        }

        $a = array(
            Horde_Icalendar_Vcard::N_FAMILY => isset($hash['lastname']) ? $hash['lastname'] : '',
            Horde_Icalendar_Vcard::N_GIVEN  => isset($hash['firstname']) ? $hash['firstname'] : '',
            Horde_Icalendar_Vcard::N_ADDL   => isset($hash['middlenames']) ? $hash['middlenames'] : '',
            Horde_Icalendar_Vcard::N_PREFIX => isset($hash['namePrefix']) ? $hash['namePrefix'] : '',
            Horde_Icalendar_Vcard::N_SUFFIX => isset($hash['nameSuffix']) ? $hash['nameSuffix'] : '',
        );
        $val = implode(';', $a);
        if (!$fields || isset($fields['N'])) {
            $vcard->setAttribute('N', $val, Horde_Mime::is8bit($val) ? $charset : array(), false, $a);
        }

        if (!$formattedname && (!$fields || isset($fields['FN']))) {
            if ($object->getValue('name')) {
                $val = $object->getValue('name');
            } elseif (!empty($this->alternativeName) &&
                isset($hash[$this->alternativeName])) {
                $val = $hash[$this->alternativeName];
            } else {
                $val = '';
            }
            $vcard->setAttribute('FN', $val, Horde_Mime::is8bit($val) ? $charset : array());
        }

        $org = array();
        if (!empty($hash['company']) ||
            (!$skipEmpty && array_key_exists('company', $hash))) {
            $org[] = $hash['company'];
        }
        if (!empty($hash['department']) ||
            (!$skipEmpty && array_key_exists('department', $hash))) {
            $org[] = $hash['department'];
        }
        if (count($org) && (!$fields || isset($fields['ORG']))) {
            $val = implode(';', $org);
            $vcard->setAttribute('ORG', $val, Horde_Mime::is8bit($val) ? $charset : array(), false, $org);
        }

        if ((!$fields || isset($fields['ADR'])) &&
            (!empty($hash['commonAddress']) ||
             !empty($hash['commonStreet']) ||
             !empty($hash['commonPOBox']) ||
             !empty($hash['commonExtended']) ||
             !empty($hash['commonCity']) ||
             !empty($hash['commonProvince']) ||
             !empty($hash['commonPostalCode']) ||
             !empty($hash['commonCountry']) ||
             (!$skipEmpty &&
              (array_key_exists('commonAddress', $hash) ||
               array_key_exists('commonStreet', $hash) ||
               array_key_exists('commonPOBox', $hash) ||
               array_key_exists('commonExtended', $hash) ||
               array_key_exists('commonCity', $hash) ||
               array_key_exists('commonProvince', $hash) ||
               array_key_exists('commonPostalCode', $hash) ||
               array_key_exists('commonCountry', $hash))))) {
            /* We can't know if this particular Turba source uses a single
             * address field or multiple for
             * street/city/province/postcode/country. Try to deal with
             * both. */
            if (isset($hash['commonAddress']) &&
                !isset($hash['commonStreet'])) {
                $hash['commonStreet'] = $hash['commonAddress'];
            }
            $a = array(
                Horde_Icalendar_Vcard::ADR_POB      => isset($hash['commonPOBox'])
                    ? $hash['commonPOBox'] : '',
                Horde_Icalendar_Vcard::ADR_EXTEND   => isset($hash['commonExtended'])
                    ? $hash['commonExtended'] : '',
                Horde_Icalendar_Vcard::ADR_STREET   => isset($hash['commonStreet'])
                    ? $hash['commonStreet'] : '',
                Horde_Icalendar_Vcard::ADR_LOCALITY => isset($hash['commonCity'])
                    ? $hash['commonCity'] : '',
                Horde_Icalendar_Vcard::ADR_REGION   => isset($hash['commonProvince'])
                    ? $hash['commonProvince'] : '',
                Horde_Icalendar_Vcard::ADR_POSTCODE => isset($hash['commonPostalCode'])
                    ? $hash['commonPostalCode'] : '',
                Horde_Icalendar_Vcard::ADR_COUNTRY  => isset($hash['commonCountry'])
                    ? Horde_Nls::getCountryISO($hash['commonCountry']) : '',
            );

            $val = implode(';', $a);
            if ($version == '2.1') {
                $params = array();
                if (Horde_Mime::is8bit($val)) {
                    $params['CHARSET'] = 'UTF-8';
                }
            } else {
                $params = array('TYPE' => '');
            }
            $vcard->setAttribute('ADR', $val, $params, true, $a);
        }

        if ((!$fields ||
             (isset($fields['ADR']) &&
              (!isset($fields['ADR']->Params['TYPE']) ||
               $this->_hasValEnum($fields['ADR']->Params['TYPE']->ValEnum, 'HOME')))) &&
            (!empty($hash['homeAddress']) ||
             !empty($hash['homeStreet']) ||
             !empty($hash['homePOBox']) ||
             !empty($hash['homeExtended']) ||
             !empty($hash['homeCity']) ||
             !empty($hash['homeProvince']) ||
             !empty($hash['homePostalCode']) ||
             !empty($hash['homeCountry']) ||
             (!$skipEmpty &&
              (array_key_exists('homeAddress', $hash) ||
               array_key_exists('homeStreet', $hash) ||
               array_key_exists('homePOBox', $hash) ||
               array_key_exists('homeExtended', $hash) ||
               array_key_exists('homeCity', $hash) ||
               array_key_exists('homeProvince', $hash) ||
               array_key_exists('homePostalCode', $hash) ||
               array_key_exists('homeCountry', $hash))))) {
            if (isset($hash['homeAddress']) && !isset($hash['homeStreet'])) {
                $hash['homeStreet'] = $hash['homeAddress'];
            }
            $a = array(
                Horde_Icalendar_Vcard::ADR_POB      => isset($hash['homePOBox'])
                    ? $hash['homePOBox'] : '',
                Horde_Icalendar_Vcard::ADR_EXTEND   => isset($hash['homeExtended'])
                    ? $hash['homeExtended'] : '',
                Horde_Icalendar_Vcard::ADR_STREET   => isset($hash['homeStreet'])
                    ? $hash['homeStreet'] : '',
                Horde_Icalendar_Vcard::ADR_LOCALITY => isset($hash['homeCity'])
                    ? $hash['homeCity'] : '',
                Horde_Icalendar_Vcard::ADR_REGION   => isset($hash['homeProvince'])
                    ? $hash['homeProvince'] : '',
                Horde_Icalendar_Vcard::ADR_POSTCODE => isset($hash['homePostalCode'])
                    ? $hash['homePostalCode'] : '',
                Horde_Icalendar_Vcard::ADR_COUNTRY  => isset($hash['homeCountry'])
                    ? Horde_Nls::getCountryISO($hash['homeCountry']) : '',
            );

            $val = implode(';', $a);
            if ($version == '2.1') {
                $params = array('HOME' => null);
                if (Horde_Mime::is8bit($val)) {
                    $params['CHARSET'] = 'UTF-8';
                }
            } else {
                $params = array('TYPE' => 'HOME');
            }
            $vcard->setAttribute('ADR', $val, $params, true, $a);
        }

        if ((!$fields ||
             (isset($fields['ADR']) &&
              (!isset($fields['ADR']->Params['TYPE']) ||
               $this->_hasValEnum($fields['ADR']->Params['TYPE']->ValEnum, 'WORK')))) &&
            (!empty($hash['workAddress']) ||
             !empty($hash['workStreet']) ||
             !empty($hash['workPOBox']) ||
             !empty($hash['workExtended']) ||
             !empty($hash['workCity']) ||
             !empty($hash['workProvince']) ||
             !empty($hash['workPostalCode']) ||
             !empty($hash['workCountry']) ||
             (!$skipEmpty &&
              (array_key_exists('workAddress', $hash) ||
               array_key_exists('workStreet', $hash) ||
               array_key_exists('workPOBox', $hash) ||
               array_key_exists('workExtended', $hash) ||
               array_key_exists('workCity', $hash) ||
               array_key_exists('workProvince', $hash) ||
               array_key_exists('workPostalCode', $hash) ||
               array_key_exists('workCountry', $hash))))) {
            if (isset($hash['workAddress']) && !isset($hash['workStreet'])) {
                $hash['workStreet'] = $hash['workAddress'];
            }
            $a = array(
                Horde_Icalendar_Vcard::ADR_POB      => isset($hash['workPOBox'])
                    ? $hash['workPOBox'] : '',
                Horde_Icalendar_Vcard::ADR_EXTEND   => isset($hash['workExtended'])
                    ? $hash['workExtended'] : '',
                Horde_Icalendar_Vcard::ADR_STREET   => isset($hash['workStreet'])
                    ? $hash['workStreet'] : '',
                Horde_Icalendar_Vcard::ADR_LOCALITY => isset($hash['workCity'])
                    ? $hash['workCity'] : '',
                Horde_Icalendar_Vcard::ADR_REGION   => isset($hash['workProvince'])
                    ? $hash['workProvince'] : '',
                Horde_Icalendar_Vcard::ADR_POSTCODE => isset($hash['workPostalCode'])
                    ? $hash['workPostalCode'] : '',
                Horde_Icalendar_Vcard::ADR_COUNTRY  => isset($hash['workCountry'])
                    ? Horde_Nls::getCountryISO($hash['workCountry']) : '',
            );

            $val = implode(';', $a);
            if ($version == '2.1') {
                $params = array('WORK' => null);
                if (Horde_Mime::is8bit($val)) {
                    $params['CHARSET'] = 'UTF-8';
                }
            } else {
                $params = array('TYPE' => 'WORK');
            }
            $vcard->setAttribute('ADR', $val, $params, true, $a);
        }

        return $vcard;
    }

    /**
     * Returns whether a ValEnum entry from a DevInf object contains a certain
     * type.
     *
     * @param array $valEnum  A ValEnum hash.
     * @param string $type    A requested attribute type.
     *
     * @return boolean  True if $type exists in $valEnum.
     */
    protected function _hasValEnum($valEnum, $type)
    {
        foreach (array_keys($valEnum) as $key) {
            if (in_array($type, explode(',', $key))) {
                return true;
            }
        }
        return false;
    }

    /**
     * Function to convert a Horde_Icalendar_Vcard object into a Turba
     * Object Hash with Turba attributes suitable as a parameter for add().
     *
     * @see add()
     *
     * @param Horde_Icalendar_Vcard $vcard  The Horde_Icalendar_Vcard object
     *                                      to parse.
     *
     * @return array  A Turba attribute hash.
     */
    public function toHash(Horde_Icalendar_Vcard $vcard)
    {
        global $attributes;

        $hash = array();
        $attr = $vcard->getAllAttributes();

        foreach ($attr as $item) {
            switch ($item['name']) {
            case 'UID':
                $hash['__uid'] = $item['value'];
                break;

            case 'FN':
                $hash['name'] = $item['value'];
                break;

            case 'N':
                $name = $item['values'];
                if (!empty($name[Horde_Icalendar_Vcard::N_FAMILY])) {
                    $hash['lastname'] = $name[Horde_Icalendar_Vcard::N_FAMILY];
                }
                if (!empty($name[Horde_Icalendar_Vcard::N_GIVEN])) {
                    $hash['firstname'] = $name[Horde_Icalendar_Vcard::N_GIVEN];
                }
                if (!empty($name[Horde_Icalendar_Vcard::N_ADDL])) {
                    $hash['middlenames'] = $name[Horde_Icalendar_Vcard::N_ADDL];
                }
                if (!empty($name[Horde_Icalendar_Vcard::N_PREFIX])) {
                    $hash['namePrefix'] = $name[Horde_Icalendar_Vcard::N_PREFIX];
                }
                if (!empty($name[Horde_Icalendar_Vcard::N_SUFFIX])) {
                    $hash['nameSuffix'] = $name[Horde_Icalendar_Vcard::N_SUFFIX];
                }
                break;

            case 'NICKNAME':
            case 'X-EPOCSECONDNAME':
                $hash['nickname'] = $item['value'];
                $hash['alias'] = $item['value'];
                break;

            // We use LABEL but also support ADR.
            case 'LABEL':
                if (isset($item['params']['HOME']) && !isset($hash['homeAddress'])) {
                    $hash['homeAddress'] = $item['value'];
                } elseif (isset($item['params']['WORK']) && !isset($hash['workAddress'])) {
                    $hash['workAddress'] = $item['value'];
                } elseif (!isset($hash['commonAddress'])) {
                    $hash['commonAddress'] = $item['value'];
                }
                break;

            case 'ADR':
                if (isset($item['params']['TYPE'])) {
                    if (!is_array($item['params']['TYPE'])) {
                        $item['params']['TYPE'] = array($item['params']['TYPE']);
                    }
                } else {
                    $item['params']['TYPE'] = array();
                    if (isset($item['params']['WORK'])) {
                        $item['params']['TYPE'][] = 'WORK';
                    }
                    if (isset($item['params']['HOME'])) {
                        $item['params']['TYPE'][] = 'HOME';
                    }
                    if (count($item['params']['TYPE']) == 0) {
                        $item['params']['TYPE'][] = 'COMMON';
                    }
                }

                $address = $item['values'];
                foreach ($item['params']['TYPE'] as $adr) {
                    switch (Horde_String::upper($adr)) {
                    case 'HOME':
                        $prefix = 'home';
                        break;

                    case 'WORK':
                        $prefix = 'work';
                        break;

                    default:
                        $prefix = 'common';
                    }

                    if (isset($hash[$prefix . 'Address'])) {
                        continue;
                    }

                    $hash[$prefix . 'Address'] = '';

                    if (!empty($address[Horde_Icalendar_Vcard::ADR_STREET])) {
                        $hash[$prefix . 'Street'] = $address[Horde_Icalendar_Vcard::ADR_STREET];
                        $hash[$prefix . 'Address'] .= $hash[$prefix . 'Street'] . "\n";
                    }
                    if (!empty($address[Horde_Icalendar_Vcard::ADR_EXTEND])) {
                        $hash[$prefix . 'Extended'] = $address[Horde_Icalendar_Vcard::ADR_EXTEND];
                        $hash[$prefix . 'Address'] .= $hash[$prefix . 'Extended'] . "\n";
                    }
                    if (!empty($address[Horde_Icalendar_Vcard::ADR_POB])) {
                        $hash[$prefix . 'POBox'] = $address[Horde_Icalendar_Vcard::ADR_POB];
                        $hash[$prefix . 'Address'] .= $hash[$prefix . 'POBox'] . "\n";
                    }
                    if (!empty($address[Horde_Icalendar_Vcard::ADR_LOCALITY])) {
                        $hash[$prefix . 'City'] = $address[Horde_Icalendar_Vcard::ADR_LOCALITY];
                        $hash[$prefix . 'Address'] .= $hash[$prefix . 'City'];
                    }
                    if (!empty($address[Horde_Icalendar_Vcard::ADR_REGION])) {
                        $hash[$prefix . 'Province'] = $address[Horde_Icalendar_Vcard::ADR_REGION];
                        $hash[$prefix . 'Address'] .= ', ' . $hash[$prefix . 'Province'];
                    }
                    if (!empty($address[Horde_Icalendar_Vcard::ADR_POSTCODE])) {
                        $hash[$prefix . 'PostalCode'] = $address[Horde_Icalendar_Vcard::ADR_POSTCODE];
                        $hash[$prefix . 'Address'] .= ' ' . $hash[$prefix . 'PostalCode'];
                    }
                    if (!empty($address[Horde_Icalendar_Vcard::ADR_COUNTRY])) {
                        include 'Horde/Nls/Countries.php';
                        $country = array_search($address[Horde_Icalendar_Vcard::ADR_COUNTRY], $countries);
                        if ($country === false) {
                            $country = $address[Horde_Icalendar_Vcard::ADR_COUNTRY];
                        }
                        $hash[$prefix . 'Country'] = $country;
                        $hash[$prefix . 'Address'] .= "\n" . $address[Horde_Icalendar_Vcard::ADR_COUNTRY];
                    }

                    $hash[$prefix . 'Address'] = trim($hash[$prefix . 'Address']);
                }
                break;

            case 'TZ':
                // We only support textual timezones.
                if (!isset($item['params']['VALUE']) ||
                    Horde_String::lower($item['params']['VALUE']) != 'text') {
                    break;
                }
                $timezones = explode(';', $item['value']);
                $available_timezones = Horde_Nls::getTimezones();
                foreach ($timezones as $timezone) {
                    $timezone = trim($timezone);
                    if (isset($available_timezones[$timezone])) {
                        $hash['timezone'] = $timezone;
                        break 2;
                    }
                }
                break;

            case 'GEO':
                if (isset($item['params']['HOME'])) {
                    $hash['homeLatitude'] = $item['value']['latitude'];
                    $hash['homeLongitude'] = $item['value']['longitude'];
                } elseif (isset($item['params']['WORK'])) {
                    $hash['workLatitude'] = $item['value']['latitude'];
                    $hash['workLongitude'] = $item['value']['longitude'];
                } else {
                    $hash['latitude'] = $item['value']['latitude'];
                    $hash['longitude'] = $item['value']['longitude'];
                }
                break;

            case 'TEL':
                if (isset($item['params']['FAX'])) {
                    if (isset($item['params']['WORK']) &&
                        !isset($hash['workFax'])) {
                        $hash['workFax'] = $item['value'];
                    } elseif (isset($item['params']['HOME']) &&
                              !isset($hash['homeFax'])) {
                        $hash['homeFax'] = $item['value'];
                    } elseif (!isset($hash['fax'])) {
                        $hash['fax'] = $item['value'];
                    }
                } elseif (isset($item['params']['PAGER']) &&
                          !isset($hash['pager'])) {
                    $hash['pager'] = $item['value'];
                } elseif (isset($item['params']['TYPE'])) {
                    if (!is_array($item['params']['TYPE'])) {
                        $item['params']['TYPE'] = array($item['params']['TYPE']);
                    }
                    foreach ($item['params']['TYPE'] as &$type) {
                        $type = Horde_String::upper($type);
                    }
                    // For vCard 3.0.
                    if (in_array('CELL', $item['params']['TYPE'])) {
                        if (in_array('HOME', $item['params']['TYPE']) &&
                            !isset($hash['homeCellPhone'])) {
                            $hash['homeCellPhone'] = $item['value'];
                        } elseif (in_array('WORK', $item['params']['TYPE']) &&
                                  !isset($hash['workCellPhone'])) {
                            $hash['workCellPhone'] = $item['value'];
                        } elseif (!isset($hash['cellPhone'])) {
                            $hash['cellPhone'] = $item['value'];
                        }
                    } elseif (in_array('FAX', $item['params']['TYPE'])) {
                        if (in_array('HOME', $item['params']['TYPE']) &&
                            !isset($hash['homeFax'])) {
                            $hash['homeFax'] = $item['value'];
                        } elseif (in_array('WORK', $item['params']['TYPE']) &&
                                  !isset($hash['workFax'])) {
                            $hash['workFax'] = $item['value'];
                        } elseif (!isset($hash['fax'])) {
                            $hash['fax'] = $item['value'];
                        }
                    } elseif (in_array('VIDEO', $item['params']['TYPE'])) {
                        if (in_array('HOME', $item['params']['TYPE']) &&
                            !isset($hash['homeVideoCall'])) {
                            $hash['homeVideoCall'] = $item['value'];
                        } elseif (in_array('WORK', $item['params']['TYPE']) &&
                                  !isset($hash['workVideoCall'])) {
                            $hash['workVideoCall'] = $item['value'];
                        } elseif (!isset($hash['videoCall'])) {
                            $hash['videoCall'] = $item['value'];
                        }
                    } elseif (in_array('PAGER', $item['params']['TYPE']) &&
                              !isset($hash['pager'])) {
                        $hash['pager'] = $item['value'];
                    } elseif (in_array('WORK', $item['params']['TYPE']) &&
                              !isset($hash['workPhone'])) {
                        $hash['workPhone'] = $item['value'];
                    } elseif (in_array('HOME', $item['params']['TYPE']) &&
                              !isset($hash['homePhone'])) {
                        $hash['homePhone'] = $item['value'];
                    } elseif (!isset($hash['phone'])) {
                        $hash['phone'] = $item['value'];
                    }
                } elseif (isset($item['params']['CELL'])) {
                    if (isset($item['params']['WORK']) &&
                        !isset($hash['workCellPhone'])) {
                        $hash['workCellPhone'] = $item['value'];
                    } elseif (isset($item['params']['HOME']) &&
                              !isset($hash['homeCellPhone'])) {
                        $hash['homeCellPhone'] = $item['value'];
                    } elseif (!isset($hash['cellPhone'])) {
                        $hash['cellPhone'] = $item['value'];
                    }
                } elseif (isset($item['params']['VIDEO'])) {
                    if (isset($item['params']['WORK']) &&
                        !isset($hash['workVideoCall'])) {
                        $hash['workVideoCall'] = $item['value'];
                    } elseif (isset($item['params']['HOME']) &&
                              !isset($hash['homeVideoCall'])) {
                        $hash['homeVideoCall'] = $item['value'];
                    } elseif (!isset($hash['videoCall'])) {
                        $hash['videoCall'] = $item['value'];
                    }
                } else {
                    if (isset($item['params']['WORK']) &&
                        !isset($hash['workPhone'])) {
                        $hash['workPhone'] = $item['value'];
                    } elseif (isset($item['params']['HOME']) &&
                              !isset($hash['homePhone'])) {
                        $hash['homePhone'] = $item['value'];
                    } else {
                        $hash['phone'] = $item['value'];
                    }
                }
                break;

            case 'EMAIL':
                $email_set = false;
                if (isset($item['params']['HOME']) &&
                    !empty($this->map['homeEmail']) &&
                    (!isset($hash['homeEmail']) ||
                     isset($item['params']['PREF']))) {
                    $e = Horde_Icalendar_Vcard::getBareEmail($item['value']);
                    $hash['homeEmail'] = $e ? $e : '';
                    $email_set = true;
                } elseif (isset($item['params']['WORK']) &&
                          !empty($this->map['workEmail']) &&
                          (!isset($hash['workEmail']) ||
                           isset($item['params']['PREF']))) {
                    $e = Horde_Icalendar_Vcard::getBareEmail($item['value']);
                    $hash['workEmail'] = $e ? $e : '';
                    $email_set = true;
                } elseif (isset($item['params']['TYPE'])) {
                    if (!is_array($item['params']['TYPE'])) {
                        $item['params']['TYPE'] = array($item['params']['TYPE']);
                    }
                    foreach ($item['params']['TYPE'] as &$type) {
                        $type = Horde_String::upper($type);
                    }
                    if (in_array('HOME', $item['params']['TYPE']) &&
                        !empty($this->map['homeEmail']) &&
                        (!isset($hash['homeEmail']) ||
                         in_array('PREF', $item['params']['TYPE']))) {
                        $e = Horde_Icalendar_Vcard::getBareEmail($item['value']);
                        $hash['homeEmail'] = $e ? $e : '';
                        $email_set = true;
                    } elseif (in_array('WORK', $item['params']['TYPE']) &&
                              !empty($this->map['workEmail']) &&
                              (!isset($hash['workEmail']) ||
                         in_array('PREF', $item['params']['TYPE']))) {
                        $e = Horde_Icalendar_Vcard::getBareEmail($item['value']);
                        $hash['workEmail'] = $e ? $e : '';
                        $email_set = true;
                    }
                }

                if (!$email_set &&
                    (!isset($hash['email']) ||
                     isset($item['params']['PREF']) ||
                     (!empty($item['params']['TYPE']) && is_array($item['params']['TYPE']) && in_array('PREF', $item['params']['TYPE'])))) {
                    $e = Horde_Icalendar_Vcard::getBareEmail($item['value']);
                    $hash['email'] = $e ? $e : '';
                }

                if (!isset($hash['emails'])) {
                    $hash['emails'] = '';
                }
                if ($e = Horde_Icalendar_Vcard::getBareEmail($item['value'])) {
                    if (strlen($hash['emails'])) {
                        $hash['emails'] .= ',';
                    }
                    $hash['emails'] .= $e;
                }
                break;

            case 'TITLE':
                $hash['title'] = $item['value'];
                break;

            case 'ROLE':
                $hash['role'] = $item['value'];
                break;

            case 'ORG':
                // The VCARD 2.1 specification requires the presence of two
                // SEMI-COLON separated fields: Organizational Name and
                // Organizational Unit. Additional fields are optional.
                $hash['company'] = !empty($item['values'][0]) ? $item['values'][0] : '';
                $hash['department'] = !empty($item['values'][1]) ? $item['values'][1] : '';
                break;

            case 'NOTE':
                $hash['notes'] = $item['value'];
                break;

            case 'CATEGORIES':
                $hash['businessCategory'] = $item['value'];
                $hash['__tags'] = $item['values'];
                break;

            case 'URL':
                if (isset($item['params']['HOME']) &&
                    !isset($hash['homeWebsite'])) {
                    $hash['homeWebsite'] = $item['value'];
                } elseif (isset($item['params']['WORK']) &&
                          !isset($hash['workWebsite'])) {
                    $hash['workWebsite'] = $item['value'];
                } elseif (!isset($hash['website'])) {
                    $hash['website'] = $item['value'];
                }
                break;

            case 'BDAY':
                if (empty($item['value'])) {
                    $hash['birthday'] = null;
                } else {
                    $hash['birthday'] = $item['value']['year'] . '-' . $item['value']['month'] . '-' .  $item['value']['mday'];
                }
                break;

            case 'PHOTO':
            case 'LOGO':
                if (isset($item['params']['VALUE']) &&
                    Horde_String::lower($item['params']['VALUE']) == 'uri') {
                    // No support for URIs yet.
                    break;
                }
                if (!isset($item['params']['ENCODING']) ||
                    (Horde_String::lower($item['params']['ENCODING']) != 'b' &&
                     Horde_String::upper($item['params']['ENCODING']) != 'BASE64')) {
                    // Invalid property.
                    break;
                }
                $type = Horde_String::lower($item['name']);
                $hash[$type] = base64_decode($item['value']);
                if (isset($item['params']['TYPE'])) {
                    $hash[$type . 'type'] = $item['params']['TYPE'];
                }
                break;

            case 'X-SIP':
                if (isset($item['params']['POC']) &&
                    !isset($hash['ptt'])) {
                    $hash['ptt'] = $item['value'];
                } elseif (isset($item['params']['VOIP']) &&
                          !isset($hash['voip'])) {
                    $hash['voip'] = $item['value'];
                } elseif (isset($item['params']['SWIS']) &&
                          !isset($hash['shareView'])) {
                    $hash['shareView'] = $item['value'];
                } elseif (!isset($hash['sip'])) {
                    $hash['sip'] = $item['value'];
                }
                break;

            case 'X-WV-ID':
                $hash['imaddress'] = $item['value'];
                break;

            case 'X-ANNIVERSARY':
                if (empty($item['value'])) {
                    $hash['anniversary'] = null;
                } else {
                    $hash['anniversary'] = $item['value']['year'] . '-' . $item['value']['month'] . '-' . $item['value']['mday'];
                }
                break;

            case 'X-CHILDREN':
                $hash['children'] = $item['value'];
                break;

            case 'X-SPOUSE':
                $hash['spouse'] = $item['value'];
                break;
            }
        }

        /* Ensure we have a valid name field. */
        $hash = $this->_parseName($hash);

        return $hash;
    }

    /**
     * Convert the contact to an ActiveSync contact message
     *
     * @param Turba_Object $object  The turba object to convert
     * @param array $options        Options:
     *   - protocolversion: (float)  The EAS version to support
     *                      DEFAULT: 2.5
     *   - bodyprefs: (array)  A BODYPREFERENCE array.
     *                DEFAULT: none (No body prefs enforced).
     *   - truncation: (integer)  Truncate event body to this length
     *                 DEFAULT: none (No truncation).
     *   - device: (Horde_ActiveSync_Device) The device object.
     *
     * @return Horde_ActiveSync_Message_Contact
     */
    public function toASContact(Turba_Object $object, array $options = array())
    {
        global $injector;

        $message = new Horde_ActiveSync_Message_Contact(array(
            'logger' => $injector->getInstance('Horde_Log_Logger'),
            'protocolversion' => $options['protocolversion'],
            'device' => !empty($options['device']) ? $options['device'] : null
        ));
        $hash = $object->getAttributes();
        if (!isset($hash['lastname']) && isset($hash['name'])) {
            $this->_guessName($hash);
        }

        // Ensure we have at least a good guess as to separate address fields.
        // Not ideal, but EAS does not have a single "address" field so we must
        // map "common" to either home or work. I choose home since
        // work/non-personal installs will be more likely to have separated
        // address fields.
        if (!empty($hash['commonAddress'])) {
            if (!isset($hash['commonStreet'])) {
                $hash['commonStreet'] = $hash['commonHome'];
            }
            foreach (array('Address', 'Street', 'POBox', 'Extended', 'City', 'Province', 'PostalCode', 'Country') as $field) {
                $hash['home' . $field] = $hash['common' . $field];
            }
        } else {
            if (isset($hash['homeAddress']) && !isset($hash['homeStreet'])) {
                $hash['homeStreet'] = $hash['homeAddress'];
            }
            if (isset($hash['workAddress']) && !isset($hash['workStreet'])) {
                $hash['workStreet'] = $hash['workAddress'];
            }
        }

        $hooks = $injector->getInstance('Horde_Core_Hooks');
        $decode_hook = $hooks->hookExists('decode_attribute', 'turba');

        foreach ($hash as $field => $value) {
            if ($decode_hook) {
                try {
                    $value = $hooks->callHook(
                        'decode_attribute',
                        'turba',
                        array($field, $value, $object)
                    );
                } catch (Turba_Exception $e) {
                    Horde::log($e);
                }
            }
            if (isset(self::$_asMap[$field])) {
                try {
                    $message->{self::$_asMap[$field]} = $value;
                } catch (InvalidArgumentException $e) {
                }
                continue;
            }

            switch ($field) {
            case 'photo':
                $message->picture = base64_encode($value);
                break;

            case 'homeCountry':
                $message->homecountry = !empty($hash['homeCountryFree'])
                    ? $hash['homeCountryFree']
                    : !empty($hash['homeCountry'])
                        ? Horde_Nls::getCountryISO($hash['homeCountry'])
                        : null;
                break;

            case 'otherCountry':
                $message->othercountry = !empty($hash['otherCountryFree'])
                    ? $hash['otherCountryFree']
                    : !empty($hash['otherCountry'])
                        ? Horde_Nls::getCountryISO($hash['otherCountry'])
                        : null;
                break;

            case 'workCountry':
                $message->businesscountry = !empty($hash['workCountryFree'])
                    ? $hash['workCountryFree']
                    : !empty($hash['workCountry'])
                        ? Horde_Nls::getCountryISO($hash['workCountry'])
                        : null;
                break;

            case 'email':
                $message->email1address = $value;
                break;

            case 'homeEmail':
                $message->email2address = $value;
                break;

            case 'workEmail':
                $message->email3address = $value;
                break;

            case 'emails':
                $address = 1;
                foreach (explode(',', $value) as $email) {
                    while ($address <= 3 &&
                           $message->{'email' . $address . 'address'}) {
                        $address++;
                    }
                    if ($address > 3) {
                        break;
                    }
                    $message->{'email' . $address . 'address'} = $email;
                    $address++;
                }
                break;

            case 'children':
                // Children FROM horde are a simple string value. Even though EAS
                // uses an array stucture to pass them, we pass as a single
                // string since we can't assure what delimter the user will
                // use and (at least in some languages) a comma can be used
                // within a full name.
                $message->children = array($value);
                break;

            case 'notes':
                if (strlen($value) && $options['protocolversion'] > Horde_ActiveSync::VERSION_TWOFIVE) {
                    $bp = $options['bodyprefs'];
                    $note = new Horde_ActiveSync_Message_AirSyncBaseBody();
                    // No HTML supported in Turba's notes. Always use plaintext.
                    $note->type = Horde_ActiveSync::BODYPREF_TYPE_PLAIN;
                    if (isset($bp[Horde_ActiveSync::BODYPREF_TYPE_PLAIN]['truncationsize']) &&
                        Horde_String::length($value) > $bp[Horde_ActiveSync::BODYPREF_TYPE_PLAIN]['truncationsize']) {
                            $note->data = Horde_String::substr($value, 0, $bp[Horde_ActiveSync::BODYPREF_TYPE_PLAIN]['truncationsize']);
                            $note->truncated = 1;
                    } else {
                        $note->data = $value;
                    }
                    $note->estimateddatasize = Horde_String::length($value);
                    $message->airsyncbasebody = $note;
                } elseif (strlen($value)) {
                    // EAS 2.5
                    $message->body = $value;
                    $message->bodysize = strlen($message->body);
                    $message->bodytruncated = 0;
                }
                break;

            case 'birthday':
            case 'anniversary':
                if (!empty($value) && $value != '0000-00-00') {
                    try {
                        $date = new Horde_Date($value);
                    } catch (Horde_Date_Exception $e) {
                        $message->$field = null;
                    }
                    // Some sanity checking to make sure the date was
                    // successfully parsed.
                    if ($date->month != 0) {
                        $message->$field = $date;
                    } else {
                        $message->$field = null;
                    }
                } else {
                    $message->$field = null;
                }
                break;
            }
        }

        /* Get tags. */
        $message->categories = $injector->getInstance('Turba_Tagger')
            ->split($object->getValue('__tags'));

        if (empty($this->fileas)) {
            $message->fileas = Turba::formatName($object);
        }

        return $message;
    }

    /**
     * Convert an ActiveSync contact message into a hash suitable for
     * importing via self::add().
     *
     * @param Horde_ActiveSync_Message_Contact $message  The contact message
     *                                                   object.
     *
     * @return array  A contact hash.
     */
    public function fromASContact(Horde_ActiveSync_Message_Contact $message)
    {
        $hash = array();

        foreach (self::$_asMap as $turbaField => $asField) {
            if (!$message->isGhosted($asField)) {
                try {
                    $hash[$turbaField] = $message->{$asField};
                } catch (InvalidArgumentException $e) {
                }
            }
        }

        // Try our best to get a name attribute;
        $hash = $this->_parseName($hash);

        /* Requires special handling */

        try {
            if ($message->getProtocolVersion() >= Horde_ActiveSync::VERSION_TWELVE) {
                if (!empty($message->airsyncbasebody)) {
                    $hash['notes'] = $message->airsyncbasebody->data;
                }
            } else {
                $hash['notes'] = $message->body;
            }
        } catch (InvalidArgumentException $e) {}

        // picture ($message->picture *should* already be base64 encdoed)
        if (!$message->isGhosted('picture')) {
            $hash['photo'] = base64_decode($message->picture);
        }

        /* Email addresses */
        $hash['emails'] = array();
        if (!$message->isGhosted('email1address')) {
            $e = Horde_Icalendar_Vcard::getBareEmail($message->email1address);
            $hash['emails'][] = $hash['email'] = $e ? $e : '';

        }
        if (!$message->isGhosted('email2address')) {
            $e = Horde_Icalendar_Vcard::getBareEmail($message->email2address);
            $hash['emails'][] = $hash['homeEmail'] = $e ? $e : '';
        }
        if (!$message->isGhosted('email3address')) {
            $e = Horde_Icalendar_Vcard::getBareEmail($message->email3address);
            $hash['emails'][] = $hash['workEmail'] = $e ? $e : '';
        }
        $hash['emails'] = implode(',', $hash['emails']);

        /* Categories */
        if (!$message->isGhosted('categories') && empty($message->categories)) {
            $hash['__tags'] = array();
        } elseif (is_array($message->categories) &&
                  count($message->categories)) {
            $hash['__tags'] = $message->categories;
        }

        /* Children */
        if (is_array($message->children) && count($message->children)) {
            // We use a comma as incoming delimiter as it's the most
            // common even though it might be used withing a name string.
            $hash['children'] = implode(', ', $message->children);
        } elseif (!$message->isGhosted('children')) {
            $hash['children'] = '';
        }

        /* Birthday and Anniversary */
        if (!empty($message->birthday)) {
            $bday = new Horde_Date($message->birthday);
            $bday->setTimezone(date_default_timezone_get());
            $hash['birthday'] = $bday->format('Y-m-d');
        } elseif (!$message->isGhosted('birthday')) {
            $hash['birthday'] = '';
        }
        if (!empty($message->anniversary)) {
            $anniversary = new Horde_Date($message->anniversary);
            $anniversary->setTimezone(date_default_timezone_get());
            $hash['anniversary'] = $anniversary->format('Y-m-d');
        } elseif (!$message->isGhosted('anniversary')) {
            $hash['anniversary'] = '';
        }

        /* Countries */
        include 'Horde/Nls/Countries.php';
        if (!empty($message->homecountry)) {
            if (!empty($this->map['homeCountryFree'])) {
                $hash['homeCountryFree'] = $message->homecountry;
            } else {
                $country = array_search($message->homecountry, $countries);
                if ($country === false) {
                    $country = $message->homecountry;
                }
                $hash['homeCountry'] = $country;
            }
        } elseif (!$message->isGhosted('homecountry')) {
            $hash['homeCountry'] = '';
        }

        if (!empty($message->businesscountry)) {
            if (!empty($this->map['workCountryFree'])) {
                $hash['workCountryFree'] = $message->businesscountry;
            } else {
                $country = array_search($message->businesscountry, $countries);
                if ($country === false) {
                    $country = $message->businesscountry;
                }
                $hash['workCountry'] = $country;
            }
        } elseif (!$message->isGhosted('businesscountry')) {
            $hash['workCountry'] = '';
        }

        if (!empty($message->othercountry)) {
            if (!empty($this->map['otherCountryFree'])) {
                $hash['otherCountryFree'] = $message->othercountry;
            } else {
                $country = array_search($message->othercountry, $countries);
                if ($country === false) {
                    $country = $message->othercountry;
                }
                $hash['otherCountry'] = $country;
            }
        } elseif (!$message->isGhosted('othercountry')) {
            $hash['otherCountry'] = '';
        }

        return $hash;
    }

    /**
     * Checks $hash for the presence of a 'name' attribute. If not found,
     * attempt to build one from other available values.
     *
     * @param  array $hash  A hash of turba attributes.
     *
     * @return array  Hash of Turba attributes, with the 'name' attribute
     *                populated.
     */
    protected function _parseName(array $hash)
    {
        if (empty($hash['name'])) {
            /* If name is a composite field, it won't be present in the
             * $this->fields array, so check for that as well. */
            if (isset($this->map['name']) &&
                is_array($this->map['name']) &&
                !empty($this->map['name']['attribute'])) {
                $fieldarray = array();
                foreach ($this->map['name']['fields'] as $mapfields) {
                    $fieldarray[] = isset($hash[$mapfields]) ?
                        $hash[$mapfields] : '';
                }
                $hash['name'] = Turba::formatCompositeField($this->map['name']['format'], $fieldarray);
            } else {
                $hash['name'] = isset($hash['firstname']) ? $hash['firstname'] : '';
                if (!empty($hash['lastname'])) {
                    $hash['name'] .= ' ' . $hash['lastname'];
                }
                $hash['name'] = trim($hash['name']);
            }
        }

        return $hash;
    }

    /**
     * Checks if the current user has the requested permissions on this
     * address book.
     *
     * @param integer $perm  The permission to check for.
     *
     * @return boolean  True if the user has permission, otherwise false.
     */
    public function hasPermission($perm)
    {
        $perms = $GLOBALS['injector']->getInstance('Horde_Perms');
        return $perms->exists('turba:sources:' . $this->_name)
            ? $perms->hasPermission('turba:sources:' . $this->_name, $GLOBALS['registry']->getAuth(), $perm)
            // Assume we have permissions if they're not explicitly set.
            : true;
    }

    /**
     * Return the name of this address book.
     * (This is the key into the cfgSources array)
     *
     * @string Address book name
     */
    public function getName()
    {
        return $this->_name;
    }

    /**
     * Return the owner to use when searching or creating contacts in
     * this address book.
     *
     * @return string  Contact owner.
     */
    public function getContactOwner()
    {
        return empty($this->_contact_owner)
            ? $this->_getContactOwner()
            : $this->_contact_owner;
    }

    /**
     * Override the contactOwner setting for this driver.
     *
     * @param string $owner  The contact owner.
     */
    public function setContactOwner($owner)
    {
        $this->_contact_owner = $owner;
    }

    /**
     * Override the name setting for this driver.
     *
     * @param string $name  The source name. This is the key into the
     *                      $cfgSources array.
     */
    public function setSourceName($name)
    {
        $this->_name = $name;
    }

    /**
     * Return the owner to use when searching or creating contacts in
     * this address book.
     *
     * @return string  Contact owner.
     */
    protected function _getContactOwner()
    {
        return $GLOBALS['registry']->getAuth();
    }

    /**
     * Creates a new Horde_Share for this source type.
     *
     * @param string $share_name  The share name
     * @param array  $params      The params for the share.
     *
     * @return Horde_Share  The share object.
     */
    public function createShare($share_name, array $params)
    {
        // If the raw address book name is not set, use the share name
        if (empty($params['params']['name'])) {
            $params['params']['name'] = $share_name;
        }

        return Turba::createShare($share_name, $params);
    }

    /**
     * Runs any actions after setting a new default tasklist.
     *
     * @param string $share  The default share ID.
     */
    public function setDefaultShare($share)
    {
    }

    /**
     * Creates an object key for a new object.
     *
     * @param array $attributes  The attributes (in driver keys) of the
     *                           object being added.
     *
     * @return string  A unique ID for the new object.
     */
    protected function _makeKey(array $attributes)
    {
        return hash('md5', mt_rand());
    }

    /**
     * Creates an object UID for a new object.
     *
     * @return string  A unique ID for the new object.
     */
    protected function _makeUid()
    {
        return strval(new Horde_Support_Guid());
    }

    /**
     * Initialize the driver.
     *
     * @throws Turba_Exception
     */
    protected function _init()
    {
    }

    /**
     * Searches the address book with the given criteria and returns a
     * filtered list of results. If the criteria parameter is an empty array,
     * all records will be returned.
     *
     * @param array $criteria       Array containing the search criteria.
     * @param array $fields         List of fields to return.
     * @param array $blobFields     Array of fields containing binary data.
     * @param boolean $count_only   Only return the count of matching entries,
     *                              not the entries themselves.
     *
     * @return array  Hash containing the search results.
     * @throws Turba_Exception
     */
    protected function _search(array $criteria, array $fields, array $blobFields = array(), $count_only = false)
    {
        throw new Turba_Exception(_("Searching is not available."));
    }

    /**
     * Reads the given data from the address book and returns the results.
     *
     * @param string $key        The primary key field to use.
     * @param mixed $ids         The ids of the contacts to load.
     * @param string $owner      Only return contacts owned by this user.
     * @param array $fields      List of fields to return.
     * @param array $blobFields  Array of fields containing binary data.
     * @param array $dateFields  Array of fields containing date data.
     *                           @since 4.2.0
     *
     * @return array  Hash containing the search results.
     * @throws Turba_Exception
     */
    protected function _read($key, $ids, $owner, array $fields,
                             array $blobFields = array(),
                             array $dateFields = array())
    {
        throw new Turba_Exception(_("Reading contacts is not available."));
    }

    /**
     * Adds the specified contact to the addressbook.
     *
     * @param array $attributes  The attribute values of the contact.
     * @param array $blob_fields  Fields that represent binary data.
     * @param array $date_fields  Fields that represent dates. @since 4.2.0
     *
     * @throws Turba_Exception
     */
    protected function _add(array $attributes, array $blob_fields = array(), array $date_fields = array())
    {
        throw new Turba_Exception(_("Adding contacts is not available."));
    }

    /**
     * Deletes the specified contact from the addressbook.
     *
     * @param string $object_key TODO
     * @param string $object_id  TODO
     *
     * @throws Turba_Exception
     */
    protected function _delete($object_key, $object_id)
    {
        throw new Turba_Exception(_("Deleting contacts is not available."));
    }

    /**
     * Saves the specified object in the SQL database.
     *
     * @param Turba_Object $object  The object to save
     *
     * @return string  The object id, possibly updated.
     * @throws Turba_Exception
     */
    protected function _save(Turba_Object $object)
    {
        throw new Turba_Exception(_("Saving contacts is not available."));
    }

    /**
     * Remove all entries owned by the specified user.
     *
     * @param string $user  The user's data to remove.
     *
     * @throws Turba_Exception
     */
    public function removeUserData($user)
    {
        throw new Turba_Exception_NotSupported(_("Removing user data is not supported in the current address book storage driver."));
    }

    /**
     * Check if the passed in share is the default share for this source.
     *
     * @param Horde_Share_Object $share  The share object.
     * @param array $srcconfig           The cfgSource entry for the share.
     *
     * @return boolean TODO
     */
    public function checkDefaultShare(Horde_Share_Object $share, array $srcconfig)
    {
        $params = @unserialize($share->get('params'));
        if (!isset($params['default'])) {
            $params['default'] = ($params['name'] == $GLOBALS['registry']->getAuth());
            $share->set('params', serialize($params));
            $share->save();
        }

        return $params['default'];
    }

    /* Countable methods. */

    /**
     * Returns the number of contacts of the current user in this address book.
     *
     * @return integer  The number of contacts that the user owns.
     * @throws Turba_Exception
     */
    public function count()
    {
        if (is_null($this->_count)) {
            $this->_count = count(
                $this->_search(array('AND' => array(
                                   array('field' => $this->toDriver('__owner'),
                                         'op' => '=',
                                         'test' => $this->getContactOwner()))),
                               array($this->toDriver('__key')))
            );
        }

        return $this->_count;
    }

    /**
     * Helper function for guessing name parts from a single name string.
     *
     * @param array $hash  The attributes array.
     */
    protected function _guessName(&$hash)
    {
        if (($pos = strpos($hash['name'], ',')) !== false) {
            // Assume Last, First
            $hash['lastname'] = Horde_String::substr($hash['name'], 0, $pos);
            $hash['firstname'] = trim(Horde_String::substr($hash['name'], $pos + 1));
        } elseif (($pos = Horde_String::rpos($hash['name'], ' ')) !== false) {
            // Assume everything after last space as lastname
            $hash['lastname'] = trim(Horde_String::substr($hash['name'], $pos + 1));
            $hash['firstname'] = Horde_String::substr($hash['name'], 0, $pos);
        } else {
            $hash['lastname'] = $hash['name'];
            $hash['firstname'] = '';
        }
    }

}
