<?php

require_once 'Horde/History.php';

/**
 * The Turba_Driver:: class provides a common abstracted interface to the
 * various directory search drivers.  It includes functions for searching,
 * adding, removing, and modifying directory entries.
 *
 * $Horde: turba/lib/Driver.php,v 1.57.2.12 2005/10/13 21:10:31 selsky Exp $
 *
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @author  Jon Parise <jon@csh.rit.edu>
 * @since   Turba 0.0.1
 * @package Turba
 */
class Turba_Driver {

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

    /**
     * The symbolic title of this source.
     *
     * @var string
     */
    var $title;

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

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

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

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

    /**
     * Whether this source is publicly searchable.
     *
     * @var boolean
     */
    var $public = false;

    /**
     * Whether this source is read-only (not editable).
     *
     * @var boolean
     */
    var $readonly = true;

    /**
     * Any admins for this source.
     *
     * @var array
     */
    var $admin = array();

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

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

    /**
     * Constructs a new Turba_Driver object.
     *
     * @param array $params  Hash containing additional configuration
     *                       parameters.
     */
    function Turba_Driver($params)
    {
        $this->_params = $params;
    }

    /**
     * Returns the current driver's additional parameters.
     *
     * @return array  Hash containing the driver's additional parameters.
     */
    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.
     */
    function hasCapability($capability)
    {
        return !empty($this->_capabilities[$capability]);
    }

