<?php
/**
 * Copyright 2010-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 2010-2017 Horde LLC
 * @license   http://www.horde.org/licenses/apache ASL
 * @package   Turba
 */

/**
 * Turba application API.
 *
 * This file defines Horde's core API interface. Other core Horde libraries
 * can interact with Turba through this API.
 *
 * @author    Michael Slusarz <slusarz@horde.org?
 * @category  Horde
 * @copyright 2010-2017 Horde LLC
 * @license   http://www.horde.org/licenses/apache ASL
 * @package   Turba
 */

/* Determine the base directories. */
if (!defined('TURBA_BASE')) {
    define('TURBA_BASE', realpath(__DIR__ . '/..'));
}

if (!defined('HORDE_BASE')) {
    /* If Horde does not live directly under the app directory, the HORDE_BASE
     * constant should be defined in config/horde.local.php. */
    if (file_exists(TURBA_BASE . '/config/horde.local.php')) {
        include TURBA_BASE . '/config/horde.local.php';
    } else {
        define('HORDE_BASE', realpath(TURBA_BASE . '/..'));
    }
}

/* Load the Horde Framework core (needed to autoload
 * Horde_Registry_Application::). */
require_once HORDE_BASE . '/lib/core.php';

use Sabre\CalDAV;
use Sabre\CardDAV;

class Turba_Application extends Horde_Registry_Application
{
    /**
     */
    public $features = array(
        'smartmobileView' => true,
        'modseq' => true,
    );

    /**
     */
    public $version = 'H5 (4.2.25)';

    /**
     */
    protected function _bootstrap()
    {
        /* Add Turba-specific factories. */
        $factories = array(
            'Turba_Shares' => 'Turba_Factory_Shares',
            'Turba_Tagger' => 'Turba_Factory_Tagger'
        );

        foreach ($factories as $key => $val) {
            $GLOBALS['injector']->bindFactory($key, $val, 'create');
        }
    }

    /**
     * Global variables defined:
     *   $addSources   - TODO
     *   $attributes - (array) Attribute data from the config/attributes.php
     *                 file.
     *   $browse_source_count - TODO
     *   $cfgSources   - TODO
     *   $copymoveSources - TODO
     */
    protected function _init()
    {
        global $conf, $injector, $registry, $session;

        if ($conf['tags']['enabled']) {
            /* For now, autoloading the Content_* classes depend on there being
             * a registry entry for the 'content' application that contains at
             * least the fileroot entry. */
            $injector->getInstance('Horde_Autoloader')
                ->addClassPathMapper(
                    new Horde_Autoloader_ClassPathMapper_Prefix('/^Content_/', $registry->get('fileroot', 'content') . '/lib/'));

            if (!class_exists('Content_Tagger')) {
                throw new Horde_Exception(_("The Content_Tagger class could not be found. Make sure the Content application is installed."));
            }
        }

        // Turba source and attribute configuration.
        $attributes = $registry->loadConfigFile('attributes.php', 'attributes', 'turba')->config['attributes'];
        $cfgSources = Turba::availableSources();

        /* UGLY UGLY UGLY - we should NOT be using this as a global
         * variable all over the place. */
        $GLOBALS['cfgSources'] = &$cfgSources;

        // See if any of our sources are configured to use Horde_Share.
        foreach ($cfgSources as $key => $cfg) {
            if (!empty($cfg['use_shares'])) {
                // Create a share instance.
                $session->set('turba', 'has_share', true);
                $cfgSources = Turba::getConfigFromShares($cfgSources);
                break;
            }
        }

        $GLOBALS['attributes'] = $attributes;
        $cfgSources = Turba::permissionsFilter($cfgSources);

        // Build the directory sources select widget.
        if (empty(Turba::$source)) {
            if (!(Turba::$source = $session->get('turba', 'source'))) {
                Turba::$source = Turba::getDefaultAddressbook();
            }
            Turba::$source = Horde_Util::getFormData('source', Turba::$source);
        }

        $GLOBALS['browse_source_count'] = 0;

        foreach (Turba::getAddressBooks() as $key => $curSource) {
            if (!empty($curSource['browse'])) {
                $GLOBALS['browse_source_count']++;
                if (empty(Turba::$source)) {
                    Turba::$source = $key;
                }
            }
        }

        if (empty($cfgSources[Turba::$source]['browse'])) {
            Turba::$source = Turba::getDefaultAddressbook();
        }
        $session->set('turba', 'source', Turba::$source);

        $GLOBALS['addSources'] = Turba::getAddressBooks(Horde_Perms::EDIT, array('require_add' => true));
        $GLOBALS['copymoveSources'] = $GLOBALS['addSources'];
        unset($GLOBALS['copymoveSources'][Turba::$source]);
    }

