diff --git a/lib/Kolab/CalDAV/CalendarBackend.php b/lib/Kolab/CalDAV/CalendarBackend.php index ba31729..5d937f6 100644 --- a/lib/Kolab/CalDAV/CalendarBackend.php +++ b/lib/Kolab/CalDAV/CalendarBackend.php @@ -1,805 +1,969 @@ * * Copyright (C) 2013, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ 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 +class CalendarBackend extends CalDAV\Backend\AbstractBackend implements CalDAV\Backend\SchedulingSupport { 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 list of mutations is stored in a Sabre\DAV\PropPatch object. * To do the actual updates, you must tell this object which properties * you're going to process with the handle() method. * * Calling the handle method is like telling the PropPatch object "I * promise I can handle updating this property". * * Read the PropPatch documenation for more info and examples. * * @param string $path * @param \Sabre\DAV\PropPatch $propPatch * @return void */ public function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) { console(__METHOD__, $calendarId, $propPatch); 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; } } } + /********** SchedulingBackend methods ***********/ + + /** + * Returns a single scheduling object for the inbox collection. + * + * The returned array should contain the following elements: + * * uri - A unique basename for the object. This will be used to + * construct a full uri. + * * calendardata - The iCalendar object + * * lastmodified - The last modification date. Can be an int for a unix + * timestamp, or a PHP DateTime object. + * * etag - A unique token that must change if the object changed. + * * size - The size of the object, in bytes. + * + * @param string $principalUri + * @param string $objectUri + * @return array + */ + public function getSchedulingObject($principalUri, $objectUri) + { + console(__METHOD__, $principalUri, $objectUri); + + if (!$this->is_current_pricipal($principalUri)) { + return array(); + } + + list($calendarId, $objectUid) = explode(':', $objectUri, 2); + $uid = VObjectUtils::uri2uid($objectUid, '.ics'); + + $event = $this->getCalendarObject($calendarId, $objectUid); + if ($event['uri']) { + $event['uri'] = $calendarId . ':' . $event['uri']; + } + + return $event; + } + + /** + * Returns all scheduling objects for the inbox collection. + * + * These objects should be returned as an array. Every item in the array + * should follow the same structure as returned from getSchedulingObject. + * + * The main difference is that 'calendardata' is optional. + * + * @param string $principalUri + * @return array + */ + public function getSchedulingObjects($principalUri) + { + console(__METHOD__, $principalUri); + + $results = array(); + + // we can only access the current user's calendars + if (!$this->is_current_pricipal($principalUri)) { + return $results; + } + + // TODO: list all pending invitation objects with a certain scheduling status ? + +/* + $query = $this->_event_filter_query(true); + foreach (kolab_storage::get_folders('event') as $storage) { + // only consider events from personal namespace folders + if ($storage->get_namespace() !== 'personal') { + continue; + } + + $folder_id = $storage->get_uid(); + foreach ($storage->select($query) as $event) { + // post-filter events to only get pending invitations + if (!$this->_event_filter_compare($event, true)) { + continue; + } + + // get tags/categories from relations + $this->load_tags($event); + + $event_uri = $folder_id . ':' . VObjectUtils::uid2uri($event['uid'], '.ics'); + $results[] = array( + 'uri' => $event_uri, + 'lastmodified' => $event['changed'] ? $event['changed']->format('U') : null, + 'etag' => self::_get_etag($event), + 'size' => $event['_size'], + ); + } + + // keep storage folder reference + if (!$this->folders[$folder_id]) { + $this->folders[$folder_id] = $storage; + } + } +*/ + + // TODO: find iTip messages in users email INBOX and move to them default calendar + // whenever a CalDAV client fetches the inbox data. This will also remove the message + // from the email inbox as the message is considered 'processed'. + + return $results; + } + + /** + * Deletes a scheduling object from the inbox collection. + * + * @param string $principalUri + * @param string $objectUri + * @return void + */ + public function deleteSchedulingObject($principalUri, $objectUri) + { + console(__METHOD__, $principalUri, $objectUri, $objectData); + } + + /** + * Creates a new scheduling object. This should land in a users' inbox. + * + * @param string $principalUri + * @param string $objectUri + * @param string $objectData + * @return void + */ + public function createSchedulingObject($principalUri, $objectUri, $objectData) + { + console(__METHOD__, $principalUri, $objectUri, $objectData); + + // accept only for current user principal (we don't have permissions for other users) + if ($this->is_current_pricipal($principalUri)) { + + } + else { + // send as iTip? + } + } + + /********** 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() + * + * @param boolean Filter for inbox events (i.e. status=NEEDS-ACTION) + * @return array List of query parameters for kolab_storage_folder::select() */ - private function _event_filter_query() + private function _event_filter_query($inbox = false) { // get email addresses of the current user $user_emails = $this->get_user_emails(); - $query = array(); + $query = $subquery = 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'); + if ($inbox) { + $subquery[] = array('tags', '=', 'x-partstat:' . $email . ':needs-action'); + $subquery[] = array('tags', '=', 'x-partstat:' . $email . ':needs-action'); + } + else { + $query[] = array('tags', '!=', 'x-partstat:' . $email . ':declined'); + } + } + + if (!empty($subquery)) { + $query[] = array($subquery, 'OR'); } return $query; } /** * Check the given event if it matches the filter * + * @param array Hash array with event properties + * @param boolean Filter for inbox events (i.e. status=NEEDS-ACTION) * @return boolean True if matches, false if not */ - private function _event_filter_compare($event) + private function _event_filter_compare($event, $inbox = false) { 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; + if (in_array($attendee['email'], $user_emails)) { + if ($attendee['status'] == 'DECLINED') { + return false; + } + else if ($inbox && $attendee['status'] == 'NEEDS-ACTION') { + return true; + } } } } - return true; + return !$inbox; } /** * 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' ); } + /** + * Helpter method to determine whether the given principal URI + * matches the authenticated user principal. + */ + private function is_current_pricipal($principalUri) + { + return $principalUri === 'principals/' . HTTPBasic::$current_user; + } + } diff --git a/lib/Kolab/CalDAV/CalendarRootNode.php b/lib/Kolab/CalDAV/CalendarRootNode.php index 62174a7..6af6e78 100644 --- a/lib/Kolab/CalDAV/CalendarRootNode.php +++ b/lib/Kolab/CalDAV/CalendarRootNode.php @@ -1,94 +1,53 @@ * * Copyright (C) 2013, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ 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 +class CalendarRootNode extends CalDAV\CalendarRoot { - /** - * 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/IMip.php b/lib/Kolab/CalDAV/IMip.php deleted file mode 100644 index c08f37a..0000000 --- a/lib/Kolab/CalDAV/IMip.php +++ /dev/null @@ -1,141 +0,0 @@ - - * - * Copyright (C) 2013, Kolab Systems AG - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -namespace Kolab\CalDAV; - -use \rcube; -use \rcube_utils; -use \Mail_mime; - -use Sabre\VObject; -use Sabre\CalDAV; -use Sabre\DAV; - -/** - * iMIP handler. - * - * This class is responsible for sending out iMIP messages. iMIP is the - * email-based transport for iTIP. iTIP deals with scheduling operations for - * iCalendar objects. - * - * If you want to customize the email that gets sent out, you can do so by - * extending this class and overriding the sendMessage method. - * - * @copyright Copyright (C) 2007-2013 Rooftop Solutions. All rights reserved. - * @author Evert Pot (http://www.rooftopsolutions.nl/) - * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License - */ -class IMip extends CalDAV\Schedule\IMip -{ - public function __construct() - { - - } - - /** - * Sends one or more iTip messages through email. - * - * @param string $originator Originator Email - * @param array $recipients Array of email addresses - * @param VObject\Component $vObject - * @param string $principal Principal Url of the originator - * @return void - */ - public function sendMessage($originator, array $recipients, VObject\Component $vObject, $principal) - { - $ics = $vObject->serialize(); - console(__METHOD__, $originator, $recipients, $principal, $ics); - - $rcube = rcube::get_instance(); - $sender = $rcube->user->get_identity(); - $sender_email = $sender['email'] ?: $rcube->get_user_email(); - $sender_name = $sender['name'] ?: $rcube->get_user_name(); - - foreach($recipients as $recipient) { - $subject = 'KolabDAV iTIP message'; - switch (strtoupper($vObject->METHOD)) { - case 'REPLY' : - $subject = 'Response for: ' . $vObject->VEVENT->SUMMARY; - break; - case 'REQUEST' : - $subject = 'Invitation for: ' .$vObject->VEVENT->SUMMARY; - break; - case 'CANCEL' : - $subject = 'Cancelled event: ' . $vObject->VEVENT->SUMMARY; - break; - } - - $sender = rcube_utils::idn_to_ascii($sender_email); - $from = format_email_recipient($sender, $sender_name); - $mailto = rcube_utils::idn_to_ascii($recipient); - - // compose multipart message using PEAR:Mail_Mime - $message = new Mail_mime("\r\n"); - $message->setParam('text_encoding', 'quoted-printable'); - $message->setParam('head_encoding', 'quoted-printable'); - $message->setParam('head_charset', RCUBE_CHARSET); - $message->setParam('text_charset', RCUBE_CHARSET . ";\r\n format=flowed"); - - // compose common headers array - $headers = array( - 'To' => $mailto, - 'From' => $from, - 'Date' => date('r'), - 'Reply-To' => $originator, - 'Message-ID' => $rcube->gen_message_id(), - 'X-Sender' => $sender, - 'Subject' => $subject, - ); - if ($agent = $rcube->config->get('useragent')) - $headers['User-Agent'] = $agent; - - $message->headers($headers); - $message->setContentType('text/calendar', array('method' => strval($vObject->method), 'charset' => RCUBE_CHARSET)); - $message->setTXTBody($ics); - - // send message through Roundcube's SMTP feature - if (!$rcube->deliver_message($message, $sender, $mailto, $smtp_error)) { - rcube::raise_error(array( - 'code' => 600, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Failed to send iTIP message to " . $mailto), - true, false); - } - } - } - - /** - * This function is reponsible for sending the actual email. - * - * @param string $to Recipient email address - * @param string $subject Subject of the email - * @param string $body iCalendar body - * @param array $headers List of headers - * @return void - */ - protected function mail($to, $subject, $body, array $headers) - { - //mail($to, $subject, $body, implode("\r\n", $headers)); - } - -} diff --git a/lib/Kolab/CalDAV/IMipPlugin.php b/lib/Kolab/CalDAV/IMipPlugin.php new file mode 100644 index 0000000..5fae8f0 --- /dev/null +++ b/lib/Kolab/CalDAV/IMipPlugin.php @@ -0,0 +1,135 @@ + + * + * Copyright (C) 2014, Kolab Systems AG + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Kolab\CalDAV; + +use \rcube; +use \rcube_utils; +use \Mail_mime; + +use Sabre\VObject; +use Sabre\CalDAV; +use Sabre\DAV; + +/** + * iMIP plugin. + * + * This class is responsible for sending out iMIP messages. iMIP is the + * email-based transport for iTIP. iTIP deals with scheduling operations for + * iCalendar objects. + */ +class IMipPlugin extends CalDAV\Schedule\IMipPlugin +{ + + /** + * Event handler for the 'schedule' event. + * + * @param ITip\Message $iTipMessage + * @return void + */ + function schedule(VObject\ITip\Message $iTipMessage) + { + console(__METHOD__, $iTipMessage->method, $iTipMessage->recipient, $iTipMessage->significantChange, $iTipMessage->scheduleStatus); + + // Not sending any emails if the system considers the update insignificant. + if (!$iTipMessage->significantChange) { + if (!$iTipMessage->scheduleStatus) { + $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email'; + } + return; + } + + $recipient = preg_replace('!^mailto:!i', '', $iTipMessage->recipient); + $summary = strval($iTipMessage->message->VEVENT->SUMMARY); + + $rcube = rcube::get_instance(); + $sender = $rcube->user->get_identity(); + $sender_email = $sender['email'] ?: $rcube->get_user_email(); + $sender_name = $sender['name'] ?: $rcube->get_user_name(); + + $subject = 'KolabDAV iTIP message'; + switch (strtoupper($iTipMessage->method)) { + case 'REPLY' : + $subject = 'Re: ' . $summary; + break; + case 'REQUEST' : + $subject = 'Invitation: ' .$summary; + break; + case 'CANCEL' : + $subject = 'Cancelled: ' . $summary; + break; + } + + $sender = rcube_utils::idn_to_ascii($sender_email); + $from = format_email_recipient($sender, $sender_name); + $mailto = rcube_utils::idn_to_ascii($recipient); + $to = format_email_recipient($mailto, $iTipMessage->recipientName); + + // copy some missing properties from master event to make it validate in our clients + if (Plugin::$parsed_vevent && strval(Plugin::$parsed_vevent->UID) == strval($iTipMessage->uid)) { + if (isset(Plugin::$parsed_vevent->DTEND)) { + $iTipMessage->message->VEVENT->DTEND = clone Plugin::$parsed_vevent->DTEND; + } + if (isset(Plugin::$parsed_vevent->STATUS)) { + $iTipMessage->message->VEVENT->STATUS = strval(Plugin::$parsed_vevent->STATUS); + } + } + + // compose multipart message using PEAR:Mail_Mime + $message = new Mail_mime("\r\n"); + $message->setParam('text_encoding', 'quoted-printable'); + $message->setParam('head_encoding', 'quoted-printable'); + $message->setParam('head_charset', RCUBE_CHARSET); + $message->setParam('text_charset', RCUBE_CHARSET . ";\r\n format=flowed"); + + // compose common headers array + $headers = array( + 'To' => $to, + 'From' => $from, + 'Date' => date('r'), + 'Reply-To' => $originator, + 'Message-ID' => $rcube->gen_message_id(), + 'X-Sender' => $sender, + 'Subject' => $subject, + ); + if ($agent = $rcube->config->get('useragent')) + $headers['User-Agent'] = $agent; + + $message->headers($headers); + $message->setContentType('text/calendar', array('method' => strval($iTipMessage->method), 'charset' => RCUBE_CHARSET)); + $message->setTXTBody($iTipMessage->message->serialize()); + + // send message through Roundcube's SMTP feature + if ($rcube->deliver_message($message, $sender, $mailto, $smtp_error)) { + $iTipMessage->scheduleStatus = '1.1;Scheduling message sent via iMip'; + } + else { + rcube::raise_error(array( + 'code' => 600, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Failed to send iTIP message to " . $mailto), + true, false); + } + } + +} diff --git a/lib/Kolab/CalDAV/Plugin.php b/lib/Kolab/CalDAV/Plugin.php index 3f71070..fce1607 100644 --- a/lib/Kolab/CalDAV/Plugin.php +++ b/lib/Kolab/CalDAV/Plugin.php @@ -1,171 +1,229 @@ * * Copyright (C) 2013, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Kolab\CalDAV; use Sabre\DAV; use Sabre\CalDAV; use Sabre\VObject; +use Sabre\HTTP; +use Sabre\HTTP\URLUtil; 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->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 + * @param bool $modified Should be set to true, if this event handler + * changed &$data. + * @param RequestInterface $request The http request. + * @param ResponseInterface $response The http response. + * @param bool $isNew Is the item a new one, or an update. * @return void */ - protected function validateICalendar(&$data, $path) + protected function validateICalendar(&$data, $path, &$modified, HTTP\RequestInterface $request, HTTP\ResponseInterface $response, $isNew) { // If it's a stream, we convert it to a string first. if (is_resource($data)) { $data = stream_get_contents($data); } + $before = md5($data); // Converting the data to unicode, if needed. $data = DAV\StringUtil::ensureUTF8($data); + if ($before !== md5($data)) + $modified = true; + 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; + // If the data starts with a [, we can reasonably assume we're dealing + // with a jCal object. + if (substr($data,0,1) === '[') { + $vobj = VObject\Reader::readJson($data); + + // Converting $data back to iCalendar, as that's what we + // technically support everywhere. + $data = $vobj->serialize(); + $modified = true; + } else { + // 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()); + throw new DAV\Exception\UnsupportedMediaType('This resource only supports 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.'); } + $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + // 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(); + list($parentPath) = URLUtil::splitPath($path); + $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]); + + if (isset($calendarProperties[$sCCS])) { + $supportedComponents = $calendarProperties[$sCCS]->getValue(); + } + else { + $supportedComponents = ['VTODO', 'VEVENT']; + } $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); + throw new CalDAV\Exception\InvalidComponentType('This resource 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'); + + // We use an extra variable to allow event handles to tell us wether + // the object was modified or not. + // + // This helps us determine if we need to re-serialize the object. + $subModified = false; + + $this->server->emit( + 'calendarObjectChange', + [ + $request, + $response, + $vobj, + $parentPath, + &$subModified, + $isNew + ] + ); + + if ($subModified) { + // An event handler told us that it modified the object. + $data = $vobj->serialize(); + + // Using md5 to figure out if there was an *actual* change. + if (!$modified && $before !== md5($data)) { + $modified = true; + } + } } /** * 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; } } \ No newline at end of file diff --git a/lib/Kolab/CalDAV/SchedulePlugin.php b/lib/Kolab/CalDAV/SchedulePlugin.php index 6ae5151..dbf4f0b 100644 --- a/lib/Kolab/CalDAV/SchedulePlugin.php +++ b/lib/Kolab/CalDAV/SchedulePlugin.php @@ -1,100 +1,100 @@ * * Copyright (C) 2014, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Kolab\CalDAV; use Sabre\DAV; use Sabre\CalDAV; use Sabre\VObject; use Sabre\HTTP; /** * Extended CalDAV Schedule plugin */ class SchedulePlugin extends CalDAV\Schedule\Plugin { /** * 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); + $email = preg_replace('!^mailto:!i', '', $email); // pass-through the pre-generatd free/busy feed from Kolab's free/busy service if ($fburl = \kolab_storage::get_freebusy_url($email)) { try { $rcube = \rcube::get_instance(); $client = new HTTP\Client(); $client->addCurlSetting(CURLOPT_SSL_VERIFYPEER, $rcube->config->get('kolab_ssl_verify_peer', true)); // authentication required $client->on('error:401', function($request, $response, &$retry, $retryCount) { if ($retryCount <= 1) { // We're only going to retry exactly once. $request->setHeader('Authorization', 'Basic ' . base64_encode(HTTPBasic::$current_user . ':' . HTTPBasic::$current_pass)); $retry = true; } }); $response = $client->send(new HTTP\Request('GET', $fburl)); // success! if ($response->getStatus() == 200) { $vcalendar = VObject\Reader::read($response->getBodyAsString(), 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/CalDAV/UserCalendars.php b/lib/Kolab/CalDAV/UserCalendars.php index 3573baa..6c7c88f 100644 --- a/lib/Kolab/CalDAV/UserCalendars.php +++ b/lib/Kolab/CalDAV/UserCalendars.php @@ -1,167 +1,118 @@ * * Copyright (C) 2013, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Kolab\CalDAV; use Sabre\DAV; use Sabre\DAVACL; use Sabre\CalDAV\Backend; use Sabre\CalDAV\Schedule; 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 +class UserCalendars extends \Sabre\CalDAV\CalendarHome implements DAV\IExtendedCollection, DAVACL\IACL { - private $outbox; - - /** - * 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); - } - } - - // add support for scheduling AKA free/busy - // TODO: remove when CalendarBackend implements SchedulingSupport - $objs[] = new Schedule\Outbox($this->principalInfo['uri']); - - // 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; - } - - /** - * Returns a single calendar, by name - * - * @param string $name - * @return Calendar - */ - public function getChild($name) - { - if ($name == 'outbox') { - return new Schedule\Outbox($this->principalInfo['uri']); - } - if ($calendar = $this->caldavBackend->getCalendarByName($name)) { - $calendar['principaluri'] = $this->principalInfo['uri']; - return new Calendar($this->caldavBackend, $calendar); - } - - throw new DAV\Exception\NotFound('Calendar with name \'' . $name . '\' could not be found'); - } - /** * Checks if a calendar exists. * * @param string $name * @return bool */ public function childExists($name) { + // Special nodes + if ($name === 'inbox' || $name === 'outbox') { + return true; + } + if ($name === 'notifications') { + return false; + } + if ($this->caldavBackend->getCalendarByName($name)) { return true; } return false; } /** * 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() { // define rights for the user's calendar root (which is in fact INBOX) return array( array( 'privilege' => '{DAV:}read', 'principal' => $this->principalInfo['uri'], 'protected' => true, ), array( 'privilege' => '{DAV:}write', 'principal' => $this->principalInfo['uri'], 'protected' => true, ), /* TODO: implement sharing support 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'); } } diff --git a/lib/Kolab/DAVACL/PrincipalBackend.php b/lib/Kolab/DAVACL/PrincipalBackend.php index d22cd8e..eb21111 100644 --- a/lib/Kolab/DAVACL/PrincipalBackend.php +++ b/lib/Kolab/DAVACL/PrincipalBackend.php @@ -1,256 +1,260 @@ * * Copyright (C) 2013, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Kolab\DAVACL; use \rcube; +use \libcalendaring; use Sabre\DAV\Exception; use Sabre\DAV\URLUtil; use Kolab\DAV\Auth\HTTPBasic; /** * Kolab Principal Backend */ 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(); + $emails = libcalendaring::get_instance()->get_user_emails(); + 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, + '{DAV:}alternate-URI-set' => array_map(function($email) { + return 'mailto:' . $email; + }, $emails), ); } 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($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, \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, $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 7bef0f6..2113143 100644 --- a/lib/Kolab/Utils/DAVBackend.php +++ b/lib/Kolab/Utils/DAVBackend.php @@ -1,256 +1,259 @@ * * Copyright (C) 2013, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ 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; }); + + // silently accept the other/non-supported properties + $propPatch->setRemainingResultCode(204); } /** * 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 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/public_html/index.php b/public_html/index.php index 348c52c..2919763 100644 --- a/public_html/index.php +++ b/public_html/index.php @@ -1,199 +1,197 @@ * * Copyright (C) 2013-2014, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ // define some environment variables used throughout the app and libraries define('KOLAB_DAV_ROOT', realpath('../')); 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(); - $server->addPlugin($caldav_plugin); - + $server->addPlugin(new \Kolab\CalDAV\Plugin()); $server->addPlugin(new \Kolab\CalDAV\SchedulePlugin()); - #$server->addPlugin(new \Kolab\CalDAV\IMipPlugin()); + $server->addPlugin(new \Kolab\CalDAV\IMipPlugin('')); } 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->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->emit('exit', array());