    /**
     * 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. This ignores composite fields.
     *
     * @param array $hash  Hash using Turba keys.
     *
     * @return array  Translated version of $hash.
     */
    function toDriverKeys($hash)
    {
        $fields = array();
        foreach ($hash as $key => $val) {
            if (isset($this->map[$key]) && !is_array($this->map[$key])) {
                $fields[$this->map[$key]] = $val;
            }
        }
        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  $hash         Hash of criteria using Turba keys.
     * @param string $search_type  OR search or AND search?
     * @param array  $strict       Fields that must be matched exactly.
     *
     * @return array  An array of search criteria.
     */
    function makeSearch($criteria, $search_type, $strict)
    {
        $search = array();
        $strict_search = array();
        foreach ($criteria as $key => $val) {
            if (isset($this->map[$key])) {
                if (is_array($this->map[$key])) {
                    $subsearch = array();
                    foreach ($this->map[$key]['fields'] as $field) {
                        $field = $this->toDriver($field);
                        if (!empty($strict[$field])) {
                            $strict_search[] = array('field' => $field, 'op' => '=', 'test' => $val);
                        } else {
                            $subsearch[] = array('field' => $field, 'op' => 'LIKE', 'test' => $val);
                        }
                    }
                    if (count($subsearch)) {
                        $search[] = array('OR' => $subsearch);
                    }
                } else {
                    if (!empty($strict[$this->map[$key]])) {
                        $strict_search[] = array('field' => $this->map[$key],
                                                 'op' => '=',
                                                 'test' => $val);
                    } else {
                        $search[] = array('field' => $this->map[$key],
                                          'op' => 'LIKE',
                                          'test' => $val);
                    }
                }
            }
        }

        if (count($strict_search) && count($search)) {
            return array('AND' => array($strict_search,
                                        array($search_type => $search)));
        } elseif (count($strict_search)) {
            return array('AND' => $strict_search);
        } elseif (count($search)) {
            return array($search_type => $search);
        } else {
            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.
     */
    function toDriver($attribute)
    {
        return isset($this->map[$attribute]) && !is_array($this->map[$attribute]) ? $this->map[$attribute] : null;
    }

    /**
     * Translates an array of hashes 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 $objects  Array of hashes using driver-specific keys.
     *
     * @return array  Translated version of $objects.
     */
    function toTurbaKeys($objects)
    {
        $attributes = array();
        foreach ($objects as $entry) {
            $new_entry = array();

            foreach ($this->map as $key => $val) {
                if (!is_array($val)) {
                    $new_entry[$key] = null;
                    if (isset($entry[$val]) && strlen($entry[$val])) {
                        $new_entry[$key] = $entry[$val];
                    }
                }
            }

            $attributes[] = $new_entry;
        }
        return $attributes;
    }

    /**
     * Searches the source based on the provided criteria.
     *
     * TODO: Allow $criteria to contain the comparison operator (<, =, >,
     *       'like') and modify the drivers accordingly.
     *
     * @param $search_criteria   Hash containing the search criteria.
     * @param $sort_criteria     The requested sort order which is passed to
     *                           Turba_List::sort().
     * @param string $search_type  (optional) Do an AND or an OR search (defaults to AND).
     *
     * @return                   The sorted, filtered list of search results.
     */
    function &search($search_criteria, $sort_criteria = 'lastname',
                     $search_type = 'AND', $sort_direction = 0)
    {
        require_once TURBA_BASE . '/lib/List.php';
        require_once TURBA_BASE . '/lib/Object.php';

        /* If this is not a public source, enforce the requirement
         * that the source's owner must be equal to the current
         * user. */
        $strict_fields = array();
        if (!$this->public && !in_array(Auth::getAuth(), $this->admin)) {
            $search_criteria['__owner'] = Auth::getAuth();
            $strict_fields = array($this->toDriver('__owner') => true);
        }

        /* Add any fields that much match exactly for this source to
         * the $strict_fields array. */
        foreach ($this->strict as $strict_field) {
            $strict_fields[$strict_field] = true;
        }

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

        /* Retrieve the search results from the driver. */
        $results = $this->_search($fields, array_values($this->fields));

        /* Translate the driver-specific fields in the result back to the more
         * generalized common Turba attributes using the map. */
        $results = $this->toTurbaKeys($results);
        if (is_a($results, 'PEAR_Error')) {
            return $results;
        }

        require_once TURBA_BASE . '/lib/Object.php';
        require_once TURBA_BASE . '/lib/Group.php';
        $list = &new Turba_List();
        foreach ($results as $attributes) {
            if (!empty($attributes['__type']) &&
                $attributes['__type'] == 'Group') {
                $list->insert(new Turba_Group($this, $attributes));
            } else {
                $list->insert(new Turba_Object($this, $attributes));
            }
        }
        $list->sort($sort_criteria, $sort_direction);

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

    /**
     * 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_AbstractObjects).
     */
    function &getObjects($objectIds)
    {
        require_once TURBA_BASE . '/lib/Object.php';
        $criteria = $this->map['__key'];

        $objects = $this->_read($criteria, $objectIds, array_values($this->fields));
        if (!is_array($objects)) {
            return $result = PEAR::raiseError(_("Requested object not found."));
        }

        $results = array();
        $objects = $this->toTurbaKeys($objects);
        foreach ($objects as $object) {
            $done = false;
            if (!empty($object['__type'])) {
                $type = ucwords($object['__type']);
                $class = 'Turba_' . $type;
                if (!class_exists($class)) {
                    @require_once TURBA_BASE . '/lib/' . $type . '.php';
                }

                if (class_exists($class)) {
                    $results[] = &new $class($this, $object);
                    $done = true;
                }
            }
            if (!$done) {
                $results[] = &new Turba_Object($this, $object);
            }
        }

        return $results;
    }

    /**
     * Retrieves one object from the source.
     *
     * @param string $objectId  The unique id of the object to retrieve.
     *
     * @return Turba_AbstractObject  The retrieved object.
     */
    function &getObject($objectId)
    {
        $result = &$this->getObjects(array($objectId));
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        } elseif (empty($result[0])) {
            return $result = PEAR::raiseError('No results');
        } else {
            if (!isset($this->map['__owner'])) {
                $result[0]->attributes['__owner'] = Auth::getAuth();
            }
            return $result[0];
        }
    }

    /**
     * Adds a new entry to the contact source.
     *
     * @param array $attributes  The attributes of the new object to add.
     *
     * @return mixed  The new __key value on success, or a PEAR_Error object
     *                on failure.
     */
    function add($attributes)
    {
        if ($this->readonly) {
            return PEAR::raiseError(_("This address book is read-only"));
        }

        if (!isset($attributes['__type'])) {
            $attributes['__type'] = 'Object';
        }
        if (isset($this->map['__owner'])) {
            $attributes['__owner'] = Auth::getAuth();
        }
        if (!isset($attributes['__uid'])) {
            $attributes['__uid'] = $this->generateUID();
        }

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

        $attributes = $this->toDriverKeys($attributes);
        $result = $this->_add($attributes);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        /* Log the creation of this item in the history log. */
        $history = &Horde_History::singleton();
        $history->log('turba:' . (!empty($attributes['__owner']) ? $attributes['__owner'] : Auth::getAuth()) . ':' . $uid, array('action' => 'add'), true);

        return $key;
    }

    /**
     * Deletes the specified entry from the contact source.
     *
     * @param string $object_id  The ID of the object to delete.
     */
    function delete($object_id)
    {
        if ($this->readonly) {
            return PEAR::raiseError(_("This address book is read-only"));
        }

        $object = &$this->getObject($object_id);
        if (is_a($object, 'PEAR_Error')) {
            return $object;
        }

        if (!Turba::hasPermission($object, 'object', PERMS_DELETE)) {
            return PEAR::raiseError(_("Permission denied"));
        }

        $result = $this->_delete($this->toDriver('__key'), $object_id);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        /* Log the deletion of this item in the history log. */
        if ($object->getValue('__uid')) {
            $history = &Horde_History::singleton();
            $history->log('turba:' . ($object->getValue('__owner') ? $object->getValue('__owner') : Auth::getAuth()) . ':' . $object->getValue('__uid'),
                          array('action' => 'delete'), true);
        }

        return true;
    }

    /**
     * Deletes all contacts from the address book.
     *
     * @return boolean  True if the operation worked.
     */
    function deleteAll()
    {
        if (!$this->hasCapability('delete_all')) {
            return PEAR::raiseError('Not supported');
        } else {
            return $this->_deleteAll();
        }
    }

    /**
     * Modifies an existing entry in the contact source.
     *
     * @param Turba_AbstractObject $object  The object to update.
     *
     * @return string  The object id, possibly updated.
     */
    function save($object)
    {
        if ($this->readonly) {
            return PEAR::raiseError(_("Address book is read-only."));
        }

        $attributes = $this->toDriverKeys($object->getAttributes());
        $key = $this->toDriverKeys(array('__key' => $object->getValue('__key')));
        list($object_key, $object_id) = each($key);

        $object_id = $this->_save($object_key, $object_id, $attributes);
        if (is_a($object_id, 'PEAR_Error')) {
            return $object_id;
        }

        /* Log the modification of this item in the history log. */
        if ($object->getValue('__uid')) {
            $history = &Horde_History::singleton();
            $history->log('turba:' . ($object->getValue('__owner') ? $object->getValue('__owner') : Auth::getAuth()) . ':' . $object->getValue('__uid'),
                          array('action' => 'modify'), true);
        }

        return $object_id;
    }

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

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

    /**
     * Generates a universal/unique identifier for a contact. This is NOT
     * something that we expect to be able to parse into an addressbook and a
     * contactId.
     *
     * @return string  A nice unique string (should be 255 chars or less).
     */
    function generateUID()
    {
        return date('YmdHis') . '.' .
            substr(base_convert(microtime(), 10, 36), -16) .
            '@' . $GLOBALS['conf']['server']['name'];
    }

    /**
     * Exports a given Turba_AbstractObject as an iCalendar vCard.
     *
     * @param Turba_AbstractObject $object  A Turba_AbstractObject.
     *
     * @return Horde_iCalendar_vcard  A Horde_iCalendar_vcard object.
     */
    function tovCard($object)
    {
        require_once 'Horde/iCalendar/vcard.php';

        $hash = $object->getAttributes();
        $iCal = &new Horde_iCalendar();
        $vcard = Horde_iCalendar::newComponent('vcard', $iCal);

        foreach ($hash as $key => $val) {
            switch ($key) {
            case 'name':
                $vcard->setAttribute('FN', $val);
                break;

            case 'nickname':
                $vcard->setAttribute('NICKNAME', $val);
                break;

            case 'homePhone':
                $vcard->setAttribute('TEL', $val, array('TYPE'=>'HOME'));
                break;

            case 'workPhone':
                $vcard->setAttribute('TEL', $val, array('TYPE'=>'WORK'));
                break;

            case 'cellPhone':
                $vcard->setAttribute('TEL', $val, array('TYPE'=>'CELL'));
                break;

            case 'fax':
                $vcard->setAttribute('TEL', $val, array('TYPE'=>'FAX'));
                break;

            case 'email':
                $vcard->setAttribute('EMAIL', Horde_iCalendar_vcard::getBareEmail($val));
                break;

            case 'title':
                $vcard->setAttribute('TITLE', $val);
                break;

            case 'company':
                $vcard->setAttribute('ORG', $val);
                break;

            case 'notes':
                $vcard->setAttribute('NOTE', $val);
                break;

            case 'website':
                $vcard->setAttribute('URL', $val);
                break;

            case 'birthday':
                $vcard->setAttribute('BDAY', $val);
                break;
            }
        }

        if (!empty($hash['lastname']) || !empty($hash['firstname'])) {
            $a = array(
                VCARD_N_FAMILY => isset($hash['lastname']) ? $hash['lastname'] : '',
                VCARD_N_GIVEN  => isset($hash['firstname']) ? $hash['firstname'] : '',
                VCARD_N_ADDL   => '',
                VCARD_N_PREFIX => '',
                VCARD_N_SUFFIX => '',
            );
            $vcard->setAttribute('N', implode(';', $a), array(), false, $a);
        }

        $a = array(
            VCARD_ADR_POB      => '',
            VCARD_ADR_EXTEND   => '',
            VCARD_ADR_STREET   => isset($hash['homeAddress']) ? $hash['homeAddress'] : '',
            VCARD_ADR_LOCALITY => isset($hash['homeCity']) ? $hash['homeCity'] : '',
            VCARD_ADR_REGION   => isset($hash['homeProvince']) ? $hash['homeProvince'] : '',
            VCARD_ADR_POSTCODE => isset($hash['homePostalCode']) ? $hash['homePostalCode'] : '',
            VCARD_ADR_COUNTRY  => isset($hash['homeCountry']) ? $hash['homeCountry'] : '',
        );
        $vcard->setAttribute('ADR', implode(';', $a), array('TYPE' => 'HOME'), true, $a);

        $a = array(
            VCARD_ADR_POB      => '',
            VCARD_ADR_EXTEND   => '',
            VCARD_ADR_STREET   => isset($hash['workAddress']) ? $hash['workAddress'] : '',
            VCARD_ADR_LOCALITY => isset($hash['workCity']) ? $hash['workCity'] : '',
            VCARD_ADR_REGION   => isset($hash['workProvince']) ? $hash['workProvince'] : '',
            VCARD_ADR_POSTCODE => isset($hash['workPostalCode']) ? $hash['workPostalCode'] : '',
            VCARD_ADR_COUNTRY  => isset($hash['workCountry']) ? $hash['workCountry'] : '',
        );
        $vcard->setAttribute('ADR', implode(';', $a), array('TYPE' => 'WORK'), true, $a);

        return $vcard;
    }

    /**
     * Static 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.
     */
    function toHash($vcard)
    {
        if (!is_a($vcard, 'Horde_iCalendar_vcard')) {
            return PEAR::raiseError('Invalid parameter for Turba_Driver::toHash(), expected Horde_iCalendar_vcard object.');
        }

        $hash = array();
        $attr = $vcard->getAllAttributes();
        foreach ($attr as $item) {
            if (empty($item['value'])) {
                continue;
            }

            switch ($item['name']) {
            case 'FN':
                $hash['name'] = $item['value'];
                break;

            case 'N':
                $name = $item['values'];
                $hash['lastname'] = $name[VCARD_N_FAMILY];
                $hash['firstname'] = $name[VCARD_N_GIVEN];
                break;

            case 'NICKNAME':
                $hash['nickname'] = $item['value'];
                break;

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

            // For vCard 3.0.
            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';
                    }
                }

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

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

                    default:
                        $prefix = null;
                    }

                    if ($prefix) {
                        $hash[$prefix . 'Address'] = '';

                        if (!empty($address[VCARD_ADR_STREET])) {
                            $hash[$prefix . 'Street'] = $address[VCARD_ADR_STREET];
                            $hash[$prefix . 'Address'] .= $hash[$prefix . 'Street'] . "\n";
                        }
                        if (!empty($address[VCARD_ADR_LOCALITY])) {
                            $hash[$prefix . 'City'] = $address[VCARD_ADR_LOCALITY];
                            $hash[$prefix . 'Address'] .= $hash[$prefix . 'City'];
                        }
                        if (!empty($address[VCARD_ADR_REGION])) {
                            $hash[$prefix . 'Province'] = $address[VCARD_ADR_REGION];
                            $hash[$prefix . 'Address'] .= ', ' . $hash[$prefix . 'Province'];
                        }
                        if (!empty($address[VCARD_ADR_POSTCODE])) {
                            $hash[$prefix . 'PostalCode'] = $address[VCARD_ADR_POSTCODE];
                            $hash[$prefix . 'Address'] .= ' ' . $hash[$prefix . 'PostalCode'];
                        }
                        if (!empty($address[VCARD_ADR_COUNTRY])) {
                            $hash[$prefix . 'Country'] = $address[VCARD_ADR_COUNTRY];
                            $hash[$prefix . 'Address'] .= "\n" . $hash[$prefix . 'Country'];
                        }

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

            case 'TEL':
                if (isset($item['params']['FAX'])) {
                    $hash['fax'] = $item['value'];
                } elseif (isset($item['params']['TYPE'])) {
                    if (!is_array($item['params']['TYPE'])) {
                        $item['params']['TYPE'] = array($item['params']['TYPE']);
                    }
                    // For vCard 3.0.
                    foreach ($item['params']['TYPE'] as $tel) {
                        if (String::upper($tel) == 'WORK') {
                            $hash['workPhone'] = $item['value'];
                        } elseif (String::upper($tel) == 'HOME') {
                            $hash['homePhone'] = $item['value'];
                        } elseif (String::upper($tel) == 'CELL') {
                            $hash['cellPhone'] = $item['value'];
                        } elseif (String::upper($tel) == 'FAX') {
                            $hash['fax'] = $item['value'];
                        }
                    }
                } else {
                    if (isset($item['params']['HOME'])) {
                        $hash['homePhone'] = $item['value'];
                    } elseif (isset($item['params']['WORK'])) {
                        $hash['workPhone'] = $item['value'];
                    } elseif (isset($item['params']['CELL'])) {
                        $hash['cellPhone'] = $item['value'];
                    } else {
                        $hash['homePhone'] = $item['value'];
                    }
                }
                break;

            case 'EMAIL':
                if (isset($item['params']['PREF']) || !isset($hash['email'])) {
                    $hash['email'] = Horde_iCalendar_vcard::getBareEmail($item['value']);
                }
                break;

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

            case 'ORG':
                $hash['company'] = $item['value'];
                break;

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

            case 'URL':
                $hash['website'] = $item['value'];
                break;

            case 'BDAY':
                $hash['birthday'] = $item['value']['year'] . '-' . $item['value']['month'] . '-' .  $item['value']['mday'];
                break;
            }
        }

        return $hash;
    }

    /**
     * Static method to construct Turba_Driver objects. Use this so that we
     * can return PEAR_Error objects if anything goes wrong.
     *
     * Should only be called by Turba_Driver::singleton().
     *
     * @see Turba_Driver::singleton()
     * @access private
     *
     * @param string $name   String containing the internal name of this
     *                       source.
     * @param array $config  Array containing the configuration information for
     *                       this source.
     */
    function &factory($name, $config)
    {
        $class = basename($config['type']);
        include_once dirname(__FILE__) . '/Driver/' . $class . '.php';
        $class = 'Turba_Driver_' . $class;
        if (class_exists($class)) {
            $driver = &new $class($config['params']);
        } else {
            return $driver = PEAR::raiseError(sprintf(_("Unable to load the definition of %s."), $class));
        }

        $result = $driver->_init();
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        /* Store name and title. */
        $driver->name = $name;
        $driver->title = $config['title'];

        /* Store and translate the map at the Source level. */
        $driver->map = $config['map'];
        foreach ($driver->map as $key => $val) {
            if (!is_array($val)) {
                $driver->fields[$key] = $val;
            }
        }

        /* Store tabs. */
        if (isset($config['tabs'])) {
            $driver->tabs = $config['tabs'];
        }

        /* Store strict fields. */
        if (isset($config['strict'])) {
            $driver->strict = $config['strict'];
        }

        /* Store admins. */
        if (isset($config['admin'])) {
            $driver->admin = $config['admin'];
        }

        /* Set flags. */
        if (isset($config['public'])) {
            $driver->public = $config['public'];
        }
        if (isset($config['readonly'])) {
            $driver->readonly = $config['readonly'] &&
                (!isset($config['admin']) || !in_array(Auth::getAuth(), $config['admin']));
        }

        return $driver;
    }

    /**
     * Attempts to return a reference to a concrete Turba_Driver instance
     * based on the $config array. It will only create a new instance if no
     * Turba_Driver instance with the same parameters currently exists.
     *
     * This method must be invoked as:
     *   $driver = &Turba_Driver::singleton()
     *
     * @param string $name   String containing the internal name of this
     *                       source.
     * @param array $config  Array containing the configuration information for
     *                       this source.
     *
     * @return Turba_Driver  The concrete Turba_Driver reference, or a
     *                       PEAR_Error on error.
     */
    function &singleton($name, $config)
    {
        static $instances = array();

        if (!isset($instances[$name])) {
            $instances[$name] = &Turba_Driver::factory($name, $config);
        }

        return $instances[$name];
    }

}