    /**
     */
    public function perms()
    {
        $cfgSources = Turba::availableSources();

        $perms = array(
            'sources' => array(
                'title' => _("Sources")
            )
        );

        // Run through every contact source.
        foreach ($cfgSources as $source => $curSource) {
            $perms['sources:' . $source] = array(
                'title' => $curSource['title']
            );
            $perms['sources:' . $source . ':max_contacts'] = array(
                'title' => _("Maximum Number of Contacts"),
                'type' => 'int'
            );
        }

        return $perms;
    }

    /**
     */
    public function getInitialPage()
    {
        global $registry;

        switch ($registry->getView()) {
        case $registry::VIEW_SMARTMOBILE:
            return strval(Horde::url('smartmobile.php')->setRaw(true));
            break;

        default:
            return null;
        }
    }

    /**
     */
    public function menu($menu)
    {
        if ($GLOBALS['browse_source_count']) {
            $menu->add(Horde::url('browse.php'), _("_Browse"), 'turba-browse', null, null, null, (($GLOBALS['prefs']->getValue('initial_page') == 'browse.php' && basename($_SERVER['PHP_SELF']) == 'index.php' && basename(dirname($_SERVER['PHP_SELF'])) != 'addressbooks') || (basename($_SERVER['PHP_SELF']) == 'browse.php' && Horde_Util::getFormData('key') != '**search')) ? 'current' : '__noselection');
        }

        $menu->add(Horde::url('search.php'), _("_Search"), 'turba-search', null, null, null, (($GLOBALS['prefs']->getValue('initial_page') == 'search.php' && basename($_SERVER['PHP_SELF']) == 'index.php' && strpos($_SERVER['PHP_SELF'], 'addressbooks/index.php') === false) || (basename($_SERVER['PHP_SELF']) == 'browse.php' && Horde_Util::getFormData('key') == '**search')) ? 'current' : null);

        /* Import/Export */
        if ($GLOBALS['conf']['menu']['import_export']) {
            $menu->add(Horde::url('data.php'), _("_Import/Export"), 'horde-data');
        }
    }

    /**
     * Add additional items to the sidebar.
     *
     * @param Horde_View_Sidebar $sidebar  The sidebar object.
     */
    public function sidebar($sidebar)
    {
        if (count($GLOBALS['addSources'])) {
            $sidebar->addNewButton(_("_New Contact"), Horde::url('add.php'));
        }

        $user = $GLOBALS['registry']->getAuth();
        $edit = Horde::url('addressbooks/edit.php');
        $url = Horde::url('');

        $sidebar->containers['my'] = array(
            'header' => array(
                'id' => 'turba-toggle-my',
                'label' => _("My Address Books"),
                'collapsed' => false,
            ),
        );
        if ($GLOBALS['registry']->getAuth() &&
            $GLOBALS['session']->get('turba', 'has_share') &&
            !empty($GLOBALS['conf']['shares']['source'])) {
            $create = true;
            $sidebar->containers['my']['header']['add'] = array(
                'url' => Horde::url('addressbooks/create.php'),
                'label' => _("Create a new Address Book"),
            );
        }
        $shares = array();
        $shared = array();
        foreach (Turba::listShares(false, Horde_Perms::SHOW) as $id => $abook) {
            $row = array(
                'selected' => $id == Turba::$source,
                'url' => $url->copy()->add('source', $id),
                'label' => $abook->get('name'),
                'edit' => $edit->copy()->add('a', $abook->getName()),
                'type' => 'radiobox',
            );
            if ($abook->get('owner') && $abook->get('owner') == $user) {
                $sidebar->addRow($row, 'my');
                if ($row['selected']) {
                    $sidebar->containers['my']['header']['collapsed'] = false;
                }
            } else {
                if ($abook->get('owner')) {
                    $row['label'] .= ' [' . $GLOBALS['registry']->convertUsername($abook->get('owner'), false) . ']';
                }
                $shared[] = $row;
            }
            $shares[$id] = true;
        }

        if (!empty($create) || count($shared)) {
            $sidebar->containers['shared'] = array(
                'header' => array(
                    'id' => 'turba-toggle-shared',
                    'label' => _("Shared Address Books"),
                    'collapsed' => true,
                ),
            );
            foreach ($shared as $row) {
                $sidebar->addRow($row, 'shared');
                if ($row['selected']) {
                    $sidebar->containers['shared']['header']['collapsed'] = false;
                }
            }
        }

        $sidebar->containers['other'] = array(
            'header' => array(
                'id' => 'turba-toggle-other',
                'label' => _("Other Address Books"),
                'collapsed' => true,
            ),
        );
        foreach (Turba::getAddressBooks(Horde_Perms::SHOW) as $id => $abook) {
            if (isset($shares[$id])) {
                continue;
            }
            $row = array(
                'selected' => $id == Turba::$source,
                'url' => $url->copy()->add('source', $id),
                'label' => $abook['title'],
                'type' => 'radiobox',
            );
            $sidebar->addRow($row, 'other');
            if ($row['selected']) {
                $sidebar->containers['other']['header']['collapsed'] = false;
            }
        }
    }

