Page MenuHomePhorge

No OneTemporary

diff --git a/config/dav.inc.php.sample b/config/dav.inc.php.sample
index cd7354e..257c1ec 100644
--- a/config/dav.inc.php.sample
+++ b/config/dav.inc.php.sample
@@ -1,121 +1,160 @@
<?php
/*
+-------------------------------------------------------------------------+
| Configuration for the Kolab DAV server |
| |
| Copyright (C) 2013, Kolab Systems AG |
| |
| 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/>. |
| |
+-------------------------------------------------------------------------+
*/
$config = array();
// The HTTP path to the iRony root directory.
// Set to / if the service is registered as document root for a virtual host
$config['base_uri'] = '/iRony/';
// User agent string written to kolab storage MIME messages
$config['useragent'] = 'Kolab DAV Server libkolab/' . RCUBE_VERSION;
// Type of Auth cache. Supported values: 'db', 'apc' and 'memcache'.
// Note: This is only for username canonification map.
$config['kolabdav_auth_cache'] = 'db';
// lifetime of the Auth cache, possible units: s, m, h, d, w
$config['kolabdav_auth_cache_ttl'] = '1h';
// enable debug console showing the internal function calls triggered
// by http requests. This will write log to /var/log/iRony/console
$config['kolabdav_console'] = false;
// enable logging of full HTTP payload
// (bitmask of these values: 2 = HTTP Requests, 4 = HTTP Responses)
$config['kolabdav_http_log'] = 0;
// expose iTip invitations from email inbox in CalDAV scheduling inbox.
// this will make capable CalDAV clients process event invitations and
// as a result, the invitation messages are removed from the email inbox.
// WARNING: this feature is still experimental and not fully implemented.
// See https://git.kolab.org/T93 for details and implementation status.
$config['kolabdav_caldav_inbox'] = false;
// Enables the CardDAV Directory Gateway Extension by exposing an
// LDAP-based address book in the pricipals address book collection.
// Properties of this option are the same as for $config['ldap_public'] entries.
// NOTE: Mapping of (additional) 'uid' and 'changed' fields is required!
/*
$config['kolabdav_ldap_directory'] = array(
'name' => 'Global Address Book',
'hosts' => 'localhost',
'port' => 389,
'use_tls' => false,
// If true the base_dn, bind_dn and bind_pass default to the user's credentials.
'user_specific' => false,
// It's possible to bind with the current user's credentials for individual address books.
// The login name is used to search for the DN to bind with
'search_base_dn' => 'ou=People,dc=example,dc=org',
'search_bind_dn' => 'uid=kolab-service,ou=Special Users,dc=example,dc=org',
'search_bind_pw' => 'Welcome2KolabSystems',
'search_filter' => '(&(objectClass=inetOrgPerson)(mail=%fu))',
// When 'user_specific' is enabled following variables can be used in base_dn/bind_dn config:
// %fu - The full username provided, assumes the username is an email
// address, uses the username_domain value if not an email address.
// %u - The username prior to the '@'.
// %d - The domain name after the '@'.
// %dc - The domain name hierarchal string e.g. "dc=test,dc=domain,dc=com"
// %dn - DN found by ldap search when search_filter/search_base_dn are used
'base_dn' => 'ou=People,dc=example,dc=org',
'bind_dn' => 'uid=kolab-service,ou=Special Users,dc=example,dc=org',
'bind_pass' => 'Welcome2KolabSystems',
'ldap_version' => 3,
'filter' => '(objectClass=inetOrgPerson)',
'search_fields' => array('displayname', 'mail'),
'sort' => array('displayname', 'sn', 'givenname', 'cn'),
'scope' => 'sub',
'searchonly' => true, // Set to false to enable listing
'sizelimit' => '1000',
'timelimit' => '0',
'fieldmap' => array(
// Roundcube => LDAP
'name' => 'displayName',
'surname' => 'sn',
'firstname' => 'givenName',
'middlename' => 'initials',
'prefix' => 'title',
'email:work' => 'mail',
'email:other' => 'alias',
'phone:main' => 'telephoneNumber',
'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
'uid' => 'nsuniqueid',
'changed' => 'modifytimestamp',
),
);
+
+// Expose all resources as an LDAP-based address book in the pricipals address book collection.
+// This enables Non-Kolab-Clients to add resources to an event.
+// Properties of this option are the same as for $config['kolabdav_ldap_directory'] entries.
+$config['kolabdav_ldap_resources'] = array(
+ 'name' => 'Global Resources',
+ 'hosts' => 'localhost',
+ 'port' => 389,
+ 'use_tls' => false,
+ 'user_specific' => false,
+ 'search_base_dn' => 'ou=People,dc=example,dc=org',
+ 'search_bind_dn' => 'uid=kolab-service,ou=Special Users,dc=example,dc=org',
+ 'search_bind_pw' => 'Welcome2KolabSystems',
+ 'search_filter' => '(&(objectClass=inetOrgPerson)(mail=%fu))',
+ 'base_dn' => 'ou=Resources,dc=example,dc=org',
+ 'bind_dn' => 'uid=kolab-service,ou=Special Users,dc=example,dc=org',
+ 'bind_pass' => 'Welcome2KolabSystems',
+ 'ldap_version' => 3,
+ 'filter' => '(|(objectclass=groupofuniquenames)(objectclass=groupofurls)(objectclass=kolabsharedfolder))',
+ 'search_fields' => array('displayname', 'mail'),
+ 'sort' => array('displayname', 'sn', 'givenname', 'cn'),
+ 'scope' => 'sub',
+ 'searchonly' => false, // Set to false to enable listing
+ 'sizelimit' => '1000',
+ 'timelimit' => '0',
+ 'fieldmap' => array(
+ // Internal => LDAP
+ 'name' => 'cn',
+ 'email' => 'mail',
+ 'owner' => 'owner',
+ 'description' => 'description',
+ 'attributes' => 'kolabdescattribute',
+ 'members' => 'uniquemember',
+ // these mappings are required for owner display
+ 'phone' => 'telephoneNumber',
+ 'mobile' => 'mobile',
+ ),
+);
+
*/
// Enable caching for LDAP directory data.
// This is recommended with 'searchonly' => false to speed-up sychronization of multiple clients
// $config['kolabdav_ldap_cache'] = 'memcache';
// $config['kolabdav_ldap_cache_ttl'] = 600; // in seconds
diff --git a/lib/Kolab/CardDAV/ContactsBackend.php b/lib/Kolab/CardDAV/ContactsBackend.php
index 4784fa4..0961852 100644
--- a/lib/Kolab/CardDAV/ContactsBackend.php
+++ b/lib/Kolab/CardDAV/ContactsBackend.php
@@ -1,1179 +1,1186 @@
<?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_mime;
use \rcube_charset;
use \kolab_storage;
use Sabre\DAV;
use Sabre\CardDAV;
use Sabre\VObject;
use Sabre\VObject\DateTimeParser;
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
{
public static $supported_address_data = array(
array('contentType' => 'text/vcard', 'version' => '3.0'),
array('contentType' => 'text/vcard', 'version' => '4.0'),
);
public $ldap_directory;
+ public $ldap_resources;
private $sources;
private $folders;
private $aliases;
private $useragent;
private $subscribed = null;
// mapping of labelled X-AB properties to known vcard fields
private $xab_labelled_map = array(
'X-ABDATE' => array(
'anniversary' => 'X-ANNIVERSARY',
),
'X-ABRELATEDNAMES' => array(
'child' => 'X-CHILDREN',
'spouse' => 'X-SPOUSE',
'manager' => 'X-MANAGER',
'assistant' => 'X-ASSISTANT',
),
);
// known labels need to be quoted specially with _$!< >!$_
private $xab_known_labels = array('anniversary','child','parent','mother','father','brother','sister','friend','spouse','manager','assistant','partner','other');
// mapping of related types to internal contact properties
private $related_map = array(
'child' => 'children',
'spouse' => 'spouse',
'manager' => 'manager',
'assistant' => 'assistant',
);
private $phonetypes = array(
'main' => 'voice',
'homefax' => 'home,fax',
'workfax' => 'work,fax',
'mobile' => 'cell',
'other' => 'textphone',
);
private $improtocols = array(
'jabber' => 'xmpp',
);
/**
* 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 = $folder->get_uid();
$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:carddav}supported-address-data' => new CardDAV\Property\SupportedAddressData(self::$supported_address_data),
);
$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]) {
DAVBackend::check_storage_folder($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);
// Reset imap cache so we work with up-to-date folders list
// We do this only when a client requests list of address books,
// and we assume clients do not ask for list too often (Bifrost#T175679)
rcube::get_instance()->get_storage()->clear_cache('mailboxes', true);
$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:carddav}supported-address-data' => new CardDAV\Property\SupportedAddressData(self::$supported_address_data),
);
}
// 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 properties for an address book.
*
* The list of mutations is stored in a Sabre\DAV\PropPatch object.
* To do the actual updates, you must tell this object which properties
* you're going to process with the handle() method.
*
* Calling the handle method is like telling the PropPatch object "I
* promise I can handle updating this property".
*
* Read the PropPatch documenation for more info and examples.
*
* @param string $addressBookId
* @param \Sabre\DAV\PropPatch $propPatch
* @return void
*/
public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch)
{
console(__METHOD__, $addressBookId, $propPatch);
if ($addressBookId == '__all__')
return false;
if ($folder = $this->get_storage_folder($addressBookId)) {
DAVBackend::handle_proppatch($folder, $propPatch);
}
}
/**
* 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, true) as $contact) {
$cards[] = array(
'id' => $contact['uid'],
'uri' => VObjectUtils::uid2uri($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 = VObjectUtils::uri2uid($cardUri, '.vcf');
// search all folders for the given card
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);
}
}
+ // read card data from LDAP resources
+ else if ($addressBookId == LDAPResources::DIRECTORY_NAME) {
+ if (is_object($this->ldap_resources)) {
+ $contact = $this->ldap_resources->getContactObject($uid);
+ }
+ }
else {
$storage = $this->get_storage_folder($addressBookId);
$contact = $storage->get_object($uid, '*');
}
if ($contact) {
return array(
'id' => $contact['uid'],
'uri' => VObjectUtils::uid2uri($contact['uid'], '.vcf'),
'lastmodified' => is_a($contact['changed'], 'DateTime') ? $contact['changed']->format('U') : null,
'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 = VObjectUtils::uri2uid($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 = VObjectUtils::uid2uri($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 = VObjectUtils::uri2uid($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 = VObjectUtils::uri2uid($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)',
'vcard4' => '[Vv][Cc]ard([/ ])?4',
);
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';
}
/**
* Helper method to determine whether the connected client supports VCard4
*/
private function is_vcard4()
{
return Plugin::$vcard_version == 'vcard4' || $this->useragent == 'vcard4';
}
/********** Data conversion utilities ***********/
/**
* Parse the given VCard string into a hash array kolab_format_contact can handle
*
* @param string VCard data block
* @param string Contact UID
*
* @return array Hash array with contact properties or null on failure
*/
public function parse_vcard($cardData, $uid = null)
{
try {
// use already parsed object
if (Plugin::$parsed_vcard && Plugin::$parsed_vcard->UID == $uid) {
$vobject = Plugin::$parsed_vcard;
}
else {
$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
*/
public function to_vcard($contact)
{
$v4 = $this->is_vcard4();
$v4_prefix = $v4 ? '' : 'X-';
$vc = new VObject\Component\VCard();
$vc->VERSION = '3.0'; // always set to 3.0 and let Sabre/DAV convert to 4.0 if necessary
$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 = 'mailto:' . urlencode(sprintf('"%s" <%s>', addcslashes($member['name'], '"'), $member['email']));
else if ($member['email'])
$value = 'mailto:' . $member['email'];
$vc->add($prop_prefix . 'MEMBER', $value);
}
}
else if ($contact['surname'] . $contact['firstname'] . $contact['middlename'] . $contact['prefix'] . $contact['suffix'] != '') {
$n = $vc->create('N');
$n->setParts(array(strval($contact['surname']), strval($contact['firstname']), strval($contact['middlename']), strval($contact['prefix']), strval($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('ROLE', $contact['profession']);
if (!empty($contact['organization']) || !empty($contact['department'])) {
$org = $vc->create('ORG');
$org->setParts(array($contact['organization'], $contact['department']));
$vc->add($org);
}
// save as RELATED for VCard 4.0
if ($v4) {
foreach ($this->related_map as $type => $field) {
if (!empty($contact[$field])) {
foreach ((array)$contact[$field] as $value) {
$vc->add($vc->create('RELATED', $value, array('type' => $type)));
}
}
}
if (is_array($contact['related'])) {
foreach ($contact['related'] as $value) {
$vc->add('RELATED', $value);
}
}
}
else {
foreach (array_values($this->related_map) as $field) {
if (!empty($contact[$field])) {
$vc->add(strtoupper('X-' . $field), join(',', (array)$contact[$field]));
}
}
}
foreach ((array)$contact['email'] as $email) {
$types = array('INTERNET');
if (!empty($email['type']))
$types = array_merge($types, explode(',', strtoupper($email['type'])));
$vc->add('EMAIL', $email['address'], array('type' => $types));
}
foreach ((array)$contact['phone'] as $phone) {
$type = $this->phonetypes[$phone['type']] ?: $phone['type'];
$params = !empty($type) ? array('type' => explode(',', strtoupper($type))) : array();
$vc->add('TEL', $phone['number'], $params);
}
foreach ((array)$contact['website'] as $website) {
$params = !empty($website['type']) ? array('type' => explode(',', strtoupper($website['type']))) : array();
$vc->add('URL', $website['url'], $params);
}
$improtocolmap = array_flip($this->improtocols);
foreach ((array)$contact['im'] as $im) {
list($prot, $val) = explode(':', $im, 2);
if ($val && !$v4) $vc->add('x-' . ($improtocolmap[$prot] ?: $prot), $val);
else $vc->add('IMPP', $im);
}
foreach ((array)$contact['address'] as $adr) {
$params = !empty($adr['type']) ? array('type' => strtoupper($adr['type'])) : array();
$vadr = $vc->create('ADR', null, $params);
$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($this->is_apple() ? 'SEX' : $v4_prefix . 'GENDER', $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($vc, 'BDAY', $contact['birthday'], false));
}
if (!empty($contact['anniversary']) && $contact['anniversary'] instanceof \DateTime) {
$contact['anniversary']->_dateonly = true;
$vc->add(VObjectUtils::datetime_prop($vc, $v4_prefix . 'ANNIVERSARY', $contact['anniversary'], false));
}
if (!empty($contact['categories'])) {
$cat = $vc->create('CATEGORIES');
$cat->setParts((array)$contact['categories']);
$vc->add($cat);
}
if (!empty($contact['freebusyurl'])) {
$vc->add('FBURL', $contact['freebusyurl']);
}
if (is_array($contact['lang'])) {
foreach ($contact['lang'] as $value) {
$vc->add('LANG', $value);
}
}
if (!empty($contact['photo'])) {
$vc->PHOTO = $contact['photo'];
$vc->PHOTO['ENCODING'] = 'b';
$vc->PHOTO['TYPE'] = strtoupper(substr(rcube_mime::image_content_type($contact['photo']), 6));
}
// add custom properties
foreach ((array)$contact['x-custom'] as $prop) {
$vc->add($prop[0], $prop[1]);
}
// send some known fields as itemN.X-AB* for Apple clients
if ($this->is_apple()) {
$this->_to_apple($contact, $vc);
}
if (!empty($contact['changed']) && is_a($contact['changed'], 'DateTime')) {
$vc->REV = $contact['changed']->format('Ymd\\THis\\Z');
}
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'] = DateTimeParser::parseDateTime(strval($vc->REV));
}
catch (\Exception $e) {
// ignore
}
}
// normalize apple-style properties
$this->_from_apple($vc);
$phonetypemap = array_flip($this->phonetypes);
$phonetypemap['fax,home'] = 'homefax';
$phonetypemap['fax,work'] = 'workfax';
// map attributes to internal fields
foreach ($vc->children as $prop) {
if (!($prop instanceof VObject\Property))
continue;
$value = strval($prop);
switch ($prop->name) {
case 'N':
list($contact['surname'], $contact['firstname'], $contact['middlename'], $contact['prefix'], $contact['suffix']) = $prop->getParts();
break;
case 'NOTE':
$contact['notes'] = $value;
break;
case 'TITLE':
$contact['jobtitle'] = $value;
break;
case 'NICKNAME':
$contact[strtolower($prop->name)] = $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::prop_filter($prop->offsetGet('type'), 'internet,pref', true));
$contact['email'][] = array('address' => $value, 'type' => strtolower($types[0] ?: 'other'));
break;
case 'URL':
$types = array_values(self::prop_filter($prop->offsetGet('type'), 'internet,pref', true));
$contact['website'][] = array('url' => $value, 'type' => strtolower($types[0]));
break;
case 'TEL':
$types = array_values(self::prop_filter($prop->offsetGet('type'), 'voice,pref', true));
$types_ = strtolower(join(',', $types));
$type = isset($phonetypemap[$types_]) ? $types_ : strtolower($types[0]);
$contact['phone'][] = array('number' => $value, 'type' => $phonetypemap[$type] ?: $type);
break;
case 'ADR':
$types = array_values(self::prop_filter($prop->offsetGet('type'), 'pref', true));
$adr = array('type' => strtolower(!empty($types) ? strval($types[0]) : $prop->parameters[0]->name));
list(,, $adr['street'], $adr['locality'], $adr['region'], $adr['code'], $adr['country']) = $prop->getParts();
$contact['address'][] = $adr;
break;
case 'BDAY':
case 'ANNIVERSARY':
case 'X-ANNIVERSARY':
// We use getDateTime() method to support partial date format from vCard 4
// and to not throw exceptions on invalid input (T2492)
if (method_exists($prop, 'getDateTime')) {
$key = $prop->name == 'BDAY' ? 'birthday' : 'anniversary';
try {
$contact[$key] = $prop->getDateTime();
$contact[$key]->_dateonly = true;
}
catch (\Exception $e) {
// ignore
}
}
break;
case 'SEX':
case 'GENDER':
case 'X-GENDER':
$contact['gender'] = $value;
break;
case 'ROLE':
case 'X-PROFESSION':
$contact['profession'] = $value;
break;
case 'X-MANAGER':
case 'X-ASSISTANT':
case 'X-CHILDREN':
case 'X-SPOUSE':
case 'X-MS-MANAGER':
case 'X-MS-ASSISTANT':
case 'X-MS-CHILDREN':
case 'X-MS-SPOUSE':
$contact[strtolower(substr($prop->name, 2))] = explode(',', $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', '', $value);
break;
case 'IMPP':
$prot = null;
if (preg_match('/^[a-z]+:/i', $value))
list($prot, $val) = explode(':', $value, 2);
else
$val = $value;
$type = strtolower((string)$prop->offsetGet('X-SERVICE-TYPE'));
$protocol = $type && (!$prot || $prot == 'aim') ? ($this->improtocols[$type] ?: $type) : $prot;
$contact['im'][] = ($this->improtocols[$protocol] ?: $protocol) . ':' . urldecode($val);
break;
case 'PHOTO':
if ($prop instanceof VObject\Property\Binary && $value) {
$contact['photo'] = $value;
}
else if ($prop instanceof VObject\Property\Uri && preg_match('|^data:image/[a-z]+;base64,|i', $value, $m)) {
$contact['photo'] = base64_decode(substr($value, strlen($m[0])));
}
break;
// VCard 4.0 properties
case 'FBURL':
$contact['freebusyurl'] = $value;
break;
case 'LANG':
$contact['lang'][] = $value;
break;
case 'RELATED':
$type = strtolower($prop->offsetGet('type'));
if ($field = $this->related_map[$type]) {
$contact[$field][] = $value;
}
else {
$contact['related'][] = $value;
}
break;
case 'KIND':
case 'X-ADDRESSBOOKSERVER-KIND':
$value_ = strtolower($value);
if ($value_ == 'group') {
$contact['_type'] = 'distribution-list';
}
else if ($value_ == 'org') {
// store vcard 4 KIND as custom property
$contact['x-custom'][] = array('X-ABSHOWAS', 'COMPANY');
}
break;
case 'MEMBER':
case 'X-ADDRESSBOOKSERVER-MEMBER':
if (strpos($value, 'urn:uuid:') === 0) {
$contact['member'][] = array('uid' => substr($value, 9));
}
else if (strpos($value, 'mailto:') === 0) {
$member = reset(\rcube_mime::decode_address_list(urldecode(substr($value, 7))));
if ($member['mailto'])
$contact['member'][] = array('email' => $member['mailto'], 'name' => $member['name']);
}
break;
// custom properties
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($value));
}
break;
}
}
if (is_array($contact['im']))
$contact['im'] = array_unique($contact['im']);
return $contact;
}
/**
* Convert Apple-style item1.X-AB* properties to flat X-AB*-Label values
*/
private function _from_apple($vc)
{
foreach ($this->xab_labelled_map as $propname => $known_map) {
foreach ($vc->select($propname) as $prop) {
$labelkey = $prop->group ? $prop->group . '.X-ABLABEL' : 'X-ABLABEL';
$labels = $vc->select($labelkey);
$field = !empty($labels) && ($label = reset($labels)) ? strtolower(trim(strval($label), '_$!<>')) : null;
if ($field) {
$prop->group = null;
$prop->name = ($known_map[$field] ?: $propname . '-' . strtoupper($field));
unset($vc->{$labelkey});
}
}
// must be an apple client :-)
$this->useragent = 'macosx';
}
}
/**
* Translate custom fields back to Apple-style item1.X-AB* properties
*/
private function _to_apple($contact, $vc)
{
$this->item_count = 1;
foreach ($this->xab_labelled_map as $propname => $known_map) {
// convert known vcard properties into labelled ones
foreach (array_flip($known_map) as $name => $label) {
if ($vc->{$name}) {
$this->_replace_with_labelled_prop($vc, $name, $propname, $label);
}
}
// translate custom properties with a matching prefix to labelled items
foreach ((array)$contact['x-custom'] as $prop) {
$name = $prop[0];
if ($vc->{$name} && strpos($name, $propname) === 0) {
$label = strtolower(substr($name, strlen($propname)+1));
$this->_replace_with_labelled_prop($vc, $name, $propname, $label);
}
}
}
}
/**
* Helper method to replace a named property with a labelled one
*/
private function _replace_with_labelled_prop($vc, $name, $propname, $label)
{
$group = 'item' . ($this->item_count++);
$prop = clone $vc->{$name};
$prop->name = $propname;
$prop->group = $group;
$vc->add($prop);
$ablabel = $vc->create('X-ABLabel');
$ablabel->group = $group;
$ablabel->setValue(in_array($label, $this->xab_known_labels) ? '_$!<'.ucfirst($label).'>!$_' : ucfirst($label));
$vc->add($ablabel);
unset($vc->{$name});
}
/**
* 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 prop_filter($arr, $values, $inverse = false)
{
if (!is_array($values)) {
$values = explode(',', $values);
}
// explode single, comma-separated value
if (count($arr) == 1 && strpos($arr[0], ',')) {
$arr = explode(',', $arr[0]);
}
$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
index 7968ab9..7135b42 100644
--- a/lib/Kolab/CardDAV/LDAPDirectory.php
+++ b/lib/Kolab/CardDAV/LDAPDirectory.php
@@ -1,577 +1,577 @@
<?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\Property;
use Kolab\Utils\VObjectUtils;
/**
* 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 $cache;
- private $query;
- private $filter;
+ protected $config;
+ protected $ldap;
+ protected $carddavBackend;
+ protected $principalUri;
+ protected $addressBookInfo = array();
+ protected $cache;
+ protected $query;
+ protected $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:carddav}supported-address-data' => new Property\SupportedAddressData(),
'principaluri' => $principalUri,
);
// 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()
{
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 = VObjectUtils::uri2uid($cardUri, '.vcf');
$record = null;
// 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' => VObjectUtils::uid2uri($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()) {
$ldap->reset();
// used cached uid mapping
$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);
if ($result->count) {
$contact = $result[0];
}
}
if ($contact) {
$this->_normalize_contact($contact);
}
}
return $contact;
}
/**
* Returns an array with all the child nodes
*
* @return DAV\INode[]
*/
function getChildren()
{
console(__METHOD__, $this->query, $this->filter);
$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' => VObjectUtils::uid2uri($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
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);
$directory_index = array();
// convert results into vcard blocks
foreach ($results as $contact) {
$this->_normalize_contact($contact);
$obj = array(
'id' => $contact['uid'],
'uri' => VObjectUtils::uid2uri($contact['uid'], '.vcf'),
'lastmodified' => $contact['_timestamp'],
'carddata' => $this->carddavBackend->to_vcard($contact),
'etag' => self::_get_etag($contact),
);
// 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;
}
/**
* 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,
),
);
return $acl;
}
/**
* 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 PropPatch $propPatch
* @return void
*/
public function propPatch(DAV\PropPatch $propPatch)
{
console(__METHOD__, $propPatch);
// NOP
}
/**
* Post-process the given contact record from rcube_ldap
*/
private function _normalize_contact(&$contact)
{
if (is_numeric($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'] = intval($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 ((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;
}
/**
* 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 backend not available, abort
if (!($ldap = $this->connect())) {
return $attribs;
}
$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 7253e9e..f4cbb18 100644
--- a/lib/Kolab/CardDAV/Plugin.php
+++ b/lib/Kolab/CardDAV/Plugin.php
@@ -1,255 +1,260 @@
<?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;
use Kolab\DAV\Auth\HTTPBasic;
/**
* 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;
// vcard version requested by the connecting client
public static $vcard_version = 'vcard3';
/**
* Initializes the plugin
*
* @param DAV\Server $server
* @return void
*/
public function initialize(DAV\Server $server)
{
parent::initialize($server);
$server->on('beforeMethod', array($this, 'beforeMethod'), 0);
$server->on('afterCreateFile', array($this, 'afterWriteContent'));
$server->on('afterWriteContent', array($this, 'afterWriteContent'));
}
/**
* Adds all CardDAV-specific properties
*
* @param DAV\PropFind $propFind
* @param DAV\INode $node
* @return void
*/
public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node)
{
- // publish global ldap address book for this principal
- if ($node instanceof DAVACL\IPrincipal && empty($this->directories) && \rcube::get_instance()->config->get('kolabdav_ldap_directory')) {
- $this->directories[] = self::ADDRESSBOOK_ROOT . '/' . $node->getName() . '/' . LDAPDirectory::DIRECTORY_NAME;
+ // publish global ldap address book and resources list for this principal
+ if ($node instanceof DAVACL\IPrincipal && empty($this->directories)) {
+ if (\rcube::get_instance()->config->get('kolabdav_ldap_directory')) {
+ $this->directories[] = self::ADDRESSBOOK_ROOT . '/' . $node->getName() . '/' . LDAPDirectory::DIRECTORY_NAME;
+ }
+ if (\rcube::get_instance()->config->get('kolabdav_ldap_resources')) {
+ $this->directories[] = self::ADDRESSBOOK_ROOT . '/' . $node->getName() . '/' . LDAPResources::DIRECTORY_NAME;
+ }
}
$propFind->handle('{' . self::NS_CARDDAV . '}addressbook-home-set', function() {
return new DAV\Property\Href($this->getAddressBookHomeForPrincipal(HTTPBasic::$current_user) . '/');
});
parent::propFindEarly($propFind, $node);
}
/**
* Handler for beforeMethod events
*/
public function beforeMethod($request, $response)
{
$method = $request->getMethod();
if ($method == 'PUT' && $request->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($request->getHeader('User-Agent'), 'Thunderbird/') > 0) {
unset($_SERVER['HTTP_IF_NONE_MATCH']);
}
}
else if ($method == 'GET' && ($accept = $request->getHeader('Accept'))) {
// determine requested vcard version from Accept: header
self::$vcard_version = parent::negotiateVCard($accept);
}
}
/**
* 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
* @param bool $modified Should be set to true, if this event handler
* changed &$data.
* @return void
*/
protected function validateVCard(&$data, &$modified)
{
// If it's a stream, we convert it to a string first.
if (is_resource($data)) {
$data = stream_get_contents($data);
}
$before = md5($data);
// Converting the data to unicode, if needed.
$data = DAV\StringUtil::ensureUTF8($data);
if (md5($data) !== $before) $modified = true;
try {
$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.');
}
}
/**
* Wrapper for Plugin::negotiateVCard() to store the requested vcard version for the backend
*/
protected function negotiateVCard($input, &$mimeType = null)
{
self::$vcard_version = parent::negotiateVCard($input, $mimeType);
return self::$vcard_version;
}
/**
* Converts a vcard blob to a different version, or jcard.
*
* (optimized version that skips parsing and re-serialization if possible)
*
* @param string $data
* @param string $target
* @return string
*/
protected function convertVCard($data, $target)
{
$version = 'vcard3';
if (is_string($data) && preg_match('/VERSION:(\d)/', $data, $m)) {
$version = 'vcard' . $m[1];
}
// no conversion needed
if ($target == $version) {
return $data;
}
return parent::convertVCard($data, $target);
}
/**
* 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 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);
$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) {
+ if ($node instanceof LDAPDirectory || $node instanceof LDAPResources) {
$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 56f76f0..076a683 100644
--- a/lib/Kolab/CardDAV/UserAddressBooks.php
+++ b/lib/Kolab/CardDAV/UserAddressBooks.php
@@ -1,131 +1,154 @@
<?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;
use Sabre\CardDAV;
/**
* UserAddressBooks class
*
* The UserAddressBooks collection contains a list of addressbooks associated with a user
*/
class UserAddressBooks extends \Sabre\CardDAV\UserAddressBooks implements DAV\IExtendedCollection, DAV\IProperties, DAVACL\IACL
{
// pseudo-singleton instance
private $ldap_directory;
+ private $ldap_resources;
/**
* 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('kolabdav_ldap_directory')) {
$objs[] = $this->getLDAPDirectory();
}
+ if (rcube::get_instance()->config->get('kolabdav_ldap_resources')) {
+ $objs[] = $this->getLDAPResources();
+ }
+
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 ($name == LDAPResources::DIRECTORY_NAME) {
+ return $this->getLDAPResources();
+ }
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('kolabdav_ldap_directory');
$config['debug'] = $rcube->config->get('ldap_debug');
$this->ldap_directory = new LDAPDirectory($config, $this->principalUri, $this->carddavBackend);
}
return $this->ldap_directory;
}
+ /**
+ * Getter for the singleton instance of the LDAP resources
+ */
+ private function getLDAPResources()
+ {
+ if (!$this->ldap_resources) {
+ $rcube = rcube::get_instance();
+ $config = $rcube->config->get('kolabdav_ldap_resources');
+ $config['debug'] = $rcube->config->get('ldap_debug');
+ $this->ldap_resources = new LDAPResources($config, $this->principalUri, $this->carddavBackend);
+ }
+
+ return $this->ldap_resources;
+ }
+
/**
* Returns the list of properties
*
* @param array $requestedProperties
* @return array
*/
public function getProperties($requestedProperties)
{
// console(__METHOD__, $requestedProperties);
$response = array();
foreach ($requestedProperties as $prop) {
switch($prop) {
case '{urn:ietf:params:xml:ns:carddav}supported-address-data':
$response[$prop] = new CardDAV\Property\SupportedAddressData(ContactsBackend::$supported_address_data);
break;
}
}
return $response;
}
/**
* Updates properties on this node.
*
* @param PropPatch $propPatch
* @return void
*/
public function propPatch(DAV\PropPatch $propPatch)
{
console(__METHOD__, $propPatch);
// NOP
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 22, 4:12 AM (17 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
80055
Default Alt Text
(84 KB)

Event Timeline