<?php
/**
 * Turba external API interface.
 *
 * $Horde: turba/lib/api.php,v 1.120.2.15 2005/10/05 13:59:43 jan Exp $
 *
 * This file defines Turba's external API interface. Other applications can
 * interact with Turba through this API.
 *
 * @package Turba
 */

$_services['perms'] = array(
    'args' => array(),
    'type' => '{urn:horde}stringArray'
);

$_services['show'] = array(
    'link' => '%application%/display.php?source=|source|&key=|key|&uid=|uid|',
);

$_services['browse'] = array(
    'args' => array('path' => 'string'),
    'type' => '{urn:horde}stringArray',
);

$_services['sources'] = array(
    'args' => array('writeable' => 'boolean'),
    'type' => '{urn:horde}stringArray',
);

$_services['fields'] = array(
    'args' => array('source' => '{urn:horde}stringArray'),
    'type' => '{urn:horde}stringArray',
);

$_services['list'] = array(
    'args' => array(),
    'type' => '{urn:horde}stringArray',
);

$_services['listBy'] = array(
    'args' => array('action' => 'string', 'timestamp' => 'int'),
    'type' => '{urn:horde}stringArray',
);

$_services['import'] = array(
    'args' => array('content' => 'string', 'contentType' => 'string', 'source' => 'string'),
    'type' => 'string',
);

$_services['export'] = array(
    'args' => array('uid' => 'string', 'contentType' => 'string'),
    'type' => 'string',
);

$_services['delete'] = array(
    'args' => array('uid' => 'string'),
    'type' => 'boolean',
);

$_services['replace'] = array(
    'args' => array('uid' => 'string', 'content' => 'string', 'contentType' => 'string'),
    'type' => 'boolean',
);

$_services['search'] = array(
    'args' => array('sources' => '{urn:horde}stringArray', 'names' => '{urn:horde}stringArray', 'fields' => '{urn:horde}stringArray'),
    'type' => '{urn:horde}stringArray',
);

$_services['getContact'] = array(
    'args' => array('source' => 'string', 'objectId' => 'string'),
    'type' => '{urn:horde}stringArray',
);

$_services['getContacts'] = array(
    'args' => array('source' => 'string', 'objectIds' => '{urn:horde}stringArray'),
    'type' => '{urn:horde}stringArray',
);

$_services['addField'] = array(
    'args' => array('address' => 'string', 'name' => 'string', 'field' => 'string', 'value' => 'string', 'source' => 'string'),
    'type' => '{urn:horde}stringArray',
);

$_services['deleteField'] = array(
    'args' => array('address' => 'string', 'field' => 'string', 'sources' => '{urn:horde}stringArray'),
    'type' => '{urn:horde}stringArray',
);

$_services['getField'] = array(
    'args' => array('address' => 'string', 'field' => 'string', 'sources' => '{urn:horde}stringArray'),
    'type' => '{urn:horde}stringArray',
);

$_services['getAllAttributeValues'] = array(
    'args' => array('field' => 'string', 'sources' => '{urn:horde}stringArray'),
    'type' => '{urn:horde}stringArray',
);

$_services['getClientSource'] = array(
    'checkperms' => false,
    'args' => array(),
    'type' => 'string',
);

$_services['getClient'] = array(
    'checkperms' => false,
    'args' => array('objectId' => 'string'),
    'type' => '{urn:horde}stringArray',
);

$_services['getClients'] = array(
    'checkperms' => false,
    'args' => array('objectIds' => '{urn:horde}stringArray'),
    'type' => '{urn:horde}stringArray',
);

$_services['addClient'] = array(
    'args' => array('attributes' => '{urn:horde}stringArray'),
    'type' => 'string',
);

$_services['updateClient'] = array(
    'args' => array('objectId' => 'string', 'attributes' => '{urn:horde}stringArray'),
    'type' => 'string',
);

$_services['deleteClient'] = array(
    'args' => array('objectId' => 'string'),
    'type' => '{urn:horde}stringArray',
);

