summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore7
-rw-r--r--composer.json29
-rw-r--r--lib/Kolab/CalDAV/Calendar.php141
-rw-r--r--lib/Kolab/CalDAV/CalendarBackend.php555
-rw-r--r--lib/Kolab/CalDAV/CalendarRootNode.php73
-rw-r--r--lib/Kolab/CalDAV/UserCalendars.php297
-rw-r--r--lib/Kolab/DAV/Auth/HTTPBasic.php79
-rw-r--r--lib/Kolab/DAVACL/PrincipalBackend.php174
-rw-r--r--lib/Kolab/Utils/CacheAPC.php86
-rw-r--r--public_html/.htaccess6
-rw-r--r--public_html/index.php121
11 files changed, 1568 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..090acce
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+config/*.inc.php
+logs/
+vendor/
+lib/plugins
+lib/Roundcube
+composer.phar
+composer.lock \ No newline at end of file
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..34eecda
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,29 @@
+{
+ "name": "kolab/webdav",
+ "description": "Kolab WebDAV/CalDAV/CardDAV Server",
+ "license": "AGPL-3.0",
+ "version": "0.1-alpha",
+ "repositories": [
+ {
+ "type": "vcs",
+ "url": "git://git.kolab.org/git/pear/Net_LDAP3"
+ },
+ {
+ "type": "pear",
+ "url": "http://pear.php.net/"
+ }
+ ],
+ "autoload": {
+ "psr-0": { "": "lib/" },
+ "classmap": [ "lib/Roundcube" ]
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "sabre/dav" : "1.8.*",
+ "pear-pear/PEAR": ">=1.9",
+ "pear-pear/Mail_Mime": ">=1.8.1",
+ "pear-pear/Mail_mimeDecode": ">=1.5.5",
+ "pear-pear/Net_IDNA2": ">=0.1.1"
+ },
+ "minimum-stability": "dev"
+} \ No newline at end of file
diff --git a/lib/Kolab/CalDAV/Calendar.php b/lib/Kolab/CalDAV/Calendar.php
new file mode 100644
index 0000000..b6bbafc
--- /dev/null
+++ b/lib/Kolab/CalDAV/Calendar.php
@@ -0,0 +1,141 @@
+<?php
+
+/**
+ * Kolab calendar storage class
+ *
+ * @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 \kolab_storage;
+use Sabre\CalDAV\Backend;
+
+/**
+ * This object represents a CalDAV calendar.
+ *
+ * A calendar can contain multiple TODO and or Events. These are represented
+ * as \Sabre\CalDAV\CalendarObject objects.
+ */
+class Calendar extends \Sabre\CalDAV\Calendar
+{
+ public $id;
+ public $ready = false;
+ public $readonly = true;
+ public $storage;
+
+ private $events = array();
+ private $imap_folder = 'INBOX/Calendar';
+ private $search_fields = array('title', 'description', 'location', '_attendees');
+
+
+ /**
+ * Default constructor
+ */
+ public function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo)
+ {
+ parent::__construct($caldavBackend, $calendarInfo);
+
+ $this->id = $calendarInfo['id'];
+ $this->imap_folder = urldecode($calendarInfo['id']);
+
+ $this->storage = $caldavBackend->get_storage_folder($this->id);
+ $this->ready = is_object($this->storage) && is_a($this->storage, 'kolab_storage_folder');
+
+ // Set readonly and alarms flags according to folder permissions
+ if ($this->ready) {
+ if ($this->storage->get_namespace() == 'personal') {
+ $this->readonly = false;
+ }
+ else {
+ $rights = $this->storage->get_myrights();
+ if ($rights && !PEAR::isError($rights)) {
+ if (strpos($rights, 'i') !== false)
+ $this->readonly = false;
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Getter for a nice and human readable name for this calendar
+ * See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
+ *
+ * @return string Name of this calendar
+ */
+ public function getName()
+ {
+ $folder = kolab_storage::object_name($this->imap_folder, $this->namespace);
+ return $folder;
+ }
+
+
+ /**
+ * 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()
+ {
+ if ($this->storage->get_namespace() == 'personal') {
+ return $this->calendarInfo['principaluri'];
+ }
+ else {
+ return null; // return $this->storage->get_owner();
+ }
+ }
+
+
+ /**
+ * Return color to display this calendar
+ */
+ public function __getColor()
+ {
+ // color is defined in folder METADATA
+ $metadata = $this->storage->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
+ if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
+ return $color;
+ }
+
+ return 'cc0000';
+ }
+
+
+ /**
+ * 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. These are
+ * currently the only supported privileges
+ * * '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()
+ {
+ // TODO: return ACL infor based on $this->storage->get_myrights()
+ return parent::getACL();
+ }
+
+}
diff --git a/lib/Kolab/CalDAV/CalendarBackend.php b/lib/Kolab/CalDAV/CalendarBackend.php
new file mode 100644
index 0000000..26e0f66
--- /dev/null
+++ b/lib/Kolab/CalDAV/CalendarBackend.php
@@ -0,0 +1,555 @@
+<?php
+
+namespace Kolab\CalDAV;
+
+use \PEAR;
+use \rcube_charset;
+use \kolab_storage;
+use Sabre\CalDAV;
+use Sabre\VObject;
+
+/**
+ * Kolab Calendaring backend.
+ *
+ * Checkout the BackendInterface for all the methods that must be implemented.
+ *
+ */
+class CalendarBackend extends CalDAV\Backend\AbstractBackend
+{
+ private $calendars;
+ private $folders;
+
+ /**
+ * Read available calendars from server
+ */
+ private function _read_calendars()
+ {
+ // already read sources
+ if (isset($this->calendars))
+ return $this->calendars;
+
+ // get all folders that have "event" type
+ $folders = kolab_storage::get_folders('event');
+ $this->calendars = $this->folders = array();
+
+ // convert to UTF8 and sort
+ $names = array();
+ foreach ($folders as $folder) {
+ $folders[$folder->name] = $folder;
+ $names[$folder->name] = rcube_charset::convert($folder->name, 'UTF7-IMAP');
+ }
+
+ asort($names, SORT_LOCALE_STRING);
+
+ foreach ($names as $utf7name => $name) {
+ $id = urlencode($utf7name);
+ $this->folders[$id] = $folders[$utf7name];
+ $this->calendars[$id] = array(
+ 'id' => $id,
+ 'uri' => $id,
+ '{DAV:}displayname' => $name,
+ '{http://apple.com/ns/ical/}calendar-color' => $this->get_color($folders[$utf7name]),
+ '{http://calendarserver.org/ns/}getctag' => '0', // TODO: Ctag is an Etag equvalent for an entire calendar
+ '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new CalDAV\Property\SupportedCalendarComponentSet(array('VEVENT')),
+ '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp' => new CalDAV\Property\ScheduleCalendarTransp('opaque'),
+ );
+ }
+
+ 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)
+ {
+ if ($this->folders[$id]) {
+ return $this->folders[$id];
+ }
+ else {
+ $storage = kolab_storage::get_folder(urldecode($id));
+ return !PEAR::isError($this->storage) ? $storage : null;
+ }
+ }
+
+ /**
+ * Helper method to extract calendar color from metadata
+ */
+ private function get_color($folder)
+ {
+ // color is defined in folder METADATA
+ $metadata = $folder->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
+ if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
+ return $color;
+ }
+
+ return '';
+ }
+
+ /**
+ * 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)
+ {
+ $this->_read_calendars();
+
+ $calendars = array();
+ foreach ($this->calendars as $id => $cal) {
+ $cal['principaluri'] = $principalUri;
+ $calendars[] = $cal;
+ }
+
+ return $calendars;
+ }
+
+ /**
+ * 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)
+ {
+ // TODO: implement this
+ }
+
+ /**
+ * 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.
+ *
+ * This method must be atomic. If one property cannot be changed, the
+ * entire operation must fail.
+ *
+ * If the operation was successful, true can be returned.
+ * If the operation failed, false can be returned.
+ *
+ * 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
+ */
+ public function updateCalendar($calendarId, array $mutations)
+ {
+ // TODO: implement this
+ return false;
+ }
+
+ /**
+ * Delete a calendar and all it's objects
+ *
+ * @param mixed $calendarId
+ * @return void
+ */
+ public function deleteCalendar($calendarId)
+ {
+ // TODO: implement this
+ }
+
+
+ /**
+ * 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
+ * * 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.
+ *
+ * The calendardata is also optional. If it's not returned
+ * 'getCalendarObject' will be called later, which *is* expected to return
+ * calendardata.
+ *
+ * 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__, $principalUri);
+ // TODO: implement this
+
+ $storage = $this->get_storage_folder($calendarId);
+
+ return array();
+ }
+
+
+ /**
+ * 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__, $principalUri);
+ // TODO: implement this
+ }
+
+
+ /**
+ * 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 = basename($objectUri, '.ics');
+ $storage = $this->get_storage_folder($calendarId);
+ $object = $this->_parse_calendar_object($calendarData);
+
+ if ($object['uid'] == $uid) {
+ if (!$storage->save($object, 'event')) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving event object to Kolab server"),
+ true, false);
+ }
+ }
+ else {
+ 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);
+ }
+
+ // TODO: generate Etag
+ }
+
+ /**
+ * 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 = basename($objectUri, '.ics');
+ $storage = $this->get_storage_folder($calendarId);
+ $object = $this->_parse_calendar_object($calendarData);
+
+ // TODO: generate Etag
+ }
+
+ /**
+ * Deletes an existing calendar object.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @return void
+ */
+ public function deleteCalendarObject($calendarId,$objectUri)
+ {
+ // TODO: implement this
+ }
+
+ /**
+ * 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.
+ *
+ * This method provides a default implementation, which parses *all* the
+ * iCalendar objects in the specified calendar.
+ *
+ * This default may well be good enough for personal use, and calendars
+ * that aren't very large. But if you anticipate high usage, big calendars
+ * or high loads, you are strongly adviced to optimize certain paths.
+ *
+ * The best way to do so is override this method and to optimize
+ * specifically for 'common filters'.
+ *
+ * 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)
+ {
+ // TODO: implement this
+ return array();
+ }
+
+
+ private function _parse_calendar_object($calendarData)
+ {
+ $vobject = VObject\Reader::read($calendarData, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
+
+ if ($vobject->name == 'VCALENDAR') {
+ foreach ($vobject->getBaseComponents('VEVENT') as $vevent) {
+ $object = $this->_to_array($vevent);
+ if (!empty($object['uid'])) {
+ return $object;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Convert the given Sabre\VObject\Component\Vevent object to the internal event format
+ */
+ private function _to_array($ve)
+ {
+ $event = array(
+ 'uid' => strval($ve->UID),
+ 'title' => strval($ve->SUMMARY),
+ 'changed' => $ve->DTSTAMP->getDateTime(),
+ 'start' => $this->_convert_datetime($ve->DTSTART),
+ 'end' => $this->_convert_datetime($ve->DTEND),
+ // set defaults
+ 'free_busy' => 'busy',
+ 'priority' => 0,
+ 'attendees' => array(),
+ );
+
+ // check for all-day dates
+ if ($event['start']->_dateonly) {
+ $event['allday'] = true;
+ }
+
+ if ($event['allday'] && is_object($event['end'])) {
+ $event['end']->sub(new \DateInterval('PT23H'));
+ }
+
+ // map other attributes to internal fields
+ $_attendees = array();
+ foreach ($ve->children as $prop) {
+ if (!($prop instanceof VObject\Property))
+ continue;
+
+ switch ($prop->name) {
+ case 'ORGANIZER':
+ break;
+
+ case 'ATTENDEE':
+ break;
+
+ case 'TRANSP':
+ $event['free_busy'] = $prop->value == 'TRANSPARENT' ? 'free' : 'busy';
+ break;
+
+ case 'STATUS':
+ if ($prop->value == 'TENTATIVE')
+ $event['free_busy'] == 'tentative';
+ break;
+
+ case 'PRIORITY':
+ if (is_numeric($prop->value))
+ $event['priority'] = $prop->value;
+ break;
+
+ case 'RRULE':
+ break;
+
+ case 'EXDATE':
+ break;
+
+ case 'RECURRENCE-ID':
+ $event['recurrence_id'] = $this->_date2time($attr['value']);
+ break;
+
+ case 'SEQUENCE':
+ $event['sequence'] = intval($prop->value);
+ break;
+
+ case 'DESCRIPTION':
+ case 'LOCATION':
+ $event[strtolower($prop->name)] = $prop->value;
+ break;
+
+ case 'CLASS':
+ case 'X-CALENDARSERVER-ACCESS':
+ //$sensitivity_map = array('PUBLIC' => 0, 'PRIVATE' => 1, 'CONFIDENTIAL' => 2);
+ //$event['sensitivity'] = $sensitivity_map[$attr['value']];
+ break;
+
+ case 'X-MICROSOFT-CDO-BUSYSTATUS':
+ if ($attr['value'] == 'OOF')
+ $event['free_busy'] == 'outofoffice';
+ else if (in_array($attr['value'], array('FREE', 'BUSY', 'TENTATIVE')))
+ $event['free_busy'] = strtolower($attr['value']);
+ break;
+ }
+ }
+
+ return $event;
+
+ // find alarms
+ if ($valarms = $ve->select('VALARM')) {
+ $action = 'DISPLAY';
+ $trigger = null;
+
+ foreach ($valarms[0]->children as $prop) {
+ switch ($prop->name) {
+ case 'TRIGGER':
+ if ($attr['params']['VALUE'] == 'DATE-TIME') {
+ $trigger = '@' . $attr['value'];
+ }
+ else {
+ $trigger = $attr['value'];
+ $offset = abs($trigger);
+ $unit = 'S';
+ if ($offset % 86400 == 0) {
+ $unit = 'D';
+ $trigger = intval($trigger / 86400);
+ }
+ else if ($offset % 3600 == 0) {
+ $unit = 'H';
+ $trigger = intval($trigger / 3600);
+ }
+ else if ($offset % 60 == 0) {
+ $unit = 'M';
+ $trigger = intval($trigger / 60);
+ }
+ }
+ break;
+
+ case 'ACTION':
+ $action = $prop->value;
+ break;
+ }
+ }
+ if ($trigger)
+ $event['alarms'] = $trigger . $unit . ':' . $action;
+ }
+
+ return $event;
+ }
+
+ /**
+ * Helper method to correctly interpret an all-day date value
+ */
+ private function _convert_datetime($prop)
+ {
+ if (empty($prop)) {
+ return null;
+ }
+ else if ($prop instanceof VObject\Property\DateTime) {
+ $dt = $prop->getDateTime();
+ if ($prop->getDateType() & VObject\Property\DateTime::DATE) {
+ $dt->_dateonly = true;
+ }
+ }
+ else if ($prop instanceof \DateTime) {
+ $dt = $prop;
+ }
+
+ return $dt;
+ }
+}
diff --git a/lib/Kolab/CalDAV/CalendarRootNode.php b/lib/Kolab/CalDAV/CalendarRootNode.php
new file mode 100644
index 0000000..5bb5dda
--- /dev/null
+++ b/lib/Kolab/CalDAV/CalendarRootNode.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Kolab\CalDAV;
+
+use \Sabre\CalDAV;
+use \Sabre\DAVACL\PrincipalBackend;
+use \Sabre\DAVACL\AbstractPrincipalCollection;
+
+/**
+ * Calendars collection
+ *
+ * This object is responsible for generating a list of calendar-homes for each
+ * user.
+ *
+ */
+class CalendarRootNode extends AbstractPrincipalCollection
+{
+ /**
+ * CalDAV backend
+ *
+ * @var Sabre\CalDAV\Backend\BackendInterface
+ */
+ protected $caldavBackend;
+
+ /**
+ * Constructor
+ *
+ * This constructor needs both an authentication and a caldav backend.
+ *
+ * By default this class will show a list of calendar collections for
+ * principals in the 'principals' collection. If your main principals are
+ * actually located in a different path, use the $principalPrefix argument
+ * to override this.
+ *
+ * @param PrincipalBackend\BackendInterface $principalBackend
+ * @param Backend\BackendInterface $caldavBackend
+ * @param string $principalPrefix
+ */
+ public function __construct(PrincipalBackend\BackendInterface $principalBackend, CalDAV\Backend\BackendInterface $caldavBackend, $principalPrefix = 'principals')
+ {
+ parent::__construct($principalBackend, $principalPrefix);
+ $this->caldavBackend = $caldavBackend;
+ }
+
+ /**
+ * Returns the nodename
+ *
+ * We're overriding this, because the default will be the 'principalPrefix',
+ * and we want it to be Sabre\CalDAV\Plugin::CALENDAR_ROOT
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return CalDAV\Plugin::CALENDAR_ROOT;
+ }
+
+ /**
+ * This method returns a node for a principal.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @param array $principal
+ * @return \Sabre\DAV\INode
+ */
+ public function getChildForPrincipal(array $principal)
+ {
+ return new UserCalendars($this->caldavBackend, $principal);
+ }
+
+}
diff --git a/lib/Kolab/CalDAV/UserCalendars.php b/lib/Kolab/CalDAV/UserCalendars.php
new file mode 100644
index 0000000..b201c6b
--- /dev/null
+++ b/lib/Kolab/CalDAV/UserCalendars.php
@@ -0,0 +1,297 @@
+<?php
+
+namespace Kolab\CalDAV;
+
+use Sabre\DAV;
+use Sabre\DAVACL;
+use Sabre\CalDAV\Backend;
+use Kolab\CalDAV\Calendar;
+
+/**
+ * The UserCalenders class contains all calendars associated to one user
+ *
+ */
+class UserCalendars extends \Sabre\CalDAV\UserCalendars implements DAV\IExtendedCollection, DAVACL\IACL
+{
+ /**
+ * CalDAV backend
+ *
+ * @var Sabre\CalDAV\Backend\BackendInterface
+ */
+ protected $caldavBackend;
+
+ /**
+ * Principal information
+ *
+ * @var array
+ */
+ protected $principalInfo;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $caldavBackend
+ * @param mixed $userUri
+ */
+ public function __construct(\Sabre\CalDAV\Backend\BackendInterface $caldavBackend, $principalInfo)
+ {
+ $this->caldavBackend = $caldavBackend;
+ $this->principalInfo = $principalInfo;
+ }
+
+ /**
+ * Returns the name of this object
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ list(,$name) = DAV\URLUtil::splitPath($this->principalInfo['uri']);
+ return $name;
+ }
+
+ /**
+ * Updates the name of this object
+ *
+ * @param string $name
+ * @return void
+ */
+ public function setName($name)
+ {
+ // TODO: implement this
+ throw new DAV\Exception\Forbidden();
+ }
+
+ /**
+ * Deletes this object
+ *
+ * @return void
+ */
+ public function delete()
+ {
+ // TODO: implement this
+ throw new DAV\Exception\Forbidden();
+ }
+
+ /**
+ * Returns the last modification date
+ *
+ * @return int
+ */
+ public function getLastModified()
+ {
+ return null;
+ }
+
+ /**
+ * Creates a new file under this object.
+ *
+ * This is currently not allowed
+ *
+ * @param string $filename
+ * @param resource $data
+ * @return void
+ */
+ public function createFile($filename, $data=null)
+ {
+ throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported');
+ }
+
+ /**
+ * Creates a new directory under this object.
+ *
+ * @param string $filename
+ * @return void
+ */
+ public function createDirectory($filename)
+ {
+ // TODO: implement this
+ throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported');
+ }
+
+ /**
+ * Returns a list of calendars
+ *
+ * @return array
+ */
+ public function getChildren()
+ {
+ $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']);
+ $objs = array();
+ foreach ($calendars as $calendar) {
+ // TODO: (later) add sharing support by implenting this all
+ if ($this->caldavBackend instanceof Backend\SharingSupport) {
+ if (isset($calendar['{http://calendarserver.org/ns/}shared-url'])) {
+ $objs[] = new SharedCalendar($this->caldavBackend, $calendar);
+ }
+ else {
+ $objs[] = new ShareableCalendar($this->caldavBackend, $calendar);
+ }
+ }
+ else {
+ $objs[] = new Calendar($this->caldavBackend, $calendar);
+ }
+ }
+
+ // TODO: add notification support (check with clients first, if anybody supports it)
+ if ($this->caldavBackend instanceof Backend\NotificationSupport) {
+ $objs[] = new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
+ }
+
+ return $objs;
+ }
+
+ /**
+ * Creates a new calendar
+ *
+ * @param string $name
+ * @param array $resourceType
+ * @param array $properties
+ * @return void
+ */
+ public function createExtendedCollection($name, array $resourceType, array $properties)
+ {
+ $isCalendar = false;
+ foreach($resourceType as $rt) {
+ switch ($rt) {
+ case '{DAV:}collection' :
+ case '{http://calendarserver.org/ns/}shared-owner' :
+ // ignore
+ break;
+ case '{urn:ietf:params:xml:ns:caldav}calendar' :
+ $isCalendar = true;
+ break;
+ default :
+ throw new DAV\Exception\InvalidResourceType('Unknown resourceType: ' . $rt);
+ }
+ }
+ if (!$isCalendar) {
+ throw new DAV\Exception\InvalidResourceType('You can only create calendars in this collection');
+ }
+
+ $this->caldavBackend->createCalendar($this->principalInfo['uri'], $name, $properties);
+ }
+
+ /**
+ * 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->principalInfo['uri'];
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public 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. These are
+ * currently the only supported privileges
+ * * '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()
+ {
+ // TODO: implement this
+ return array(
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalInfo['uri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->principalInfo['uri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalInfo['uri'] . '/calendar-proxy-read',
+ 'protected' => true,
+ ),
+ );
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl)
+ {
+ // TODO: implement this
+ throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ public function getSupportedPrivilegeSet()
+ {
+ // TODO: implement this
+ return null;
+ }
+
+ /**
+ * This method is called when a user replied to a request to share.
+ *
+ * This method should return the url of the newly created calendar if the
+ * share was accepted.
+ *
+ * @param string href The sharee who is replying (often a mailto: address)
+ * @param int status One of the SharingPlugin::STATUS_* constants
+ * @param string $calendarUri The url to the calendar thats being shared
+ * @param string $inReplyTo The unique id this message is a response to
+ * @param string $summary A description of the reply
+ * @return null|string
+ */
+ public function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null)
+ {
+ if (!$this->caldavBackend instanceof Backend\SharingSupport) {
+ throw new DAV\Exception\NotImplemented('Sharing support is not implemented by this backend.');
+ }
+
+ return $this->caldavBackend->shareReply($href, $status, $calendarUri, $inReplyTo, $summary);
+ }
+
+}
diff --git a/lib/Kolab/DAV/Auth/HTTPBasic.php b/lib/Kolab/DAV/Auth/HTTPBasic.php
new file mode 100644
index 0000000..5bfa7f4
--- /dev/null
+++ b/lib/Kolab/DAV/Auth/HTTPBasic.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Kolab\DAV\Auth;
+
+use \rcube;
+use \rcube_user;
+use \rcube_utils;
+use Kolab\Utils\CacheAPC;
+
+/**
+ *
+ */
+class HTTPBasic extends \Sabre\DAV\Auth\Backend\AbstractBasic
+{
+ // Make the current user name availabel to all classes
+ public static $current_user;
+
+ /**
+ * Validates a username and password
+ *
+ * This method should return true or false depending on if login
+ * succeeded.
+ *
+ * @param string $username
+ * @param string $password
+ * @return bool
+ */
+ protected function validateUserPass($username, $password)
+ {
+ $rcube = rcube::get_instance();
+ $cache = CacheAPC::get_instance('kolabdav:auth');
+
+ // Here we need IDNA ASCII
+ $host = rcube_utils::idn_to_ascii($rcube->config->get('default_host', 'localhost'));
+ $user = rcube_utils::idn_to_ascii($username);
+ $port = $rcube->config->get('default_port', 143);
+
+ $_host = parse_url($host);
+ if ($_host['host']) {
+ $host = $_host['host'];
+ $ssl = (isset($_host['scheme']) && in_array($_host['scheme'], array('ssl','imaps','tls'))) ? $_host['scheme'] : null;
+ if (!empty($_host['port']))
+ $port = $_host['port'];
+ else if ($ssl && $ssl != 'tls' && (!$port || $port == 143))
+ $port = 993;
+ }
+
+ // check if we already canonified this username
+ if ($auth_user = $cache->get($user)) {
+ $user = $auth_user;
+ }
+ else { // load kolab_auth plugin to resolve the canonical username
+ $rcube->plugins->load_plugin('kolab_auth');
+ }
+
+ // let plugins do their work
+ $auth = $rcube->plugins->exec_hook('authenticate', array(
+ 'host' => $host,
+ 'user' => $user,
+ 'pass' => $password,
+ ));
+
+ // authenticate user against the IMAP server
+ $imap = $rcube->get_storage();
+ $success = $imap->connect($auth['host'], $auth['user'], $auth['pass'], $port, $ssl);
+
+ if ($success) {
+ self::$current_user = $auth['user'];
+ if (!$auth_user) {
+ $cache->set($user, $auth['user']);
+ }
+
+ // register a rcube_user object for global access
+ $rcube->user = new rcube_user(null, array('username' => $auth['user'], 'mail_host' => $auth['host']));
+ }
+
+ return $success;
+ }
+}
diff --git a/lib/Kolab/DAVACL/PrincipalBackend.php b/lib/Kolab/DAVACL/PrincipalBackend.php
new file mode 100644
index 0000000..721a113
--- /dev/null
+++ b/lib/Kolab/DAVACL/PrincipalBackend.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace Kolab\DAVACL;
+
+use Sabre\DAV\Exception;
+use Sabre\DAV\URLUtil;
+use Kolab\DAV\Auth\HTTPBasic;
+
+/**
+ * Kolab Principal Backend
+ */
+class PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend\BackendInterface
+{
+ protected $fieldmap = array(
+ // The users' real name.
+ '{DAV:}displayname' => 'displayname',
+
+ // The users' primary email-address.
+ '{http://sabredav.org/ns}email-address' => 'email',
+
+ /**
+ * This property is actually used by the CardDAV plugin, where it gets
+ * mapped to {http://calendarserver.orgi/ns/}me-card.
+ */
+ '{http://sabredav.org/ns}vcard-url' => 'vcardurl',
+ );
+
+ /**
+ * Sets up the backend.
+ */
+ public function __construct()
+ {
+
+ }
+
+ /**
+ * Returns a pricipal record for the currently authenticated user
+ */
+ public function getCurrentUser()
+ {
+ if (HTTPBasic::$current_user) {
+ return array(
+ 'uri' => '/' . HTTPBasic::$current_user,
+ '{DAV:}displayname' => HTTPBasic::$current_user,
+ );
+ }
+
+ 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)
+ {
+ $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)
+ {
+ list($prefix,$name) = explode('/', $path);
+
+ if ($prefix == 'principals' && $name == HTTPBasic::$current_user) {
+ return $this->getCurrentUser();
+ }
+
+ 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);
+ 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: for now the user principal has only its own groups
+ return array(
+ '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)
+ {
+ return 0;
+ }
+
+ function searchPrincipals($prefixPath, array $searchProperties)
+ {
+ return 0;
+ }
+
+}
diff --git a/lib/Kolab/Utils/CacheAPC.php b/lib/Kolab/Utils/CacheAPC.php
new file mode 100644
index 0000000..e075876
--- /dev/null
+++ b/lib/Kolab/Utils/CacheAPC.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Kolab\Utils;
+
+/**
+ * Utility class that provides a simple API to local APC cache
+ */
+class CacheAPC
+{
+ private static $instances = array();
+
+ private $prefix = 'kolabdav:';
+ private $ttl = 600; // Default Time To Live
+ private $enabled = false; // APC enabled?
+ private $local = array(); // local in-memory cache
+
+ /**
+ * Singleton getter
+ *
+ * @param string Cache domain used to prefix cache entries
+ * @return object CacheAPC instance for the given domain
+ */
+ public static function get_instance($domain = '')
+ {
+ if (!self::$instances[$domain]) {
+ self::$instances[$domain] = new CacheAPC($domain);
+ }
+
+ return self::$instances[$domain];
+ }
+
+ /**
+ * Private constructor
+ */
+ private function CacheAPC($domain)
+ {
+ if (!empty($domain))
+ $this->prefix = $domain . ':';
+
+ $this->enabled = extension_loaded('apc');
+ }
+
+ /**
+ * Get data from cache
+ */
+ function get($key)
+ {
+ if (isset($this->local[$key])) {
+ return $this->local[$key];
+ }
+
+ if ($this->enabled) {
+ $success = false;
+ $data = apc_fetch($this->prefix . $key, $success);
+ return $success ? $data : null;
+ }
+ }
+
+ /**
+ * Save data to cache
+ */
+ function set($key, $data, $ttl = 0)
+ {
+ $this->local[$key] = $data;
+ if ($this->enabled) {
+ return apc_store($this->prefix . $key, $data, $ttl ?: $this->ttl);
+ }
+
+ return true;
+ }
+
+ /**
+ * Deelete a cache entry
+ */
+ function del($key)
+ {
+ unset($this->local[$key]);
+
+ if ($this->enabled) {
+ apc_delete($this->prefix . $key);
+ }
+ }
+
+}
+
+
diff --git a/public_html/.htaccess b/public_html/.htaccess
new file mode 100644
index 0000000..f2eca1a
--- /dev/null
+++ b/public_html/.htaccess
@@ -0,0 +1,6 @@
+RewriteEngine On
+
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule (.*) index.php [qsappend,last]
+
diff --git a/public_html/index.php b/public_html/index.php
new file mode 100644
index 0000000..15181b9
--- /dev/null
+++ b/public_html/index.php
@@ -0,0 +1,121 @@
+<?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.1.0
+ * @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/>.
+ */
+
+// define som environment variables used thoughout the app and libraries
+define('KOLAB_DAV_ROOT', realpath('../'));
+
+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);
+
+// UTC is easy to work with, and usually recommended for any application.
+date_default_timezone_set('UTC');
+
+
+/**
+ * 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 both dependencies and local lib
+require_once KOLAB_DAV_ROOT . '/vendor/autoload.php';
+
+// load the Roundcube framework
+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->plugins->init($rcube);
+$rcube->plugins->load_plugins(array('libkolab'));
+
+// convenience function, you know it well :-)
+function console() { call_user_func_array(array('rcube', 'console'), func_get_args()); }
+
+
+// quick & dirty request debugging
+if ($debug = $rcube->config->get('kolab_dav_debug')) {
+ $http_headers = $_SERVER['SERVER_PROTOCOL'] . ' ' . $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . "\n";
+ foreach (apache_request_headers() as $hdr => $value) {
+ $http_headers .= "$hdr: $value\n";
+ }
+ $rcube->write_log('davdebug', $http_headers . "\n" . $HTTP_RAW_POST_DATA);
+ ob_start(); // turn on output buffering
+}
+
+
+// Make sure this setting is turned on and reflects the root url of the *DAV server.
+$base_uri = slashify(substr(dirname($_SERVER['SCRIPT_FILENAME']), strlen($_SERVER['DOCUMENT_ROOT'])));
+
+
+// create the various backend instances
+$auth_backend = new \Kolab\DAV\Auth\HTTPBasic();
+$principal_backend = new \Kolab\DAVACL\PrincipalBackend();
+//$carddav_backend = new \Kolab\CardDAV\ContactsBackend();
+$caldav_backend = new \Kolab\CalDAV\CalendarBackend();
+
+
+// Build the directory tree
+// This is an array which contains the 'top-level' directories in the WebDAV server.
+$nodes = array(
+ // /principals
+ new \Sabre\CalDAV\Principal\Collection($principal_backend),
+ // /calendars
+ new \Kolab\CalDAV\CalendarRootNode($principal_backend, $caldav_backend),
+ // /addressbook
+ // new \Sabre\CardDAV\AddressBookRoot($principalBackend, $carddavBackend),
+);
+
+// the object tree needs in turn to be passed to the server class
+$server = new \Sabre\DAV\Server($nodes);
+$server->setBaseUri($base_uri);
+
+// register some plugins
+$server->addPlugin(new \Sabre\DAV\Auth\Plugin($auth_backend, 'KolabDAV'));
+//$server->addPlugin(new \Sabre\DAVACL\Plugin()); // we'll add that later
+$server->addPlugin(new \Sabre\CalDAV\Plugin());
+//$server->addPlugin(new \Sabre\CardDAV\Plugin());
+$server->addPlugin(new \Sabre\DAV\Browser\Plugin());
+
+// finally, process the request
+$server->exec();
+
+
+// catch server response in debug log
+if ($debug) {
+ $rcube->write_log('davdebug', "RESPONSE:\n" . ob_get_contents());
+ ob_end_flush();
+}
+