    /**
     * Returns values for <configspecial> configuration settings.
     *
     * @param string $what  Either 'client-fields' or 'sources'.
     *
     * @return array  The values for the requested configuration setting.
     */
    public function configSpecialValues($what)
    {
        switch ($what) {
        case 'client-fields':
            try {
                $fields = $GLOBALS['registry']->call('clients/clientFields');
            } catch (Horde_Exception $e) {
                return array();
            }
            $f = array();
            foreach ($fields as $field) {
                $f[$field['name']] = $field['label'];
            }
            return $f;

        case 'sources':
            try {
                $addressbooks = Turba::getAddressBooks(Horde_Perms::READ);
            } catch (Horde_Exception $e) {
                return array();
            }
            foreach ($addressbooks as &$addressbook) {
                $addressbook = $addressbook['title'];
            }

            $addressbooks[''] = _("None");
            return $addressbooks;
        }
    }

    /**
     */
    public function removeUserData($user)
    {
        $err = sprintf(
            _("There was an error removing an address book for %s"),
            $user
        );

        /* We need a clean copy of the $cfgSources array here.*/
        $cfgSources = Turba::availableSources();
        foreach ($cfgSources as $sourceId => $source) {
            if (empty($source['use_shares'])) {
                // Shares not enabled for this source
                try {
                    $driver = $GLOBALS['injector']
                        ->getInstance('Turba_Factory_Driver')
                        ->create($source, $sourceId);
                } catch (Turba_Exception $e) {
                    Horde::log($e, 'ERR');
                    continue;
                }

                try {
                    $driver->removeUserData($user);
                } catch (Turba_Exception_NotSupported $e) {
                    continue;
                } catch (Turba_Exception $e) {
                    Horde::log($e, 'ERR');
                    throw new Turba_Exception($err);
                }
            }
        }

        /* Only attempt share removal if we have shares configured */
        if (!$GLOBALS['session']->get('turba', 'has_share')) {
            return;
        }

        $turba_shares = $GLOBALS['injector']->getInstance('Turba_Shares');
        $shares = $turba_shares->listShares(
            $user,
            array('attributes' => $user)
        );

        // Look for the deleted user's shares and remove them
        $sources = Turba::getConfigFromShares(
            $cfgSources,
            true,
            array('shares' => $shares, 'auth_user' => $user)
        );

        foreach ($shares as $share) {
            $config = $sources[$share->getName()];
            try {
                $driver = $GLOBALS['injector']
                    ->getInstance('Turba_Factory_Driver')
                    ->create($config, $share->getName(), $sources);
            } catch (Turba_Exception $e) {
                continue;
            }

            try {
                $driver->removeUserData($user);
            } catch (Turba_Exception_NotSupported $e) {
                continue;
            } catch (Turba_Exception $e) {
                Horde::log($e, 'ERR');
                throw new Turba_Exception($err);
            }
        }

        /* Get a list of all shares this user has perms to and remove the
         * perms. */
        try {
            $shares = $turba_shares->listShares($user);
            foreach ($shares as $share) {
                $share->removeUser($user);
            }
        } catch (Horde_Share_Exception $e) {
            Horde::log($e, 'ERR');
            throw new Turba_Exception($err);
        }
    }