$_services['searchClients'] = array(
    'checkperms' => false,
    'args' => array('names' => '{urn:horde}stringArray', 'fields' => '{urn:horde}stringArray'),
    'type' => '{urn:horde}stringArray',
);


function _turba_perms()
{
    static $perms = array();
    if (!empty($perms)) {
        return $perms;
    }

    @define('TURBA_BASE', dirname(__FILE__) . '/..');
    require_once TURBA_BASE . '/lib/base.php';
    global $cfgSources;

    $perms['tree']['turba']['sources'] = false;
    $perms['title']['turba:sources'] = _("Sources");

    // Run through every contact source.
    foreach ($cfgSources as $source => $curSource) {
        $perms['tree']['turba']['sources'][$source] = false;
        $perms['title']['turba:sources:' . $source] = $curSource['title'];
    }

    return $perms;
}

function _turba_sources($writeable = false)
{
    require_once dirname(__FILE__) . '/base.php';
    global $cfgSources;

    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
        return array();
    }

    $sources = array();
    foreach ($cfgSources as $key => $entry) {
        if (!$writeable || (!$entry['readonly'] ||
                            (isset($entry['admin']) && in_array(Auth::getAuth(), $entry['admin'])))) {
            $sources[$key] = $entry['title'];
        }
    }

    return $sources;
}

function _turba_fields($source = '')
{
    require_once dirname(__FILE__) . '/base.php';
    global $cfgSources, $attributes;

    if (empty($source) || !isset($cfgSources[$source])) {
        return PEAR::raiseError(_("Invalid address book"), 'horde.error', null, null, $source);
    }

    $fields = array();
    foreach ($cfgSources[$source]['map'] as $field_name => $null) {
        if (substr($field_name, 0, 2) != '__') {
            $fields[$field_name] = array('name' => $field_name,
                                         'type' => $attributes[$field_name]['type'],
                                         'label' => $attributes[$field_name]['label'],
                                         'search' => in_array($field_name, $cfgSources[$source]['search']));
        }
    }

    return $fields;
}

/**
 * Browses through Turba's object tree.
 *
 * @param string $path  The level of the tree to browse.
 *
 * @return array  Content of the specified path.
 */
function _turba_browse($path = '')
{
    require_once dirname(__FILE__) . '/base.php';
    global $registry, $cfgSources;

    if (substr($path, 0, 5) == 'turba') {
        $path = substr($path, 5);
    }
    if (substr($path, 0, 1) == '/') {
        $path = substr($path, 1);
    }
    if (substr($path, -1) == '/') {
        $path = substr($path, 0, -1);
    }

    $addressbooks = $registry->callByPackage('turba', 'sources');
    if (is_a($addressbooks, 'PEAR_Error')) {
        return $addressbooks;
    }

    if (empty($path)) {
        $results = array();
        foreach ($addressbooks as $addressbook => $name) {
            $results['turba/' . $addressbook] =
                array('name' => $name,
                      'icon' => $registry->getImageDir() . '/turba.png',
                      'browseable' => true);
        }
        return $results;
    } elseif (isset($addressbooks[$path])) {
        /* Load the Turba driver. */
        $driver = &Turba_Driver::singleton($path, $cfgSources[$path]);
        if (is_a($driver, 'PEAR_Error')) {
            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $cfgSources[$path]);
        }
        $contacts = $driver->search(array());
        if (is_a($contacts, 'PEAR_Error')) {
            return $contacts;
        }

        $results = array();
        $contacts->reset();
        while ($contact = $contacts->next()) {
            $results['turba/' . $contact->getSource() . '/' . $contact->getValue('__key')] =
                array('name' => Turba::formatName($contact),
                      'browseable' => false);
        }
        return $results;
    } else {
        $path = explode('/', $path);
        if (count($path) == 2 && isset($addressbooks[$path[0]])) {
            // Load the Turba driver.
            $driver = &Turba_Driver::singleton($path[0], $cfgSources[$path[0]]);
            if (is_a($driver, 'PEAR_Error')) {
                return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $cfgSources[$path]);
            }
            
            $contact = &$driver->getObject($path[1]);
            if (is_a($contact, 'PEAR_Error')) {
                return $contact;
            }

            $data = _turba_export($contact->getValue('__uid'), 'text/x-vcard', $contact->getSource());
        }
    }

    return PEAR::raiseError($path . ' does not exist or permission denied');
}

