From 4c27c1607e81d3b98c67656fee99c01edc4c025f Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 12 Mar 2014 17:14:17 +0100 Subject: Optionally cache LDAP directory listing and record payloads --- config/dav.inc.php.sample | 9 +++ lib/Kolab/CardDAV/ContactsBackend.php | 8 +++ lib/Kolab/CardDAV/LDAPDirectory.php | 126 ++++++++++++++++++++++++++++------ lib/Kolab/CardDAV/Plugin.php | 3 +- 4 files changed, 125 insertions(+), 21 deletions(-) diff --git a/config/dav.inc.php.sample b/config/dav.inc.php.sample index 349ca28..d66ad6d 100644 --- a/config/dav.inc.php.sample +++ b/config/dav.inc.php.sample @@ -93,6 +93,10 @@ $config['kolabdav_ldap_directory'] = array( 'phone:work' => 'alternateTelephoneNumber', 'phone:mobile' => 'mobile', 'phone:work2' => 'blackberry', + 'street' => 'street', + 'zipcode' => 'postalCode', + 'locality' => 'l', + 'organization' => 'o', 'jobtitle' => 'title', 'photo' => 'jpegphoto', // required for internal handling and caching @@ -101,3 +105,8 @@ $config['kolabdav_ldap_directory'] = array( ), ); */ + +// Enable caching for LDAP directory data. +// This is recommended with 'searchonly' => false to speed-up sychronization of multiple clients +// $rcmail_config['kolabdav_ldap_cache'] = 'memcache'; +// $rcmail_config['kolabdav_ldap_cache_ttl'] = 600; // in seconds diff --git a/lib/Kolab/CardDAV/ContactsBackend.php b/lib/Kolab/CardDAV/ContactsBackend.php index 5ce2be7..b9f0ff8 100644 --- a/lib/Kolab/CardDAV/ContactsBackend.php +++ b/lib/Kolab/CardDAV/ContactsBackend.php @@ -39,6 +39,8 @@ use Kolab\Utils\VObjectUtils; */ class ContactsBackend extends CardDAV\Backend\AbstractBackend { + public $ldap_directory; + private $sources; private $folders; private $aliases; @@ -303,6 +305,12 @@ class ContactsBackend extends CardDAV\Backend\AbstractBackend if ($addressBookId == '__all__') { $contact = $this->get_card_by_uid($uid, $storage); } + // read card data from LDAP directory + else if ($addressBookId == LDAPDirectory::DIRECTORY_NAME) { + if (is_object($this->ldap_directory)) { + $contact = $this->ldap_directory->getContactObject($uid); + } + } else { $storage = $this->get_storage_folder($addressBookId); $contact = $storage->get_object($uid, '*'); diff --git a/lib/Kolab/CardDAV/LDAPDirectory.php b/lib/Kolab/CardDAV/LDAPDirectory.php index 70fae38..6604e3e 100644 --- a/lib/Kolab/CardDAV/LDAPDirectory.php +++ b/lib/Kolab/CardDAV/LDAPDirectory.php @@ -46,7 +46,7 @@ class LDAPDirectory extends DAV\Collection implements \Sabre\CardDAV\IDirectory, private $carddavBackend; private $principalUri; private $addressBookInfo = array(); - private $uid2id = array(); + private $cache; private $query; private $filter; @@ -68,6 +68,18 @@ class LDAPDirectory extends DAV\Collection implements \Sabre\CardDAV\IDirectory, // used for vcard serialization $this->carddavBackend = $carddavBackend ?: new ContactsBackend(); + $this->carddavBackend->ldap_directory = $this; + + // initialize cache + $rcube = rcube::get_instance(); + if ($rcube->config->get('kolabdav_ldap_cache')) { + $this->cache = $rcube->get_cache_shared('kolabdav_ldap'); + + // expunge cache every now and then + if (rand(0,10) === 0) { + $this->cache->expunge(); + } + } } private function connect() @@ -117,12 +129,44 @@ class LDAPDirectory extends DAV\Collection implements \Sabre\CardDAV\IDirectory, $uid = basename($cardUri, '.vcf'); $record = null; - // TODO: get from cache + // get from cache + $cache_key = $uid; + if ($this->cache && ($cached = $this->cache->get($cache_key))) { + return new LDAPCard($this->carddavBackend, $this->addressBookInfo, $cached); + } + + if ($contact = $this->getContactObject($uid)) { + $obj = array( + 'id' => $contact['uid'], + 'uri' => $contact['uid'] . '.vcf', + 'lastmodified' => $contact['_timestamp'], + 'carddata' => $this->carddavBackend->to_vcard($contact), + 'etag' => self::_get_etag($contact), + ); + + // cache this object + if ($this->cache) { + $this->cache->set($cache_key, $obj); + } + + return new LDAPCard($this->carddavBackend, $this->addressBookInfo, $obj); + } + + throw new DAV\Exception\NotFound('Card not found'); + } + + /** + * Read contact object from LDAP + */ + function getContactObject($uid) + { + $contact = null; if ($ldap = $this->connect()) { // used cached uid mapping - if ($ID = $this->uid2id[$uid]) { - $contact = $ldap->get_record($ID, true); + $cached_index = $this->cache ? $this->cache->get('index') : array(); + if ($cached_index[$uid]) { + $contact = $ldap->get_record($cached_index[$uid][0], true); } else { // query for uid $result = $ldap->search('uid', $uid, 1, true, true); @@ -133,19 +177,10 @@ class LDAPDirectory extends DAV\Collection implements \Sabre\CardDAV\IDirectory, if ($contact) { $this->_normalize_contact($contact); - $obj = array( - 'id' => $contact['uid'], - 'uri' => $contact['uid'] . '.vcf', - 'lastmodified' => $contact['_timestamp'], - 'carddata' => $this->carddavBackend->to_vcard($contact), - 'etag' => self::_get_etag($contact), - ); - - return new LDAPCard($this->carddavBackend, $this->addressBookInfo, $obj); } } - throw new DAV\Exception\NotFound('Card not found'); + return $contact; } /** @@ -159,6 +194,21 @@ class LDAPDirectory extends DAV\Collection implements \Sabre\CardDAV\IDirectory, $children = array(); + // return cached index + if (!$this->query && !$this->config['searchonly'] && $this->cache && ($cached_index = $this->cache->get('index'))) { + foreach ($cached_index as $uid => $c) { + $obj = array( + 'id' => $uid, + 'uri' => $uid . '.vcf', + 'etag' => $c[1], + 'lastmodified' => $c[2], + ); + $children[] = new LDAPCard($this->carddavBackend, $this->addressBookInfo, $obj); + } + + return $children; + } + // query LDAP if we have a search query or listing is allowed if (($this->query || !$this->config['searchonly']) && ($ldap = $this->connect())) { // set pagesize from query limit attribute @@ -175,8 +225,9 @@ class LDAPDirectory extends DAV\Collection implements \Sabre\CardDAV\IDirectory, } $results = $ldap->list_records(null); + $directory_index = array(); - // convert restuls into vcard blocks + // convert results into vcard blocks foreach ($results as $contact) { $this->_normalize_contact($contact); @@ -188,11 +239,22 @@ class LDAPDirectory extends DAV\Collection implements \Sabre\CardDAV\IDirectory, 'etag' => self::_get_etag($contact), ); - // TODO: cache result - $this->uid2id[$contact['uid']] = $contact['ID']; + // cache record + $cache_key = $contact['uid']; + if ($this->cache) { + $this->cache->set($cache_key, $obj); + } + + $directory_index[$contact['uid']] = array($contact['ID'], $obj['etag'], $contact['_timestamp']); + // add CardDAV node $children[] = new LDAPCard($this->carddavBackend, $this->addressBookInfo, $obj); } + + // cache the full listing + if (empty($this->filter) && $this->cache) { + $this->cache->set('index', $directory_index); + } } return $children; @@ -346,13 +408,13 @@ class LDAPDirectory extends DAV\Collection implements \Sabre\CardDAV\IDirectory, private function _normalize_contact(&$contact) { if (is_numeric($contact['changed'])) { - $contact['_timestamp'] = $contact['changed']; + $contact['_timestamp'] = intval($contact['changed']); $contact['changed'] = new \DateTime('@' . $contact['changed']); } else if (!empty($contact['changed'])) { try { $contact['changed'] = new \DateTime($contact['changed']); - $contact['_timestamp'] = $contact['changed']->format('U'); + $contact['_timestamp'] = intval($contact['changed']->format('U')); } catch (Exception $e) { $contact['changed'] = null; @@ -362,11 +424,35 @@ class LDAPDirectory extends DAV\Collection implements \Sabre\CardDAV\IDirectory, // map col:subtype fields to a list that the vcard serialization function understands foreach (array('email' => 'address', 'phone' => 'number', 'website' => 'url') as $col => $prop) { foreach (rcube_ldap::get_col_values($col, $contact) as $type => $values) { - foreach ($values as $value) { + foreach ((array)$values as $value) { $contact[$col][] = array($prop => $value, 'type' => $type); } } + unset($contact[$col.':'.$type]); + } + + $addresses = array(); + foreach (rcube_ldap::get_col_values('address', $contact) as $type => $values) { + foreach ((array)$values as $adr) { + // skip empty address + $adr = array_filter($adr); + if (empty($adr)) + continue; + + $addresses[] = array( + 'type' => $type, + 'street' => $adr['street'], + 'locality' => $adr['locality'], + 'code' => $adr['zipcode'], + 'region' => $adr['region'], + 'country' => $adr['country'], + ); + } + + unset($contact['address:'.$type]); } + + $contact['address'] = $addresses; } /** diff --git a/lib/Kolab/CardDAV/Plugin.php b/lib/Kolab/CardDAV/Plugin.php index adf8151..1441d29 100644 --- a/lib/Kolab/CardDAV/Plugin.php +++ b/lib/Kolab/CardDAV/Plugin.php @@ -158,7 +158,8 @@ class Plugin extends CardDAV\Plugin $node = $this->server->tree->getNodeForPath(($uri = $this->server->getRequestUri())); console(__METHOD__, $uri); - // fix some stupid mistakes in queries sent by the SOGo connector + // fix some bogus parameters in queries sent by the SOGo connector. + // issue submitted in http://www.sogo.nu/bugs/view.php?id=2655 $xpath = new \DOMXPath($dom); $xpath->registerNameSpace('card', Plugin::NS_CARDDAV); -- cgit v0.12