    /* Topbar method. */

    /**
     */
    public function topbarCreate(Horde_Tree_Renderer_Base $tree, $parent = null,
                                 array $params = array())
    {
        $add = Horde::url('add.php');
        $browse = Horde::url('browse.php');

        if ($GLOBALS['addSources']) {
            $newimg = Horde_Themes::img('menu/new.png');

            $tree->addNode(array(
                'id' => $parent . '__new',
                'parent' => $parent,
                'label' => _("New Contact"),
                'params' => array(
                    'icon' => $newimg,
                    'url' => $add
                )
            ));

            foreach ($GLOBALS['addSources'] as $addressbook => $config) {
                $tree->addNode(array(
                    'id' => $parent . $addressbook . '__new',
                    'parent' => $parent . '__new',
                    'label' => sprintf(_("in %s"), $config['title']),
                    'expanded' => false,
                    'params' => array(
                        'icon' => $newimg,
                        'url' => $add->copy()->add('source', $addressbook)
                    )
                ));
            }
        }

        foreach (Turba::getAddressBooks() as $addressbook => $config) {
            if (!empty($config['browse'])) {
                $tree->addNode(array(
                    'id' => $parent . $addressbook,
                    'parent' => $parent,
                    'label' => $config['title'],
                    'expanded' => false,
                    'params' => array(
                        'icon' => Horde_Themes::img('menu/browse.png'),
                        'url' => $browse->copy()->add('source', $addressbook)
                    )
                ));
            }
        }

        $tree->addNode(array(
            'id' => $parent . '__search',
            'parent' => $parent,
            'label' => _("Search"),
            'expanded' => false,
            'params' => array(
                'icon' => Horde_Themes::img('search.png'),
                'url' => Horde::url('search.php')
            )
        ));
    }

    /* Download data. */

