Page MenuHomePhorge

No OneTemporary

Size
155 KB
Referenced Files
None
Subscribers
None
diff --git a/composer.json b/composer.json
index c95e29d..053ce31 100644
--- a/composer.json
+++ b/composer.json
@@ -1,33 +1,29 @@
{
"name": "kolab/irony",
"description": "iRony - The Kolab WebDAV/CalDAV/CardDAV Server",
"license": "AGPL-3.0",
- "version": "0.3-dev",
+ "version": "0.4-dev",
"repositories": [
{
"type": "pear",
"url": "http://pear.php.net/"
},
- {
- "type": "vcs",
- "url": "https://github.com/thomascube/sabre-vobject.git"
- },
{
"type": "vcs",
"url": "git://git.kolab.org/git/pear/Net_LDAP3"
}
],
"require": {
- "php": ">=5.3.3",
- "sabre/dav" : "1.8.9"
+ "php": ">=5.4.1",
+ "sabre/dav" : "~2.1.0"
},
"require-dev": {
"pear/mail_mime": ">=1.8.9",
"pear/mail_mime-decode": ">=1.5.5",
"pear/http_request2": ">=2.1.1",
"pear-pear.php.net/net_idna2": ">=0.1.1",
"pear-pear.php.net/net_ldap2": ">=2.0.12",
"kolab/Net_LDAP3": "dev-master"
},
"minimum-stability": "dev"
}
diff --git a/lib/Kolab/CalDAV/CalendarBackend.php b/lib/Kolab/CalDAV/CalendarBackend.php
index df4e7d4..ba31729 100644
--- a/lib/Kolab/CalDAV/CalendarBackend.php
+++ b/lib/Kolab/CalDAV/CalendarBackend.php
@@ -1,824 +1,805 @@
<?php
/**
* SabreDAV Calendaring 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\CalDAV;
use \PEAR;
use \rcube;
use \rcube_charset;
use \kolab_storage;
use \kolab_storage_config;
use \libcalendaring;
use Kolab\Utils\DAVBackend;
use Kolab\Utils\VObjectUtils;
use Kolab\DAV\Auth\HTTPBasic;
use Sabre\DAV;
use Sabre\CalDAV;
use Sabre\VObject;
/**
* Kolab Calendaring backend.
*
* Checkout the Sabre\CalDAV\Backend\BackendInterface for all the methods that must be implemented.
*
*/
class CalendarBackend extends CalDAV\Backend\AbstractBackend
{
private $calendars;
private $folders;
private $aliases;
private $useragent;
private $subscribed = null;
/**
* Read available calendar folders from server
*/
private function _read_calendars()
{
// already read sources
if (isset($this->calendars))
return $this->calendars;
// get all folders that have "event" type
$folders = array_merge(kolab_storage::get_folders('event', $this->subscribed), kolab_storage::get_folders('task', $this->subscribed));
$this->calendars = $this->folders = $this->aliases = array();
$order = 1;
foreach (kolab_storage::sort_folders($folders) as $folder) {
$id = $folder->get_uid();
$this->folders[$id] = $folder;
$fdata = $folder->get_imap_data(); // fetch IMAP folder data for CTag generation
$this->calendars[$id] = array(
'id' => $id,
'uri' => $id,
'{DAV:}displayname' => html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET),
'{http://apple.com/ns/ical/}calendar-color' => '#' . $folder->get_color('FF0000') . 'FF',
'{http://calendarserver.org/ns/}getctag' => sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']),
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new CalDAV\Property\SupportedCalendarComponentSet(array(DAVBackend::$caldav_type_component_map[$folder->type])),
'{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp' => new CalDAV\Property\ScheduleCalendarTransp('opaque'),
'{http://apple.com/ns/ical/}calendar-order' => $order++,
);
$this->aliases[$folder->name] = $id;
// these properties are used for sharing supprt (not yet active)
if (false && $folder->get_namespace() != 'personal') {
$rights = $folder->get_myrights();
$this->calendars[$id]['{http://calendarserver.org/ns/}shared-url'] = '/calendars/' . $folder->get_owner() . '/' . $id;
$this->calendars[$id]['{http://calendarserver.org/ns/}owner-principal'] = $folder->get_owner();
$this->calendars[$id]['{http://sabredav.org/ns}read-only'] = strpos($rights, 'i') === false;
}
}
return $this->calendars;
}
/**
* Getter for a kolab_storage_folder representing the calendar for the given ID
*
* @param string Calendar ID
* @return object kolab_storage_folder instance
*/
public function get_storage_folder($id)
{
// resolve alias name
if ($this->aliases[$id]) {
$id = $this->aliases[$id];
}
if ($this->folders[$id]) {
return $this->folders[$id];
}
else {
return DAVBackend::get_storage_folder($id, '');
}
}
/**
* Returns a list of calendars for a principal.
*
* Every calendars is an array with the following keys:
* * id, a unique id that will be used by other functions to modify the
* calendar. This can be the same as the uri or a database key.
* * uri, which the basename of the uri with which the calendar is
* accessed.
* * principaluri. The owner of the calendar. Almost always the same as
* principalUri passed to this method.
*
* Furthermore it can contain webdav properties in clark notation. A very
* common one is '{DAV:}displayname'.
*
* @param string $principalUri
* @return array
*/
public function getCalendarsForUser($principalUri)
{
console(__METHOD__, $principalUri);
$this->_read_calendars();
$calendars = array();
foreach ($this->calendars as $id => $cal) {
$this->calendars[$id]['principaluri'] = $principalUri;
$calendars[] = $this->calendars[$id];
}
return $calendars;
}
/**
* Returns calendar properties for a specific node identified by name/uri
*
* @param string Node name/uri
* @return array Hash array with calendar properties or null if not found
*/
public function getCalendarByName($calendarUri)
{
console(__METHOD__, $calendarUri);
$this->_read_calendars();
$id = $calendarUri;
// resolve aliases (calendar by folder name)
if ($this->aliases[$calendarUri]) {
$id = $this->aliases[$calendarUri];
}
if ($this->calendars[$id] && empty($this->calendars[$id]['principaluri'])) {
$this->calendars[$id]['principaluri'] = 'principals/' . HTTPBasic::$current_user;
}
// retry with subscribed = false (#2701)
if (empty($this->calendars[$id]) && $id != 'outbox' && $this->subscribed === null && rcube::get_instance()->config->get('kolab_use_subscriptions')) {
$this->subscribed = false;
unset($this->calendars);
return $this->getCalendarByName($calendarUri);
}
return $this->calendars[$id];
}
/**
* Creates a new calendar for a principal.
*
* If the creation was a success, an id must be returned that can be used to reference
* this calendar in other methods, such as updateCalendar.
*
* @param string $principalUri
* @param string $calendarUri
* @param array $properties
* @return void
*/
public function createCalendar($principalUri, $calendarUri, array $properties)
{
console(__METHOD__, $calendarUri, $properties);
return DAVBackend::folder_create('event', $properties, $calendarUri);
}
/**
* Updates properties for a calendar.
*
- * The mutations array uses the propertyName in clark-notation as key,
- * and the array value for the property value. In the case a property
- * should be deleted, the property value will be null.
+ * 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.
*
- * This method must be atomic. If one property cannot be changed, the
- * entire operation must fail.
+ * Calling the handle method is like telling the PropPatch object "I
+ * promise I can handle updating this property".
*
- * If the operation was successful, true can be returned.
- * If the operation failed, false can be returned.
+ * Read the PropPatch documenation for more info and examples.
*
- * Deletion of a non-existent property is always successful.
- *
- * Lastly, it is optional to return detailed information about any
- * failures. In this case an array should be returned with the following
- * structure:
- *
- * array(
- * 403 => array(
- * '{DAV:}displayname' => null,
- * ),
- * 424 => array(
- * '{DAV:}owner' => null,
- * )
- * )
- *
- * In this example it was forbidden to update {DAV:}displayname.
- * (403 Forbidden), which in turn also caused {DAV:}owner to fail
- * (424 Failed Dependency) because the request needs to be atomic.
- *
- * @param mixed $calendarId
- * @param array $mutations
- * @return bool|array
+ * @param string $path
+ * @param \Sabre\DAV\PropPatch $propPatch
+ * @return void
*/
- public function updateCalendar($calendarId, array $mutations)
+ public function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch)
{
- console(__METHOD__, $calendarId, $mutations);
+ console(__METHOD__, $calendarId, $propPatch);
- $folder = $this->get_storage_folder($calendarId);
- return DAVBackend::folder_update($folder, $mutations);
+ if ($folder = $this->get_storage_folder($calendarId)) {
+ DAVBackend::handle_propatch($folder, $propPatch);
+ }
}
/**
* Delete a calendar and all it's objects
*
* @param mixed $calendarId
* @return void
*/
public function deleteCalendar($calendarId)
{
console(__METHOD__, $calendarId);
$folder = $this->get_storage_folder($calendarId);
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 calendar objects within a calendar.
*
* Every item contains an array with the following keys:
* * id - unique identifier which will be used for subsequent updates
* * calendardata - The iCalendar-compatible calendar data (optional)
* * uri - a unique key which will be used to construct the uri. This can be any arbitrary string.
* * lastmodified - a timestamp of the last modification time
* * etag - An arbitrary string, surrounded by double-quotes. (e.g.: "abcdef"')
* * calendarid - The calendarid as it was passed to this function.
* * size - The size of the calendar objects, in bytes.
*
* Note that the etag is optional, but it's highly encouraged to return for
* speed reasons.
*
* If neither etag or size are specified, the calendardata will be
* used/fetched to determine these numbers. If both are specified the
* amount of times this is needed is reduced by a great degree.
*
* @param mixed $calendarId
* @return array
*/
public function getCalendarObjects($calendarId)
{
console(__METHOD__, $calendarId);
$events = array();
$query = $this->_event_filter_query();
$storage = $this->get_storage_folder($calendarId);
if ($storage) {
foreach ($storage->select($query) as $event) {
// post-filter events to suppress declined invitations
if (!$this->_event_filter_compare($event)) {
continue;
}
// get tags/categories from relations
$this->load_tags($event);
$events[] = array(
'id' => $event['uid'],
'uri' => VObjectUtils::uid2uri($event['uid'], '.ics'),
'lastmodified' => $event['changed'] ? $event['changed']->format('U') : null,
'calendarid' => $calendarId,
'etag' => self::_get_etag($event),
'size' => $event['_size'],
);
}
}
return $events;
}
/**
* Returns information from a single calendar object, based on it's object
* uri.
*
* The returned array must have the same keys as getCalendarObjects. The
* 'calendardata' object is required here though, while it's not required
* for getCalendarObjects.
*
* @param mixed $calendarId
* @param string $objectUri
* @return array
*/
public function getCalendarObject($calendarId, $objectUri)
{
console(__METHOD__, $calendarId, $objectUri);
$uid = VObjectUtils::uri2uid($objectUri, '.ics');
$storage = $this->get_storage_folder($calendarId);
// attachment content is requested
if (preg_match('!^(.+).ics:attachment:(\d+):.+$!', $objectUri, $m)) {
$uid = VObjectUtils::uri2uid($m[1]);
$part = $m[2];
}
if ($storage && ($event = $storage->get_object($uid))) {
// deliver attachment content directly
if ($part && !empty($event['_attachments'])) {
foreach ($event['_attachments'] as $attachment) {
if ($attachment['id'] == $part) {
header('Content-Type: ' . $attachment['mimetype']);
header('Content-Disposition: inline; filename="' . $attachment['name'] . '"');
$storage->get_attachment($uid, $part, null, true);
exit;
}
}
}
// map attributes
$event['attachments'] = $event['_attachments'];
// compose an absolute URI for referencing object attachments
$base_uri = DAVBackend::abs_url(array(
CalDAV\Plugin::CALENDAR_ROOT,
preg_replace('!principals/!', '', $this->calendars[$calendarId]['principaluri']),
$calendarId,
VObjectUtils::uid2uri($event['uid'], '.ics'),
));
// get tags/categories from relations
$this->load_tags($event);
// default response
return array(
'id' => $event['uid'],
'uri' => VObjectUtils::uid2uri($event['uid'], '.ics'),
'lastmodified' => $event['changed'] ? $event['changed']->format('U') : null,
'calendarid' => $calendarId,
'calendardata' => $this->_to_ical($event, $base_uri, $storage),
'etag' => self::_get_etag($event),
);
}
return array();
}
/**
* Creates a new calendar object.
*
* It is possible return an etag from this function, which will be used in
* the response to this PUT request. Note that the ETag must be surrounded
* by double-quotes.
*
* However, you should only really return this ETag if you don't mangle the
* calendar-data. If the result of a subsequent GET to this object is not
* the exact same as this request body, you should omit the ETag.
*
* @param mixed $calendarId
* @param string $objectUri
* @param string $calendarData
* @return string|null
*/
public function createCalendarObject($calendarId, $objectUri, $calendarData)
{
console(__METHOD__, $calendarId, $objectUri, $calendarData);
$uid = VObjectUtils::uri2uid($objectUri, '.ics');
$storage = $this->get_storage_folder($calendarId);
$object = $this->parse_calendar_data($calendarData, $uid);
if (empty($object) || empty($object['uid'])) {
throw new DAV\Exception('Parse error: not a valid iCalendar 2.0 object');
}
// if URI doesn't match the content's UID, the object might already exist!
if ($object['uid'] != $uid && $storage->get_object($object['uid'])) {
$objectUri = VObjectUtils::uid2uri($object['uid'], '.ics');
Plugin::$redirect_basename = $objectUri;
return $this->updateCalendarObject($calendarId, $objectUri, $calendarData);
}
// map attachments attribute
$object['_attachments'] = $object['attachments'];
unset($object['attachments']);
// remove categories from object data (only for tasks yet)
if ($object['_type'] == 'task') {
$tags = (array)$object['categories'];
unset($object['categories']);
}
$success = $storage->save($object, $object['_type']);
if (!$success) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving $object[_type] object to Kolab server"),
true, false);
throw new DAV\Exception('Error saving calendar object to backend');
}
// save tag relations on success (only available for tasks yet)
if ($object['_type'] == 'task') {
$this->save_tags($uid, $tags);
$object['categories'] = $tags; // add again for etag computation
}
// send Location: header if URI doesn't match object's UID (Bug #2109)
if ($object['uid'] != $uid) {
Plugin::$redirect_basename = VObjectUtils::uid2uri($object['uid'], '.ics');
}
// return new Etag
return $success ? self::_get_etag($object) : null;
}
/**
* Updates an existing calendarobject, based on it's uri.
*
* It is possible return an etag from this function, which will be used in
* the response to this PUT request. Note that the ETag must be surrounded
* by double-quotes.
*
* However, you should only really return this ETag if you don't mangle the
* calendar-data. If the result of a subsequent GET to this object is not
* the exact same as this request body, you should omit the ETag.
*
* @param mixed $calendarId
* @param string $objectUri
* @param string $calendarData
* @return string|null
*/
public function updateCalendarObject($calendarId, $objectUri, $calendarData)
{
console(__METHOD__, $calendarId, $objectUri, $calendarData);
$uid = VObjectUtils::uri2uid($objectUri, '.ics');
$storage = $this->get_storage_folder($calendarId);
$object = $this->parse_calendar_data($calendarData, $uid);
if (empty($object)) {
throw new DAV\Exception('Parse error: not a valid iCalendar 2.0 object');
}
// sanity check
if ($object['uid'] != $uid) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error creating calendar object: UID doesn't match object URI"),
true, false);
throw new DAV\Exception\NotFound("UID doesn't match object URI");
}
// copy meta data (starting with _) from old object
$old = $storage->get_object($uid);
foreach ((array)$old as $key => $val) {
if (!isset($object[$key]) && $key[0] == '_')
$object[$key] = $val;
}
// process attachments
if (/* user agent known to handle attachments inline */ !empty($object['attachments'])) {
$object['_attachments'] = $object['attachments'];
unset($object['attachments']);
// mark all existing attachments as deleted (update is always absolute)
foreach ($old['_attachments'] as $key => $attach) {
$object['_attachments'][$key] = false;
}
}
// remove categories from object data (only for tasks yet)
if ($object['_type'] == 'task') {
$tags = (array)$object['categories'];
unset($object['categories']);
}
// 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 event object to Kolab server"),
true, false);
Plugin::$redirect_basename = null;
throw new DAV\Exception('Error saving event object to backend');
}
// save tag relations on success (only available for tasks yet)
if ($object['_type'] == 'task') {
$this->save_tags($uid, $tags);
$object['categories'] = $tags; // add again for etag computation
}
// return new Etag
return self::_get_etag($object);
}
/**
* Deletes an existing calendar object.
*
* @param mixed $calendarId
* @param string $objectUri
* @return void
*/
public function deleteCalendarObject($calendarId, $objectUri)
{
console(__METHOD__, $calendarId, $objectUri);
$uid = VObjectUtils::uri2uid($objectUri, '.ics');
if ($storage = $this->get_storage_folder($calendarId)) {
if ($storage->delete($uid)) {
$this->save_tags($uid, null);
}
}
}
/**
* Performs a calendar-query on the contents of this calendar.
*
* The calendar-query is defined in RFC4791 : CalDAV. Using the
* calendar-query it is possible for a client to request a specific set of
* object, based on contents of iCalendar properties, date-ranges and
* iCalendar component types (VTODO, VEVENT).
*
* This method should just return a list of (relative) urls that match this
* query.
*
* The list of filters are specified as an array. The exact array is
* documented by Sabre\CalDAV\CalendarQueryParser.
*
* Note that it is extremely likely that getCalendarObject for every path
* returned from this method will be called almost immediately after. You
* may want to anticipate this to speed up these requests.
*
* Requests that are extremely common are:
* * requests for just VEVENTS
* * requests for just VTODO
* * requests with a time-range-filter on either VEVENT or VTODO.
*
* ..and combinations of these requests. It may not be worth it to try to
* handle every possible situation and just rely on the (relatively
* easy to use) CalendarQueryValidator to handle the rest.
*
* Note that especially time-range-filters may be difficult to parse. A
* time-range filter specified on a VEVENT must for instance also handle
* recurrence rules correctly.
* A good example of how to interprete all these filters can also simply
* be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
* as possible, so it gives you a good idea on what type of stuff you need
* to think of.
*
* @param mixed $calendarId
* @param array $filters
* @return array
*/
public function calendarQuery($calendarId, array $filters)
{
console(__METHOD__, $calendarId, $filters);
// build kolab storage query from $filters
$query = $this->_event_filter_query();
foreach ((array)$filters['comp-filters'] as $filter) {
if ($filter['name'] != 'VEVENT')
continue;
if (is_array($filter['time-range'])) {
if (!empty($filter['time-range']['end'])) {
$query[] = array('dtstart', '<=', $filter['time-range']['end']);
}
if (!empty($filter['time-range']['start'])) {
$query[] = array('dtend', '>=', $filter['time-range']['start']);
}
}
}
$results = array();
if ($storage = $this->get_storage_folder($calendarId)) {
foreach ($storage->select($query) as $event) {
// post-filter events to suppress declined invitations
if ($this->_event_filter_compare($event)) {
$results[] = $event['uid'] . '.ics';
}
}
}
return $results;
}
/**
* Set User-Agent string of the connected client
*/
public function setUserAgent($uastring)
{
$ua_classes = array(
'ical' => 'iCal/\d',
'outlook' => 'iCal4OL/\d',
'lightning' => 'Lightning/\d',
);
foreach ($ua_classes as $class => $regex) {
if (preg_match("!$regex!", $uastring)) {
$this->useragent = $class;
break;
}
}
}
/********** Data conversion utilities ***********/
/**
* Get object tags
*/
private function load_tags(&$event)
{
// tag relations are only available for tasks yet
if ($event['_type'] != 'task') {
return;
}
$config = kolab_storage_config::get_instance();
$tags = $config->get_tags($event['uid']);
if (!empty($tags)) {
$event['categories'] = array();
}
foreach ($tags as $tag) {
$event['categories'][] = $tag['name'];
// modify changed time if relation is newer
if ($tag['changed'] && !$event['changed'] || $tag['changed'] > $event['changed']) {
$event['changed'] = $tag['changed'];
}
}
}
/**
* Update object tags
*/
private function save_tags($uid, $tags)
{
$config = kolab_storage_config::get_instance();
$config->save_tags($uid, $tags);
}
/**
* Parse the given iCal string into a hash array kolab_format_event can handle
*
* @param string iCal data block
* @return array Hash array with event properties or null on failure
*/
private function parse_calendar_data($calendarData, $uid)
{
try {
$ical = libcalendaring::get_ical();
// use already parsed object
if (Plugin::$parsed_vevent && Plugin::$parsed_vevent->UID == $uid) {
$objects = $ical->import_from_vobject(Plugin::$parsed_vcalendar);
}
else {
$objects = $ical->import($calendarData);
}
// return the first object
if (count($objects)) {
return $objects[0];
}
}
catch (VObject\ParseException $e) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "iCal data parse error: " . $e->getMessage()),
true, false);
}
return null;
}
/**
* Build a valid iCal format block from the given event
*
* @param array Hash array with event/task properties from libkolab
* @param string Absolute URI referenceing this event object
* @param object RECURRENCE-ID property when serializing a recurrence exception
* @return mixed VCALENDAR string containing the VEVENT data
* or VObject\VEvent object with a recurrence exception instance
* @see: \libvcalendar::export()
*/
private function _to_ical($event, $base_uri, $storage, $recurrence_id = null)
{
$ical = libcalendaring::get_ical();
$ical->set_prodid('-//Kolab//iRony DAV Server ' . KOLAB_DAV_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN');
$ical->set_agent($this->useragent == 'ical' ? 'Apple' : '');
// list attachments as absolute URIs for Thunderbird
if ($this->useragent == 'lightning') {
$ical->set_attach_uri($base_uri . ':attachment:{{id}}:{{name}}');
$get_attachment = null;
}
else { // embed attachments for others
$get_attachment = function($id, $event) use ($storage) {
return $storage->get_attachment($event['uid'], $id);
};
}
return $ical->export(array($event), null, false, $get_attachment, false);
}
/**
* Wrapper for libcalendaring::get_user_emails()
*/
private function get_user_emails()
{
$emails = libcalendaring::get_instance()->get_user_emails();
if (empty($emails)) {
$emails = array(HTTPBasic::$current_user);
}
return $emails;
}
/**
* Provide basic query for kolab_storage_folder::select()
*/
private function _event_filter_query()
{
// get email addresses of the current user
$user_emails = $this->get_user_emails();
$query = array();
// add query to exclude declined invitations
foreach ($user_emails as $email) {
$query[] = array('tags', '!=', 'x-partstat:' . $email . ':declined');
// $query[] = array('tags', '!=', 'x-partstat:' . $email . ':needs-action');
}
return $query;
}
/**
* Check the given event if it matches the filter
*
* @return boolean True if matches, false if not
*/
private function _event_filter_compare($event)
{
static $user_emails;
if (!is_array($user_emails)) {
$user_emails = $this->get_user_emails();
}
if (is_array($event['attendees'])) {
foreach ($event['attendees'] as $attendee) {
if (in_array($attendee['email'], $user_emails) && $attendee['status'] == 'DECLINED') {
return false;
}
}
}
return true;
}
/**
* Generate an Etag string from the given event data
*
* @param array Hash array with event properties from libkolab
* @return string Etag string
*/
private static function _get_etag($event)
{
return sprintf('"%s-%d-%s"',
substr(md5($event['uid']), 0, 16),
$event['_msguid'],
!empty($event['categories']) ? substr(md5(join(',', (array)$event['categories'])), 0, 16) : '0'
);
}
}
diff --git a/lib/Kolab/CalDAV/Plugin.php b/lib/Kolab/CalDAV/Plugin.php
index 5f8d7f2..815f37b 100644
--- a/lib/Kolab/CalDAV/Plugin.php
+++ b/lib/Kolab/CalDAV/Plugin.php
@@ -1,239 +1,239 @@
<?php
/**
* Extended CalDAV 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\CalDAV;
use Sabre\DAV;
use Sabre\CalDAV;
use Sabre\VObject;
use Kolab\DAV\Auth\HTTPBasic;
/**
* Extended CalDAV plugin to tweak data validation
*/
class Plugin extends CalDAV\Plugin
{
// make already parsed text/calednar blocks available for later use
public static $parsed_vcalendar;
public static $parsed_vevent;
// allow the backend to force a redirect Location
public static $redirect_basename;
/**
* Initializes the plugin
*
* @param DAV\Server $server
* @return void
*/
public function initialize(DAV\Server $server)
{
parent::initialize($server);
- $server->subscribeEvent('afterCreateFile', array($this, 'afterWriteContent'));
- $server->subscribeEvent('afterWriteContent', array($this, 'afterWriteContent'));
+ $server->on('afterCreateFile', array($this, 'afterWriteContent'));
+ $server->on('afterWriteContent', array($this, 'afterWriteContent'));
}
/**
* 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 string $path
* @return void
*/
protected function validateICalendar(&$data, $path)
{
// If it's a stream, we convert it to a string first.
if (is_resource($data)) {
$data = stream_get_contents($data);
}
// Converting the data to unicode, if needed.
$data = DAV\StringUtil::ensureUTF8($data);
try {
// modification: Set options to be more tolerant when parsing extended or invalid properties
$vobj = VObject\Reader::read($data, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
// keep the parsed object in memory for later processing
if ($vobj->name == 'VCALENDAR') {
self::$parsed_vcalendar = $vobj;
foreach ($vobj->getBaseComponents() as $vevent) {
if ($vevent->name == 'VEVENT' || $vevent->name == 'VTODO') {
self::$parsed_vevent = $vevent;
break;
}
}
}
}
catch (VObject\ParseException $e) {
throw new DAV\Exception\UnsupportedMediaType('This resource requires valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
}
if ($vobj->name !== 'VCALENDAR') {
throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
}
// Get the Supported Components for the target calendar
list($parentPath,$object) = DAV\URLUtil::splitPath($path);
$calendarProperties = $this->server->getProperties($parentPath,array('{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'));
$supportedComponents = $calendarProperties['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']->getValue();
$foundType = null;
$foundUID = null;
foreach($vobj->getComponents() as $component) {
switch($component->name) {
case 'VTIMEZONE':
continue 2;
case 'VEVENT':
case 'VTODO':
case 'VJOURNAL':
if (is_null($foundType)) {
$foundType = $component->name;
if (!in_array($foundType, $supportedComponents)) {
throw new CalDAV\Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
}
if (!isset($component->UID)) {
throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID');
}
$foundUID = (string)$component->UID;
} else {
if ($foundType !== $component->name) {
throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType);
}
if ($foundUID !== (string)$component->UID) {
throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
}
}
break;
default:
throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
}
}
if (!$foundType)
throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
}
/**
* Returns a list of features for the DAV: HTTP header.
* Including 'calendar-schedule' to enable scheduling support in Thunderbird Lightning.
*
* @return array
*/
public function getFeatures()
{
$features = parent::getFeatures();
$features[] = 'calendar-schedule';
return $features;
}
/**
* Returns free-busy information for a specific address. The returned
* data is an array containing the following properties:
*
* calendar-data : A VFREEBUSY VObject
* request-status : an iTip status code.
* href: The principal's email address, as requested
*
* @param string $email address
* @param \DateTime $start
* @param \DateTime $end
* @param VObject\Component $request
* @return array
*/
protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request)
{
console(__METHOD__, $email, $start, $end);
$email = preg_replace('/^mailto:/', '', $email);
// pass-through the pre-generatd free/busy feed from Kolab's free/busy service
if ($fburl = \kolab_storage::get_freebusy_url($email)) {
// use PEAR::HTTP_Request2 for data fetching
// @include_once('HTTP/Request2.php');
try {
$rcube = \rcube::get_instance();
$request = new \HTTP_Request2($fburl);
$request->setConfig(array(
'store_body' => true,
'follow_redirects' => true,
'ssl_verify_peer' => $rcube->config->get('kolab_ssl_verify_peer', true),
));
$response = $request->send();
// authentication required
if ($response->getStatus() == 401) {
$request->setAuth(HTTPBasic::$current_user, HTTPBasic::$current_pass);
$response = $request->send();
}
// success!
if ($response->getStatus() == 200) {
$vcalendar = VObject\Reader::read($response->getBody(), VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
return array(
'calendar-data' => $vcalendar,
'request-status' => '2.0;Success',
'href' => 'mailto:' . $email,
);
}
}
catch (\Exception $e) {
// log failures
\rcube::raise_error($e, true, false);
}
}
else {
// generate free/busy data from this user's calendars
return parent::getFreeBusyForEmail($email, $start, $end, $request);
}
// return "not found"
return array(
'request-status' => '3.7;Could not find principal',
'href' => 'mailto:' . $email,
);
}
}
\ No newline at end of file
diff --git a/lib/Kolab/CardDAV/ContactsBackend.php b/lib/Kolab/CardDAV/ContactsBackend.php
index 1a92fe0..2dff2c7 100644
--- a/lib/Kolab/CardDAV/ContactsBackend.php
+++ b/lib/Kolab/CardDAV/ContactsBackend.php
@@ -1,1138 +1,1153 @@
<?php
/**
* SabreDAV Contacts backend for Kolab.
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\CardDAV;
use \rcube;
use \rcube_charset;
use \kolab_storage;
use Sabre\DAV;
use Sabre\CardDAV;
use Sabre\VObject;
+use 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;
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',
);
/**
* 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]) {
return $this->folders[$id];
}
else {
return DAVBackend::get_storage_folder($id, 'contact');
}
}
/**
* Returns the list of addressbooks for a specific user.
*
* @param string $principalUri
* @return array
*/
public function getAddressBooksForUser($principalUri)
{
console(__METHOD__, $principalUri, $this->useragent);
$this->_read_sources();
// special case for the apple address book which only supports one (!) address book
if ($this->useragent == 'macosx' && count($this->sources) > 1) {
$source = $this->getAddressBookByName('__all__');
$source['principaluri'] = $principalUri;
return array($source);
}
$addressBooks = array();
foreach ($this->sources as $id => $source) {
$source['principaluri'] = $principalUri;
$addressBooks[] = $source;
}
return $addressBooks;
}
/**
* Returns properties for a specific node identified by name/uri
*
* @param string Node name/uri
* @return array Hash array with addressbook properties or null if not found
*/
public function getAddressBookByName($addressBookUri)
{
console(__METHOD__, $addressBookUri);
$this->_read_sources();
$id = $addressBookUri;
// return the magic *single* address book for Apple's Address Book App
if ($id == '__all__') {
$ctags = array();
foreach ($this->sources as $source) {
$ctags[] = $source['{http://calendarserver.org/ns/}getctag'];
}
return array(
'id' => '__all__',
'uri' => '__all__',
'{DAV:}displayname' => 'All',
'{http://calendarserver.org/ns/}getctag' => join(':', $ctags),
'{urn:ietf:params:xml:ns: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 an addressbook's properties
+ * Updates properties for an address book.
*
- * See Sabre\DAV\IProperties for a description of the mutations array, as
- * well as the return value.
+ * 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.
*
- * @param mixed $addressBookId
- * @param array $mutations
- * @see Sabre\DAV\IProperties::updateProperties
- * @return bool|array
+ * 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, array $mutations)
+ public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch)
{
- console(__METHOD__, $addressBookId, $mutations);
+ console(__METHOD__, $addressBookId, $propPatch);
if ($addressBookId == '__all__')
return false;
- $folder = $this->get_storage_folder($addressBookId);
- return $folder ? DAVBackend::folder_update($folder, $mutations) : false;
+ if ($folder = $this->get_storage_folder($addressBookId)) {
+ DAVBackend::handle_propatch($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) 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);
}
}
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 $this->useragent == 'vcard4';
}
/********** Data conversion utilities ***********/
private $phonetypes = array(
'main' => 'voice',
'homefax' => 'fax',
'workfax' => 'fax',
'mobile' => 'cell',
'other' => 'textphone',
);
private $improtocols = array(
'jabber' => 'xmpp',
);
/**
* Parse the given VCard string into a hash array kolab_format_contact can handle
*
* @param string VCard data block
* @return array Hash array with contact properties or null on failure
*/
private function parse_vcard($cardData, $uid)
{
try {
// use already parsed object
if (Plugin::$parsed_vcard && Plugin::$parsed_vcard->UID == $uid) {
$vobject = Plugin::$parsed_vcard;
}
else {
VObject\Property::$classMap['REV'] = 'Sabre\\VObject\\Property\\DateTime';
$vobject = VObject\Reader::read($cardData, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
}
if ($vobject && $vobject->name == 'VCARD') {
$contact = $this->_to_array($vobject);
if (!empty($contact['uid'])) {
return $contact;
}
}
}
catch (VObject\ParseException $e) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "VCard data parse error: " . $e->getMessage()),
true, false);
}
return null;
}
/**
* Build a valid VCard format block from the given contact record
*
* @param array Hash array with contact properties from libkolab
* @return string VCARD string containing the contact data
*/
public function to_vcard($contact)
{
$v4 = $this->is_vcard4();
$v4_prefix = $v4 ? '' : 'X-';
- $vc = VObject\Component::create('VCARD');
- $vc->version = $v4 ? '4.0' : '3.0';
- $vc->prodid = '-//Kolab//iRony DAV Server ' . KOLAB_DAV_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
+ $vc = new VObject\Component\VCard();
+ $vc->VERSION = $v4 ? '4.0' : '3.0';
+ $vc->PRODID = '-//Kolab//iRony DAV Server ' . KOLAB_DAV_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
$vc->add('UID', $contact['uid']);
$vc->add('FN', $contact['name']);
// distlists are KIND:group
if ($contact['_type'] == 'distribution-list') {
// group cards are actually vcard version 4
if (!$this->is_apple()) {
$vc->version = '4.0';
$prop_prefix = '';
}
else {
// prefix group properties for Apple
$prop_prefix = 'X-ADDRESSBOOKSERVER-';
}
$vc->add($prop_prefix . 'KIND', 'group');
foreach ((array)$contact['member'] as $member) {
if ($member['uid'])
$value = 'urn:uuid:' . $member['uid'];
else if ($member['email'] && $member['name'])
$value = '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 = VObject\Property::create('N');
- $n->setParts(array($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 = VObject\Property::create('ORG');
+ $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(VObject\Property::create('RELATED', $value, array('type' => $type)));
+ $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) {
- $vemail = VObject\Property::create('EMAIL', $email['address'], array('type' => 'INTERNET'));
- if (!empty($email['type']))
- $vemail->offsetSet(null, new VObject\Parameter('type', strtoupper($email['type'])));
+ $vemail = $vc->create('EMAIL', $email['address'], array('type' => 'INTERNET'));
+ if (!empty($email['type'])) {
+ $vemail['type']->addValue(strtoupper($email['type']));
+ }
$vc->add($vemail);
}
foreach ((array)$contact['phone'] as $phone) {
$type = $this->phonetypes[$phone['type']] ?: $phone['type'];
$vc->add('TEL', $phone['number'], array('type' => strtoupper($type)));
}
foreach ((array)$contact['website'] as $website) {
$vc->add('URL', $website['url'], array('type' => strtoupper($website['type'])));
}
$improtocolmap = array_flip($this->improtocols);
foreach ((array)$contact['im'] as $im) {
list($prot, $val) = explode(':', $im, 2);
if ($val) $vc->add('x-' . ($improtocolmap[$prot] ?: $prot), $val);
else $vc->add('IMPP', $im);
}
foreach ((array)$contact['address'] as $adr) {
- $vadr = VObject\Property::create('ADR', null, array('type' => strtoupper($adr['type'])));
+ $vadr = $vc->create('ADR', null, array('type' => strtoupper($adr['type'])));
$vadr->setParts(array('','', $adr['street'], $adr['locality'], $adr['region'], $adr['code'], $adr['country']));
$vc->add($vadr);
}
if (!empty($contact['notes']))
$vc->add('NOTE', $contact['notes']);
if (!empty($contact['gender']))
$vc->add($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('BDAY', $contact['birthday'], false));
+ $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($v4_prefix . 'ANNIVERSARY', $contact['anniversary'], false));
+ $vc->add(VObjectUtils::datetime_prop($vc, $v4_prefix . 'ANNIVERSARY', $contact['anniversary'], false));
}
if (!empty($contact['categories'])) {
- $cat = VObject\Property::create('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 = base64_encode($contact['photo']);
- $vc->PHOTO->add('BASE64', null);
+ $vc->PHOTO = $contact['photo'];
+ $vc->PHOTO['ENCODING'] = 'b';
}
// 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']))
- $vc->add(VObjectUtils::datetime_prop('REV', $contact['changed'], true));
+ if (!empty($contact['changed']) && is_a($contact['changed'], 'DateTime'))
+ $vc->REV = $contact['changed']->format('Ymd\\THis\\Z');
+
+ // convert to VCard4.0
+ // if ($v4) {
+ // $vc->convert(VObject\Document::VCARD40);
+ //}
return $vc->serialize();
}
/**
* Convert the given Sabre\VObject\Component\Vcard object to a libkolab compatible contact format
*
* @param object Vcard object to convert
* @return array Hash array with contact properties
*/
private function _to_array($vc)
{
$contact = array(
'_type' => 'contact',
'uid' => strval($vc->UID),
'name' => strval($vc->FN),
'x-custom' => array(),
);
if ($vc->REV) {
- try { $contact['changed'] = $vc->REV->getDateTime(); }
+ try {
+ $contact['changed'] = DateTimeParser::parseDateTime(strval($vc->REV));
+ }
catch (\Exception $e) {
- try { $contact['changed'] = new \DateTime(strval($vc->REV)); }
- catch (\Exception $e) { }
+ // ignore
}
}
// normalize apple-style properties
$this->_from_apple($vc);
$phonetypemap = array_flip($this->phonetypes);
// 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'] = $prop->value;
+ $contact['notes'] = $value;
break;
case 'TITLE':
- $contact['jobtitle'] = $prop->value;
+ $contact['jobtitle'] = $value;
break;
case 'NICKNAME':
- $contact[strtolower($prop->name)] = $prop->value;
+ $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::array_filter($prop->offsetGet('type'), 'internet,pref', true));
- $contact['email'][] = array('address' => $prop->value, 'type' => strtolower($types[0] ?: 'other'));
+ $contact['email'][] = array('address' => $value, 'type' => strtolower($types[0] ?: 'other'));
break;
case 'URL':
$types = array_values(self::array_filter($prop->offsetGet('type'), 'internet,pref', true));
- $contact['website'][] = array('url' => $prop->value, 'type' => strtolower($types[0]));
+ $contact['website'][] = array('url' => $value, 'type' => strtolower($types[0]));
break;
case 'TEL':
$types = array_values(self::array_filter($prop->offsetGet('type'), 'internet,pref', true));
$type = strtolower($types[0]);
- $contact['phone'][] = array('number' => $prop->value, 'type' => $phonetypemap[$type] ?: $type);
+ $contact['phone'][] = array('number' => $value, 'type' => $phonetypemap[$type] ?: $type);
break;
case 'ADR':
$type = $prop->offsetGet('type') ?: $prop->parameters[0];
- $adr = array('type' => strtolower($type->value ?: $type->name));
+ $adr = array('type' => strtolower(strval($type) ?: $type->name));
list(,, $adr['street'], $adr['locality'], $adr['region'], $adr['code'], $adr['country']) = $prop->getParts();
$contact['address'][] = $adr;
break;
case 'BDAY':
- $contact['birthday'] = new \DateTime($prop->value);
+ $contact['birthday'] = new \DateTime($value);
$contact['birthday']->_dateonly = true;
break;
case 'ANNIVERSARY':
case 'X-ANNIVERSARY':
- $contact['anniversary'] = new \DateTime($prop->value);
+ $contact['anniversary'] = new \DateTime($value);
$contact['anniversary']->_dateonly = true;
break;
case 'SEX':
case 'GENDER':
case 'X-GENDER':
- $contact['gender'] = $prop->value;
+ $contact['gender'] = $value;
break;
case 'ROLE':
case 'X-PROFESSION':
- $contact['profession'] = $prop->value;
+ $contact['profession'] = $value;
break;
case 'X-MANAGER':
case 'X-ASSISTANT':
case 'X-CHILDREN':
case 'X-SPOUSE':
- $contact[strtolower(substr($prop->name, 2))] = explode(',', $prop->value);
+ $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', '', $prop->value);
+ $contact['im'][] = ($this->improtocols[$protocol] ?: $protocol) . ':' . preg_replace('/^[a-z]+:/i', '', $value);
break;
case 'IMPP':
$prot = null;
- if (preg_match('/^[a-z]+:/i', $prop->value))
- list($prot, $val) = explode(':', $prop->value, 2);
+ if (preg_match('/^[a-z]+:/i', $value))
+ list($prot, $val) = explode(':', $value, 2);
else
- $val = $prop->value;
+ $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':
$param = $prop->offsetGet('encoding') ?: $prop->parameters[0];
- if ($param->value && (strtolower($param->value) == 'b' || strtolower($param->value) == 'base64') || strtolower($param->name) == 'base64') {
- $contact['photo'] = base64_decode($prop->value);
+ if (($pvalue = $param->getValue()) && (strtolower($pvalue) == 'b' || strtolower($pvalue) == 'base64') || strtolower($param->name) == 'base64') {
+ $contact['photo'] = $value;
}
break;
// VCard 4.0 properties
case 'FBURL':
- $contact['freebusyurl'] = $prop->value;
+ $contact['freebusyurl'] = $value;
break;
case 'LANG':
- $contact['lang'][] = $prop->value;
+ $contact['lang'][] = $value;
break;
case 'RELATED':
$type = strtolower($prop->offsetGet('type'));
if ($field = $this->related_map[$type]) {
- $contact[$field][] = $prop->value;
+ $contact[$field][] = $value;
}
else {
- $contact['related'][] = $prop->value;
+ $contact['related'][] = $value;
}
break;
case 'KIND':
case 'X-ADDRESSBOOKSERVER-KIND':
- if (strtolower($prop->value) == 'group') {
+ if (strtolower($value) == 'group') {
$contact['_type'] = 'distribution-list';
}
break;
case 'MEMBER':
case 'X-ADDRESSBOOKSERVER-MEMBER':
- if (strpos($prop->value, 'urn:uuid:') === 0) {
- $contact['member'][] = array('uid' => substr($prop->value, 9));
+ if (strpos($value, 'urn:uuid:') === 0) {
+ $contact['member'][] = array('uid' => substr($value, 9));
}
- else if (strpos($prop->value, 'mailto:') === 0) {
- $member = reset(\rcube_mime::decode_address_list(urldecode(substr($prop->value, 7))));
+ 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($prop->value));
+ $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($label->value, '_$!<>')) : null;
+ $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 (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 = new VObject\Property('X-ABLabel');
- $ablabel->name = 'X-ABLabel';
+ $ablabel = $vc->create('X-ABLabel');
$ablabel->group = $group;
- $ablabel->value = in_array($label, $this->xab_known_labels) ? '_$!<'.ucfirst($label).'>!$_' : ucfirst($label);
+ $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 array_filter($arr, $values, $inverse = false)
{
if (!is_array($values)) {
$values = explode(',', $values);
}
$result = array();
$keep = array_flip((array)$values);
if (!empty($arr)) {
foreach ($arr as $key => $val) {
if ($inverse != isset($keep[strtolower($val)])) {
$result[$key] = $val;
}
}
}
return $result;
}
/**
* Generate an Etag string from the given contact data
*
* @param array Hash array with contact properties from libkolab
* @return string Etag string
*/
private static function _get_etag($contact)
{
return sprintf('"%s-%d"', substr(md5($contact['uid']), 0, 16), $contact['_msguid']);
}
}
diff --git a/lib/Kolab/CardDAV/LDAPDirectory.php b/lib/Kolab/CardDAV/LDAPDirectory.php
index 4030e11..7968ab9 100644
--- a/lib/Kolab/CardDAV/LDAPDirectory.php
+++ b/lib/Kolab/CardDAV/LDAPDirectory.php
@@ -1,578 +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;
/**
* 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,
+ * Updates properties on this node.
*
- * @param array $mutations
- * @return bool|array
+ * @param PropPatch $propPatch
+ * @return void
*/
- function updateProperties($mutations)
+ public function propPatch(DAV\PropPatch $propPatch)
{
- console(__METHOD__, $mutations);
- return false;
+ 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 1441d29..ef8a0ec 100644
--- a/lib/Kolab/CardDAV/Plugin.php
+++ b/lib/Kolab/CardDAV/Plugin.php
@@ -1,204 +1,204 @@
<?php
/**
* Extended CardDAV plugin for the Kolab DAV server
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Kolab\CardDAV;
use Sabre\DAV;
use Sabre\DAVACL;
use Sabre\CardDAV;
use Sabre\VObject;
/**
* Extended CardDAV plugin to tweak data validation
*/
class Plugin extends CardDAV\Plugin
{
// make already parsed vcard blocks available for later use
public static $parsed_vcard;
// allow the backend to force a redirect Location
public static $redirect_basename;
/**
* Initializes the plugin
*
* @param DAV\Server $server
* @return void
*/
public function initialize(DAV\Server $server)
{
parent::initialize($server);
- $server->subscribeEvent('beforeMethod', array($this, 'beforeMethod'));
- $server->subscribeEvent('afterCreateFile', array($this, 'afterWriteContent'));
- $server->subscribeEvent('afterWriteContent', array($this, 'afterWriteContent'));
+ $server->on('beforeMethod', array($this, 'beforeMethod'));
+ $server->on('afterCreateFile', array($this, 'afterWriteContent'));
+ $server->on('afterWriteContent', array($this, 'afterWriteContent'));
}
/**
* Adds all CardDAV-specific properties
*
* @param string $path
* @param DAV\INode $node
* @param array $requestedProperties
* @param array $returnedProperties
* @return void
*/
public function beforeGetProperties($path, DAV\INode $node, array &$requestedProperties, array &$returnedProperties)
{
// publish global ldap address book for this principal
if ($node instanceof DAVACL\IPrincipal && empty($this->directories) && \rcube::get_instance()->config->get('kolabdav_ldap_directory')) {
$this->directories[] = self::ADDRESSBOOK_ROOT . '/' . $node->getName() . '/' . LDAPDirectory::DIRECTORY_NAME;
}
parent::beforeGetProperties($path, $node, $requestedProperties, $returnedProperties);
}
/**
* Handler for beforeMethod events
*/
public function beforeMethod($method, $uri)
{
if ($method == 'PUT' && $this->server->httpRequest->getHeader('If-None-Match') == '*') {
// In-None-Match: * is only valid with PUT requests creating a new resource.
// SOGo Conenctor for Thunderbird also sends it with update requests which then fail
// in the Server::checkPreconditions().
// See https://issues.kolab.org/show_bug.cgi?id=2589 and http://www.sogo.nu/bugs/view.php?id=1624
// This is a work-around for the buggy SOGo connector and should be removed once fixed.
if (strpos($this->server->httpRequest->getHeader('User-Agent'), 'Thunderbird/') > 0) {
unset($_SERVER['HTTP_IF_NONE_MATCH']);
}
}
}
/**
* Inject some additional HTTP response headers
*/
public function afterWriteContent($uri, $node)
{
// send Location: header to corrected URI
if (self::$redirect_basename) {
$path = explode('/', $uri);
array_pop($path);
array_push($path, self::$redirect_basename);
$this->server->httpResponse->setHeader('Location', $this->server->getBaseUri() . join('/', array_map('urlencode', $path)));
self::$redirect_basename = null;
}
}
/**
* Checks if the submitted iCalendar data is in fact, valid.
*
* An exception is thrown if it's not.
*
* @param resource|string $data
* @return void
*/
protected function validateVCard(&$data)
{
// If it's a stream, we convert it to a string first.
if (is_resource($data)) {
$data = stream_get_contents($data);
}
// Converting the data to unicode, if needed.
$data = DAV\StringUtil::ensureUTF8($data);
try {
VObject\Property::$classMap['REV'] = 'Sabre\\VObject\\Property\\DateTime';
$vobj = VObject\Reader::read($data, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
if ($vobj->name == 'VCARD')
$this->parsed_vcard = $vobj;
}
catch (VObject\ParseException $e) {
throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid vcard data. Parse error: ' . $e->getMessage());
}
if ($vobj->name !== 'VCARD') {
throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.');
}
if (!isset($vobj->UID)) {
throw new DAV\Exception\BadRequest('Every vcard must have a UID.');
}
}
/**
* This function handles the addressbook-query REPORT
*
* This report is used by the client to filter an addressbook based on a
* complex query.
*
* @param \DOMNode $dom
* @return void
*/
protected function addressbookQueryReport($dom)
{
$node = $this->server->tree->getNodeForPath(($uri = $this->server->getRequestUri()));
console(__METHOD__, $uri);
// fix some 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) {
$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 6beb78a..56f76f0 100644
--- a/lib/Kolab/CardDAV/UserAddressBooks.php
+++ b/lib/Kolab/CardDAV/UserAddressBooks.php
@@ -1,136 +1,131 @@
<?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;
/**
* 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();
}
return $objs;
}
/**
* Returns a single addressbook, by name
*
* @param string $name
* @return \AddressBook
*/
public function getChild($name)
{
if ($name == LDAPDirectory::DIRECTORY_NAME) {
return $this->getLDAPDirectory();
}
if ($addressbook = $this->carddavBackend->getAddressBookByName($name)) {
$addressbook['principaluri'] = $this->principalUri;
return new AddressBook($this->carddavBackend, $addressbook);
}
throw new DAV\Exception\NotFound('Addressbook with name \'' . $name . '\' could not be found');
}
/**
* Getter for the singleton instance of the LDAP directory
*/
private function getLDAPDirectory()
{
if (!$this->ldap_directory) {
$rcube = rcube::get_instance();
$config = $rcube->config->get('kolabdav_ldap_directory');
$config['debug'] = $rcube->config->get('ldap_debug');
$this->ldap_directory = new LDAPDirectory($config, $this->principalUri, $this->carddavBackend);
}
return $this->ldap_directory;
}
/**
* 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 such as the display name and description
+ * Updates properties on this node.
*
- * @param array $mutations
- * @return array
+ * @param PropPatch $propPatch
+ * @return void
*/
- public function updateProperties($mutations)
+ public function propPatch(DAV\PropPatch $propPatch)
{
- $errors = array();
-
- foreach ($mutations as $prop => $val) {
- $errors[403][$prop] = null;
- }
-
- return $errors;
+ console(__METHOD__, $propPatch);
+ // NOP
}
}
diff --git a/lib/Kolab/DAVACL/PrincipalBackend.php b/lib/Kolab/DAVACL/PrincipalBackend.php
index 908aca3..d22cd8e 100644
--- a/lib/Kolab/DAVACL/PrincipalBackend.php
+++ b/lib/Kolab/DAVACL/PrincipalBackend.php
@@ -1,255 +1,256 @@
<?php
/**
* SabreDAV Principals Backend implementation 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\DAVACL;
use \rcube;
use Sabre\DAV\Exception;
use Sabre\DAV\URLUtil;
use Kolab\DAV\Auth\HTTPBasic;
/**
* Kolab Principal Backend
*/
-class PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend\BackendInterface
+class PrincipalBackend extends \Sabre\DAVACL\PrincipalBackend\AbstractBackend implements \Sabre\DAVACL\PrincipalBackend\BackendInterface
{
/**
* Sets up the backend.
*/
public function __construct()
{
}
/**
* Returns a pricipal record for the currently authenticated user
*/
public function getCurrentUser()
{
// console(__METHOD__, HTTPBasic::$current_user);
if (HTTPBasic::$current_user) {
$user_email = rcube::get_instance()->get_user_email();
return array(
'uri' => 'principals/' . HTTPBasic::$current_user,
'{DAV:}displayname' => HTTPBasic::$current_user,
'{http://sabredav.org/ns}email-address' => $user_email,
'{http://calendarserver.org/ns/}email-address-set' => $user_email,
);
}
return false;
}
/**
* Returns a list of principals based on a prefix.
*
* This prefix will often contain something like 'principals'. You are only
* expected to return principals that are in this base path.
*
* You are expected to return at least a 'uri' for every user, you can
* return any additional properties if you wish so. Common properties are:
* {DAV:}displayname
* {http://sabredav.org/ns}email-address - This is a custom SabreDAV
* field that's actualy injected in a number of other properties. If
* you have an email address, use this property.
*
* @param string $prefixPath
* @return array
*/
public function getPrincipalsByPrefix($prefixPath)
{
console(__METHOD__, $prefixPath);
$principals = array();
if ($prefixPath == 'principals') {
// TODO: list users from LDAP
// we currently only advertise the authenticated user
if ($user = $this->getCurrentUser()) {
$principals[] = $user;
}
}
return $principals;
}
/**
* Returns a specific principal, specified by it's path.
* The returned structure should be the exact same as from
* getPrincipalsByPrefix.
*
* @param string $path
* @return array
*/
public function getPrincipalByPath($path)
{
// console(__METHOD__, $path);
list($prefix,$name) = explode('/', $path);
if ($prefix == 'principals' && $name == HTTPBasic::$current_user) {
return $this->getCurrentUser();
}
else if ($prefix == 'principals' && \rcube_utils::check_email($name, false)) {
// TODO: do a user lookup in LDAP
list($localname,$domain) = explode('@', $name);
return array(
'uri' => $path,
'{DAV:}displayname' => $localname,
'{http://sabredav.org/ns}email-address' => $name,
'{http://calendarserver.org/ns/}email-address-set' => $name,
);
}
return null;
}
/**
* Returns the list of members for a group-principal
*
* @param string $principal
* @return array
*/
public function getGroupMemberSet($principal)
{
// TODO: for now the group principal has only one member, the user itself
list($prefix, $name) = URLUtil::splitPath($principal);
- $principal = $this->getPrincipalByPath($prefix);
+ $principal = $this->getPrincipalByPath($principal);
if (!$principal) throw new Exception('Principal not found');
return array(
$prefix
);
}
/**
* Returns the list of groups a principal is a member of
*
* @param string $principal
* @return array
*/
public function getGroupMembership($principal)
{
list($prefix,$name) = URLUtil::splitPath($principal);
$group_membership = array();
if ($prefix == 'principals') {
$principal = $this->getPrincipalByPath($principal);
if (!$principal) throw new Exception('Principal not found');
// TODO: implement full calendar delegation (with information from LDAP kolabDelegate)
return array(
// Calendar delegation is not supported by our backend
//'principals/'.$name.'/calendar-proxy-read',
//'principals/'.$name.'/calendar-proxy-write',
// The addressbook groups are not supported in Sabre,
// see http://groups.google.com/group/sabredav-discuss/browse_thread/thread/ef2fa9759d55f8c#msg_5720afc11602e753
//'principals/'.$name.'/addressbook-proxy-read',
//'principals/'.$name.'/addressbook-proxy-write',
);
}
return $group_membership;
}
/**
* Updates the list of group members for a group principal.
*
* The principals should be passed as a list of uri's.
*
* @param string $principal
* @param array $members
* @return void
*/
public function setGroupMemberSet($principal, array $members)
{
throw new Exception('Setting members of the group is not supported yet');
}
- function updatePrincipal($path, $mutations)
+ function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch)
{
return 0;
}
/**
* This method is used to search for principals matching a set of
* properties.
*
* This search is specifically used by RFC3744's principal-property-search
* REPORT. You should at least allow searching on
* http://sabredav.org/ns}email-address.
*
* The actual search should be a unicode-non-case-sensitive search. The
* keys in searchProperties are the WebDAV property names, while the values
* are the property values to search on.
*
* If multiple properties are being searched on, the search should be
* AND'ed.
*
* This method should simply return an array with full principal uri's.
*
* If somebody attempted to search on a property the backend does not
* support, you should simply return 0 results.
*
* @param string $prefixPath
* @param array $searchProperties
+ * @param string $test
* @return array
*/
- function searchPrincipals($prefixPath, array $searchProperties)
+ function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof')
{
console(__METHOD__, $prefixPath, $searchProperties);
$email = null;
$results = array();
$current_user = $this->getCurrentUser();
foreach($searchProperties as $property => $value) {
// check search property against the current user
if ($current_user[$property] == $value) {
$results[] = $current_user['uri'];
continue;
}
switch($property) {
case '{http://sabredav.org/ns}email-address':
$email = $value;
break;
case '{DAV:}displayname':
default :
// Unsupported property
return array();
}
}
// we only support search by email
if (!empty($email)) {
// TODO: search via LDAP
}
return array_unique($results);
}
}
diff --git a/lib/Kolab/Utils/DAVBackend.php b/lib/Kolab/Utils/DAVBackend.php
index cb3e56b..7bef0f6 100644
--- a/lib/Kolab/Utils/DAVBackend.php
+++ b/lib/Kolab/Utils/DAVBackend.php
@@ -1,229 +1,256 @@
<?php
/**
* Utility class providing a simple API to PHP's APC cache
*
* @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\Utils;
use \rcube;
use \kolab_storage;
use \rcube_utils;
use \rcube_charset;
/**
*
*/
class DAVBackend
{
const IMAP_UID_KEY = '/shared/vendor/kolab/uniqueid';
const IMAP_UID_KEY_PRIVATE = '/private/vendor/kolab/uniqueid';
const IMAP_UID_KEY_CYRUS = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
public static $caldav_type_component_map = array('event' => 'VEVENT', 'task' => 'VTODO');
/**
* Getter for a kolab_storage_folder with the given UID
*
* @param string Folder UID (saved in annotation)
* @param string Kolab folder type (for selecting candidates)
* @return object \kolab_storage_folder instance
*/
public static function get_storage_folder($uid, $type)
{
foreach (kolab_storage::get_folders($type, false) as $folder) {
if ($folder->get_uid() == $uid)
return $folder;
}
return null;
}
/**
* Build an absolute URL with the given parameters
*/
public static function abs_url($parts = array())
{
$schema = 'http';
$default_port = 80;
if (rcube_utils::https_check()) {
$schema = 'https';
$default_port = 443;
}
$url = $schema . '://' . $_SERVER['HTTP_HOST'];
if ($_SERVER['SERVER_PORT'] != $default_port)
$url .= ':' . $_SERVER['SERVER_PORT'];
if (dirname($_SERVER['SCRIPT_NAME']) != '/')
$url .= dirname($_SERVER['SCRIPT_NAME']);
$url .= '/' . join('/', array_map('urlencode', $parts));
return $url;
}
+ /**
+ * Set callback handler for property changes on the given folder
+ *
+ * @param object $folder kolab_storage_folder instance to operate on
+ * @param oject $propPatch PropPatch instance with the property updates
+ */
+ public static function handle_propatch($folder, \Sabre\DAV\PropPatch $propPatch)
+ {
+ $propPatch->handle(
+ array('{DAV:}displayname','{http://apple.com/ns/ical/}calendar-color'),
+ function($mutations) use ($folder) {
+ $result = DAVBackend::folder_update($folder, $mutations);
+ if (is_array($result)) {
+ $ret = array();
+ foreach ($result as $code => $props) {
+ foreach (array_keys($props) as $prop) {
+ $ret[$prop] = $code;
+ }
+ }
+ }
+ else {
+ $ret = $result;
+ }
+ return $ret;
+ });
+ }
+
/**
* Updates properties for a recourse (kolab folder)
*
* The mutations array uses the propertyName in clark-notation as key,
* and the array value for the property value. In the case a property
* should be deleted, the property value will be null.
*
* This method must be atomic. If one property cannot be changed, the
* entire operation must fail.
*
* If the operation was successful, true is returned.
* If the operation failed, detailed information about any
* failures is returned.
*
* @param object $folder kolab_storage_folder instance to operate on
- * @param array $mutations Hash array with propeties to change
- * @return bool|array
+ * @param object $mutations Hash array with propeties to change
+ * @return void
*/
public static function folder_update($folder, array $mutations)
{
$errors = array();
$updates = array();
foreach ($mutations as $prop => $val) {
switch ($prop) {
case '{DAV:}displayname':
// abort if name didn't change
if ($val == html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET)) {
break;
}
// restrict renaming to personal folders only
if ($folder->get_namespace() == 'personal') {
$parts = preg_split('!(\s*/\s*|\s+[»:]\s+)!', $val);
$updates['oldname'] = $folder->name;
$updates['name'] = array_pop($parts);
$updates['parent'] = join('/', $parts);
}
else {
$updates['displayname'] = $val;
}
break;
case '{http://apple.com/ns/ical/}calendar-color':
$newcolor = substr(trim($val, '#'), 0, 6);
if (strcasecmp($newcolor, $folder->get_color())) {
$updates['color'] = $newcolor;
}
break;
case '{urn:ietf:params:xml:ns:caldav}calendar-description':
default:
// unsupported property
$errors[403][$prop] = null;
}
}
// execute folder update
if (!empty($updates)) {
// 'name' and 'parent' properties are always required
if (empty($updates['name'])) {
$parts = explode('/', $folder->name);
$updates['name'] = rcube_charset::convert(array_pop($parts), 'UTF7-IMAP');
$updates['parent'] = join('/', $parts);
$updates['oldname'] = $folder->name;
}
if (!kolab_storage::folder_update($updates)) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error updating properties for folder $folder->name:" . kolab_storage::$last_error),
true, false);
return false;
}
}
return empty($errors) ? true : $errors;
}
/**
* Creates a new resource (i.e. IMAP folder) of a given type
*
* If the creation was a success, an id must be returned that can be used to reference
* this resource in other methods.
*
* @param array $properties
* @param string $type
* @param string $uid
* @return false|string
*/
public function folder_create($type, array $properties, $uid)
{
$props = array(
'type' => $type,
'name' => '',
'subscribed' => true,
);
foreach ($properties as $prop => $val) {
switch ($prop) {
case '{DAV:}displayname':
$parts = explode('/', $val);
$props['name'] = array_pop($parts);
$props['parent'] = join('/', $parts);
break;
case '{http://apple.com/ns/ical/}calendar-color':
$props['color'] = substr(trim($val, '#'), 0, 6);
break;
case '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set':
$type_map = array_flip(self::$caldav_type_component_map);
$comp_types = $val->getValue();
$comp_type = $comp_types[0];
if (!empty($type_map[$comp_type]))
$type = $type_map[$comp_type];
break;
case '{urn:ietf:params:xml:ns:caldav}calendar-description':
default:
// unsupported property
}
}
// use UID as name if it doesn't seem to be a real UID
// TODO: append number to default "Untitled" folder name if one already exists
if (empty($props['name'])) {
$props['name'] = strlen($uid) < 16 ? $uid : 'Untitled';
}
if (!($fname = kolab_storage::folder_update($props))) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error creating a new $type folder '$props[name]':" . kolab_storage::$last_error),
true, false);
return false;
}
// save UID in folder annotations
if ($folder = kolab_storage::get_folder($fname)) {
$folder->set_uid($uid);
}
return $uid;
}
}
diff --git a/lib/Kolab/Utils/DAVLogger.php b/lib/Kolab/Utils/DAVLogger.php
index c1c3067..94a39ba 100644
--- a/lib/Kolab/Utils/DAVLogger.php
+++ b/lib/Kolab/Utils/DAVLogger.php
@@ -1,195 +1,195 @@
<?php
/**
* Utility class logging DAV requests
*
* @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\Utils;
use \rcube;
use Sabre\DAV;
use Kolab\DAV\Auth\HTTPBasic;
/**
* Utility class to log debug information about processed DAV requests
*/
class DAVLogger extends DAV\ServerPlugin
{
const CONSOLE = 1;
const HTTP_REQUEST = 2;
const HTTP_RESPONSE = 4;
private $rcube;
private $server;
private $method;
private $loglevel;
/**
* Default constructor
*/
public function __construct($level = 1)
{
$this->rcube = rcube::get_instance();
$this->loglevel = $level;
}
/**
* This initializes the plugin.
* This method should set up the required event subscriptions.
*
* @param Server $server
*/
public function initialize(DAV\Server $server)
{
$this->server = $server;
- $server->subscribeEvent('beforeMethod', array($this, '_beforeMethod'), 15);
- $server->subscribeEvent('exception', array($this, '_exception'));
- $server->subscribeEvent('exit', array($this, '_exit'));
+ $server->on('beforeMethod', array($this, '_beforeMethod'), 15);
+ $server->on('exception', array($this, '_exception'));
+ $server->on('exit', array($this, '_exit'));
// replace $server->httpResponse with a derived class that can do logging
$server->httpResponse = new HTTPResponse();
}
/**
* Handler for 'beforeMethod' events
*/
public function _beforeMethod($method, $uri)
{
$this->method = $method;
// turn on per-user http logging if the destination file exists
if ($this->loglevel < 2 && $this->rcube->config->get('kolabdav_user_debug', false)
&& ($log_dir = $this->user_log_dir()) && file_exists($log_dir . '/httpraw')) {
$this->loglevel |= (self::HTTP_REQUEST | self::HTTP_RESPONSE);
}
// log full HTTP request data
if ($this->loglevel & self::HTTP_REQUEST) {
$request = $this->server->httpRequest;
$content_type = $request->getHeader('CONTENT_TYPE');
if (strpos($content_type, 'text/') === 0 || strpos($content_type, 'application/xml') === 0) {
$http_body = $request->getBody(true);
// Hack for reading php:://input because that stream can only be read once.
// This is why we re-populate the request body with the existing data.
$request->setBody($http_body);
}
else if (!empty($content_type)) {
$http_body = '[binary data]';
}
// catch all headers
$http_headers = array();
foreach (apache_request_headers() as $hdr => $value) {
if (strtolower($hdr) == 'authorization') {
$method = preg_match('/^((basic|digest)\s+)/i', $value, $m) ? $m[1] : '';
$value = $method . str_repeat('*', strlen($value) - strlen($method));
}
$http_headers[$hdr] = "$hdr: $value";
}
- $this->write_log('httpraw', $request->getMethod() . ' ' . $request->getUri() . ' ' . $_SERVER['SERVER_PROTOCOL'] . "\n" .
+ $this->write_log('httpraw', $request->getMethod() . ' ' . $request->getUrl() . ' ' . $_SERVER['SERVER_PROTOCOL'] . "\n" .
join("\n", $http_headers) . "\n\n" . $http_body);
}
// log to console
if ($this->loglevel & self::CONSOLE) {
$this->write_log('console', $method . ' ' . $uri);
}
}
/**
* Handler for 'exception' events
*/
public function _exception($e)
{
// log to console
$this->console(get_class($e) . ' (EXCEPTION)', $e->getMessage() /*, $e->getTraceAsString()*/);
}
/**
* Handler for 'exit' events
*/
public function _exit()
{
if ($this->loglevel & self::CONSOLE) {
$time = microtime(true) - KOLAB_DAV_START;
if (function_exists('memory_get_usage'))
$mem = round(memory_get_usage() / 1024 / 1024, 1) . 'MB';
if (function_exists('memory_get_peak_usage'))
$mem .= '/' . round(memory_get_peak_usage() / 1024 / 1024, 1) . 'MB';
$this->write_log('console', sprintf("/%s: %0.4f sec; %s", $this->method, $time, $mem));
}
// log full HTTP reponse
if ($this->loglevel & self::HTTP_RESPONSE) {
$this->write_log('httpraw', "RESPONSE: " . $this->server->httpResponse->dump());
}
}
/**
* Wrapper for rcube::cosole() to write per-user logs
*/
public function console(/* ... */)
{
if ($this->loglevel & self::CONSOLE) {
$msg = array();
foreach (func_get_args() as $arg) {
$msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
}
$this->write_log('console', join(";\n", $msg));
}
}
/**
* Wrapper for rcube::write_log() that can write per-user logs
*/
public function write_log($filename, $msg)
{
// dump data per user
if ($this->rcube->config->get('kolabdav_user_debug', false)) {
if ($this->user_log_dir()) {
$filename = HTTPBasic::$current_user . '/' . $filename;
}
else {
return; // don't log
}
}
rcube::write_log($filename, $msg);
}
/**
* Get the per-user log directory
*/
private function user_log_dir()
{
$log_dir = $this->rcube->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
$user_log_dir = $log_dir . '/' . HTTPBasic::$current_user;
return HTTPBasic::$current_user && is_writable($user_log_dir) ? $user_log_dir : false;
}
}
\ No newline at end of file
diff --git a/lib/Kolab/Utils/HTTPResponse.php b/lib/Kolab/Utils/HTTPResponse.php
index 4f4e72d..7fefcba 100644
--- a/lib/Kolab/Utils/HTTPResponse.php
+++ b/lib/Kolab/Utils/HTTPResponse.php
@@ -1,92 +1,93 @@
<?php
/**
* Utility class representing a HTTP response with logging capabilities
*
* @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\Utils;
/**
* This class represents a HTTP response.
*/
class HTTPResponse extends \Sabre\HTTP\Response
{
- private $status;
- private $body = '';
- private $headers = array();
+ protected $status;
+ protected $body = '';
+ protected $_headers = array();
/**
* Sends an HTTP status header to the client.
*
* @param int $code HTTP status code
* @return bool
*/
public function sendStatus($code)
{
$this->status = $this->getStatusMessage($code, $this->defaultHttpVersion);
return parent::sendStatus($code);
}
/**
* Sets an HTTP header for the response
*
* @param string $name
* @param string $value
* @param bool $replace
* @return bool
*/
- public function setHeader($name, $value, $replace = true) {
- $this->headers[$name] = $value;
+ public function setHeader($name, $value, $replace = true)
+ {
+ $this->_headers[$name] = $value;
return parent::setHeader($name, $value, $replace);
}
/**
* Sends the entire response body
*
* This method can accept either an open filestream, or a string.
*
* @param mixed $body
* @return void
*/
public function sendBody($body)
{
if (is_resource($body)) {
fpassthru($body);
$this->body = '[binary data]';
}
else {
echo $body;
$this->body .= $body;
}
}
/**
* Dump the response data for logging
*/
public function dump()
{
$result_headers = '';
- foreach ($this->headers as $hdr => $value) {
+ foreach ($this->_headers as $hdr => $value) {
$result_headers .= "\n$hdr: " . $value;
}
return $this->status . $result_headers . "\n\n" . $this->body;
}
}
\ No newline at end of file
diff --git a/lib/Kolab/Utils/VObjectUtils.php b/lib/Kolab/Utils/VObjectUtils.php
index 82f934b..9131597 100644
--- a/lib/Kolab/Utils/VObjectUtils.php
+++ b/lib/Kolab/Utils/VObjectUtils.php
@@ -1,84 +1,93 @@
<?php
/**
* Utility class providing functions for VObject data encoding
*
* @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\Utils;
use Sabre\VObject\Property;
/**
* Helper class proviting utility functions for VObject data encoding
*/
class VObjectUtils
{
/**
* Convert an object URI into a valid UID value
*/
public static function uri2uid($uri, $suffix = '')
{
$base = basename($uri, $suffix);
$uid = strtr($base, array('%2F' => '/'));
// assume full URL encoding
if (preg_match('/%[A-F0-9]{2}/', $uid)) {
return urldecode($base);
}
return $uid;
}
/**
* Encode an object UID into a valid URI
*/
public static function uid2uri($uid, $suffix = '')
{
$encode = strpos($uid, '/') !== false;
return ($encode ? urlencode($uid) : $uid) . $suffix;
}
/**
* Create a Sabre\VObject\Property instance from a PHP DateTime object
*
* @param string Property name
* @param object DateTime
*/
- public static function datetime_prop($name, $dt, $utc = false)
+ public static function datetime_prop($root, $name, $dt, $utc = false)
{
- $vdt = new Property\DateTime($name);
- $vdt->setDateTime($dt, $dt->_dateonly ? Property\DateTime::DATE : ($utc ? Property\DateTime::UTC : Property\DateTime::LOCALTZ));
+ if ($utc) {
+ $dt->setTimeZone(new \DateTimeZone('UTC'));
+ }
+
+ $vdt = $root->createProperty($name);
+ $vdt->setValue($dt);
+
+ if ($dt->_dateonly) {
+ $vdt['VALUE'] = 'DATE';
+ }
+
return $vdt;
}
/**
* Copy values from one hash array to another using a key-map
*/
public static function map_keys($values, $map)
{
$out = array();
foreach ($map as $from => $to) {
if (isset($values[$from]))
$out[$to] = $values[$from];
}
return $out;
}
}
\ No newline at end of file
diff --git a/public_html/index.php b/public_html/index.php
index 04e7f53..9b9c201 100644
--- a/public_html/index.php
+++ b/public_html/index.php
@@ -1,197 +1,197 @@
<?php
/**
* iRony, the Kolab WebDAV/CalDAV/CardDAV Server
*
* This is the public API to provide *DAV-based access to the Kolab Groupware backend
*
- * @version 0.3-dev
+ * @version 0.4-dev
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013-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/>.
*/
// define some environment variables used throughout the app and libraries
define('KOLAB_DAV_ROOT', realpath('../'));
-define('KOLAB_DAV_VERSION', '0.3-dev');
+define('KOLAB_DAV_VERSION', '0.4-dev');
define('KOLAB_DAV_START', microtime(true));
define('RCUBE_INSTALL_PATH', KOLAB_DAV_ROOT . '/');
define('RCUBE_CONFIG_DIR', KOLAB_DAV_ROOT . '/config/');
define('RCUBE_PLUGINS_DIR', KOLAB_DAV_ROOT . '/lib/plugins/');
// suppress error notices
ini_set('error_reporting', E_ALL &~ E_NOTICE &~ E_STRICT);
/**
* Mapping PHP errors to exceptions.
*
* While this is not strictly needed, it makes a lot of sense to do so. If an
* E_NOTICE or anything appears in your code, this allows SabreDAV to intercept
* the issue and send a proper response back to the client (HTTP/1.1 500).
*/
function exception_error_handler($errno, $errstr, $errfile, $errline ) {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
//set_error_handler("exception_error_handler");
// use composer's autoloader for dependencies
$loader = require_once(KOLAB_DAV_ROOT . '/vendor/autoload.php');
$loader->set('Kolab', array(KOLAB_DAV_ROOT . '/lib')); // register iRony namespace(s)
$loader->setUseIncludePath(true); // enable include_path to load PEAR classes from their default location
// load the Roundcube framework with its autoloader
require_once KOLAB_DAV_ROOT . '/lib/Roundcube/bootstrap.php';
// Roundcube framework initialization
$rcube = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
$rcube->config->load_from_file(RCUBE_CONFIG_DIR . 'dav.inc.php');
// Load plugins
$plugins = (array)$rcube->config->get('kolabdav_plugins', array('kolab_auth'));
$required = array('libkolab', 'libcalendaring');
$rcube->plugins->init($rcube);
$rcube->plugins->load_plugins($plugins, $required);
// enable logger
if ($rcube->config->get('kolabdav_console') || $rcube->config->get('kolabdav_user_debug')) {
$logger = new \Kolab\Utils\DAVLogger((\Kolab\Utils\DAVLogger::CONSOLE | $rcube->config->get('kolabdav_http_log', 0)));
}
// convenience function, you know it well :-)
function console()
{
global $logger;
if ($logger) {
call_user_func_array(array($logger, 'console'), func_get_args());
}
}
// Make sure this setting is turned on and reflects the root url of the *DAV server.
$base_uri = $rcube->config->get('base_uri', slashify(substr(dirname($_SERVER['SCRIPT_FILENAME']), strlen($_SERVER['DOCUMENT_ROOT']))));
// add filename to base URI when called without mod_rewrite (e.g. /dav/index.php/calendar)
if (strpos($_SERVER['REQUEST_URI'], 'index.php'))
$base_uri .= 'index.php/';
// create the various backend instances
$auth_backend = new \Kolab\DAV\Auth\HTTPBasic();
$principal_backend = new \Kolab\DAVACL\PrincipalBackend();
$services = array();
foreach (array('CALDAV','CARDDAV','WEBDAV') as $skey) {
if (getenv($skey))
$services[$skey] = 1;
}
// no config means *all* services
if (empty($services))
$services = array('CALDAV' => 1, 'CARDDAV' => 1, 'WEBDAV' => 1);
// add chwala directories to include path for autoloading
if ($services['WEBDAV']) {
$include_path = ini_get('include_path') . PATH_SEPARATOR;
$include_path .= KOLAB_DAV_ROOT . '/lib/FileAPI' . PATH_SEPARATOR;
$include_path .= KOLAB_DAV_ROOT . '/lib/FileAPI/kolab' . PATH_SEPARATOR;
$include_path .= KOLAB_DAV_ROOT . '/lib/FileAPI/ext';
set_include_path($include_path);
}
// Build the directory tree
// This is an array which contains the 'top-level' directories in the WebDAV server.
if ($services['CALDAV'] || $services['CARDDAV']) {
$nodes = array(
new \Kolab\CalDAV\Principal\Collection($principal_backend),
);
if ($services['CALDAV']) {
$caldav_backend = new \Kolab\CalDAV\CalendarBackend();
$caldav_backend->setUserAgent($_SERVER['HTTP_USER_AGENT']);
$nodes[] = new \Kolab\CalDAV\CalendarRootNode($principal_backend, $caldav_backend);
}
if ($services['CARDDAV']) {
$carddav_backend = new \Kolab\CardDAV\ContactsBackend();
$carddav_backend->setUserAgent($_SERVER['HTTP_USER_AGENT']);
$nodes[] = new \Kolab\CardDAV\AddressBookRoot($principal_backend, $carddav_backend);
}
if ($services['WEBDAV']) {
$nodes[] = new \Kolab\DAV\Collection(\Kolab\DAV\Collection::ROOT_DIRECTORY);
}
}
// register WebDAV service as root
else if ($services['WEBDAV']) {
$nodes = new \Kolab\DAV\Collection('');
}
// the object tree needs in turn to be passed to the server class
$server = new \Sabre\DAV\Server($nodes);
$server->setBaseUri($base_uri);
// connect logger
if (is_object($logger)) {
$server->addPlugin($logger);
}
// register some plugins
$server->addPlugin(new \Sabre\DAV\Auth\Plugin($auth_backend, 'KolabDAV'));
$server->addPlugin(new \Sabre\DAVACL\Plugin());
if ($services['CALDAV']) {
$caldav_plugin = new \Kolab\CalDAV\Plugin();
- $caldav_plugin->setIMipHandler(new \Kolab\CalDAV\IMip());
+ #$caldav_plugin->setIMipHandler(new \Kolab\CalDAV\IMip());
$server->addPlugin($caldav_plugin);
}
if ($services['CARDDAV']) {
$server->addPlugin(new \Kolab\CardDAV\Plugin());
}
if ($services['WEBDAV']) {
// the lock manager is responsible for making sure users don't overwrite each others changes.
$locks_backend = new \Kolab\DAV\Locks\Chwala(\Kolab\DAV\Collection::ROOT_DIRECTORY);
$server->addPlugin(new \Sabre\DAV\Locks\Plugin($locks_backend));
// intercept some of the garbage files operation systems tend to generate when mounting a WebDAV share
$server->addPlugin(new \Kolab\DAV\TempFilesPlugin(KOLAB_DAV_ROOT . '/temp'));
}
// HTML UI for browser-based access (recommended only for development)
if (getenv('DAVBROWSER')) {
$server->addPlugin(new \Sabre\DAV\Browser\Plugin());
}
// log exceptions in iRony error log
-$server->subscribeEvent('exception', function($e){
+$server->on('exception', function($e){
if (!($e instanceof \Sabre\DAV\Exception) || $e->getHTTPCode() == 500) {
rcube::raise_error(array(
'code' => 500,
'type' => 'php',
'file' => $e->getFile(),
'line' => $e->getLine(),
'message' => $e->getMessage() . " (error 500)\n" . $e->getTraceAsString(),
), true, false);
}
});
// finally, process the request
$server->exec();
// trigger log
-$server->broadcastEvent('exit', array());
+$server->emit('exit', array());

File Metadata

Mime Type
text/x-diff
Expires
Sat, Jan 31, 11:17 AM (22 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426282
Default Alt Text
(155 KB)

Event Timeline