/**
 * Returns an array of UIDs for all contacts that the current user is
 * authorized to see.
 *
 * @return array  An array of UIDs for all contacts the user can access.
 */
function _turba_list()
{
}

/**
 * Returns an array of UIDs for contacts that have had $action happen
 * since $timestamp.
 *
 * @param string  $action     The action to check for - add, modify, or delete.
 * @param integer $timestamp  The time to start the search.
 *
 * @return array  An array of UIDs matching the action and time criteria.
 */
function &_turba_listBy($action, $timestamp)
{
    require_once dirname(__FILE__) . '/base.php';
    require_once 'Horde/History.php';

    $history = &Horde_History::singleton();
    $histories = $history->getByTimestamp('>', $timestamp, array(array('op' => '=', 'field' => 'action', 'value' => $action)), 'turba');
    if (is_a($histories, 'PEAR_Error')) {
        return $histories;
    }

    return array_keys($histories);
}

/**
 * Import a contact represented in the specified contentType.
 *
 * @param string $content      The content of the contact.
 * @param string $contentType  What format is the data in? Currently supports
 *                             array and text/x-vcard.
 * @param string $sourcet      The source into which the contact will be
 *                             imported.
 *
 * @return string  The new UID, or false on failure.
 */
function _turba_import($content, $contentType = 'array', $source = '')
{
    require_once dirname(__FILE__) . '/base.php';
    global $cfgSources;

    /* Check existance of and permissions on the specified source. */
    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources) || !isset($cfgSources[$source])) {
        return PEAR::raiseError(_("Invalid address book"), 'horde.warning');
    }

    if ($cfgSources[$source]['readonly']
        && (!isset($cfgSources[$source]['admin'])
        || !in_array(Auth::getAuth(), $cfgSources[$source]['admin']))) {
        return PEAR::raiseError(_("Permission Denied"), 'horde.error', null, null, $source);
    }

    if (!is_a($content, 'Horde_iCalendar_vcard')) {
        switch ($contentType) {
        case 'array':
            break;

        case 'text/x-vcard':
            require_once 'Horde/iCalendar.php';
            $iCal = &new Horde_iCalendar();
            if (!$iCal->parsevCalendar($content)) {
                return PEAR::raiseError(_("There was an error importing the iCalendar data."));
            }
            switch ($iCal->getComponentCount()) {
            case 0:
                return PEAR::raiseError(_("No vCard data was found."));

            case 1:
                $content = $iCal->getComponent(0);
                break;

            default:
                $ids = array();
                foreach ($iCal->getComponents() as $c) {
                    if (is_a($content, 'Horde_iCalendar_vcard')) {
                        $result = _turba_import($content, $contentType, $source);
                        if (is_a($result, 'PEAR_Error')) {
                            return $result;
                        }
                        $ids[] = $result;
                    }
                }
                return $ids;
            }
            break;

        default:
            return PEAR::raiseError(_("Invalid Content-Type"));
        }
    }

    $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
    if (is_a($driver, 'PEAR_Error')) {
        return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
    }

    if (is_a($content, 'Horde_iCalendar_vcard')) {
        $content = $driver->toHash($content);
    }

    $result = $driver->search($content);
    if (is_a($result, 'PEAR_Error')) {
        return $result;
    } elseif ($result->count() > 0) {
        return PEAR::raiseError(_("Already Exists"), 'horde.message', null, null, $source);
    }

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

    $object = &$driver->getObject($result);
    return $object->getValue('__uid');
}