    /**
     * @throws Turba_Exception
     * @throws Horde_Exception_NotFound
     */
    public function download(Horde_Variables $vars)
    {
        global $attributes, $cfgSources, $injector;

        switch ($vars->actionID) {
        case 'download_file':
            /* Get the object. */
            if (!isset($cfgSources[$vars->source])) {
                throw new Turba_Exception(_("The contact you requested does not exist."));
            }

            $object = $injector->getInstance('Turba_Factory_Driver')->create($vars->source)->getObject($vars->key);

            /* Check permissions. */
            if (!$object->hasPermission(Horde_Perms::READ)) {
                throw new Turba_Exception(_("You do not have permission to view this contact."));
            }

            try {
                return array(
                    'data' => $object->vfsInit()->read(Turba::VFS_PATH . '/' . $object->getValue('__uid'), $vars->file),
                    'name' => $vars->file
                );
            } catch (Horde_Vfs_Exception $e) {
                Horde::log($e, 'ERR');
                throw new Turba_Exception(sprintf(_("Access denied to %s"), $vars->file));
            }

        case 'export':
            $sources = array();
            if ($vars->objectkeys) {
                foreach ($vars->objectkeys as $objectkey) {
                    list($source, $key) = explode(':', $objectkey, 2);
                    if (!isset($sources[$source])) {
                        $sources[$source] = array();
                    }
                    $sources[$source][] = $key;
                }
            } else {
                if (!isset($vars->source) && !empty($cfgSources)) {
                    reset($cfgSources);
                    $vars->source = key($cfgSources);
                }
                $sources[$vars->source] = array();
            }

            if ($vcard = in_array($vars->exportID, array(Horde_Data::EXPORT_VCARD, 'vcard30'))) {
                $version = ($vars->exportID == 'vcard30') ? '3.0' : '2.1';
            }

            $all_fields = $data = array();
            $tfd = $injector->getInstance('Turba_Factory_Driver');

            foreach ($sources as $source => $objectkeys) {
                /* Create a Turba storage instance. */
                $driver = $tfd->create($source);
                $blobs = $driver->getBlobs();

                /* Get the full, sorted contact list. */
                try {
                    $results = count($objectkeys)
                        ? $driver->getObjects($objectkeys)
                        : $driver->search(array())->objects;
                } catch (Turba_Exception $e) {
                    throw new Turba_Exception(sprintf(_("Failed to search the directory: %s"), $e->getMessage()));
                }

                $fields = array_keys($driver->map);
                $all_fields = array_merge($all_fields, $fields);

                $params = $driver->getParams();
                foreach ($results as $ob) {
                    if ($vcard) {
                        $data[] = $driver->tovCard($ob, $version, null, true);
                    } else {
                        $row = array();
                        foreach ($fields as $field) {
                            if (substr($field, 0, 2) == '__' ||
                                isset($blobs[$field])) {
                                continue;
                            }
                            $attribute = $ob->getValue($field);
                            if ($attributes[$field]['type'] == 'date') {
                                $row[$field] = strftime('%Y-%m-%d', $attribute);
                            } elseif ($attributes[$field]['type'] == 'time') {
                                $row[$field] = strftime('%R', $attribute);
                            } elseif ($attributes[$field]['type'] == 'datetime') {
                                $row[$field] = strftime('%Y-%m-%d %R', $attribute);
                            } else {
                                $row[$field] = Horde_String::convertCharset($attribute, 'UTF-8', $params['charset']);
                            }
                        }
                        $data[] = $row;
                    }
                }
            }

            /* Make sure that all rows have the same columns if exporting from
             * different sources. */
            if (!$vcard && count($sources) > 1) {
                for ($i = 0; $i < count($data); $i++) {
                    foreach ($all_fields as $field) {
                        if (!isset($data[$i][$field])) {
                            $data[$i][$field] = '';
                        }
                    }
                }
            }

            switch ($vars->exportID) {
            case Horde_Data::EXPORT_CSV:
                $injector->getInstance('Horde_Core_Factory_Data')->create('Csv', array('cleanup' => array($this, 'cleanupData')))->exportFile(_("contacts.csv"), $data, true);
                exit;

            case Horde_Data::EXPORT_OUTLOOKCSV:
                $injector->getInstance('Horde_Core_Factory_Data')->create('Outlookcsv', array('cleanup' => array($this, 'cleanupData')))->exportFile(_("contacts.csv"), $data, true, array_flip($this->getOutlookMapping()));
                exit;

            case Horde_Data::EXPORT_TSV:
                $injector->getInstance('Horde_Core_Factory_Data')->create('Tsv', array('cleanup' => array($this, 'cleanupData')))->exportFile(_("contacts.tsv"), $data, true);
                exit;

            case Horde_Data::EXPORT_VCARD:
            case 'vcard30':
                $injector->getInstance('Horde_Core_Factory_Data')->create('Vcard', array('cleanup' => array($this, 'cleanupData')))->exportFile(_("contacts.vcf"), $data, true);
                exit;

            case 'ldif':
                $ldif = new Turba_Data_Ldif(array(
                    'browser' => $injector->getInstance('Horde_Browser'),
                    'vars' => Horde_Variables::getDefaultVariables(),
                    'cleanup' => array($this, 'cleanupData')
                ));
                $ldif->exportFile(_("contacts.ldif"), $data, true);
                exit;
            }

            break;
        }
    }

    /**
     */
    public function cleanupData()
    {
        $GLOBALS['import_step'] = 1;
        return Horde_Data::IMPORT_FILE;
    }

    /**
     */
    public function getOutlookMapping()
    {
        return array(
            'Title' => 'namePrefix',
            'First Name' => 'firstname',
            'Middle Name' => 'middlenames',
            'Last Name' => 'lastname',
            'Nickname' => 'nickname',
            'Suffix' => 'nameSuffix',
            'Company' => 'company',
            'Department' => 'department',
            'Job Title' => 'title',
            'Business Street' => 'workStreet',
            'Business City' => 'workCity',
            'Business State' => 'workProvince',
            'Business Postal Code' => 'workPostalCode',
            'Business Country' => 'workCountry',
            'Home Street' => 'homeStreet',
            'Home City' => 'homeCity',
            'Home State' => 'homeProvince',
            'Home Postal Code' => 'homePostalCode',
            'Home Country' => 'homeCountry',
            'Business Fax' => 'workFax',
            'Business Phone' => 'workPhone',
            'Home Phone' => 'homePhone',
            'Mobile Phone' => 'cellPhone',
            'Pager' => 'pager',
            'Anniversary' => 'anniversary',
            'Assistant\'s Name' => 'assistant',
            'Birthday' => 'birthday',
            'Business Address PO Box' => 'workPOBox',
            'Categories' => '__tags',
            'Children' => 'children',
            'E-mail Address' => 'email',
            'Home Address PO Box' => 'homePOBox',
            'Initials' => 'initials',
            'Internet Free Busy' => 'freebusyUrl',
            'Language' => 'language',
            'Notes' => 'notes',
            'Profession' => 'role',
            'Office Location' => 'office',
            'Spouse' => 'spouse',
            'Web Page' => 'website',
        );
    }

