Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F86199
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
84 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Nov 21, 10:21 PM (11 h, 31 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
80055
Default Alt Text
(84 KB)
Attached To
Mode
R5 irony
Attached
Detach File
Event Timeline
Log In to Comment