Page MenuHomePhorge

No OneTemporary

diff --git a/lib/Kolab/CardDAV/ContactsBackend.php b/lib/Kolab/CardDAV/ContactsBackend.php
index f43edb5..5ce2be7 100644
--- a/lib/Kolab/CardDAV/ContactsBackend.php
+++ b/lib/Kolab/CardDAV/ContactsBackend.php
@@ -1,983 +1,983 @@
<?php
/**
* SabreDAV Contacts backend for Kolab.
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\CardDAV;
use \rcube;
use \rcube_charset;
use \kolab_storage;
use Sabre\DAV;
use Sabre\CardDAV;
use Sabre\VObject;
use Kolab\Utils\DAVBackend;
use Kolab\Utils\VObjectUtils;
/**
* Kolab Contacts backend.
*
* Checkout the Sabre\CardDAV\Backend\BackendInterface for all the methods that must be implemented.
*/
class ContactsBackend extends CardDAV\Backend\AbstractBackend
{
private $sources;
private $folders;
private $aliases;
private $useragent;
private $subscribed = null;
/**
* Read available contact folders from server
*/
private function _read_sources()
{
// already read sources
if (isset($this->sources))
return $this->sources;
// get all folders that have "contact" type
$folders = kolab_storage::get_folders('contact', $this->subscribed);
$this->sources = $this->folders = $this->aliases = array();
foreach (kolab_storage::sort_folders($folders) as $folder) {
$id = DAVBackend::get_uid($folder);
$fdata = $folder->get_imap_data(); // fetch IMAP folder data for CTag generation
$this->folders[$id] = $folder;
$this->sources[$id] = array(
'id' => $id,
'uri' => $id,
'{DAV:}displayname' => html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET),
'{http://calendarserver.org/ns/}getctag' => sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']),
'{urn:ietf:params:xml:ns:caldav}supported-address-data' => new CardDAV\Property\SupportedAddressData(),
);
$this->aliases[$folder->name] = $id;
// map default folder to the magic 'all' resource
if ($folder->default)
$this->aliases['__all__'] = $id;
}
return $this->sources;
}
/**
* Getter for a kolab_storage_folder representing the address book for the given ID
*
* @param string Folder ID
* @return object kolab_storage_folder instance
*/
public function get_storage_folder($id)
{
// resolve alias name
if ($this->aliases[$id]) {
$id = $this->aliases[$id];
}
if ($this->folders[$id]) {
return $this->folders[$id];
}
else {
return DAVBackend::get_storage_folder($id, 'contact');
}
}
/**
* Returns the list of addressbooks for a specific user.
*
* @param string $principalUri
* @return array
*/
public function getAddressBooksForUser($principalUri)
{
console(__METHOD__, $principalUri, $this->useragent);
$this->_read_sources();
// special case for the apple address book which only supports one (!) address book
if ($this->useragent == 'macosx' && count($this->sources) > 1) {
$source = $this->getAddressBookByName('__all__');
$source['principaluri'] = $principalUri;
return array($source);
}
$addressBooks = array();
foreach ($this->sources as $id => $source) {
$source['principaluri'] = $principalUri;
$addressBooks[] = $source;
}
return $addressBooks;
}
/**
* Returns properties for a specific node identified by name/uri
*
* @param string Node name/uri
* @return array Hash array with addressbook properties or null if not found
*/
public function getAddressBookByName($addressBookUri)
{
console(__METHOD__, $addressBookUri);
$this->_read_sources();
$id = $addressBookUri;
// return the magic *single* address book for Apple's Address Book App
if ($id == '__all__') {
$ctags = array();
foreach ($this->sources as $source) {
$ctags[] = $source['{http://calendarserver.org/ns/}getctag'];
}
return array(
'id' => '__all__',
'uri' => '__all__',
'{DAV:}displayname' => 'All',
'{http://calendarserver.org/ns/}getctag' => join(':', $ctags),
'{urn:ietf:params:xml:ns:caldav}supported-address-data' => new CardDAV\Property\SupportedAddressData(),
);
}
// resolve aliases (addressbook by folder name)
if ($this->aliases[$addressBookUri]) {
$id = $this->aliases[$addressBookUri];
}
// retry with subscribed = false (#2701)
if (empty($this->sources[$id]) && $this->subscribed === null && rcube::get_instance()->config->get('kolab_use_subscriptions')) {
$this->subscribed = false;
unset($this->sources);
return $this->getAddressBookByName($addressBookUri);
}
return $this->sources[$id];
}
/**
* Updates an addressbook's properties
*
* See Sabre\DAV\IProperties for a description of the mutations array, as
* well as the return value.
*
* @param mixed $addressBookId
* @param array $mutations
* @see Sabre\DAV\IProperties::updateProperties
* @return bool|array
*/
public function updateAddressBook($addressBookId, array $mutations)
{
console(__METHOD__, $addressBookId, $mutations);
if ($addressBookId == '__all__')
return false;
$folder = $this->get_storage_folder($addressBookId);
return $folder ? DAVBackend::folder_update($folder, $mutations) : false;
}
/**
* Creates a new address book
*
* @param string $principalUri
* @param string $url Just the 'basename' of the url.
* @param array $properties
* @return void
*/
public function createAddressBook($principalUri, $url, array $properties)
{
console(__METHOD__, $principalUri, $url, $properties);
return DAVBackend::folder_create('contact', $properties, $url);
}
/**
* Deletes an entire addressbook and all its contents
*
* @param int $addressBookId
* @return void
*/
public function deleteAddressBook($addressBookId)
{
console(__METHOD__, $addressBookId);
if ($addressBookId == '__all__')
return;
$folder = $this->get_storage_folder($addressBookId);
if ($folder && !kolab_storage::folder_delete($folder->name)) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting calendar folder $folder->name"),
true, false);
}
}
/**
* Returns all cards for a specific addressbook id.
*
* This method should return the following properties for each card:
* * carddata - raw vcard data
* * uri - Some unique url
* * lastmodified - A unix timestamp
* * etag - A unique etag. This must change every time the card changes.
* * size - The size of the card in bytes.
*
* If these last two properties are provided, less time will be spent
* calculating them. If they are specified, you can also ommit carddata.
* This may speed up certain requests, especially with large cards.
*
* @param mixed $addressBookId
* @return array
*/
public function getCards($addressBookId)
{
console(__METHOD__, $addressBookId);
// recursively fetch contacts from all folders
if ($addressBookId == '__all__') {
$cards = array();
foreach ($this->sources as $id => $source) {
$cards = array_merge($cards, $this->getCards($id));
}
return $cards;
}
$groups_support = $this->useragent != 'thunderbird';
$query = array(array('type', '=', $groups_support ? array('contact','distribution-list') : 'contact'));
$cards = array();
if ($storage = $this->get_storage_folder($addressBookId)) {
foreach ($storage->select($query) as $contact) {
$cards[] = array(
'id' => $contact['uid'],
'uri' => $contact['uid'] . '.vcf',
'lastmodified' => is_a($contact['changed'], 'DateTime') ? $contact['changed']->format('U') : null,
'etag' => self::_get_etag($contact),
'size' => $contact['_size'],
);
}
}
return $cards;
}
/**
* Returns a specfic card.
*
* The same set of properties must be returned as with getCards. The only
* exception is that 'carddata' is absolutely required.
*
* @param mixed $addressBookId
* @param string $cardUri
* @return array
*/
public function getCard($addressBookId, $cardUri)
{
console(__METHOD__, $addressBookId, $cardUri);
$uid = basename($cardUri, '.vcf');
// search all folders for the given card
if ($addressBookId == '__all__') {
$contact = $this->get_card_by_uid($uid, $storage);
}
else {
$storage = $this->get_storage_folder($addressBookId);
$contact = $storage->get_object($uid, '*');
}
if ($contact) {
return array(
'id' => $contact['uid'],
'uri' => $contact['uid'] . '.vcf',
'lastmodified' => is_a($contact['changed'], 'DateTime') ? $contact['changed']->format('U') : null,
- 'carddata' => $this->_to_vcard($contact),
+ 'carddata' => $this->to_vcard($contact),
'etag' => self::_get_etag($contact),
);
}
return array();
}
/**
* Creates a new card.
*
* The addressbook id will be passed as the first argument. This is the
* same id as it is returned from the getAddressbooksForUser method.
*
* The cardUri is a base uri, and doesn't include the full path. The
* cardData argument is the vcard body, and is passed as a string.
*
* It is possible to return an ETag from this method. This ETag is for the
* newly created resource, and must be enclosed with double quotes (that
* is, the string itself must contain the double quotes).
*
* You should only return the ETag if you store the carddata as-is. If a
* subsequent GET request on the same card does not have the same body,
* byte-by-byte and you did return an ETag here, clients tend to get
* confused.
*
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
* @return string|null
*/
public function createCard($addressBookId, $cardUri, $cardData)
{
console(__METHOD__, $addressBookId, $cardUri, $cardData);
$uid = basename($cardUri, '.vcf');
$storage = $this->get_storage_folder($addressBookId);
$object = $this->parse_vcard($cardData, $uid);
if (empty($object) || empty($object['uid'])) {
throw new DAV\Exception('Parse error: not a valid VCard object');
}
// if URI doesn't match the content's UID, the object might already exist!
$cardUri = $object['uid'] . '.vcf';
if ($object['uid'] != $uid && $this->getCard($addressBookId, $cardUri)) {
Plugin::$redirect_basename = $cardUri;
return $this->updateCard($addressBookId, $cardUri, $cardData);
}
$success = $storage->save($object, $object['_type']);
if (!$success) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server"),
true, false);
throw new DAV\Exception('Error saving contact card to backend');
}
// send Location: header if URI doesn't match object's UID (Bug #2109)
if ($object['uid'] != $uid) {
Plugin::$redirect_basename = $cardUri;
}
// return new Etag
return $success ? self::_get_etag($object) : null;
}
/**
* Updates a card.
*
* The addressbook id will be passed as the first argument. This is the
* same id as it is returned from the getAddressbooksForUser method.
*
* The cardUri is a base uri, and doesn't include the full path. The
* cardData argument is the vcard body, and is passed as a string.
*
* It is possible to return an ETag from this method. This ETag should
* match that of the updated resource, and must be enclosed with double
* quotes (that is: the string itself must contain the actual quotes).
*
* If you don't return an ETag, you can just return null.
*
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
* @return string|null
*/
public function updateCard($addressBookId, $cardUri, $cardData)
{
console(__METHOD__, $addressBookId, $cardUri, $cardData);
$uid = basename($cardUri, '.vcf');
$object = $this->parse_vcard($cardData, $uid);
// sanity check
if ($object['uid'] != $uid) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error creating contact object: UID doesn't match object URI"),
true, false);
throw new DAV\Exception\NotFound("UID doesn't match object URI");
}
if ($addressBookId == '__all__') {
$old = $this->get_card_by_uid($uid, $storage);
}
else {
if ($storage = $this->get_storage_folder($addressBookId))
$old = $storage->get_object($uid);
}
if (!$storage) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Unable to find storage folder for contact $addressBookId/$cardUri"),
true, false);
throw new DAV\Exception\NotFound("Invalid address book URI");
}
if (!$this->is_writeable($storage)) {
throw new DAV\Exception\Forbidden('Insufficient privileges to update this card');
}
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($object[$key]) && $key[0] == '_')
$object[$key] = $val;
}
// save object
$saved = $storage->save($object, $object['_type'], $uid);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server"),
true, false);
Plugin::$redirect_basename = null;
throw new DAV\Exception('Error saving contact card to backend');
}
// return new Etag
return self::_get_etag($object);
}
/**
* Deletes a card
*
* @param mixed $addressBookId
* @param string $cardUri
* @return bool
*/
public function deleteCard($addressBookId, $cardUri)
{
console(__METHOD__, $addressBookId, $cardUri);
$uid = basename($cardUri, '.vcf');
if ($addressBookId == '__all__') {
$this->get_card_by_uid($uid, $storage);
}
else {
$storage = $this->get_storage_folder($addressBookId);
}
if (!$storage || !$this->is_writeable($storage)) {
throw new DAV\Exception\MethodNotAllowed('Insufficient privileges to delete this card');
}
if ($storage) {
return $storage->delete($uid);
}
return false;
}
/**
* Set User-Agent string of the connected client
*/
public function setUserAgent($uastring)
{
$ua_classes = array(
'thunderbird' => 'Thunderbird/\d',
'macosx' => '(Mac OS X/.+)?AddressBook/\d(.+\sCardDAVPlugin)?',
'ios' => '(iOS/\d|[Dd]ata[Aa]ccessd/\d)',
);
foreach ($ua_classes as $class => $regex) {
if (preg_match("!$regex!", $uastring)) {
$this->useragent = $class;
break;
}
}
}
/**
* Find an object and the containing folder by UID
*
* @param string Object UID
* @param object Return parameter for the kolab_storage_folder instance
* @return array|false
*/
private function get_card_by_uid($uid, &$storage)
{
$obj = kolab_storage::get_object($uid, 'contact');
if ($obj) {
$storage = kolab_storage::get_folder($obj['_mailbox']);
return $obj;
}
return false;
}
/**
* Internal helper method to determine whether the given kolab_storage_folder is writeable
*
*/
private function is_writeable($storage)
{
$rights = $storage->get_myrights();
return (strpos($rights, 'i') !== false || $storage->get_namespace() == 'personal');
}
/**
* Helper method to determine whether the connected client is an Apple device
*/
private function is_apple()
{
return $this->useragent == 'macosx' || $this->useragent == 'ios';
}
/********** Data conversion utilities ***********/
private $phonetypes = array(
'main' => 'voice',
'homefax' => 'fax',
'workfax' => 'fax',
'mobile' => 'cell',
'other' => 'textphone',
);
private $improtocols = array(
'jabber' => 'xmpp',
);
/**
* Parse the given VCard string into a hash array kolab_format_contact can handle
*
* @param string VCard data block
* @return array Hash array with contact properties or null on failure
*/
private function parse_vcard($cardData, $uid)
{
try {
// use already parsed object
if (Plugin::$parsed_vcard && Plugin::$parsed_vcard->UID == $uid) {
$vobject = Plugin::$parsed_vcard;
}
else {
VObject\Property::$classMap['REV'] = 'Sabre\\VObject\\Property\\DateTime';
$vobject = VObject\Reader::read($cardData, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
}
if ($vobject && $vobject->name == 'VCARD') {
$contact = $this->_to_array($vobject);
if (!empty($contact['uid'])) {
return $contact;
}
}
}
catch (VObject\ParseException $e) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "VCard data parse error: " . $e->getMessage()),
true, false);
}
return null;
}
/**
* Build a valid VCard format block from the given contact record
*
* @param array Hash array with contact properties from libkolab
* @return string VCARD string containing the contact data
*/
- private function _to_vcard($contact)
+ public function to_vcard($contact)
{
$vc = VObject\Component::create('VCARD');
$vc->version = '3.0';
$vc->prodid = '-//Kolab//iRony DAV Server ' . KOLAB_DAV_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
$vc->add('UID', $contact['uid']);
$vc->add('FN', $contact['name']);
// distlists are KIND:group
if ($contact['_type'] == 'distribution-list') {
// group cards are actually vcard version 4
if (!$this->is_apple()) {
$vc->version = '4.0';
$prop_prefix = '';
}
else {
// prefix group properties for Apple
$prop_prefix = 'X-ADDRESSBOOKSERVER-';
}
$vc->add($prop_prefix . 'KIND', 'group');
foreach ((array)$contact['member'] as $member) {
if ($member['uid'])
$value = 'urn:uuid:' . $member['uid'];
else if ($member['email'] && $member['name'])
$value = urlencode(sprintf('mailto:"%s" <%s>', addcslashes($member['name'], '"'), $member['email']));
else if ($member['email'])
$value = urlencode('mailto:' . $member['email']);
$vc->add($prop_prefix . 'MEMBER', $value);
}
}
else if ($contact['surname'] . $contact['firstname'] . $contact['middlename'] . $contact['prefix'] . $contact['suffix'] != '') {
$n = VObject\Property::create('N');
$n->setParts(array($contact['surname'], $contact['firstname'], $contact['middlename'], $contact['prefix'], $contact['suffix']));
$vc->add($n);
}
if (!empty($contact['nickname']))
$vc->add('NICKNAME', $contact['nickname']);
if (!empty($contact['jobtitle']))
$vc->add('TITLE', $contact['jobtitle']);
if (!empty($contact['profession']))
$vc->add('X-PROFESSION', $contact['profession']);
if (!empty($contact['organization']) || !empty($contact['department'])) {
$org = VObject\Property::create('ORG');
$org->setParts(array($contact['organization'], $contact['department']));
$vc->add($org);
}
// TODO: save as RELATED
if (!empty($contact['assistant']))
$vc->add('X-ASSISTANT', join(',', (array)$contact['assistant']));
if (!empty($contact['manager']))
$vc->add('X-MANAGER', join(',', (array)$contact['manager']));
if (!empty($contact['spouse']))
$vc->add('X-SPOUSE', join(',', (array)$contact['spouse']));
if (!empty($contact['children']))
$vc->add('X-CHILDREN', join(',', (array)$contact['children']));
foreach ((array)$contact['email'] as $email) {
$vc->add('EMAIL', $email['address'], array('type' => rtrim('INTERNET,' . strtoupper($email['type']), ',')));
}
foreach ((array)$contact['phone'] as $phone) {
$type = $this->phonetypes[$phone['type']] ?: $phone['type'];
$vc->add('TEL', $phone['number'], array('type' => strtoupper($type)));
}
foreach ((array)$contact['website'] as $website) {
$vc->add('URL', $website['url'], array('type' => strtoupper($website['type'])));
}
$improtocolmap = array_flip($this->improtocols);
foreach ((array)$contact['im'] as $im) {
list($prot, $val) = explode(':', $im);
if ($val) $vc->add('x-' . ($improtocolmap[$prot] ?: $prot), $val);
else $vc->add('IMPP', $im);
}
foreach ((array)$contact['address'] as $adr) {
$vadr = VObject\Property::create('ADR', null, array('type' => strtoupper($adr['type'])));
$vadr->setParts(array('','', $adr['street'], $adr['locality'], $adr['region'], $adr['code'], $adr['country']));
$vc->add($vadr);
}
if (!empty($contact['notes']))
$vc->add('NOTE', $contact['notes']);
if (!empty($contact['gender']))
$vc->add('SEX', $contact['gender']);
// convert date cols to DateTime objects
foreach (array('birthday','anniversary') as $key) {
if (!empty($contact[$key]) && !$contact[$key] instanceof \DateTime) {
try {
$contact[$key] = new \DateTime(\rcube_utils::clean_datestr($contact[$key]));
}
catch (\Exception $e) {
$contact[$key] = null;
}
}
}
if (!empty($contact['birthday']) && $contact['birthday'] instanceof \DateTime) {
// FIXME: Date values are ignored by Thunderbird
$contact['birthday']->_dateonly = true;
$vc->add(VObjectUtils::datetime_prop('BDAY', $contact['birthday'], false));
}
if (!empty($contact['anniversary']) && $contact['anniversary'] instanceof \DateTime) {
$contact['anniversary']->_dateonly = true;
$vc->add(VObjectUtils::datetime_prop('ANNIVERSARY', $contact['anniversary'], false));
}
if (!empty($contact['categories'])) {
$cat = VObject\Property::create('CATEGORIES');
$cat->setParts((array)$contact['categories']);
$vc->add($cat);
}
if (!empty($contact['freebusyurl']))
$vc->add('FBURL', $contact['freebusyurl']);
if (!empty($contact['photo'])) {
$vc->PHOTO = base64_encode($contact['photo']);
$vc->PHOTO->add('BASE64', null);
}
// add custom properties
foreach ((array)$contact['x-custom'] as $prop) {
$vc->add($prop[0], $prop[1]);
}
// send anniversary field as itemN.X-ABDATE
if ($this->is_apple() && !empty($contact['anniversary'])) {
$vc->add(VObjectUtils::datetime_prop('iRony.X-ABDATE', $contact['anniversary'], false));
$vc->add('iRony.X-ABLabel', '_$!<Anniversary>!$_');
unset($vc->ANNIVERSARY);
}
if (!empty($contact['changed']))
$vc->add(VObjectUtils::datetime_prop('REV', $contact['changed'], true));
return $vc->serialize();
}
/**
* Convert the given Sabre\VObject\Component\Vcard object to a libkolab compatible contact format
*
* @param object Vcard object to convert
* @return array Hash array with contact properties
*/
private function _to_array($vc)
{
$contact = array(
'_type' => 'contact',
'uid' => strval($vc->UID),
'name' => strval($vc->FN),
'x-custom' => array(),
);
if ($vc->REV) {
try { $contact['changed'] = $vc->REV->getDateTime(); }
catch (\Exception $e) {
try { $contact['changed'] = new \DateTime(strval($vc->REV)); }
catch (\Exception $e) { }
}
}
// map Apple proprietary anniversary field to regular field
foreach ($vc->select('X-ABDATE') as $prop) {
$labelkey = $prop->group ? $prop->group . '.X-ABLABEL' : 'X-ABLABEL';
$labels = $vc->select($labelkey);
if (!empty($labels) && ($label = reset($labels)) && strtolower(trim($label->value, '_$!<>')) == 'anniversary') {
$prop->group = null;
$prop->name = 'ANNIVERSARY';
unset($vc->{$labelkey});
break;
}
}
$phonetypemap = array_flip($this->phonetypes);
// map attributes to internal fields
foreach ($vc->children as $prop) {
if (!($prop instanceof VObject\Property))
continue;
switch ($prop->name) {
case 'N':
list($contact['surname'], $contact['firstname'], $contact['middlename'], $contact['prefix'], $contact['suffix']) = $prop->getParts();
break;
case 'NOTE':
$contact['notes'] = $prop->value;
break;
case 'TITLE':
$contact['jobtitle'] = $prop->value;
break;
case 'NICKNAME':
$contact[strtolower($prop->name)] = $prop->value;
break;
case 'ORG':
list($contact['organization'], $contact['department']) = $prop->getParts();
break;
case 'CATEGORY':
case 'CATEGORIES':
$contact['categories'] = $prop->getParts();
break;
case 'EMAIL':
$types = array_values(self::array_filter($prop->offsetGet('type'), 'internet,pref', true));
$contact['email'][] = array('address' => $prop->value, 'type' => strtolower($types[0] ?: 'other'));
break;
case 'URL':
$types = array_values(self::array_filter($prop->offsetGet('type'), 'internet,pref', true));
$contact['website'][] = array('url' => $prop->value, 'type' => strtolower($types[0]));
break;
case 'TEL':
$types = array_values(self::array_filter($prop->offsetGet('type'), 'internet,pref', true));
$type = strtolower($types[0]);
$contact['phone'][] = array('number' => $prop->value, 'type' => $phonetypemap[$type] ?: $type);
break;
case 'ADR':
$type = $prop->offsetGet('type');
$adr = array('type' => strtolower($type));
list(,, $adr['street'], $adr['locality'], $adr['region'], $adr['code'], $adr['country']) = $prop->getParts();
$contact['address'][] = $adr;
break;
case 'BDAY':
$contact['birthday'] = new \DateTime($prop->value);
$contact['birthday']->_dateonly = true;
break;
case 'ANNIVERSARY':
case 'X-ANNIVERSARY':
$contact['anniversary'] = new \DateTime($prop->value);
$contact['anniversary']->_dateonly = true;
break;
case 'SEX':
case 'X-GENDER':
$contact['gender'] = $prop->value;
break;
case 'X-PROFESSION':
$contact[strtolower(substr($prop->name, 2))] = $prop->value;
break;
case 'X-MANAGER':
case 'X-ASSISTANT':
case 'X-CHILDREN':
case 'X-SPOUSE':
$contact[strtolower(substr($prop->name, 2))] = explode(',', $prop->value);
break;
case 'X-JABBER':
case 'X-ICQ':
case 'X-MSN':
case 'X-AIM':
case 'X-YAHOO':
case 'X-SKYPE':
$protocol = strtolower(substr($prop->name, 2));
$contact['im'][] = ($this->improtocols[$protocol] ?: $protocol) . ':' . preg_replace('/^[a-z]+:/i', '', $prop->value);
break;
case 'IMPP':
$type = strtolower((string)$prop->offsetGet('X-SERVICE-TYPE'));
$protocol = $type && !preg_match('/^[a-z]+:/i', $prop->value) ? ($this->improtocols[$type] ?: $type) . ':' : '';
$contact['im'][] = $protocol . urldecode($prop->value);
break;
case 'PHOTO':
$param = $prop->offsetGet('encoding') ?: $prop->parameters[0];
if ($param->value && (strtolower($param->value) == 'b' || strtolower($param->value) == 'base64') || strtolower($param->name) == 'base64') {
$contact['photo'] = base64_decode($prop->value);
}
break;
case 'KIND':
case 'X-ADDRESSBOOKSERVER-KIND':
if (strtolower($prop->value) == 'group') {
$contact['_type'] = 'distribution-list';
}
break;
case 'MEMBER':
case 'X-ADDRESSBOOKSERVER-MEMBER':
if (strpos($prop->value, 'urn:uuid:') === 0) {
$contact['member'][] = array('uid' => substr($prop->value, 9));
}
else if (strpos($prop->value, 'mailto:') === 0) {
$member = reset(\rcube_mime::parse_address_list(urldecode(substr($prop->value, 7))));
if ($member['address'])
$contact['member'][] = array('email' => $member['address'], 'name' => $member['name']);
}
break;
case 'CUSTOM1':
case 'CUSTOM2':
case 'CUSTOM3':
case 'CUSTOM4':
default:
if (substr($prop->name, 0, 2) == 'X-' || substr($prop->name, 0, 6) == 'CUSTOM') {
$prefix = $prop->group ? $prop->group . '.' : '';
$contact['x-custom'][] = array($prefix . $prop->name, strval($prop->value));
}
break;
}
}
if (is_array($contact['im']))
$contact['im'] = array_unique($contact['im']);
return $contact;
}
/**
* Extract array values by a filter
*
* @param array Array to filter
* @param keys Array or comma separated list of values to keep
* @param boolean Invert key selection: remove the listed values
*
* @return array The filtered array
*/
private static function array_filter($arr, $values, $inverse = false)
{
if (!is_array($values)) {
$values = explode(',', $values);
}
$result = array();
$keep = array_flip((array)$values);
if (!empty($arr)) {
foreach ($arr as $key => $val) {
if ($inverse != isset($keep[strtolower($val)])) {
$result[$key] = $val;
}
}
}
return $result;
}
/**
* Generate an Etag string from the given contact data
*
* @param array Hash array with contact properties from libkolab
* @return string Etag string
*/
private static function _get_etag($contact)
{
return sprintf('"%s-%d"', substr(md5($contact['uid']), 0, 16), $contact['_msguid']);
}
}
diff --git a/lib/Kolab/CardDAV/LDAPDirectory.php b/lib/Kolab/CardDAV/LDAPDirectory.php
new file mode 100644
index 0000000..622ce29
--- /dev/null
+++ b/lib/Kolab/CardDAV/LDAPDirectory.php
@@ -0,0 +1,484 @@
+<?php
+
+/**
+ * CardDAV Directory class providing read-only access
+ * to an LDAP-based global address book.
+ *
+ * This implements the CardDAV Directory Gateway Extension suggested by Apple Inc.
+ * http://tools.ietf.org/html/draft-daboo-carddav-directory-gateway-02
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Kolab\CardDAV;
+
+use \rcube;
+use \rcube_ldap;
+use \rcube_ldap_generic;
+use Sabre\DAV;
+use Sabre\DAVACL;
+use Sabre\CardDAV\Card;
+use Sabre\CardDAV\Property;
+
+/**
+ * CardDAV Directory Gateway implementation
+ */
+class LDAPDirectory extends DAV\Collection implements \Sabre\CardDAV\IDirectory, DAV\IProperties, DAVACL\IACL
+{
+ const DIRECTORY_NAME = 'ldap-directory';
+
+ private $config;
+ private $ldap;
+ private $carddavBackend;
+ private $principalUri;
+ private $addressBookInfo = array();
+ private $uid2id = array();
+ private $query;
+ private $filter;
+
+ /**
+ * Default constructor
+ */
+ function __construct($config, $principalUri, $carddavBackend = null)
+ {
+ $this->config = $config;
+ $this->principalUri = $principalUri;
+
+ $this->addressBookInfo = array(
+ 'id' => self::DIRECTORY_NAME,
+ 'uri' => self::DIRECTORY_NAME,
+ '{DAV:}displayname' => $config['name'] ?: "LDAP Directory",
+ '{urn:ietf:params:xml:ns:caldav}supported-address-data' => new Property\SupportedAddressData(),
+ 'principaluri' => $principalUri,
+ );
+
+ // used for vcard serialization
+ $this->carddavBackend = $carddavBackend ?: new ContactsBackend();
+ }
+
+ private function connect()
+ {
+ if (!isset($this->ldap)) {
+ $this->ldap = new rcube_ldap($this->config, $this->config['debug']);
+ $this->ldap->set_pagesize($this->config['sizelimit'] ?: 10000);
+ }
+
+ return $this->ldap->ready ? $this->ldap : null;
+ }
+
+ /**
+ * Set parsed addressbook-query object for filtering
+ */
+ function setAddressbookQuery($query)
+ {
+ $this->query = $query;
+ $this->filter = $this->addressbook_query2ldap_filter($query);
+ }
+
+ /**
+ * Returns the name of the node.
+ *
+ * This is used to generate the url.
+ *
+ * @return string
+ */
+ function getName()
+ {
+ return self::DIRECTORY_NAME;
+ }
+
+ /**
+ * Returns a specific child node, referenced by its name
+ *
+ * This method must throw Sabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @return DAV\INode
+ */
+ function getChild($cardUri)
+ {
+ console(__METHOD__, $cardUri);
+
+ $uid = basename($cardUri, '.vcf');
+ $record = null;
+
+ // TODO: get from cache
+
+ if ($ldap = $this->connect()) {
+ // used cached uid mapping
+ if ($ID = $this->uid2id[$uid]) {
+ $record = $ldap->get_record($ID, true);
+ }
+ else { // query for uid
+ $result = $ldap->search('uid', $uid, 1, true, true);
+ if ($result->count) {
+ $record = $result[0];
+ }
+ }
+
+ if ($record) {
+ $this->_normalize_contact($record);
+ $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 Card($this->carddavBackend, $this->addressBookInfo, $obj);
+ }
+ }
+
+ throw new DAV\Exception\NotFound('Card not found');
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return DAV\INode[]
+ */
+ function getChildren()
+ {
+ console(__METHOD__, $this->query, $this->filter);
+
+ $children = array();
+
+ // 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
+ if ($this->query && $this->query->limit) {
+ $this->ldap->set_pagesize(intval($this->query->limit));
+ }
+
+ // set the prepared LDAP filter derived from the addressbook-query
+ if ($this->query && !empty($this->filter)) {
+ $ldap->set_search_set($this->filter);
+ }
+ else {
+ $ldap->set_search_set(null);
+ }
+
+ $results = $ldap->list_records(null);
+
+ // convert restuls into vcard blocks
+ foreach ($results as $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),
+ );
+
+ // TODO: cache result
+ $this->uid2id[$contact['uid']] = $contact['ID'];
+
+ $children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
+ }
+ }
+
+ return $children;
+ }
+
+ /**
+ * Returns a list of properties for this node.
+ *
+ * The properties list is a list of propertynames the client requested,
+ * encoded in clark-notation {xmlnamespace}tagname
+ *
+ * If the array is empty, it means 'all properties' were requested.
+ *
+ * @param array $properties
+ * @return array
+ */
+ public function getProperties($properties)
+ {
+ console(__METHOD__, $properties);
+
+ $response = array();
+ foreach ($properties as $propertyName) {
+ if (isset($this->addressBookInfo[$propertyName])) {
+ $response[$propertyName] = $this->addressBookInfo[$propertyName];
+ }
+ else if ($propertyName == '{DAV:}getlastmodified') {
+ $response[$propertyName] = new DAV\Property\GetLastModified($this->getLastModified());
+ }
+ }
+
+ return $response;
+
+ }
+
+ /**
+ * Returns the last modification time, as a unix timestamp
+ *
+ * @return int
+ */
+ function getLastModified()
+ {
+ console(__METHOD__);
+ return time();
+ }
+
+ /**
+ * Deletes the entire addressbook.
+ *
+ * @return void
+ */
+ public function delete()
+ {
+ throw new DAV\Exception\MethodNotAllowed('Deleting directories is not allowed');
+ }
+
+ /**
+ * Renames the addressbook
+ *
+ * @param string $newName
+ * @return void
+ */
+ public function setName($newName)
+ {
+ throw new DAV\Exception\MethodNotAllowed('Renaming directories not allowed');
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner()
+ {
+ return $this->principalUri;
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ function getGroup()
+ {
+ return null;
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to be updated.
+ *
+ * @return array
+ */
+ public function getACL()
+ {
+ $acl = array(
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalUri,
+ 'protected' => true,
+ ),
+ );
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * @param array $acl
+ * @return void
+ */
+ function setACL(array $acl)
+ {
+ throw new DAV\Exception\MethodNotAllowed('Changing ACL for directories is not allowed');
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ function getSupportedPrivilegeSet()
+ {
+ return null;
+ }
+
+ /**
+ * Updates properties on this node,
+ *
+ * @param array $mutations
+ * @return bool|array
+ */
+ function updateProperties($mutations)
+ {
+ console(__METHOD__, $mutations);
+ return false;
+ }
+
+ /**
+ * Post-process the given contact record from rcube_ldap
+ */
+ private function _normalize_contact(&$contact)
+ {
+ if (is_numeric($contact['changed'])) {
+ $contact['_timestamp'] = $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');
+ }
+ catch (Exception $e) {
+ $contact['changed'] = null;
+ }
+ }
+
+ // 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) {
+ $contact[$col][] = array($prop => $value, 'type' => $type);
+ }
+ }
+ }
+ }
+
+ /**
+ * Translate the given AddressBookQueryParser object into an LDAP filter
+ */
+ private function addressbook_query2ldap_filter($query)
+ {
+ $criterias = array();
+
+ foreach ($query->filters as $filter) {
+ $ldap_attrs = $this->map_property2ldap($filter['name']);
+ $ldap_filter = ''; $count = 0;
+
+ // unknown attribute, skip
+ if (empty($ldap_attrs)) {
+ continue;
+ }
+
+ foreach ((array)$filter['text-matches'] as $matcher) {
+ // case-insensitive matching
+ if (in_array($matcher['collation'], array('i;unicode-casemap', 'i;ascii-casemap'))) {
+ $matcher['value'] = mb_strtolower($matcher['value']);
+ }
+ $value = rcube_ldap_generic::quote_string($matcher['value']);
+ $ldap_match = '';
+
+ // this assumes fuzzy search capabilities of the LDAP backend
+ switch ($matcher['match-type']) {
+ case 'contains':
+ $wp = $ws = '*';
+ break;
+ case 'starts-with':
+ $ws = '*';
+ break;
+ case 'ends-with':
+ $wp = '*';
+ break;
+ default:
+ $wp = $ws = '';
+ }
+
+ // OR query for all attributes involved
+ if (count($ldap_attrs) > 1) {
+ $ldap_match .= '(|';
+ }
+ foreach ($ldap_attrs as $attr) {
+ $ldap_match .= "($attr=$wp$value$ws)";
+ }
+ if (count($ldap_attrs) > 1) {
+ $ldap_match .= ')';
+ }
+
+ // negate the filter
+ if ($matcher['negate-condition']) {
+ $ldap_match = '(!' . $ldap_match . ')';
+ }
+
+ $ldap_filter .= $ldap_match;
+ $count++;
+ }
+
+ if ($count > 1) {
+ $criterias[] = '(' . ($filter['test'] == 'allof' ? '&' : '|') . $ldap_filter . ')';
+ }
+ else if (!empty($ldap_filter)) {
+ $criterias[] = $ldap_filter;
+ }
+ }
+
+ return empty($criterias) ? '' : sprintf('(%s%s)', $query->test == 'allof' ? '&' : '|', join('', $criterias));
+ }
+
+ /**
+ * Map a vcard property to an LDAP attribute
+ */
+ private function map_property2ldap($propname)
+ {
+ $attribs = array();
+ $ldap = $this->connect();
+
+ $vcard_fieldmap = array(
+ 'FN' => array('name'),
+ 'N' => array('surname','firstname','middlename'),
+ 'ADR' => array('street','locality','region','code','country'),
+ 'TITLE' => array('jobtitle'),
+ 'ORG' => array('organization','department'),
+ 'TEL' => array('phone'),
+ 'URL' => array('website'),
+ 'ROLE' => array('profession'),
+ 'BDAY' => array('birthday'),
+ 'IMPP' => array('im'),
+ );
+
+ $fields = $vcard_fieldmap[$propname] ?: array(strtolower($propname));
+ foreach ($fields as $field) {
+ if ($ldap->coltypes[$field]) {
+ $attribs = array_merge($attribs, (array)$ldap->coltypes[$field]['attributes']);
+ }
+ }
+
+ return $attribs;
+ }
+
+ /**
+ * Generate an Etag string from the given contact data
+ *
+ * @param array Hash array with contact properties from libkolab
+ * @return string Etag string
+ */
+ private static function _get_etag($contact)
+ {
+ return sprintf('"%s-%d"', substr(md5($contact['uid']), 0, 16), $contact['_timestamp']);
+ }
+}
diff --git a/lib/Kolab/CardDAV/Plugin.php b/lib/Kolab/CardDAV/Plugin.php
index 2abf0a8..1456e78 100644
--- a/lib/Kolab/CardDAV/Plugin.php
+++ b/lib/Kolab/CardDAV/Plugin.php
@@ -1,127 +1,203 @@
<?php
/**
* Extended CardDAV plugin for the Kolab DAV server
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\CardDAV;
use Sabre\DAV;
+use Sabre\DAVACL;
use Sabre\CardDAV;
use Sabre\VObject;
/**
* Extended CardDAV plugin to tweak data validation
*/
class Plugin extends CardDAV\Plugin
{
// make already parsed vcard blocks available for later use
public static $parsed_vcard;
// allow the backend to force a redirect Location
public static $redirect_basename;
/**
* Initializes the plugin
*
* @param DAV\Server $server
* @return void
*/
public function initialize(DAV\Server $server)
{
parent::initialize($server);
$server->subscribeEvent('beforeMethod', array($this, 'beforeMethod'));
$server->subscribeEvent('afterCreateFile', array($this, 'afterWriteContent'));
$server->subscribeEvent('afterWriteContent', array($this, 'afterWriteContent'));
}
+ /**
+ * Adds all CardDAV-specific properties
+ *
+ * @param string $path
+ * @param DAV\INode $node
+ * @param array $requestedProperties
+ * @param array $returnedProperties
+ * @return void
+ */
+ public function beforeGetProperties($path, DAV\INode $node, array &$requestedProperties, array &$returnedProperties)
+ {
+ // publish global ldap address book for this principal
+ if ($node instanceof DAVACL\IPrincipal && empty($this->directories) && \rcube::get_instance()->config->get('global_ldap_directory')) {
+ $this->directories[] = self::ADDRESSBOOK_ROOT . '/' . $node->getName() . '/' . LDAPDirectory::DIRECTORY_NAME;
+ }
+
+ parent::beforeGetProperties($path, $node, $requestedProperties, $returnedProperties);
+ }
+
/**
* Handler for beforeMethod events
*/
public function beforeMethod($method, $uri)
{
if ($method == 'PUT' && $this->server->httpRequest->getHeader('If-None-Match') == '*') {
// In-None-Match: * is only valid with PUT requests creating a new resource.
// SOGo Conenctor for Thunderbird also sends it with update requests which then fail
// in the Server::checkPreconditions().
// See https://issues.kolab.org/show_bug.cgi?id=2589 and http://www.sogo.nu/bugs/view.php?id=1624
// This is a work-around for the buggy SOGo connector and should be removed once fixed.
if (strpos($this->server->httpRequest->getHeader('User-Agent'), 'Thunderbird/') > 0) {
unset($_SERVER['HTTP_IF_NONE_MATCH']);
}
}
}
/**
* Inject some additional HTTP response headers
*/
public function afterWriteContent($uri, $node)
{
// send Location: header to corrected URI
if (self::$redirect_basename) {
$path = explode('/', $uri);
array_pop($path);
array_push($path, self::$redirect_basename);
$this->server->httpResponse->setHeader('Location', $this->server->getBaseUri() . join('/', array_map('urlencode', $path)));
self::$redirect_basename = null;
}
}
/**
* Checks if the submitted iCalendar data is in fact, valid.
*
* An exception is thrown if it's not.
*
* @param resource|string $data
* @return void
*/
protected function validateVCard(&$data)
{
// If it's a stream, we convert it to a string first.
if (is_resource($data)) {
$data = stream_get_contents($data);
}
// Converting the data to unicode, if needed.
$data = DAV\StringUtil::ensureUTF8($data);
try {
VObject\Property::$classMap['REV'] = 'Sabre\\VObject\\Property\\DateTime';
$vobj = VObject\Reader::read($data, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
if ($vobj->name == 'VCARD')
$this->parsed_vcard = $vobj;
}
catch (VObject\ParseException $e) {
throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid vcard data. Parse error: ' . $e->getMessage());
}
if ($vobj->name !== 'VCARD') {
throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.');
}
if (!isset($vobj->UID)) {
throw new DAV\Exception\BadRequest('Every vcard must have a UID.');
}
}
+ /**
+ * This function handles the addressbook-query REPORT
+ *
+ * This report is used by the client to filter an addressbook based on a
+ * complex query.
+ *
+ * @param \DOMNode $dom
+ * @return void
+ */
+ protected function addressbookQueryReport($dom)
+ {
+ $node = $this->server->tree->getNodeForPath(($uri = $this->server->getRequestUri()));
+ console(__METHOD__, $uri);
+
+ // fix some stupid mistakes in queries sent by the SOGo connector
+ $xpath = new \DOMXPath($dom);
+ $xpath->registerNameSpace('card', Plugin::NS_CARDDAV);
+
+ $filters = $xpath->query('/card:addressbook-query/card:filter');
+ if ($filters->length === 1) {
+ $filter = $filters->item(0);
+ $propFilters = $xpath->query('card:prop-filter', $filter);
+ for ($ii=0; $ii < $propFilters->length; $ii++) {
+ $propFilter = $propFilters->item($ii);
+ $name = $propFilter->getAttribute('name');
+
+ // attribute 'mail' => EMAIL
+ if ($name == 'mail') {
+ $propFilter->setAttribute('name', 'EMAIL');
+ }
+
+ $textMatches = $xpath->query('card:text-match', $propFilter);
+ for ($jj=0; $jj < $textMatches->length; $jj++) {
+ $textMatch = $textMatches->item($jj);
+ $collation = $textMatch->getAttribute('collation');
+
+ // 'i;unicasemap' is a non-standard collation
+ if ($collation == 'i;unicasemap') {
+ $textMatch->setAttribute('collation', 'i;unicode-casemap');
+ }
+ }
+ }
+ }
+
+ // query on LDAP node: pass along filter query
+ if ($node instanceof LDAPDirectory) {
+ $query = new CardDAV\AddressBookQueryParser($dom);
+ $query->parse();
+
+ // set query and ...
+ $node->setAddressbookQuery($query);
+ }
+
+ // ... proceed with default action
+ parent::addressbookQueryReport($dom);
+ }
}
\ No newline at end of file
diff --git a/lib/Kolab/CardDAV/UserAddressBooks.php b/lib/Kolab/CardDAV/UserAddressBooks.php
index 02707f0..db71bbe 100644
--- a/lib/Kolab/CardDAV/UserAddressBooks.php
+++ b/lib/Kolab/CardDAV/UserAddressBooks.php
@@ -1,67 +1,94 @@
<?php
/**
* SabreDAV UserAddressBooks derived class for the Kolab.
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\CardDAV;
+use \rcube;
use Sabre\DAV;
use Sabre\DAVACL;
/**
* UserAddressBooks class
*
* The UserAddressBooks collection contains a list of addressbooks associated with a user
*/
class UserAddressBooks extends \Sabre\CardDAV\UserAddressBooks implements DAV\IExtendedCollection, DAVACL\IACL
{
+ // pseudo-singleton instance
+ private $ldap_directory;
+
/**
* Returns a list of addressbooks
*
* @return array
*/
public function getChildren()
{
$addressbooks = $this->carddavBackend->getAddressbooksForUser($this->principalUri);
$objs = array();
foreach($addressbooks as $addressbook) {
$objs[] = new AddressBook($this->carddavBackend, $addressbook);
}
+
+ if (rcube::get_instance()->config->get('global_ldap_directory')) {
+ $objs[] = $this->getLDAPDirectory();
+ }
+
return $objs;
}
/**
* Returns a single addressbook, by name
*
* @param string $name
* @return \AddressBook
*/
public function getChild($name)
{
+ if ($name == LDAPDirectory::DIRECTORY_NAME) {
+ return $this->getLDAPDirectory();
+ }
+
if ($addressbook = $this->carddavBackend->getAddressBookByName($name)) {
$addressbook['principaluri'] = $this->principalUri;
return new AddressBook($this->carddavBackend, $addressbook);
}
throw new DAV\Exception\NotFound('Addressbook with name \'' . $name . '\' could not be found');
}
+ /**
+ * Getter for the singleton instance of the LDAP directory
+ */
+ private function getLDAPDirectory()
+ {
+ if (!$this->ldap_directory) {
+ $rcube = rcube::get_instance();
+ $config = $rcube->config->get('global_ldap_directory');
+ $config['debug'] = $rcube->config->get('ldap_debug');
+ $this->ldap_directory = new LDAPDirectory($config, $this->principalUri, $this->carddavBackend);
+ }
+
+ return $this->ldap_directory;
+ }
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 1, 8:42 AM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
10075200
Default Alt Text
(60 KB)

Event Timeline