/**
 * Export a contact, identified by UID, in the requested contentType.
 *
 * @param string $uid          Identify the contact to export.
 * @param string $contentType  What format should the data be in? Currently
 *                             supports text/x-vcard.
 *
 * @return string  The requested data.
 */
function _turba_export($uid, $contentType)
{
    require_once dirname(__FILE__) . '/base.php';
    global $cfgSources;

    if (is_array($contentType)) {
        $options = $contentType;
        $contentType = $options['ContentType'];
        unset($options['ContentType']);
    } else {
        $options = array();
    }

    // Need to reference some sort of mapping here to turn the UID
    // into a source id and contact id:
    //
    // $source = $uid['addressbook'];
    // $objectId = $uid['contactId'];
    return PEAR::raiseError('Turba needs a UID map');

    if (empty($source) || !isset($cfgSources[$source])) {
        return PEAR::raiseError(_("Invalid address book"), 'horde.error', null, null, $source);
    }

    if (empty($objectId)) {
        return PEAR::raiseError(_("Invalid ID"), 'horde.error', null, null, $source);
    }

    if ($cfgSources[$source]['readonly']
        && (!isset($cfgSources[$source]['admin'])
            || !in_array(Auth::getAuth(), $cfgSources[$source]['admin']))) {
        return PEAR::raiseError(_("Permission Denied"), 'horde.error', null, null, $source);
    }

    // If the objectId isn't in $source in the first place, just return
    // true. Otherwise, try to delete it and return success or failure.
    $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
    if (is_a($driver, 'PEAR_Error')) {
        return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
    }
    $result = $driver->search(array('__key' => $objectId));
    if (is_a($result, 'PEAR_Error')) {
        return $result;
    } elseif ($result->count() == 0) {
        return PEAR::raiseError(_("Object not found"), 'horde.error', null, null, $source);
        return true;
    } elseif ($result->count() > 1) {
        return PEAR::raiseError("Internal Horde Error: multiple turba objects with same objectId.", 'horde.error', null, null, $source);
    }

    switch ($contentType) {
    case 'text/x-vcard':
        require_once 'Horde/iCalendar.php';
        $iCal = &new Horde_iCalendar();
        $export = '';
        foreach ($result->objects as $obj) {
            $vcard = $driver->tovCard($obj);
            $vcard->setParameter('FN', $options);
            $vcard->setParameter('ORG', $options);
            $vcard->setParameter('ADR', $options);
            $vcard->setParameter('LABEL', $options);
            $vcard->setParameter('NICKNAME', $options);
            $vcard->setParameter('TITLE', $options);
            $vcard->setParameter('NOTE', $options);
            $vcard->setParameter('N', $options);
            // vCards are not enclosed in BEGIN:VCALENDAR..END:VCALENDAR so
            // export the individual cards instead:
            $export .= $vcard->exportvCalendar();
            $iCal->addComponent($vcard);
        }
        return $export;

    default:
        return PEAR::raiseError(_("Unsupported Content-Type."));
    }
}

/**
 * Deletes a contact identified by UID.
 *
 * @param string|array $uid  Identify the contact to delete, either a single
 *                           UID or an array.
 *
 * @return boolean  Success or failure.
 */
function _turba_delete($uid)
{
    // Handle an arrray of UIDs for convenience of deleting multiple contacts
    // at once.
    if (is_array($uid)) {
        foreach ($uid as $g) {
            $result = _turba_delete($uid);
            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }
        }

        return true;
    }

    require_once dirname(__FILE__) . '/base.php';
    global $cfgSources;

    // Need to reference some sort of mapping here to turn the UID into a
    // source id and contact id:
    //
    // $source = $uid['addressbook'];
    // $objectId = $uid['contactId'];
    return PEAR::raiseError('Turba needs a UID map');

    if (empty($source) || !isset($cfgSources[$source])) {
        return PEAR::raiseError(_("Invalid address book"), 'horde.error', null, null, $source);
    }

    if (empty($objectId)) {
        return PEAR::raiseError(_("Invalid ID"), 'horde.error', null, null, $source);
    }

    if ($cfgSources[$source]['readonly']
        && (!isset($cfgSources[$source]['admin'])
            || !in_array(Auth::getAuth(), $cfgSources[$source]['admin']))) {
        return PEAR::raiseError(_("Permission Denied"), 'horde.error', null, null, $source);
    }

    // If the objectId isn't in $source in the first place, just return
    // true. Otherwise, try to delete it and return success or failure.
    $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
    if (is_a($driver, 'PEAR_Error')) {
        return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
    }
    $result = $driver->search(array('__key' => $objectId));
    if (is_a($result, 'PEAR_Error')) {
        return $result;
    } elseif ($result->count() == 0) {
        return true;
    } else {
        return $driver->delete($objectId);
    }
}