    /* DAV methods. */

    /**
     */
    public function davGetCollections($user)
    {
        global $injector, $registry;

        $hordeUser = $registry->convertUsername($user, true);
        $dav = $injector->getInstance('Horde_Dav_Storage');
        $factory = $injector->getInstance('Turba_Shares');
        $books = array();
        foreach (Turba::getAddressBooks(Horde_Perms::SHOW) as $id => $book) {
            $readOnly = false;
            switch ($book['type']) {
            // Ugly hack! There is currently no clean way to retrieve address
            // books that the user "owns", or to find out if a SQL/LDAP/Kolab
            // address book contains per-user or global contacts.
            case 'share':
                $share = $factory->getShare($id);
                if (($user == '-system-' && strlen($share->get('owner'))) ||
                    ($user != '-system-' &&
                     $hordeUser != $share->get('owner') &&
                     $hordeUser != $registry->getAuth())) {
                    continue 2;
                }
                $readOnly = !$share->hasPermission($hordeUser, Horde_Perms::EDIT);
                break;

            case 'facebook':
            case 'favourites':
            case 'vbook':
                if ($user == '-system-') {
                    continue 2;
                }
                $readOnly = true;
                break;

            default:
                if (!Turba::permissionsFilter(array($id => $book), Horde_Perms::EDIT)) {
                    $readOnly = true;
                }
                break;
            }
            try {
                $id = $dav->getExternalCollectionId($id, 'contacts') ?: $id;
            } catch (Horde_Dav_Exception $e) {
            }
            $books[] = array(
                'id' => $id,
                'uri' => $id,
                'principaluri' => 'principals/' . $user,
                '{DAV:}displayname' => $book['title'],
                '{' . CardDAV\Plugin::NS_CARDDAV . '}supported-address-data'
                    => new CardDAV\Property\SupportedAddressData(
                        array(
                            array(
                                'contentType' => 'text/directory',
                                'version' => '3.0'
                            ),
                            array(
                                'contentType' => 'text/vcard',
                                'version' => '3.0'
                            ),
                            array(
                                'contentType' => 'text/x-vcard',
                                'version' => '2.1'
                            ),
                        )
                    ),
                '{http://sabredav.org/ns}read-only' => $readOnly
            );
        }
        return $books;
    }

    /**
     */
    public function davGetObjects($collection)
    {
        $dav = $GLOBALS['injector']
            ->getInstance('Horde_Dav_Storage');

        $internal = $dav->getInternalCollectionId($collection, 'contacts') ?: $collection;
        $driver = $GLOBALS['injector']
            ->getInstance('Turba_Factory_Driver')
            ->create($internal);
        if (!$driver->hasPermission(Horde_Perms::READ)) {
            throw new Turba_Exception("Address Book does not exist or no permission to edit");
        }

        $list = $driver->search(array());
        $list->reset();
        $contacts = array();
        while ($contact = $list->next()) {
            $id = $contact->getValue('__key');
            $modified = $contact->lastModification();
            try {
                $id = $dav->getExternalObjectId($id, $internal) ?: $id . '.vcf';
            } catch (Horde_Dav_Exception $e) {
            }
            $contacts[] = array(
                'id' => $id,
                'uri' => $id,
                'lastmodified' => $modified,
                'etag' => '"' . md5($contact->getValue('__key') . '|' . $modified) . '"',
            );
        }

        return $contacts;
    }

