diff options
author | Thomas Bruederli <bruederli@kolabsys.com> | 2013-02-20 20:21:48 (GMT) |
---|---|---|
committer | Thomas Bruederli <bruederli@kolabsys.com> | 2013-02-20 20:21:48 (GMT) |
commit | 8d4b2846f83412c6125b623a9b2ff98a6787a188 (patch) | |
tree | 00d16f0ea84dac526f0b8904487b87ca4deeb191 | |
download | iRony-8d4b2846f83412c6125b623a9b2ff98a6787a188.tar.gz |
Initial import
-rw-r--r-- | .gitignore | 7 | ||||
-rw-r--r-- | composer.json | 29 | ||||
-rw-r--r-- | lib/Kolab/CalDAV/Calendar.php | 141 | ||||
-rw-r--r-- | lib/Kolab/CalDAV/CalendarBackend.php | 555 | ||||
-rw-r--r-- | lib/Kolab/CalDAV/CalendarRootNode.php | 73 | ||||
-rw-r--r-- | lib/Kolab/CalDAV/UserCalendars.php | 297 | ||||
-rw-r--r-- | lib/Kolab/DAV/Auth/HTTPBasic.php | 79 | ||||
-rw-r--r-- | lib/Kolab/DAVACL/PrincipalBackend.php | 174 | ||||
-rw-r--r-- | lib/Kolab/Utils/CacheAPC.php | 86 | ||||
-rw-r--r-- | public_html/.htaccess | 6 | ||||
-rw-r--r-- | public_html/index.php | 121 |
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(); +} + |