/**
 * Replaces the contact identified by UID with the content represented in the
 * specified contentType.
 *
 * @param string $uid          Idenfity the contact to replace.
 * @param string $content      The content of the contact.
 * @param string $contentType  What format is the data in? Currently supports
 *                             array and text/x-vcard.
 *
 * @return boolean  Success or failure.
 */
function _turba_replace($uid, $content, $contentType)
{
    require_once dirname(__FILE__) . '/base.php';
    global $cfgSources;

    // Need to reference some sort of mapping here to turn the UID into a
    // source id and contact id:
    //
    // $source = $uid['addressbook'];
    // $objectId = $uid['contactId'];
    return PEAR::raiseError('Turba needs a UID map');

    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources) || !isset($cfgSources[$source])) {
        return PEAR::raiseError(_("Invalid address book"), 'horde.warning');
    }

    if (empty($objectId)) {
        return PEAR::raiseError(_("Invalid objectId"), 'horde.error', null, null, $source);
    }

    /* Check permissions. */
    $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
    if (is_a($driver, 'PEAR_Error')) {
        return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
    }
    $object = $driver->getObject($objectId);
    if (is_a($object, 'PEAR_Error')) {
        return $object;
    } elseif (!Turba::hasPermission($object, 'object', PERMS_EDIT)) {
        return PEAR::raiseError(_("Permission Denied"), 'horde.warning');
    }

    switch ($contentType) {
    case 'array':
        break;

    default:
        return PEAR::raiseError(_("Invalid Content-Type"));
    }

    foreach ($content as $attribute => $value) {
        if ($attribute != '__key') {
            $object->setValue($attribute, $value);
        }
    }

    return $object->store();
}

