diff --git a/lib/Kolab/CalDAV/CalendarBackend.php b/lib/Kolab/CalDAV/CalendarBackend.php index edcc022..27cda9e 100644 --- a/lib/Kolab/CalDAV/CalendarBackend.php +++ b/lib/Kolab/CalDAV/CalendarBackend.php @@ -1,1197 +1,1198 @@ * * 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 \rcube_message; 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 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; $default_calendar_id = null; 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++, ); if ($folder->default && $folder->type == 'event') { $default_calendar_id = $id; } $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; } } // put default calendar on top of the list: // {urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL is derived from the first item on this list if ($default_calendar_id) { $this->calendars = array($default_calendar_id => $this->calendars[$default_calendar_id]) + $this->calendars; } 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]) { DAVBackend::check_storage_folder($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); if (!$this->is_current_pricipal($principalUri)) { return array(); } // Reset imap cache so we work with up-to-date folders list // We do this only when a client requests list of calendars, // and we assume clients do not ask for list too often (Bifrost#T175679) rcube::get_instance()->get_storage()->clear_cache('mailboxes', true); $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]) && !in_array($id, array('inbox','outbox','notifications')) && $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_proppatch($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, true) as $event) { // post-filter events to suppress declined invitations if (!$this->_event_filter_compare($event)) { continue; } // Make sure to not return invalid objects if (empty($event) || empty($event['uid'])) { 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']); } } if (is_array($filter['prop-filters'])) { foreach ($filter['prop-filters'] as $prop_filter) { $match = $prop_filter['text-match']; if ($match['value']) { $op = $match['negate-condition'] ? '!=' : '='; switch ($prop_filter['name']) { case 'UID': $query[] = array('uid', $op, $match['value']); break; } } } } } $results = array(); if ($storage = $this->get_storage_folder($calendarId)) { foreach ($storage->select($query, true) as $event) { // post-filter events to suppress declined invitations if ($this->_event_filter_compare($event)) { $results[] = VObjectUtils::uid2uri($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(); } $uid = VObjectUtils::uri2uid($objectUri, '.ics'); list($msguid, $mime_id) = explode('-', $uid, 2); $message = new rcube_message($msguid, 'INBOX'); if ($ical = $message->get_part_content($mime_id, null, false, 0, false)) { if ($event = $this->parse_calendar_data($ical, null)) { if ($event['_type'] == 'event') { $event['_msguid'] = $msguid; return array( 'uri' => $objectUri, 'calendardata' => $ical, 'lastmodified' => $event['changed'] ? $event['changed']->format('U') : null, 'etag' => self::_get_etag($event), ); } } } return array(); } /** * 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(); $threshold = new \DateTime('now - 7 days'); // we can only access the current user's calendars (if enabled) if (!$this->is_current_pricipal($principalUri) || !rcube::get_instance()->config->get('kolabdav_caldav_inbox', false)) { return $results; } // find iTip messages in users email INBOX and extract the ics attachment. foreach ($this->search_email_inbox() as $msg) { list($msguid, $mime_id) = $msg; $message = new rcube_message($msguid, 'INBOX'); if ($ical = $message->get_part_content($mime_id, null, false, 0, false)) { if ($event = $this->parse_calendar_data($ical, null)) { if ($event['_type'] != 'event') { continue; } // filter past event invitations if (is_a($event['end'], '\\DateTime') && $event['end'] < $threshold && empty($event['recurrence'])) { console(sprintf('Skip iTip message for past event: %s // %s // %s', $event['uid'], $event['title'], $event['end']->format('c'))); continue; } $event['_msguid'] = $msguid; $results[] = array( 'uri' => VObjectUtils::uid2uri($msguid . '-' . $mime_id, '.ics'), 'calendardata' => $ical, 'lastmodified' => $event['changed'] ? $event['changed']->format('U') : null, 'etag' => self::_get_etag($event), ); } } } 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); $rcube = rcube::get_instance(); // we can only access the current user's inbox (if enabled) if (!$this->is_current_pricipal($principalUri) || !$rcube->config->get('kolabdav_caldav_inbox', false)) { return; } // get the referenced iTip message from email INBOX and // copy it to the default calendar. This will also remove the message // from the email inbox as the message is considered 'processed'. $uid = VObjectUtils::uri2uid($objectUri, '.ics'); list($msguid, $mime_id) = explode('-', $uid, 2); $message = new rcube_message($msguid, 'INBOX'); if ($ical = $message->get_part_content($mime_id, null, false, 0, false)) { if ($object = $this->parse_calendar_data($ical, null)) { if ($object['_type'] != 'event') { return; } console('Copy iTip Schedule object', $object); // get default calendar and search for an existing copy $calendars = $this->_read_calendars(); $calendarId = reset(array_keys($calendars)); // select private/confidential calendar folder if (!empty($object['sensitivity'])) { foreach ($calendars as $calid => $calprop) { if (($folder = $this->folders[$calid]) && $object['sensitivity'] === $folder->subtype) { $calendarId = $calid; } } } $storage = $this->get_storage_folder($calendarId); $existing = $storage->get_object($object['uid']); $update = true; // copy meta data (starting with _) from old object if (!empty($existing)) { // ignore update if existing is newer if ($existing['sequence'] > $object['sequence']) { $update = false; } foreach ((array)$existing as $key => $val) { if (!isset($object[$key]) && $key[0] == '_') $object[$key] = $val; } } // act according to the scheduling method switch ($object['_method']) { case 'REQUEST': // store the new version break; case 'REPLY': if (!empty($existing)) { // TODO: only update attendee status(es) on the existing event // as in pykolab/wallace/module_invitationpolicy/process_itip_reply() // FIXME: replies can refer to single recurrence instances $attendee = null; $object = $existing; $update = false; } else { $update = false; } break; case 'CANCEL': // set status to cancelled if (!empty($existing)) { $existing['cancelled'] = true; $existing['status'] = 'cancelled'; $object = $existing; } else { $update = false; } break; default: console('iTip method ' . $object['_method'] . ' not supported; ignoring'); $update = false; } if ($update) { // map attachments attribute $object['_attachments'] = $object['attachments']; + // @phpstan-ignore-next-line unset($object['attachments']); $success = $storage->save($object, $object['_type'], $existing['uid']); 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'); } } // remove iTip message from email inbox or move to Trash instead ? $imap = $rcube->get_storage(); $done = $imap->move_message($msguid, 'Trash', 'INBOX'); console("MOVE $msguid to Trash", $done); //console("DELETE $msguid", $imap->delete_message($msguid, 'INBOX')); } } } /** * 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? } } /** * Return the ctag value for the scheduling inbox */ public function getSchedulingInboxCtag($principalUri) { $rcube = rcube::get_instance(); if ($this->is_current_pricipal($principalUri) && $rcube->config->get('kolabdav_caldav_inbox', false)) { // we could use the INBOX imap folder properties but these are likely subject to // frequent changes without new invitations. Let's count potential iTip messages: $candidates = $this->search_email_inbox(); if (count($candidates)) { $fdata = $rcube->storage->folder_data('INBOX'); return sprintf('%d-%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT'], count($candidates) ); } } return "empty-000"; } /** * */ protected function search_email_inbox() { $result = array(); $rcube = rcube::get_instance(); $imap = $rcube->get_storage(); // FIXME: search by content-type doesn't return any results from Cyrus IMAP // $query = $imap->search_once('INBOX', 'UNDELETED OR OR HEADER Content-Type text/calendar HEADER Content-Type multipart/mixed HEADER Content-Type multipart/alternative'); $query = $imap->search_once('INBOX', 'UNDELETED'); if ($query && $query->count() > 0) { foreach ($query->get() as $msguid) { // get bodystructure and check for iTip parts $message = new rcube_message($msguid, 'INBOX'); foreach ((array)$message->mime_parts as $part) { if (self::part_is_itip($part)) { $result[] = array($msguid, $part->mime_id); } } } } return $result; } /** * Checks if specified message part is a vcalendar data * * @param rcube_message_part Part object * @return boolean True if part is of type vcard */ protected static function part_is_itip($part) { return ( in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) || ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename)) ) && !empty($part->ctype_parameters['method']); } /********** 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)) { $objects[0]['_method'] = $ical->method; 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); }; } $events = array($event); // add more instances from exceptions (not recurrence) to the output if (!empty($event['exceptions']) && empty($event['recurrence'])) { $events = array_merge($events, $event['exceptions']); } return $ical->export($events, 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($inbox = false) { // get email addresses of the current user $user_emails = $this->get_user_emails(); $query = $subquery = array(); // add query to exclude declined invitations foreach ($user_emails as $email) { 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, $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)) { if ($attendee['status'] == 'DECLINED') { return false; } else if ($inbox && $attendee['status'] == 'NEEDS-ACTION') { 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/SchedulePlugin.php b/lib/Kolab/CalDAV/SchedulePlugin.php index 7500084..fc159bc 100644 --- a/lib/Kolab/CalDAV/SchedulePlugin.php +++ b/lib/Kolab/CalDAV/SchedulePlugin.php @@ -1,103 +1,103 @@ * * 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; use Kolab\DAV\Auth\HTTPBasic; /** * 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 \DateTimeInterface $start + * @param \DateTimeInterface $end * @param VObject\Component $request * @return array */ - protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request) + protected function getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request) { console(__METHOD__, $email, $start, $end); $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, $start, $end)) { 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); $vcalendar->METHOD = 'REPLY'; return array( 'calendar-data' => $vcalendar, 'request-status' => '2.0;Success', 'href' => 'mailto:' . $email, ); } } catch (\Exception $e) { // log failures \rcube::raise_error($e, true, false); } } else { // generate free/busy data from this user's calendars return parent::getFreeBusyForEmail($email, $start, $end, $request); } // return "not found" return array( 'request-status' => '3.7;Could not find principal', 'href' => 'mailto:' . $email, ); } -} \ No newline at end of file +} diff --git a/lib/Kolab/CardDAV/LDAPDirectory.php b/lib/Kolab/CardDAV/LDAPDirectory.php index 489d80b..1487f5e 100644 --- a/lib/Kolab/CardDAV/LDAPDirectory.php +++ b/lib/Kolab/CardDAV/LDAPDirectory.php @@ -1,579 +1,579 @@ * * 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\CardDAV; use \rcube; use \rcube_ldap; use \rcube_ldap_generic; use Sabre\DAV; use Sabre\DAVACL; use Sabre\CardDAV\Property; use Kolab\Utils\VObjectUtils; /** * CardDAV Directory Gateway implementation */ class LDAPDirectory extends DAV\Collection implements \Sabre\CardDAV\IDirectory, DAV\IProperties, DAVACL\IACL { const DIRECTORY_NAME = 'ldap-directory'; protected $config; protected $ldap; protected $carddavBackend; protected $principalUri; protected $addressBookInfo = array(); protected $cache; protected $query; protected $filter; /** * Default constructor */ function __construct($config, $principalUri, $carddavBackend = null) { $this->config = $config; $this->principalUri = $principalUri; $this->addressBookInfo = array( 'id' => self::DIRECTORY_NAME, 'uri' => self::DIRECTORY_NAME, '{DAV:}displayname' => $config['name'] ?: "LDAP Directory", '{urn:ietf:params:xml:ns:carddav}supported-address-data' => new Property\SupportedAddressData(), 'principaluri' => $principalUri, ); // used for vcard serialization $this->carddavBackend = $carddavBackend ?: new ContactsBackend(); $this->carddavBackend->ldap_directory = $this; // initialize cache $rcube = rcube::get_instance(); if ($rcube->config->get('kolabdav_ldap_cache')) { $this->cache = $rcube->get_cache_shared('kolabdav_ldap'); // expunge cache every now and then if (rand(0,10) === 0) { $this->cache->expunge(); } } } private function connect() { if (!isset($this->ldap)) { $this->ldap = new rcube_ldap($this->config, $this->config['debug']); $this->ldap->set_pagesize($this->config['sizelimit'] ?: 10000); } return $this->ldap->ready ? $this->ldap : null; } /** * Set parsed addressbook-query object for filtering */ function setAddressbookQuery($query) { $this->query = $query; $this->filter = $this->addressbook_query2ldap_filter($query); } /** * Returns the name of the node. * * This is used to generate the url. * * @return string */ function getName() { return self::DIRECTORY_NAME; } /** * Returns a specific child node, referenced by its name * * This method must throw Sabre\DAV\Exception\NotFound if the node does not * exist. * * @param string $name * @return DAV\INode */ function getChild($cardUri) { console(__METHOD__, $cardUri); $uid = VObjectUtils::uri2uid($cardUri, '.vcf'); $record = null; // get from cache $cache_key = $uid; if ($this->cache && ($cached = $this->cache->get($cache_key))) { return new LDAPCard($this->carddavBackend, $this->addressBookInfo, $cached); } if ($contact = $this->getContactObject($uid)) { $obj = array( 'id' => $contact['uid'], 'uri' => VObjectUtils::uid2uri($contact['uid'], '.vcf'), 'lastmodified' => $contact['_timestamp'], 'carddata' => $this->carddavBackend->to_vcard($contact), 'etag' => self::_get_etag($contact), ); // cache this object if ($this->cache) { $this->cache->set($cache_key, $obj); } return new LDAPCard($this->carddavBackend, $this->addressBookInfo, $obj); } throw new DAV\Exception\NotFound('Card not found'); } /** * Read contact object from LDAP */ function getContactObject($uid) { $contact = null; if ($ldap = $this->connect()) { $ldap->reset(); // used cached uid mapping $cached_index = $this->cache ? $this->cache->get('index') : array(); if ($cached_index[$uid]) { $contact = $ldap->get_record($cached_index[$uid][0], true); } else { // query for uid $result = $ldap->search('uid', $uid, 1, true, true); if ($result->count) { $contact = $result[0]; } } if ($contact) { $this->_normalize_contact($contact); } } return $contact; } /** * Returns an array with all the child nodes * * @return DAV\INode[] */ function getChildren() { console(__METHOD__, $this->query, $this->filter); $children = array(); // return cached index if (!$this->query && !$this->config['searchonly'] && $this->cache && ($cached_index = $this->cache->get('index'))) { foreach ($cached_index as $uid => $c) { $obj = array( 'id' => $uid, 'uri' => VObjectUtils::uid2uri($uid, '.vcf'), 'etag' => $c[1], 'lastmodified' => $c[2], ); $children[] = new LDAPCard($this->carddavBackend, $this->addressBookInfo, $obj); } return $children; } // query LDAP if we have a search query or listing is allowed if (($this->query || !$this->config['searchonly']) && ($ldap = $this->connect())) { // set pagesize from query limit attribute if ($this->query && $this->query->limit) { $this->ldap->set_pagesize(intval($this->query->limit)); } // set the prepared LDAP filter derived from the addressbook-query if ($this->query && !empty($this->filter)) { $ldap->set_search_set($this->filter); } else { $ldap->set_search_set(null); } $results = $ldap->list_records(null); $directory_index = array(); // convert results into vcard blocks foreach ($results as $contact) { $this->_normalize_contact($contact); $obj = array( 'id' => $contact['uid'], 'uri' => VObjectUtils::uid2uri($contact['uid'], '.vcf'), 'lastmodified' => $contact['_timestamp'], 'carddata' => $this->carddavBackend->to_vcard($contact), 'etag' => self::_get_etag($contact), ); // cache record $cache_key = $contact['uid']; if ($this->cache) { $this->cache->set($cache_key, $obj); } $directory_index[$contact['uid']] = array($contact['ID'], $obj['etag'], $contact['_timestamp']); // add CardDAV node $children[] = new LDAPCard($this->carddavBackend, $this->addressBookInfo, $obj); } // cache the full listing if (empty($this->filter) && $this->cache) { $this->cache->set('index', $directory_index); } } return $children; } /** * Returns a list of properties for this node. * * The properties list is a list of propertynames the client requested, * encoded in clark-notation {xmlnamespace}tagname * * If the array is empty, it means 'all properties' were requested. * * @param array $properties * @return array */ public function getProperties($properties) { console(__METHOD__, $properties); $response = array(); foreach ($properties as $propertyName) { if (isset($this->addressBookInfo[$propertyName])) { $response[$propertyName] = $this->addressBookInfo[$propertyName]; } else if ($propertyName == '{DAV:}getlastmodified') { $response[$propertyName] = new DAV\Property\GetLastModified($this->getLastModified()); } } return $response; } /** * Returns the last modification time, as a unix timestamp * * @return int */ function getLastModified() { console(__METHOD__); return time(); } /** * Deletes the entire addressbook. * * @return void */ public function delete() { throw new DAV\Exception\MethodNotAllowed('Deleting directories is not allowed'); } /** * Renames the addressbook * * @param string $newName * @return void */ public function setName($newName) { throw new DAV\Exception\MethodNotAllowed('Renaming directories not allowed'); } /** * Returns the owner principal * * This must be a url to a principal, or null if there's no owner * * @return string|null */ public function getOwner() { return $this->principalUri; } /** * Returns a group principal * * This must be a url to a principal, or null if there's no owner * * @return string|null */ function getGroup() { return null; } /** * Returns a list of ACE's for this node. * * Each ACE has the following properties: * * 'privilege', a string such as {DAV:}read or {DAV:}write * * 'principal', a url to the principal who owns the node * * 'protected' (optional), indicating that this ACE is not allowed to be updated. * * @return array */ public function getACL() { $acl = array( array( 'privilege' => '{DAV:}read', 'principal' => $this->principalUri, 'protected' => true, ), ); return $acl; } /** * Updates the ACL * * @param array $acl * @return void */ function setACL(array $acl) { throw new DAV\Exception\MethodNotAllowed('Changing ACL for directories is not allowed'); } /** * Returns the list of supported privileges for this node. * * If null is returned from this method, the default privilege set is used, * which is fine for most common usecases. * * @return array|null */ function getSupportedPrivilegeSet() { return null; } /** * Updates properties on this node. * * @param PropPatch $propPatch * @return void */ public function propPatch(DAV\PropPatch $propPatch) { console(__METHOD__, $propPatch); // NOP } /** * Post-process the given contact record from rcube_ldap */ private function _normalize_contact(&$contact) { if (is_numeric($contact['changed'])) { $contact['_timestamp'] = intval($contact['changed']); $contact['changed'] = new \DateTime('@' . $contact['changed']); } else if (!empty($contact['changed'])) { try { // 2018 05 14 06 22 31 .0Z 2018-05-14T06:22:31 $contact['changed'] = preg_replace('/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.0Z$/', '$1-$2-$3T$4:$5:$6', $contact['changed']); $contact['changed'] = new \DateTime($contact['changed']); $contact['_timestamp'] = intval($contact['changed']->format('U')); } - catch (Exception $e) { + catch (\Exception $e) { $contact['changed'] = null; } } // map col:subtype fields to a list that the vcard serialization function understands foreach (array('email' => 'address', 'phone' => 'number', 'website' => 'url') as $col => $prop) { foreach (rcube_ldap::get_col_values($col, $contact) as $type => $values) { foreach ((array)$values as $value) { $contact[$col][] = array($prop => $value, 'type' => $type); } } unset($contact[$col.':'.$type]); } $addresses = array(); foreach (rcube_ldap::get_col_values('address', $contact) as $type => $values) { foreach ((array)$values as $adr) { // skip empty address $adr = array_filter($adr); if (empty($adr)) continue; $addresses[] = array( 'type' => $type, 'street' => $adr['street'], 'locality' => $adr['locality'], 'code' => $adr['zipcode'], 'region' => $adr['region'], 'country' => $adr['country'], ); } unset($contact['address:'.$type]); } $contact['address'] = $addresses; } /** * Translate the given AddressBookQueryParser object into an LDAP filter */ private function addressbook_query2ldap_filter($query) { $criterias = array(); foreach ($query->filters as $filter) { $ldap_attrs = $this->map_property2ldap($filter['name']); $ldap_filter = ''; $count = 0; // unknown attribute, skip if (empty($ldap_attrs)) { continue; } foreach ((array)$filter['text-matches'] as $matcher) { // case-insensitive matching if (in_array($matcher['collation'], array('i;unicode-casemap', 'i;ascii-casemap'))) { $matcher['value'] = mb_strtolower($matcher['value']); } $value = rcube_ldap_generic::quote_string($matcher['value']); $ldap_match = ''; // this assumes fuzzy search capabilities of the LDAP backend switch ($matcher['match-type']) { case 'contains': $wp = $ws = '*'; break; case 'starts-with': $ws = '*'; break; case 'ends-with': $wp = '*'; break; default: $wp = $ws = ''; } // OR query for all attributes involved if (count($ldap_attrs) > 1) { $ldap_match .= '(|'; } foreach ($ldap_attrs as $attr) { $ldap_match .= "($attr=$wp$value$ws)"; } if (count($ldap_attrs) > 1) { $ldap_match .= ')'; } // negate the filter if ($matcher['negate-condition']) { $ldap_match = '(!' . $ldap_match . ')'; } $ldap_filter .= $ldap_match; $count++; } if ($count > 1) { $criterias[] = '(' . ($filter['test'] == 'allof' ? '&' : '|') . $ldap_filter . ')'; } else if (!empty($ldap_filter)) { $criterias[] = $ldap_filter; } } return empty($criterias) ? '' : sprintf('(%s%s)', $query->test == 'allof' ? '&' : '|', join('', $criterias)); } /** * Map a vcard property to an LDAP attribute */ private function map_property2ldap($propname) { $attribs = array(); // LDAP backend not available, abort if (!($ldap = $this->connect())) { return $attribs; } $vcard_fieldmap = array( 'FN' => array('name'), 'N' => array('surname','firstname','middlename'), 'ADR' => array('street','locality','region','code','country'), 'TITLE' => array('jobtitle'), 'ORG' => array('organization','department'), 'TEL' => array('phone'), 'URL' => array('website'), 'ROLE' => array('profession'), 'BDAY' => array('birthday'), 'IMPP' => array('im'), ); $fields = $vcard_fieldmap[$propname] ?: array(strtolower($propname)); foreach ($fields as $field) { if ($ldap->coltypes[$field]) { $attribs = array_merge($attribs, (array)$ldap->coltypes[$field]['attributes']); } } return $attribs; } /** * Generate an Etag string from the given contact data * * @param array Hash array with contact properties from libkolab * @return string Etag string */ private static function _get_etag($contact) { return sprintf('"%s-%d"', substr(md5($contact['uid']), 0, 16), $contact['_timestamp']); } } diff --git a/lib/Kolab/DAV/TempFilesPlugin.php b/lib/Kolab/DAV/TempFilesPlugin.php index 5a19cd6..2c0138c 100644 --- a/lib/Kolab/DAV/TempFilesPlugin.php +++ b/lib/Kolab/DAV/TempFilesPlugin.php @@ -1,85 +1,86 @@ * * 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\DAV; use Sabre\DAV; use Kolab\DAV\Auth\HTTPBasic; /** * Temporary File Filter Plugin * * The purpose of this filter is to intercept some of the garbage files * operation systems and applications tend to generate when mounting * a WebDAV share as a disk. */ class TempFilesPlugin extends DAV\TemporaryFileFilterPlugin { protected $baseDir; + private $dataDir; /** * Creates the plugin. * * @param string $baseDir Temp directoy base path */ public function __construct($baseDir) { $this->baseDir = $baseDir; } /** * This method returns the directory where the temporary files should be stored. * * @return string */ protected function getDataDir() { if (!$this->dataDir) { $this->dataDir = $this->baseDir . '/' . str_replace('@', '_', HTTPBasic::$current_user); if (!is_dir($this->dataDir)) { mkdir($this->dataDir); } // run a cleanup routine on every 100th request if (rand(0,100) == 100) { $this->cleanup(); } } return $this->dataDir; } /** * Tempfile cleanup routine to remove files not touched for 24 hours */ protected function cleanup() { $expires = time() - 86400; foreach (glob($this->dataDir . '/*.tempfile') as $file) { if (filemtime($file) < $expires) { unlink($file); } } } } diff --git a/lib/Kolab/Utils/DAVBackend.php b/lib/Kolab/Utils/DAVBackend.php index 2e47b31..9776a51 100644 --- a/lib/Kolab/Utils/DAVBackend.php +++ b/lib/Kolab/Utils/DAVBackend.php @@ -1,304 +1,304 @@ * * 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; use Sabre\DAV; /** * */ class DAVBackend { 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) { self::check_storage_folder($folder); return $folder; } } - self::check_storage_folder(null); + throw new DAV\Exception\NotFound('The requested collection was not found'); } /** * Check the given storage folder instance for validity and throw * the right exceptions according to the error state. */ public static function check_storage_folder($folder) { if (empty($folder)) { throw new DAV\Exception\NotFound('The requested collection was not found'); } if (!$folder->valid || $folder->get_error()) { $error = $folder->get_error(); if ($error === kolab_storage::ERROR_IMAP_CONN) { throw new DAV\Exception\ServiceUnavailable('The service is temporarily unavailable (Storage failure)'); } else if ($error === kolab_storage::ERROR_CACHE_DB) { throw new DAV\Exception\ServiceUnavailable('The service is temporarily unavailable (Cache failure)'); } else if ($error === kolab_storage::ERROR_NO_PERMISSION) { throw new DAV\Exception\Forbidden('Access to this collection is not permitted'); } else if ($error === kolab_storage::ERROR_INVALID_FOLDER) { throw new DAV\Exception\NotFound('The requested collection was not found'); } throw new DAV\Exception('Internal Server Error'); } } /** * 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; } /** * Callback handler for property changes on the given folder * * @param kolab_storage_folder $folder Folder object * @param \Sabre\DAV\PropPatch $propPatch Property updates */ public static function handle_proppatch($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 + * @return bool|array */ 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; } // This is to fix potential MacOS client bug where // it sets the calendar name to the folder uid if ($val === $folder->get_uid()) { break; } // restrict renaming to personal folders only if ($folder->get_namespace() == 'personal') { // Sanity check, displayname can't be deleted if ($val === null) { break; } $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); throw new DAV\Exception('Internal Server Error'); } } 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) + public static 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])) $props['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); throw new DAV\Exception('Internal Server Error'); } // save UID in folder annotations if ($folder = kolab_storage::get_folder($fname)) { $folder->set_uid($uid); } return $uid; } }