    /**
     */
    public function davGetObject($collection, $object)
    {
        $dav = $GLOBALS['injector']
            ->getInstance('Horde_Dav_Storage');

        $internal = $dav->getInternalCollectionId($collection, 'contacts') ?: $collection;
        $driver = $GLOBALS['injector']
            ->getInstance('Turba_Factory_Driver')
            ->create($internal);
        if (!$driver->hasPermission(Horde_Perms::READ)) {
            throw new Turba_Exception("Address Book does not exist or no permission to edit");
        }

        try {
            $object = $dav->getInternalObjectId($object, $internal) ?: preg_replace('/\.vcf$/', '', $object);
        } catch (Horde_Dav_Exception $e) {
        }
        $contact = $driver->getObject($object);
        $id = $contact->getValue('__key');
        $modified = $contact->lastModification();
        try {
            $id = $dav->getExternalObjectId($id, $internal) ?: $id . '.vcf';
        } catch (Horde_Dav_Exception $e) {
        }

        $data = $driver->tovCard($contact, '3.0', null, true)
            ->exportvCalendar();

        return array(
            'id' => $id,
            'carddata' => $data,
            'uri' => $id,
            'lastmodified' => $modified,
            'etag' => '"' . md5($contact->getValue('__key') . '|' . $modified) . '"',
            'size' => strlen($data),
        );
    }

    /**
     */
    public function davPutObject($collection, $object, $data)
    {
        $dav = $GLOBALS['injector']
            ->getInstance('Horde_Dav_Storage');

        $internal = $dav->getInternalCollectionId($collection, 'contacts') ?: $collection;
        $driver = $GLOBALS['injector']
            ->getInstance('Turba_Factory_Driver')
            ->create($internal);
        if (!$driver->hasPermission(Horde_Perms::EDIT)) {
            throw new Turba_Exception("Address Book does not exist or no permission to edit");
        }

        $ical = new Horde_Icalendar();
        if (!$ical->parsevCalendar($data)) {
            throw new Turba_Exception(_("There was an error importing the vCard data."));
        }

        foreach ($ical->getComponents() as $content) {
            if (!($content instanceof Horde_Icalendar_Vcard)) {
                continue;
            }

            $contact = $driver->toHash($content);

            try {
                try {
                    $existing_id = $dav->getInternalObjectId($object, $internal)
                        ?: preg_replace('/\.vcf$/', '', $object);
                } catch (Horde_Dav_Exception $e) {
                    $existing_id = $object;
                }
                $existing_contact = $driver->getObject($existing_id);
                /* Check if our contact is newer then the existing - get the
                 * contact's history. */
                $modified = $existing_contact->lastModification();
                try {
                    if (!empty($modified) &&
                        $content->getAttribute('LAST-MODIFIED')->before($modified)) {
                        /* LAST-MODIFIED timestamp of existing entry is newer:
                         * don't replace it. */
                        continue;
                    }
                } catch (Horde_Icalendar_Exception $e) {
                }
                foreach ($contact as $attribute => $value) {
                    if ($attribute != '__key') {
                        $existing_contact->setValue($attribute, $value);
                    }
                }
                $existing_contact->store();
            } catch (Horde_Exception_NotFound $e) {
                $id = $driver->add($contact);
                $dav->addObjectMap($id, $object, $internal);
            }
        }
    }

    /**
     */
    public function davDeleteObject($collection, $object)
    {
        $dav = $GLOBALS['injector']->getInstance('Horde_Dav_Storage');

        $internal = $dav->getInternalCollectionId($collection, 'contacts') ?: $collection;
        $driver = $GLOBALS['injector']
            ->getInstance('Turba_Factory_Driver')
            ->create($internal);
        if (!$driver->hasPermission(Horde_Perms::DELETE)) {
            throw new Turba_Exception("Address Book does not exist or no permission to edit");
        }

        try {
            $object = $dav->getInternalObjectId($object, $internal)
                ?: preg_replace('/\.vcf$/', '', $object);
        } catch (Horde_Dav_Exception $e) {
        }
        $driver->delete($object);

        try {
            $dav->deleteExternalObjectId($object, $internal);
        } catch (Horde_Dav_Exception $e) {
        }
    }

    /**
     * Returns the last modification (or creation) date of a contact.
     *
     * @param string $collection  A address book ID.
     * @param string $object      A contact UID.
     *
     * @return integer  Timestamp of the last modification.
     */
    protected function _modified($collection, $uid)
    {
        $history = $GLOBALS['injector']
            ->getInstance('Horde_History');
        $modified = $history->getActionTimestamp(
            'turba:' . $collection . ':' . $uid,
            'modify'
        );
        if (!$modified) {
            $modified = $history->getActionTimestamp(
                'turba:' . $collection . ':' . $uid,
                'add'
            );
        }
        return $modified;
    }
}