function _turba_search($names = array(), $sources = array(), $fields = array())
{
    require_once dirname(__FILE__) . '/base.php';
    global $cfgSources, $attributes;

    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
        return array();
    }

    if (count($sources) == 0) {
        $sources = array(key($cfgSources));
    }

    $results = array();
    $seen = array();
    foreach ($sources as $source) {
        if (!isset($cfgSources[$source])) {
            continue;
        }

        $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
        if (is_a($driver, 'PEAR_Error')) {
            return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
        }

        foreach ($names as $name) {
            $criteria = array();
            if (isset($fields[$source])) {
                foreach ($fields[$source] as $field) {
                    $criteria[$field] = trim($name);
                }
            }
            if (count($criteria) == 0) {
                $criteria['name'] = trim($name);
            }
            $res = $driver->search($criteria, 'lastname', 'OR');

            if (!is_a($res, 'Turba_List')) {
                continue;
            }

            while ($ob = $res->next()) {
                if (!$ob->isGroup()) {
                    /* Not a group. */
                    $att = $ob->getAttributes();
                    $email = array();
                    foreach (array_keys($att) as $key) {
                        if ($ob->getValue($key) && isset($attributes[$key]) &&
                            $attributes[$key]['type'] == 'email') {
                            $email[] = $ob->getValue($key);
                        }
                    }
                    if (count($email)) {
                        for ($i = 0; $i < count($email); $i++) {
                            $seen_key = trim(String::lower($ob->getValue('name'))) . '/' . trim(String::lower($email[$i]));
                            if (!empty($seen[$seen_key])) {
                                continue;
                            }
                            $seen[$seen_key] = true;
                            if (!isset($results[$name])) {
                                $results[$name] = array();
                            }
                            $results[$name][] = array_merge($att,
                                                    array('id' => $att['__key'],
                                                          'name' => $ob->getValue('name'),
                                                          'email' => $email[$i],
                                                          '__type' => 'Object',
                                                          'source' => $source));
                        }
                    } else {
                        if (!isset($results[$name])) {
                            $results[$name] = array();
                        }
                        $results[$name][] = array_merge($att,
                                                array('id' => $att['__key'],
                                                      'name' => $ob->getValue('name'),
                                                      'email' => null,
                                                      '__type' => 'Object',
                                                      'source' => $source));
                    }
                } else {
                    /* Is a distribution list. */
                    $listatt = $ob->getAttributes();
                    $seeninlist = array();
                    $members = $ob->listMembers();

                    if (is_a($members, 'Turba_List')) {
                        if ($members->count() > 0) {
                            if (!isset($results[$name])) {
                                $results[$name] = array();
                            }
                            $emails = array();
                            while ($ob = $members->next()) {
                                $att = $ob->getAttributes();
                                foreach ($att as $key => $value) {
                                    if (!empty($value) && isset($attributes[$key]) &&
                                        $attributes[$key]['type'] == 'email' &&
                                        empty($seeninlist[trim(String::lower($att['name'])) . trim(String::lower($value))])) {

                                        $emails[] = $value;
                                        $seeninlist[trim(String::lower($att['name'])) . trim(String::lower($value))] = true;
                                    }
                                }
                            }
                                $results[$name][] = array('name' => $listatt['name'], 'email' => implode(', ', $emails), 'id' => $listatt['__key'], 'source' => $source);
                        }
                    }
                }
            }
        }
    }

    return $results;
}

function _turba_getContact($source = '', $objectId = '')
{
    require_once dirname(__FILE__) . '/base.php';
    global $cfgSources;

    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
        return array();
    }

    if (isset($cfgSources[$source])) {
        $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
        if (is_a($driver, 'PEAR_Error')) {
            return $driver;
        }

        $object = $driver->getObject($objectId);
        if (is_a($object, 'PEAR_Error')) {
            return $object;
        }

        /* Check permissions. */
        if (!Turba::hasPermission($object, 'object', PERMS_READ)) {
            return PEAR::raiseError(_("Permission Denied"), 'horde.error', null, null, $source);
        }

        $attributes = array();
        foreach ($cfgSources[$source]['map'] as $field => $map) {
            $attributes[$field] = $object->getValue($field);
        }
        return $attributes;
    }

    return array();
}

function _turba_getContacts($source = '', $objectIds = array())
{
    require_once dirname(__FILE__) . '/base.php';
    global $cfgSources;
    $results = array();
    if (!is_array($objectIds)) {
        $objectIds = array($objectIds);
    }

    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
        return array();
    }

    if (isset($cfgSources[$source])) {
        $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
        if (is_a($driver, 'PEAR_Error')) {
            return $driver;
        }

        $objects = $driver->getObjects($objectIds);
        if (is_a($objects, 'PEAR_Error')) {
            return $objects;
        }

        foreach ($objects as $object) {
            /* Check permissions on this object. */
            if (!Turba::hasPermission($object, 'object', PERMS_READ)) {
                continue;
            }

            $attributes = array();
            foreach ($cfgSources[$source]['map'] as $field => $map) {
                $attributes[$field] = $object->getValue($field);
            }
            $results[] = $attributes;
        }
    }

    return $results;
}

function _turba_getAllAttributeValues($field = '', $sources = array())
{
    require_once dirname(__FILE__) . '/base.php';
    global $cfgSources;

    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
        return array();
    }

    if (!count($sources)) {
        $sources = array(key($cfgSources));
    }

    $results = array();
    foreach ($sources as $source) {
        if (isset($cfgSources[$source])) {
            $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
            if (is_a($driver, 'PEAR_Error')) {
                return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
            }
            $res = $driver->search(array());
            if (!is_a($res, 'Turba_List')) {
                return PEAR::raiseError(_("Search failed"), 'horde.error', null, null, $source);
            }

            while ($ob = $res->next()) {
                if ($ob->hasValue($field)) {
                    $results[$ob->getValue('source') . ':' . $ob->getValue('__key')] = array(
                        'name' => $ob->getValue('name'),
                        'email' => $ob->getValue('email'),
                        $field => $ob->getValue($field));
                }
            }
        }
    }

    return $results;
}

function _turba_getClientSource()
{
    return !empty($GLOBALS['conf']['client']['addressbook']) ? $GLOBALS['conf']['client']['addressbook'] : false;
}

function _turba_getClient($objectId = '')
{
    return $GLOBALS['registry']->call('clients/getContact', array('source' => $GLOBALS['conf']['client']['addressbook'],
                                                                  'objectId' => $objectId));
}

function _turba_getClients($objectIds = array())
{
    return $GLOBALS['registry']->call('clients/getContacts', array('source' => $GLOBALS['conf']['client']['addressbook'],
                                                                   'objectIds' => $objectIds));
}

function _turba_addClient($attributes = array())
{
    return $GLOBALS['registry']->call('clients/import', array('content' => $attributes,
                                                              'contentType' => 'array',
                                                              'source' => $GLOBALS['registry']->call('clients/getClientSource')));
}

function _turba_updateClient($objectId = '', $attributes = array())
{
    return $GLOBALS['registry']->call('clients/replace', array('uid' => $GLOBALS['registry']->call('clients/getClientSource') . ':' . $objectId,
                                                               'content' => $attributes,
                                                               'contentType' => 'array'));
}

function _turba_deleteClient($objectId = '')
{
    return $GLOBALS['registry']->call('clients/delete', array($GLOBALS['registry']->call('clients/getClientSource') . ':' . $objectId));
}

function _turba_searchClients($names = array(), $fields = array())
{
    return $GLOBALS['registry']->call('clients/search', array('names' => $names,
                                                              'sources' => array($GLOBALS['conf']['client']['addressbook']),
                                                              'fields' => $fields));
}

function _turba_addField($address = '', $name = '', $field = '', $value = '', $source = '')
{
    require_once dirname(__FILE__) . '/base.php';
    global $cfgSources;

    if (empty($source) || !isset($cfgSources[$source])) {
        return PEAR::raiseError(_("Invalid address book"), 'horde.error', null, null, $source);
    }

    if (empty($address)) {
        return PEAR::raiseError(_("Invalid email"), 'horde.error', null, null, $source);
    }

    if (empty($name)) {
        return PEAR::raiseError(_("Invalid name"), 'horde.error', null, null, $source);
    }

    if (empty($value)) {
        return PEAR::raiseError(_("Invalid entry"), 'horde.error', null, null, $source);
    }

    if ($cfgSources[$source]['readonly']
        && (!isset($cfgSources[$source]['admin'])
            || !in_array(Auth::getAuth(), $cfgSources[$source]['admin']))) {
        return PEAR::raiseError(_("Permission Denied"), 'horde.error', null, null, $source);
    }

    $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
    if (is_a($driver, 'PEAR_Error')) {
        return PEAR::raiseError(sprintf(_("Connection failed: %s"), $driver->getMessage()), 'horde.error', null, null, $source);
    }
    $res = $driver->search(array('email' => trim($address)), null, 'AND');
    if (is_a($res, 'PEAR_Error')) {
        return PEAR::raiseError(sprintf(_("Search failed: %s"), $res->getMessage()), 'horde.message', null, null, $source);
    }

    if ($res->count() > 1) {
        $res2 = $driver->search(array('email' => trim($address), 'name' => trim($name)), null, 'AND');
        if (is_a($res2, 'PEAR_Error')) {
            return PEAR::raiseError(sprintf(_("Search failed: %s"), $res2->getMessage()), 'horde.message', null, null, $source);
        }

        if (!$res2->count()) {
            return PEAR::raiseError(sprintf(_("Multiple persons with address [%s], but none with name [%s] already exist"), trim($address), trim($name)), 'horde.message', null, null, $source);
        }

        $res3 = $driver->search(array('email' => $address, 'name' => $name, $field => $value));
        if (is_a($res3, 'PEAR_Error')) {
            return PEAR::raiseError(sprintf(_("Search failed: %s"), $res3->getMessage()), 'horde.message', null, null, $source);
        }

        if ($res3->count()) {
            return PEAR::raiseError(sprintf(_("This person already has a %s entry in the address book"), $field), 'horde.message', null, null, $source);
        }

        $ob = $res2->next();
        $ob->setValue($field, $value);
        $ob->store();
    } elseif ($res->count() == 1) {
        $res4 = $driver->search(array('email' => $address, $field => $value));
        if (is_a($res4, 'PEAR_Error')) {
            return PEAR::raiseError(sprintf(_("Search failed: %s"), $res4->getMessage()), 'horde.message', null, null, $source);
        }

        if ($res4->count()) {
            return PEAR::raiseError(sprintf(_("This person already has a %s entry in the address book"), $field), 'horde.message', null, null, $source);
        }

        $ob = $res->next();
        $ob->setValue($field, $value);
        $ob->store();
    } else {
        return $driver->add(array('email' => $address, 'name' => $name, $field => $value, '__owner' => Auth::getAuth()));
    }

    return;
}

function _turba_getField($address = '', $field = '', $sources = array())
{
    require_once dirname(__FILE__) . '/base.php';
    global $cfgSources;

    if (empty($address)) {
        return PEAR::raiseError(_("Invalid email"), 'horde.error');
    }

    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
        return array();
    }

    if (!count($sources)) {
        if (!count($cfgSources)) {
            return PEAR::raiseError(_("No address books found."));
        }
        $sources = array(key($cfgSources));
    }

    $count = 0;
    foreach ($sources as $source) {
        if (!isset($cfgSources[$source])) {
            continue;
        }

        $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
        if (is_a($driver, 'PEAR_Error')) {
            continue;
        }

        $list = $driver->search(array('email' => $address));
        if (!is_a($list, 'Turba_List')) {
            continue;
        }

        while ($ob = $list->next()) {
            if ($ob->hasValue($field)) {
                $count++;
                $result = $ob->getValue($field);
            }
        }
    }

    if ($count > 1) {
        return PEAR::raiseError(_("More than 1 entry found"), 'horde.warning', null, null, $source);
    } elseif (!isset($result)) {
        return PEAR::raiseError(sprintf(_("No %s entry found for %s"), $field, $address), 'horde.warning', null, null, $source);
    }

    return $result;
}

function _turba_deleteField($address = '', $field = '', $sources = array())
{
    require_once dirname(__FILE__) . '/base.php';
    global $cfgSources;

    if (empty($address)) {
        return PEAR::raiseError(_("Invalid email"), 'horde.error');
    }

    if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources)) {
        return array();
    }

    if (count($sources) == 0) {
        $sources = array(key($cfgSources));
    }

    $success = false;

    foreach ($sources as $source) {
        if (isset($cfgSources[$source])) {
            $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
            if (is_a($driver, 'PEAR_Error')) {
                continue;
            }

            $res = $driver->search(array('email' => $address));
            if (is_a($res, 'Turba_List')) {
                if ($res->count() > 1) {
                    continue;
                }

                $ob = $res->next();
                if (is_object($ob) && $ob->hasValue($field)) {
                    $ob->setValue($field, '');
                    $ob->store();
                    $success = true;
                }
            }
        }
    }

    if (!$success) {
        return PEAR::raiseError(sprintf(_("No %s entry found for %s"), $field, $address), 'horde.error');
    }

    return;
}
