diff options
Diffstat (limited to 'vendor/sabre/dav/lib/CalDAV')
67 files changed, 12220 insertions, 0 deletions
diff --git a/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php b/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php new file mode 100644 index 000000000..d58b4a46e --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php @@ -0,0 +1,226 @@ +<?php + +namespace Sabre\CalDAV\Backend; + +use Sabre\VObject; +use Sabre\CalDAV; + +/** + * Abstract Calendaring backend. Extend this class to create your own backends. + * + * Checkout the BackendInterface for all the methods that must be implemented. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +abstract class AbstractBackend implements BackendInterface { + + /** + * 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 + */ + function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) { + + } + + /** + * Returns a list of calendar objects. + * + * This method should work identical to getCalendarObject, but instead + * return all the calendar objects in the list as an array. + * + * If the backend supports this, it may allow for some speed-ups. + * + * @param mixed $calendarId + * @param array $uris + * @return array + */ + function getMultipleCalendarObjects($calendarId, array $uris) { + + return array_map(function($uri) use ($calendarId) { + return $this->getCalendarObject($calendarId, $uri); + }, $uris); + + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by \Sabre\CalDAV\CalendarQueryParser. + * + * Note that it is extremely likely that getCalendarObject for every path + * returned from this method will be called almost immediately after. You + * may want to anticipate this to speed up these requests. + * + * This method provides a default implementation, which parses *all* the + * iCalendar objects in the specified calendar. + * + * This default may well be good enough for personal use, and calendars + * that aren't very large. But if you anticipate high usage, big calendars + * or high loads, you are strongly adviced to optimize certain paths. + * + * The best way to do so is override this method and to optimize + * specifically for 'common filters'. + * + * Requests that are extremely common are: + * * requests for just VEVENTS + * * requests for just VTODO + * * requests with a time-range-filter on either VEVENT or VTODO. + * + * ..and combinations of these requests. It may not be worth it to try to + * handle every possible situation and just rely on the (relatively + * easy to use) CalendarQueryValidator to handle the rest. + * + * Note that especially time-range-filters may be difficult to parse. A + * time-range filter specified on a VEVENT must for instance also handle + * recurrence rules correctly. + * A good example of how to interprete all these filters can also simply + * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct + * as possible, so it gives you a good idea on what type of stuff you need + * to think of. + * + * @param mixed $calendarId + * @param array $filters + * @return array + */ + function calendarQuery($calendarId, array $filters) { + + $result = []; + $objects = $this->getCalendarObjects($calendarId); + + foreach ($objects as $object) { + + if ($this->validateFilterForObject($object, $filters)) { + $result[] = $object['uri']; + } + + } + + return $result; + + } + + /** + * This method validates if a filter (as passed to calendarQuery) matches + * the given object. + * + * @param array $object + * @param array $filters + * @return bool + */ + protected function validateFilterForObject(array $object, array $filters) { + + // Unfortunately, setting the 'calendardata' here is optional. If + // it was excluded, we actually need another call to get this as + // well. + if (!isset($object['calendardata'])) { + $object = $this->getCalendarObject($object['calendarid'], $object['uri']); + } + + $vObject = VObject\Reader::read($object['calendardata']); + + $validator = new CalDAV\CalendarQueryValidator(); + $result = $validator->validate($vObject, $filters); + + // Destroy circular references so PHP will GC the object. + $vObject->destroy(); + + return $result; + + } + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $principalUri + * @param string $uid + * @return string|null + */ + function getCalendarObjectByUID($principalUri, $uid) { + + // Note: this is a super slow naive implementation of this method. You + // are highly recommended to optimize it, if your backend allows it. + foreach ($this->getCalendarsForUser($principalUri) as $calendar) { + + // We must ignore calendars owned by other principals. + if ($calendar['principaluri'] !== $principalUri) { + continue; + } + + // Ignore calendars that are shared. + if (isset($calendar['{http://sabredav.org/ns}owner-principal']) && $calendar['{http://sabredav.org/ns}owner-principal'] !== $principalUri) { + continue; + } + + $results = $this->calendarQuery( + $calendar['id'], + [ + 'name' => 'VCALENDAR', + 'prop-filters' => [], + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'is-not-defined' => false, + 'time-range' => null, + 'comp-filters' => [], + 'prop-filters' => [ + [ + 'name' => 'UID', + 'is-not-defined' => false, + 'time-range' => null, + 'text-match' => [ + 'value' => $uid, + 'negate-condition' => false, + 'collation' => 'i;octet', + ], + 'param-filters' => [], + ], + ] + ] + ], + ] + ); + if ($results) { + // We have a match + return $calendar['uri'] . '/' . $results[0]; + } + + } + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php b/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php new file mode 100644 index 000000000..7513fb60d --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php @@ -0,0 +1,268 @@ +<?php + +namespace Sabre\CalDAV\Backend; + +/** + * Every CalDAV backend must at least implement this interface. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface BackendInterface { + + /** + * Returns a list of calendars for a principal. + * + * Every project 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 is 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'. + * + * Many clients also require: + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * For this property, you can just return an instance of + * Sabre\CalDAV\Property\SupportedCalendarComponentSet. + * + * If you return {http://sabredav.org/ns}read-only and set the value to 1, + * ACL will automatically be put in read-only mode. + * + * @param string $principalUri + * @return array + */ + function getCalendarsForUser($principalUri); + + /** + * 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 + */ + function createCalendar($principalUri, $calendarUri, array $properties); + + /** + * 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 documentation for more info and examples. + * + * @param string $path + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch); + + /** + * Delete a calendar and all its objects + * + * @param mixed $calendarId + * @return void + */ + function deleteCalendar($calendarId); + + /** + * Returns all calendar objects within a calendar. + * + * Every item contains an array with the following keys: + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string, but making sure it ends with '.ics' is a + * good idea. This is only the basename, or filename, not the full + * path. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * '"abcdef"') + * * size - The size of the calendar objects, in bytes. + * * component - optional, a string containing the type of object, such + * as 'vevent' or 'vtodo'. If specified, this will be used to populate + * the Content-Type header. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * If neither etag or size are specified, the calendardata will be + * used/fetched to determine these numbers. If both are specified the + * amount of times this is needed is reduced by a great degree. + * + * @param mixed $calendarId + * @return array + */ + function getCalendarObjects($calendarId); + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * This method must return null if the object did not exist. + * + * @param mixed $calendarId + * @param string $objectUri + * @return array|null + */ + function getCalendarObject($calendarId, $objectUri); + + /** + * Returns a list of calendar objects. + * + * This method should work identical to getCalendarObject, but instead + * return all the calendar objects in the list as an array. + * + * If the backend supports this, it may allow for some speed-ups. + * + * @param mixed $calendarId + * @param array $uris + * @return array + */ + function getMultipleCalendarObjects($calendarId, array $uris); + + /** + * Creates a new calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible to 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 + */ + function createCalendarObject($calendarId, $objectUri, $calendarData); + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * 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 + */ + function updateCalendarObject($calendarId, $objectUri, $calendarData); + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param mixed $calendarId + * @param string $objectUri + * @return void + */ + function deleteCalendarObject($calendarId, $objectUri); + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by Sabre\CalDAV\CalendarQueryParser. + * + * Note that it is extremely likely that getCalendarObject for every path + * returned from this method will be called almost immediately after. You + * may want to anticipate this to speed up these requests. + * + * This method provides a default implementation, which parses *all* the + * iCalendar objects in the specified calendar. + * + * This default may well be good enough for personal use, and calendars + * that aren't very large. But if you anticipate high usage, big calendars + * or high loads, you are strongly adviced to optimize certain paths. + * + * The best way to do so is override this method and to optimize + * specifically for 'common filters'. + * + * Requests that are extremely common are: + * * requests for just VEVENTS + * * requests for just VTODO + * * requests with a time-range-filter on either VEVENT or VTODO. + * + * ..and combinations of these requests. It may not be worth it to try to + * handle every possible situation and just rely on the (relatively + * easy to use) CalendarQueryValidator to handle the rest. + * + * Note that especially time-range-filters may be difficult to parse. A + * time-range filter specified on a VEVENT must for instance also handle + * recurrence rules correctly. + * A good example of how to interprete all these filters can also simply + * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct + * as possible, so it gives you a good idea on what type of stuff you need + * to think of. + * + * @param mixed $calendarId + * @param array $filters + * @return array + */ + function calendarQuery($calendarId, array $filters); + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $principalUri + * @param string $uid + * @return string|null + */ + function getCalendarObjectByUID($principalUri, $uid); + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php b/vendor/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php new file mode 100644 index 000000000..19b9b22a7 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php @@ -0,0 +1,46 @@ +<?php + +namespace Sabre\CalDAV\Backend; + +use Sabre\CalDAV\Xml\Notification\NotificationInterface; + +/** + * Adds caldav notification support to a backend. + * + * Note: This feature is experimental, and may change in between different + * SabreDAV versions. + * + * Notifications are defined at: + * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-notifications.txt + * + * These notifications are basically a list of server-generated notifications + * displayed to the user. Users can dismiss notifications by deleting them. + * + * The primary usecase is to allow for calendar-sharing. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface NotificationSupport extends BackendInterface { + + /** + * Returns a list of notifications for a given principal url. + * + * @param string $principalUri + * @return NotificationInterface[] + */ + function getNotificationsForPrincipal($principalUri); + + /** + * This deletes a specific notifcation. + * + * This may be called by a client once it deems a notification handled. + * + * @param string $principalUri + * @param NotificationInterface $notification + * @return void + */ + function deleteNotification($principalUri, NotificationInterface $notification); + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Backend/PDO.php b/vendor/sabre/dav/lib/CalDAV/Backend/PDO.php new file mode 100644 index 000000000..76b69dc6e --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Backend/PDO.php @@ -0,0 +1,1210 @@ +<?php + +namespace Sabre\CalDAV\Backend; + +use Sabre\VObject; +use Sabre\CalDAV; +use Sabre\DAV; +use Sabre\DAV\Exception\Forbidden; + +/** + * PDO CalDAV backend + * + * This backend is used to store calendar-data in a PDO database, such as + * sqlite or MySQL + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport { + + /** + * We need to specify a max date, because we need to stop *somewhere* + * + * On 32 bit system the maximum for a signed integer is 2147483647, so + * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results + * in 2038-01-19 to avoid problems when the date is converted + * to a unix timestamp. + */ + const MAX_DATE = '2038-01-01'; + + /** + * pdo + * + * @var \PDO + */ + protected $pdo; + + /** + * The table name that will be used for calendars + * + * @var string + */ + public $calendarTableName = 'calendars'; + + /** + * The table name that will be used for calendar objects + * + * @var string + */ + public $calendarObjectTableName = 'calendarobjects'; + + /** + * The table name that will be used for tracking changes in calendars. + * + * @var string + */ + public $calendarChangesTableName = 'calendarchanges'; + + /** + * The table name that will be used inbox items. + * + * @var string + */ + public $schedulingObjectTableName = 'schedulingobjects'; + + /** + * The table name that will be used for calendar subscriptions. + * + * @var string + */ + public $calendarSubscriptionsTableName = 'calendarsubscriptions'; + + /** + * List of CalDAV properties, and how they map to database fieldnames + * Add your own properties by simply adding on to this array. + * + * Note that only string-based properties are supported here. + * + * @var array + */ + public $propertyMap = [ + '{DAV:}displayname' => 'displayname', + '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description', + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone', + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + ]; + + /** + * List of subscription properties, and how they map to database fieldnames. + * + * @var array + */ + public $subscriptionPropertyMap = [ + '{DAV:}displayname' => 'displayname', + '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate', + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos', + '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms', + '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments', + ]; + + /** + * Creates the backend + * + * @param \PDO $pdo + */ + function __construct(\PDO $pdo) { + + $this->pdo = $pdo; + + } + + /** + * Returns a list of calendars for a principal. + * + * Every project 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. This is just the 'base uri' or 'filename' of the calendar. + * * 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'. + * + * Many clients also require: + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * For this property, you can just return an instance of + * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet. + * + * If you return {http://sabredav.org/ns}read-only and set the value to 1, + * ACL will automatically be put in read-only mode. + * + * @param string $principalUri + * @return array + */ + function getCalendarsForUser($principalUri) { + + $fields = array_values($this->propertyMap); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'synctoken'; + $fields[] = 'components'; + $fields[] = 'principaluri'; + $fields[] = 'transparent'; + + // Making fields a comma-delimited list + $fields = implode(', ', $fields); + $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM " . $this->calendarTableName . " WHERE principaluri = ? ORDER BY calendarorder ASC"); + $stmt->execute([$principalUri]); + + $calendars = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $components = []; + if ($row['components']) { + $components = explode(',', $row['components']); + } + + $calendar = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{' . CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ? $row['synctoken'] : '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0', + '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet($components), + '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'), + ]; + + + foreach ($this->propertyMap as $xmlName => $dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + $calendars[] = $calendar; + + } + + return $calendars; + + } + + /** + * Creates a new calendar for a principal. + * + * If the creation was a success, an id must be returned that can be used + * to reference this calendar in other methods, such as updateCalendar. + * + * @param string $principalUri + * @param string $calendarUri + * @param array $properties + * @return string + */ + function createCalendar($principalUri, $calendarUri, array $properties) { + + $fieldNames = [ + 'principaluri', + 'uri', + 'synctoken', + 'transparent', + ]; + $values = [ + ':principaluri' => $principalUri, + ':uri' => $calendarUri, + ':synctoken' => 1, + ':transparent' => 0, + ]; + + // Default value + $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + $fieldNames[] = 'components'; + if (!isset($properties[$sccs])) { + $values[':components'] = 'VEVENT,VTODO'; + } else { + if (!($properties[$sccs] instanceof CalDAV\Xml\Property\SupportedCalendarComponentSet)) { + throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet'); + } + $values[':components'] = implode(',', $properties[$sccs]->getValue()); + } + $transp = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp'; + if (isset($properties[$transp])) { + $values[':transparent'] = $properties[$transp]->getValue() === 'transparent'; + } + + foreach ($this->propertyMap as $xmlName => $dbName) { + if (isset($properties[$xmlName])) { + + $values[':' . $dbName] = $properties[$xmlName]; + $fieldNames[] = $dbName; + } + } + + $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarTableName . " (" . implode(', ', $fieldNames) . ") VALUES (" . implode(', ', array_keys($values)) . ")"); + $stmt->execute($values); + + return $this->pdo->lastInsertId(); + + } + + /** + * 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 $calendarId + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) { + + $supportedProperties = array_keys($this->propertyMap); + $supportedProperties[] = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp'; + + $propPatch->handle($supportedProperties, function($mutations) use ($calendarId) { + $newValues = []; + foreach ($mutations as $propertyName => $propertyValue) { + + switch ($propertyName) { + case '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' : + $fieldName = 'transparent'; + $newValues[$fieldName] = $propertyValue->getValue() === 'transparent'; + break; + default : + $fieldName = $this->propertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + break; + } + + } + $valuesSql = []; + foreach ($newValues as $fieldName => $value) { + $valuesSql[] = $fieldName . ' = ?'; + } + + $stmt = $this->pdo->prepare("UPDATE " . $this->calendarTableName . " SET " . implode(', ', $valuesSql) . " WHERE id = ?"); + $newValues['id'] = $calendarId; + $stmt->execute(array_values($newValues)); + + $this->addChange($calendarId, "", 2); + + return true; + + }); + + } + + /** + * Delete a calendar and all it's objects + * + * @param string $calendarId + * @return void + */ + function deleteCalendar($calendarId) { + + $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarTableName . ' WHERE id = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarChangesTableName . ' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + } + + /** + * Returns all calendar objects within a calendar. + * + * Every item contains an array with the following keys: + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string, but making sure it ends with '.ics' is a + * good idea. This is only the basename, or filename, not the full + * path. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * ' "abcdef"') + * * size - The size of the calendar objects, in bytes. + * * component - optional, a string containing the type of object, such + * as 'vevent' or 'vtodo'. If specified, this will be used to populate + * the Content-Type header. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * If neither etag or size are specified, the calendardata will be + * used/fetched to determine these numbers. If both are specified the + * amount of times this is needed is reduced by a great degree. + * + * @param string $calendarId + * @return array + */ + function getCalendarObjects($calendarId) { + + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $result = []; + foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => $row['calendarid'], + 'size' => (int)$row['size'], + 'component' => strtolower($row['componenttype']), + ]; + } + + return $result; + + } + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * This method must return null if the object did not exist. + * + * @param string $calendarId + * @param string $objectUri + * @return array|null + */ + function getCalendarObject($calendarId, $objectUri) { + + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$row) return null; + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => $row['calendarid'], + 'size' => (int)$row['size'], + 'calendardata' => $row['calendardata'], + 'component' => strtolower($row['componenttype']), + ]; + + } + + /** + * Returns a list of calendar objects. + * + * This method should work identical to getCalendarObject, but instead + * return all the calendar objects in the list as an array. + * + * If the backend supports this, it may allow for some speed-ups. + * + * @param mixed $calendarId + * @param array $uris + * @return array + */ + function getMultipleCalendarObjects($calendarId, array $uris) { + + $query = 'SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri IN ('; + // Inserting a whole bunch of question marks + $query .= implode(',', array_fill(0, count($uris), '?')); + $query .= ')'; + + $stmt = $this->pdo->prepare($query); + $stmt->execute(array_merge([$calendarId], $uris)); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'calendarid' => $row['calendarid'], + 'size' => (int)$row['size'], + 'calendardata' => $row['calendardata'], + 'component' => strtolower($row['componenttype']), + ]; + + } + return $result; + + } + + + /** + * Creates a new calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * 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 + */ + function createCalendarObject($calendarId, $objectUri, $calendarData) { + + $extraData = $this->getDenormalizedData($calendarData); + + $stmt = $this->pdo->prepare('INSERT INTO ' . $this->calendarObjectTableName . ' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)'); + $stmt->execute([ + $calendarId, + $objectUri, + $calendarData, + time(), + $extraData['etag'], + $extraData['size'], + $extraData['componentType'], + $extraData['firstOccurence'], + $extraData['lastOccurence'], + $extraData['uid'], + ]); + $this->addChange($calendarId, $objectUri, 1); + + return '"' . $extraData['etag'] . '"'; + + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * 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 + */ + function updateCalendarObject($calendarId, $objectUri, $calendarData) { + + $extraData = $this->getDenormalizedData($calendarData); + + $stmt = $this->pdo->prepare('UPDATE ' . $this->calendarObjectTableName . ' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarData, time(), $extraData['etag'], $extraData['size'], $extraData['componentType'], $extraData['firstOccurence'], $extraData['lastOccurence'], $extraData['uid'], $calendarId, $objectUri]); + + $this->addChange($calendarId, $objectUri, 2); + + return '"' . $extraData['etag'] . '"'; + + } + + /** + * Parses some information from calendar objects, used for optimized + * calendar-queries. + * + * Returns an array with the following keys: + * * etag - An md5 checksum of the object without the quotes. + * * size - Size of the object in bytes + * * componentType - VEVENT, VTODO or VJOURNAL + * * firstOccurence + * * lastOccurence + * * uid - value of the UID property + * + * @param string $calendarData + * @return array + */ + protected function getDenormalizedData($calendarData) { + + $vObject = VObject\Reader::read($calendarData); + $componentType = null; + $component = null; + $firstOccurence = null; + $lastOccurence = null; + $uid = null; + foreach ($vObject->getComponents() as $component) { + if ($component->name !== 'VTIMEZONE') { + $componentType = $component->name; + $uid = (string)$component->UID; + break; + } + } + if (!$componentType) { + throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); + } + if ($componentType === 'VEVENT') { + $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); + // Finding the last occurence is a bit harder + if (!isset($component->RRULE)) { + if (isset($component->DTEND)) { + $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); + } elseif (isset($component->DURATION)) { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate = $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue())); + $lastOccurence = $endDate->getTimeStamp(); + } elseif (!$component->DTSTART->hasTime()) { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate = $endDate->modify('+1 day'); + $lastOccurence = $endDate->getTimeStamp(); + } else { + $lastOccurence = $firstOccurence; + } + } else { + $it = new VObject\Recur\EventIterator($vObject, (string)$component->UID); + $maxDate = new \DateTime(self::MAX_DATE); + if ($it->isInfinite()) { + $lastOccurence = $maxDate->getTimeStamp(); + } else { + $end = $it->getDtEnd(); + while ($it->valid() && $end < $maxDate) { + $end = $it->getDtEnd(); + $it->next(); + + } + $lastOccurence = $end->getTimeStamp(); + } + + } + } + + // Destroy circular references to PHP will GC the object. + $vObject->destroy(); + + return [ + 'etag' => md5($calendarData), + 'size' => strlen($calendarData), + 'componentType' => $componentType, + 'firstOccurence' => $firstOccurence, + 'lastOccurence' => $lastOccurence, + 'uid' => $uid, + ]; + + } + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param string $calendarId + * @param string $objectUri + * @return void + */ + function deleteCalendarObject($calendarId, $objectUri) { + + $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + + $this->addChange($calendarId, $objectUri, 3); + + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by \Sabre\CalDAV\CalendarQueryParser. + * + * Note that it is extremely likely that getCalendarObject for every path + * returned from this method will be called almost immediately after. You + * may want to anticipate this to speed up these requests. + * + * This method provides a default implementation, which parses *all* the + * iCalendar objects in the specified calendar. + * + * This default may well be good enough for personal use, and calendars + * that aren't very large. But if you anticipate high usage, big calendars + * or high loads, you are strongly adviced to optimize certain paths. + * + * The best way to do so is override this method and to optimize + * specifically for 'common filters'. + * + * Requests that are extremely common are: + * * requests for just VEVENTS + * * requests for just VTODO + * * requests with a time-range-filter on a VEVENT. + * + * ..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. + * + * This specific implementation (for the PDO) backend optimizes filters on + * specific components, and VEVENT time-ranges. + * + * @param string $calendarId + * @param array $filters + * @return array + */ + function calendarQuery($calendarId, array $filters) { + + $componentType = null; + $requirePostFilter = true; + $timeRange = null; + + // if no filters were specified, we don't need to filter after a query + if (!$filters['prop-filters'] && !$filters['comp-filters']) { + $requirePostFilter = false; + } + + // Figuring out if there's a component filter + if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) { + $componentType = $filters['comp-filters'][0]['name']; + + // Checking if we need post-filters + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) { + $requirePostFilter = false; + } + // There was a time-range filter + if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) { + $timeRange = $filters['comp-filters'][0]['time-range']; + + // If start time OR the end time is not specified, we can do a + // 100% accurate mysql query. + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) { + $requirePostFilter = false; + } + } + + } + + if ($requirePostFilter) { + $query = "SELECT uri, calendardata FROM " . $this->calendarObjectTableName . " WHERE calendarid = :calendarid"; + } else { + $query = "SELECT uri FROM " . $this->calendarObjectTableName . " WHERE calendarid = :calendarid"; + } + + $values = [ + 'calendarid' => $calendarId, + ]; + + if ($componentType) { + $query .= " AND componenttype = :componenttype"; + $values['componenttype'] = $componentType; + } + + if ($timeRange && $timeRange['start']) { + $query .= " AND lastoccurence > :startdate"; + $values['startdate'] = $timeRange['start']->getTimeStamp(); + } + if ($timeRange && $timeRange['end']) { + $query .= " AND firstoccurence < :enddate"; + $values['enddate'] = $timeRange['end']->getTimeStamp(); + } + + $stmt = $this->pdo->prepare($query); + $stmt->execute($values); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + if ($requirePostFilter) { + if (!$this->validateFilterForObject($row, $filters)) { + continue; + } + } + $result[] = $row['uri']; + + } + + return $result; + + } + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $principalUri + * @param string $uid + * @return string|null + */ + function getCalendarObjectByUID($principalUri, $uid) { + + $query = <<<SQL +SELECT + calendars.uri AS calendaruri, calendarobjects.uri as objecturi +FROM + $this->calendarObjectTableName AS calendarobjects +LEFT JOIN + $this->calendarTableName AS calendars + ON calendarobjects.calendarid = calendars.id +WHERE + calendars.principaluri = ? + AND + calendarobjects.uid = ? +SQL; + + $stmt = $this->pdo->prepare($query); + $stmt->execute([$principalUri, $uid]); + + if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $row['calendaruri'] . '/' . $row['objecturi']; + } + + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken in the specified calendar. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property this is needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $calendarId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * @return array + */ + function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) { + + // Current synctoken + $stmt = $this->pdo->prepare('SELECT synctoken FROM ' . $this->calendarTableName . ' WHERE id = ?'); + $stmt->execute([ $calendarId ]); + $currentToken = $stmt->fetchColumn(0); + + if (is_null($currentToken)) return null; + + $result = [ + 'syncToken' => $currentToken, + 'added' => [], + 'modified' => [], + 'deleted' => [], + ]; + + if ($syncToken) { + + $query = "SELECT uri, operation FROM " . $this->calendarChangesTableName . " WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken"; + if ($limit > 0) $query .= " LIMIT " . (int)$limit; + + // Fetching all changes + $stmt = $this->pdo->prepare($query); + $stmt->execute([$syncToken, $currentToken, $calendarId]); + + $changes = []; + + // This loop ensures that any duplicates are overwritten, only the + // last change on a node is relevant. + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $changes[$row['uri']] = $row['operation']; + + } + + foreach ($changes as $uri => $operation) { + + switch ($operation) { + case 1 : + $result['added'][] = $uri; + break; + case 2 : + $result['modified'][] = $uri; + break; + case 3 : + $result['deleted'][] = $uri; + break; + } + + } + } else { + // No synctoken supplied, this is the initial sync. + $query = "SELECT uri FROM " . $this->calendarObjectTableName . " WHERE calendarid = ?"; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$calendarId]); + + $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + } + return $result; + + } + + /** + * Adds a change record to the calendarchanges table. + * + * @param mixed $calendarId + * @param string $objectUri + * @param int $operation 1 = add, 2 = modify, 3 = delete. + * @return void + */ + protected function addChange($calendarId, $objectUri, $operation) { + + $stmt = $this->pdo->prepare('INSERT INTO ' . $this->calendarChangesTableName . ' (uri, synctoken, calendarid, operation) SELECT ?, synctoken, ?, ? FROM ' . $this->calendarTableName . ' WHERE id = ?'); + $stmt->execute([ + $objectUri, + $calendarId, + $operation, + $calendarId + ]); + $stmt = $this->pdo->prepare('UPDATE ' . $this->calendarTableName . ' SET synctoken = synctoken + 1 WHERE id = ?'); + $stmt->execute([ + $calendarId + ]); + + } + + /** + * Returns a list of subscriptions for a principal. + * + * Every subscription is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * subscription. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the subscription. + * * principaluri. The owner of the subscription. Almost always the same as + * principalUri passed to this method. + * * source. Url to the actual feed + * + * Furthermore, all the subscription info must be returned too: + * + * 1. {DAV:}displayname + * 2. {http://apple.com/ns/ical/}refreshrate + * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos + * should not be stripped). + * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms + * should not be stripped). + * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if + * attachments should not be stripped). + * 7. {http://apple.com/ns/ical/}calendar-color + * 8. {http://apple.com/ns/ical/}calendar-order + * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * (should just be an instance of + * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of + * default components). + * + * @param string $principalUri + * @return array + */ + function getSubscriptionsForUser($principalUri) { + + $fields = array_values($this->subscriptionPropertyMap); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'source'; + $fields[] = 'principaluri'; + $fields[] = 'lastmodified'; + + // Making fields a comma-delimited list + $fields = implode(', ', $fields); + $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM " . $this->calendarSubscriptionsTableName . " WHERE principaluri = ? ORDER BY calendarorder ASC"); + $stmt->execute([$principalUri]); + + $subscriptions = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $subscription = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + 'source' => $row['source'], + 'lastmodified' => $row['lastmodified'], + + '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VTODO', 'VEVENT']), + ]; + + foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { + if (!is_null($row[$dbName])) { + $subscription[$xmlName] = $row[$dbName]; + } + } + + $subscriptions[] = $subscription; + + } + + return $subscriptions; + + } + + /** + * Creates a new subscription for a principal. + * + * If the creation was a success, an id must be returned that can be used to reference + * this subscription in other methods, such as updateSubscription. + * + * @param string $principalUri + * @param string $uri + * @param array $properties + * @return mixed + */ + function createSubscription($principalUri, $uri, array $properties) { + + $fieldNames = [ + 'principaluri', + 'uri', + 'source', + 'lastmodified', + ]; + + if (!isset($properties['{http://calendarserver.org/ns/}source'])) { + throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions'); + } + + $values = [ + ':principaluri' => $principalUri, + ':uri' => $uri, + ':source' => $properties['{http://calendarserver.org/ns/}source']->getHref(), + ':lastmodified' => time(), + ]; + + foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { + if (isset($properties[$xmlName])) { + + $values[':' . $dbName] = $properties[$xmlName]; + $fieldNames[] = $dbName; + } + } + + $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarSubscriptionsTableName . " (" . implode(', ', $fieldNames) . ") VALUES (" . implode(', ', array_keys($values)) . ")"); + $stmt->execute($values); + + return $this->pdo->lastInsertId(); + + } + + /** + * Updates a subscription + * + * 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 mixed $subscriptionId + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateSubscription($subscriptionId, DAV\PropPatch $propPatch) { + + $supportedProperties = array_keys($this->subscriptionPropertyMap); + $supportedProperties[] = '{http://calendarserver.org/ns/}source'; + + $propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) { + + $newValues = []; + + foreach ($mutations as $propertyName => $propertyValue) { + + if ($propertyName === '{http://calendarserver.org/ns/}source') { + $newValues['source'] = $propertyValue->getHref(); + } else { + $fieldName = $this->subscriptionPropertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + } + + } + + // Now we're generating the sql query. + $valuesSql = []; + foreach ($newValues as $fieldName => $value) { + $valuesSql[] = $fieldName . ' = ?'; + } + + $stmt = $this->pdo->prepare("UPDATE " . $this->calendarSubscriptionsTableName . " SET " . implode(', ', $valuesSql) . ", lastmodified = ? WHERE id = ?"); + $newValues['lastmodified'] = time(); + $newValues['id'] = $subscriptionId; + $stmt->execute(array_values($newValues)); + + return true; + + }); + + } + + /** + * Deletes a subscription + * + * @param mixed $subscriptionId + * @return void + */ + function deleteSubscription($subscriptionId) { + + $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarSubscriptionsTableName . ' WHERE id = ?'); + $stmt->execute([$subscriptionId]); + + } + + /** + * Returns a single scheduling object. + * + * 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 + */ + function getSchedulingObject($principalUri, $objectUri) { + + $stmt = $this->pdo->prepare('SELECT uri, calendardata, lastmodified, etag, size FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ? AND uri = ?'); + $stmt->execute([$principalUri, $objectUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$row) return null; + + return [ + 'uri' => $row['uri'], + 'calendardata' => $row['calendardata'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'size' => (int)$row['size'], + ]; + + } + + /** + * 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 + */ + function getSchedulingObjects($principalUri) { + + $stmt = $this->pdo->prepare('SELECT id, calendardata, uri, lastmodified, etag, size FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ?'); + $stmt->execute([$principalUri]); + + $result = []; + foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'calendardata' => $row['calendardata'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"' . $row['etag'] . '"', + 'size' => (int)$row['size'], + ]; + } + + return $result; + + } + + /** + * Deletes a scheduling object + * + * @param string $principalUri + * @param string $objectUri + * @return void + */ + function deleteSchedulingObject($principalUri, $objectUri) { + + $stmt = $this->pdo->prepare('DELETE FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ? AND uri = ?'); + $stmt->execute([$principalUri, $objectUri]); + + } + + /** + * Creates a new scheduling object. This should land in a users' inbox. + * + * @param string $principalUri + * @param string $objectUri + * @param string $objectData + * @return void + */ + function createSchedulingObject($principalUri, $objectUri, $objectData) { + + $stmt = $this->pdo->prepare('INSERT INTO ' . $this->schedulingObjectTableName . ' (principaluri, calendardata, uri, lastmodified, etag, size) VALUES (?, ?, ?, ?, ?, ?)'); + $stmt->execute([$principalUri, $objectData, $objectUri, time(), md5($objectData), strlen($objectData) ]); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php b/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php new file mode 100644 index 000000000..6ec0bf06b --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php @@ -0,0 +1,65 @@ +<?php + +namespace Sabre\CalDAV\Backend; + +/** + * Implementing this interface adds CalDAV Scheduling support to your caldav + * server, as defined in rfc6638. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface SchedulingSupport extends BackendInterface { + + /** + * 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 + */ + function getSchedulingObject($principalUri, $objectUri); + + /** + * 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 + */ + function getSchedulingObjects($principalUri); + + /** + * Deletes a scheduling object from the inbox collection. + * + * @param string $principalUri + * @param string $objectUri + * @return void + */ + function deleteSchedulingObject($principalUri, $objectUri); + + /** + * Creates a new scheduling object. This should land in a users' inbox. + * + * @param string $principalUri + * @param string $objectUri + * @param string $objectData + * @return void + */ + function createSchedulingObject($principalUri, $objectUri, $objectData); + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Backend/SharingSupport.php b/vendor/sabre/dav/lib/CalDAV/Backend/SharingSupport.php new file mode 100644 index 000000000..6a11b0ab1 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Backend/SharingSupport.php @@ -0,0 +1,243 @@ +<?php + +namespace Sabre\CalDAV\Backend; + +/** + * Adds support for sharing features to a CalDAV server. + * + * Note: This feature is experimental, and may change in between different + * SabreDAV versions. + * + * Early warning: Currently SabreDAV provides no implementation for this. This + * is, because in it's current state there is no elegant way to do this. + * The problem lies in the fact that a real CalDAV server with sharing support + * would first need email support (with invite notifications), and really also + * a browser-frontend that allows people to accept or reject these shares. + * + * In addition, the CalDAV backends are currently kept as independent as + * possible, and should not be aware of principals, email addresses or + * accounts. + * + * Adding an implementation for Sharing to standard-sabredav would contradict + * these goals, so for this reason this is currently not implemented, although + * it may very well in the future; but probably not before SabreDAV 2.0. + * + * The interface works however, so if you implement all this, and do it + * correctly sharing _will_ work. It's not particularly easy, and I _urge you_ + * to make yourself acquainted with the following document first: + * + * https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt + * + * An overview + * =========== + * + * Implementing this interface will allow a user to share his or her calendars + * to other users. Effectively, when a calendar is shared the calendar will + * show up in both the Sharer's and Sharee's calendar-home root. + * This interface adds a few methods that ensure that this happens, and there + * are also a number of new requirements in the base-class you must now follow. + * + * + * How it works + * ============ + * + * When a user shares a calendar, the updateShares() method will be called with + * a list of sharees that are now added, and a list of sharees that have been + * removed. + * Removal is instant, but when a sharee is added the sharee first gets a + * chance to accept or reject the invitation for a share. + * + * After a share is accepted, the calendar will be returned from + * getUserCalendars for both the sharer, and the sharee. + * + * If the sharee deletes the calendar, only their share gets deleted. When the + * owner deletes a calendar, it will be removed for everybody. + * + * + * Notifications + * ============= + * + * During all these sharing operations, a lot of notifications are sent back + * and forward. + * + * Whenever the list of sharees for a calendar has been changed (they have been + * added, removed or modified) all sharees should get a notification for this + * change. + * This notification is always represented by: + * + * Sabre\CalDAV\Notifications\Notification\Invite + * + * In the case of an invite, the sharee may reply with an 'accept' or + * 'decline'. These are always represented by: + * + * Sabre\CalDAV\Notifications\Notification\InviteReply + * + * + * Calendar access by sharees + * ========================== + * + * As mentioned earlier, shared calendars must now also be returned for + * getCalendarsForUser for sharees. A few things change though. + * + * The following properties must be specified: + * + * 1. {http://calendarserver.org/ns/}shared-url + * + * This property MUST contain the url to the original calendar, that is.. the + * path to the calendar from the owner. + * + * 2. {http://sabredav.org/ns}owner-principal + * + * This is a url to to the principal who is sharing the calendar. + * + * 3. {http://sabredav.org/ns}read-only + * + * This should be either 0 or 1, depending on if the user has read-only or + * read-write access to the calendar. + * + * Only when this is done, the calendar will correctly be marked as a calendar + * that's shared to him, thus allowing clients to display the correct interface + * and ACL enforcement. + * + * If a sharee deletes their calendar, only their instance of the calendar + * should be deleted, the original should still exists. + * Pretty much any 'dead' WebDAV properties on these shared calendars should be + * specific to a user. This means that if the displayname is changed by a + * sharee, the original is not affected. This is also true for: + * * The description + * * The color + * * The order + * * And any other dead properties. + * + * Properties like a ctag should not be different for multiple instances of the + * calendar. + * + * Lastly, objects *within* calendars should also have user-specific data. The + * two things that are user-specific are: + * * VALARM objects + * * The TRANSP property + * + * This _also_ implies that if a VALARM is deleted by a sharee for some event, + * this has no effect on the original VALARM. + * + * Understandably, the this last requirement is one of the hardest. + * Realisticly, I can see people ignoring this part of the spec, but that could + * cause a different set of issues. + * + * + * Publishing + * ========== + * + * When a user publishes a url, the server should generate a 'publish url'. + * This is a read-only url, anybody can use to consume the calendar feed. + * + * Calendars are in one of two states: + * * published + * * unpublished + * + * If a calendar is published, the following property should be returned + * for each calendar in getCalendarsForUser. + * + * {http://calendarserver.org/ns/}publish-url + * + * This element should contain a {DAV:}href element, which points to the + * public url that does not require authentication. Unlike every other href, + * this url must be absolute. + * + * Ideally, the following property is always returned + * + * {http://calendarserver.org/ns/}pre-publish-url + * + * This property should contain the url that the calendar _would_ have, if it + * were to be published. iCal uses this to display the url, before the user + * will actually publish it. + * + * + * Selectively disabling publish or share feature + * ============================================== + * + * If Sabre\CalDAV\Property\AllowedSharingModes is returned from + * getCalendarsForUser, this allows the server to specify whether either sharing, + * or publishing is supported. + * + * This allows a client to determine in advance which features are available, + * and update the interface appropriately. If this property is not returned by + * the backend, the SharingPlugin automatically injects it and assumes both + * features are available. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface SharingSupport extends NotificationSupport { + + /** + * Updates the list of shares. + * + * The first array is a list of people that are to be added to the + * calendar. + * + * Every element in the add array has the following properties: + * * href - A url. Usually a mailto: address + * * commonName - Usually a first and last name, or false + * * summary - A description of the share, can also be false + * * readOnly - A boolean value + * + * Every element in the remove array is just the address string. + * + * Note that if the calendar is currently marked as 'not shared' by and + * this method is called, the calendar should be 'upgraded' to a shared + * calendar. + * + * @param mixed $calendarId + * @param array $add + * @param array $remove + * @return void + */ + function updateShares($calendarId, array $add, array $remove); + + /** + * Returns the list of people whom this calendar is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * This method may be called by either the original instance of the + * calendar, as well as the shared instances. In the case of the shared + * instances, it is perfectly acceptable to return an empty array in case + * there are privacy concerns. + * + * @param mixed $calendarId + * @return array + */ + function getShares($calendarId); + + /** + * This method is called when a user replied to a request to share. + * + * If the user chose to accept the share, this method should return the + * newly created calendar url. + * + * @param string href The sharee who is replying (often a mailto: address) + * @param int status One of the SharingPlugin::STATUS_* constants + * @param string $calendarUri The url to the calendar thats being shared + * @param string $inReplyTo The unique id this message is a response to + * @param string $summary A description of the reply + * @return null|string + */ + function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null); + + /** + * Publishes a calendar + * + * @param mixed $calendarId + * @param bool $value + * @return void + */ + function setPublishStatus($calendarId, $value); + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php b/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php new file mode 100644 index 000000000..a39289f5e --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php @@ -0,0 +1,89 @@ +<?php + +namespace Sabre\CalDAV\Backend; + +use Sabre\DAV; + +/** + * Every CalDAV backend must at least implement this interface. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface SubscriptionSupport extends BackendInterface { + + /** + * Returns a list of subscriptions for a principal. + * + * Every subscription is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * subscription. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the subscription. + * * principaluri. The owner of the subscription. Almost always the same as + * principalUri passed to this method. + * + * Furthermore, all the subscription info must be returned too: + * + * 1. {DAV:}displayname + * 2. {http://apple.com/ns/ical/}refreshrate + * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos + * should not be stripped). + * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms + * should not be stripped). + * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if + * attachments should not be stripped). + * 6. {http://calendarserver.org/ns/}source (Must be a + * Sabre\DAV\Property\Href). + * 7. {http://apple.com/ns/ical/}calendar-color + * 8. {http://apple.com/ns/ical/}calendar-order + * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * (should just be an instance of + * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of + * default components). + * + * @param string $principalUri + * @return array + */ + function getSubscriptionsForUser($principalUri); + + /** + * Creates a new subscription for a principal. + * + * If the creation was a success, an id must be returned that can be used to reference + * this subscription in other methods, such as updateSubscription. + * + * @param string $principalUri + * @param string $uri + * @param array $properties + * @return mixed + */ + function createSubscription($principalUri, $uri, array $properties); + + /** + * Updates a subscription + * + * 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 mixed $subscriptionId + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updateSubscription($subscriptionId, DAV\PropPatch $propPatch); + + /** + * Deletes a subscription. + * + * @param mixed $subscriptionId + * @return void + */ + function deleteSubscription($subscriptionId); + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Backend/SyncSupport.php b/vendor/sabre/dav/lib/CalDAV/Backend/SyncSupport.php new file mode 100644 index 000000000..a934505f9 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Backend/SyncSupport.php @@ -0,0 +1,81 @@ +<?php + +namespace Sabre\CalDAV\Backend; + +/** + * WebDAV-sync support for CalDAV backends. + * + * In order for backends to advertise support for WebDAV-sync, this interface + * must be implemented. + * + * Implementing this can result in a significant reduction of bandwidth and CPU + * time. + * + * For this to work, you _must_ return a {http://sabredav.org/ns}sync-token + * property from getCalendarsFromUser. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface SyncSupport extends BackendInterface { + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken in the specified calendar. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ); + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property This is * needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $calendarId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * @return array + */ + function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null); + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Calendar.php b/vendor/sabre/dav/lib/CalDAV/Calendar.php new file mode 100644 index 000000000..ff8e19b15 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Calendar.php @@ -0,0 +1,527 @@ +<?php + +namespace Sabre\CalDAV; + +use Sabre\DAV; +use Sabre\DAVACL; +use Sabre\DAV\PropPatch; + +/** + * This object represents a CalDAV calendar. + * + * A calendar can contain multiple TODO and or Events. These are represented + * as \Sabre\CalDAV\CalendarObject objects. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Calendar implements ICalendar, DAV\IProperties, DAV\Sync\ISyncCollection, DAV\IMultiGet { + + /** + * This is an array with calendar information + * + * @var array + */ + protected $calendarInfo; + + /** + * CalDAV backend + * + * @var Backend\BackendInterface + */ + protected $caldavBackend; + + /** + * Constructor + * + * @param Backend\BackendInterface $caldavBackend + * @param array $calendarInfo + */ + function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo) { + + $this->caldavBackend = $caldavBackend; + $this->calendarInfo = $calendarInfo; + + } + + /** + * Returns the name of the calendar + * + * @return string + */ + function getName() { + + return $this->calendarInfo['uri']; + + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + * + * @param PropPatch $propPatch + * @return void + */ + function propPatch(PropPatch $propPatch) { + + return $this->caldavBackend->updateCalendar($this->calendarInfo['id'], $propPatch); + + } + + /** + * Returns the list of properties + * + * @param array $requestedProperties + * @return array + */ + function getProperties($requestedProperties) { + + $response = []; + + foreach ($this->calendarInfo as $propName => $propValue) { + + if ($propName[0] === '{') + $response[$propName] = $this->calendarInfo[$propName]; + + } + return $response; + + } + + /** + * Returns a calendar object + * + * The contained calendar objects are for example Events or Todo's. + * + * @param string $name + * @return \Sabre\CalDAV\ICalendarObject + */ + function getChild($name) { + + $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name); + + if (!$obj) throw new DAV\Exception\NotFound('Calendar object not found'); + + $obj['acl'] = $this->getChildACL(); + + return new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj); + + } + + /** + * Returns the full list of calendar objects + * + * @return array + */ + function getChildren() { + + $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']); + $children = []; + foreach ($objs as $obj) { + $obj['acl'] = $this->getChildACL(); + $children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj); + } + return $children; + + } + + /** + * This method receives a list of paths in it's first argument. + * It must return an array with Node objects. + * + * If any children are not found, you do not have to return them. + * + * @param string[] $paths + * @return array + */ + function getMultipleChildren(array $paths) { + + $objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths); + $children = []; + foreach ($objs as $obj) { + $obj['acl'] = $this->getChildACL(); + $children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj); + } + return $children; + + } + + /** + * Checks if a child-node exists. + * + * @param string $name + * @return bool + */ + function childExists($name) { + + $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name); + if (!$obj) + return false; + else + return true; + + } + + /** + * Creates a new directory + * + * We actually block this, as subdirectories are not allowed in calendars. + * + * @param string $name + * @return void + */ + function createDirectory($name) { + + throw new DAV\Exception\MethodNotAllowed('Creating collections in calendar objects is not allowed'); + + } + + /** + * Creates a new file + * + * The contents of the new file must be a valid ICalendar string. + * + * @param string $name + * @param resource $calendarData + * @return string|null + */ + function createFile($name, $calendarData = null) { + + if (is_resource($calendarData)) { + $calendarData = stream_get_contents($calendarData); + } + return $this->caldavBackend->createCalendarObject($this->calendarInfo['id'], $name, $calendarData); + + } + + /** + * Deletes the calendar. + * + * @return void + */ + function delete() { + + $this->caldavBackend->deleteCalendar($this->calendarInfo['id']); + + } + + /** + * Renames the calendar. Note that most calendars use the + * {DAV:}displayname to display a name to display a name. + * + * @param string $newName + * @return void + */ + function setName($newName) { + + throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported'); + + } + + /** + * Returns the last modification date as a unix timestamp. + * + * @return void + */ + function getLastModified() { + + return null; + + } + + /** + * Returns the owner principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + function getOwner() { + + return $this->calendarInfo['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. 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 + */ + function getACL() { + + $acl = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner() . '/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner() . '/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + + ]; + if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner(), + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner() . '/calendar-proxy-write', + 'protected' => true, + ]; + } + + return $acl; + + } + + /** + * This method returns the ACL's for calendar objects in this calendar. + * The result of this method automatically gets passed to the + * calendar-object nodes in the calendar. + * + * @return array + */ + function getChildACL() { + + $acl = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner() . '/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner() . '/calendar-proxy-read', + 'protected' => true, + ], + + ]; + if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner(), + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner() . '/calendar-proxy-write', + 'protected' => true, + ]; + + } + return $acl; + + } + + /** + * Updates the ACL + * + * This method will receive a list of new ACE's. + * + * @param array $acl + * @return void + */ + function setACL(array $acl) { + + throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported'); + + } + + /** + * Returns the list of supported privileges for this node. + * + * The returned data structure is a list of nested privileges. + * See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple + * standard structure. + * + * If null is returned from this method, the default privilege set is used, + * which is fine for most common usecases. + * + * @return array|null + */ + function getSupportedPrivilegeSet() { + + $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet(); + + // We need to inject 'read-free-busy' in the tree, aggregated under + // {DAV:}read. + foreach ($default['aggregates'] as &$agg) { + + if ($agg['privilege'] !== '{DAV:}read') continue; + + $agg['aggregates'][] = [ + 'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy', + ]; + + } + return $default; + + } + + /** + * 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. + * + * @param array $filters + * @return array + */ + function calendarQuery(array $filters) { + + return $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters); + + } + + /** + * This method returns the current sync-token for this collection. + * This can be any string. + * + * If null is returned from this function, the plugin assumes there's no + * sync information available. + * + * @return string|null + */ + function getSyncToken() { + + if ( + $this->caldavBackend instanceof Backend\SyncSupport && + isset($this->calendarInfo['{DAV:}sync-token']) + ) { + return $this->calendarInfo['{DAV:}sync-token']; + } + if ( + $this->caldavBackend instanceof Backend\SyncSupport && + isset($this->calendarInfo['{http://sabredav.org/ns}sync-token']) + ) { + return $this->calendarInfo['{http://sabredav.org/ns}sync-token']; + } + + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken and the current collection. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The syncToken property should reflect the *current* syncToken of the + * collection, as reported getSyncToken(). This is needed here too, to + * ensure the operation is atomic. + * + * If the syncToken is specified as null, this is an initial sync, and all + * members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The second argument is basically the 'depth' of the report. If it's 1, + * you only have to report changes that happened only directly in immediate + * descendants. If it's 2, it should also include changes from the nodes + * below the child collections. (grandchildren) + * + * The third (optional) argument allows a client to specify how many + * results should be returned at most. If the limit is not specified, it + * should be treated as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * @return array + */ + function getChanges($syncToken, $syncLevel, $limit = null) { + + if (!$this->caldavBackend instanceof Backend\SyncSupport) { + return null; + } + + return $this->caldavBackend->getChangesForCalendar( + $this->calendarInfo['id'], + $syncToken, + $syncLevel, + $limit + ); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/CalendarHome.php b/vendor/sabre/dav/lib/CalDAV/CalendarHome.php new file mode 100644 index 000000000..a53f829e2 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/CalendarHome.php @@ -0,0 +1,430 @@ +<?php + +namespace Sabre\CalDAV; + +use Sabre\DAV; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\MkCol; +use Sabre\DAVACL; +use Sabre\HTTP\URLUtil; + +/** + * The CalendarHome represents a node that is usually in a users' + * calendar-homeset. + * + * It contains all the users' calendars, and can optionally contain a + * notifications collection, calendar subscriptions, a users' inbox, and a + * users' outbox. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class CalendarHome implements DAV\IExtendedCollection, DAVACL\IACL { + + /** + * CalDAV backend + * + * @var Sabre\CalDAV\Backend\BackendInterface + */ + protected $caldavBackend; + + /** + * Principal information + * + * @var array + */ + protected $principalInfo; + + /** + * Constructor + * + * @param Backend\BackendInterface $caldavBackend + * @param mixed $userUri + */ + function __construct(Backend\BackendInterface $caldavBackend, $principalInfo) { + + $this->caldavBackend = $caldavBackend; + $this->principalInfo = $principalInfo; + + } + + /** + * Returns the name of this object + * + * @return string + */ + function getName() { + + list(, $name) = URLUtil::splitPath($this->principalInfo['uri']); + return $name; + + } + + /** + * Updates the name of this object + * + * @param string $name + * @return void + */ + function setName($name) { + + throw new DAV\Exception\Forbidden(); + + } + + /** + * Deletes this object + * + * @return void + */ + function delete() { + + throw new DAV\Exception\Forbidden(); + + } + + /** + * Returns the last modification date + * + * @return int + */ + function getLastModified() { + + return null; + + } + + /** + * Creates a new file under this object. + * + * This is currently not allowed + * + * @param string $filename + * @param resource $data + * @return void + */ + function createFile($filename, $data = null) { + + throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported'); + + } + + /** + * Creates a new directory under this object. + * + * This is currently not allowed. + * + * @param string $filename + * @return void + */ + function createDirectory($filename) { + + throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported'); + + } + + /** + * Returns a single calendar, by name + * + * @param string $name + * @return Calendar + */ + function getChild($name) { + + // Special nodes + if ($name === 'inbox' && $this->caldavBackend instanceof Backend\SchedulingSupport) { + return new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']); + } + if ($name === 'outbox' && $this->caldavBackend instanceof Backend\SchedulingSupport) { + return new Schedule\Outbox($this->principalInfo['uri']); + } + if ($name === 'notifications' && $this->caldavBackend instanceof Backend\NotificationSupport) { + return new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); + } + + // Calendars + foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) { + if ($calendar['uri'] === $name) { + if ($this->caldavBackend instanceof Backend\SharingSupport) { + if (isset($calendar['{http://calendarserver.org/ns/}shared-url'])) { + return new SharedCalendar($this->caldavBackend, $calendar); + } else { + return new ShareableCalendar($this->caldavBackend, $calendar); + } + } else { + return new Calendar($this->caldavBackend, $calendar); + } + } + } + + if ($this->caldavBackend instanceof Backend\SubscriptionSupport) { + foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) { + if ($subscription['uri'] === $name) { + return new Subscriptions\Subscription($this->caldavBackend, $subscription); + } + } + + } + + throw new NotFound('Node with name \'' . $name . '\' could not be found'); + + } + + /** + * Checks if a calendar exists. + * + * @param string $name + * @return bool + */ + function childExists($name) { + + try { + return !!$this->getChild($name); + } catch (NotFound $e) { + return false; + } + + } + + /** + * Returns a list of calendars + * + * @return array + */ + function getChildren() { + + $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']); + $objs = []; + foreach ($calendars as $calendar) { + 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); + } + } + + if ($this->caldavBackend instanceof Backend\SchedulingSupport) { + $objs[] = new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']); + $objs[] = new Schedule\Outbox($this->principalInfo['uri']); + } + + // We're adding a notifications node, if it's supported by the backend. + if ($this->caldavBackend instanceof Backend\NotificationSupport) { + $objs[] = new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); + } + + // If the backend supports subscriptions, we'll add those as well, + if ($this->caldavBackend instanceof Backend\SubscriptionSupport) { + foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) { + $objs[] = new Subscriptions\Subscription($this->caldavBackend, $subscription); + } + } + + return $objs; + + } + + /** + * Creates a new calendar or subscription. + * + * @param string $name + * @param MkCol $mkCol + * @throws DAV\Exception\InvalidResourceType + * @return void + */ + function createExtendedCollection($name, MkCol $mkCol) { + + $isCalendar = false; + $isSubscription = false; + foreach ($mkCol->getResourceType() as $rt) { + switch ($rt) { + case '{DAV:}collection' : + case '{http://calendarserver.org/ns/}shared-owner' : + // ignore + break; + case '{urn:ietf:params:xml:ns:caldav}calendar' : + $isCalendar = true; + break; + case '{http://calendarserver.org/ns/}subscribed' : + $isSubscription = true; + break; + default : + throw new DAV\Exception\InvalidResourceType('Unknown resourceType: ' . $rt); + } + } + + $properties = $mkCol->getRemainingValues(); + $mkCol->setRemainingResultCode(201); + + if ($isSubscription) { + if (!$this->caldavBackend instanceof Backend\SubscriptionSupport) { + throw new DAV\Exception\InvalidResourceType('This backend does not support subscriptions'); + } + $this->caldavBackend->createSubscription($this->principalInfo['uri'], $name, $properties); + + } elseif ($isCalendar) { + $this->caldavBackend->createCalendar($this->principalInfo['uri'], $name, $properties); + + } else { + throw new DAV\Exception\InvalidResourceType('You can only create calendars and subscriptions in this collection'); + + } + + } + + /** + * Returns the owner principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + function getOwner() { + + return $this->principalInfo['uri']; + + } + + /** + * Returns a group principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + function getGroup() { + + return null; + + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + function getACL() { + + return [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalInfo['uri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->principalInfo['uri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write', + 'protected' => true, + ], + [ + '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 + */ + function setACL(array $acl) { + + throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported'); + + } + + /** + * Returns the list of supported privileges for this node. + * + * The returned data structure is a list of nested privileges. + * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple + * standard structure. + * + * If null is returned from this method, the default privilege set is used, + * which is fine for most common usecases. + * + * @return array|null + */ + function getSupportedPrivilegeSet() { + + return null; + + } + + /** + * This method is called when a user replied to a request to share. + * + * This method should return the url of the newly created calendar if the + * share was accepted. + * + * @param string href The sharee who is replying (often a mailto: address) + * @param int status One of the SharingPlugin::STATUS_* constants + * @param string $calendarUri The url to the calendar thats being shared + * @param string $inReplyTo The unique id this message is a response to + * @param string $summary A description of the reply + * @return null|string + */ + function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null) { + + if (!$this->caldavBackend instanceof Backend\SharingSupport) { + throw new DAV\Exception\NotImplemented('Sharing support is not implemented by this backend.'); + } + + return $this->caldavBackend->shareReply($href, $status, $calendarUri, $inReplyTo, $summary); + + } + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $uid + * @return string|null + */ + function getCalendarObjectByUID($uid) { + + return $this->caldavBackend->getCalendarObjectByUID($this->principalInfo['uri'], $uid); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/CalendarObject.php b/vendor/sabre/dav/lib/CalDAV/CalendarObject.php new file mode 100644 index 000000000..393ca4cbd --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/CalendarObject.php @@ -0,0 +1,290 @@ +<?php + +namespace Sabre\CalDAV; + +/** + * The CalendarObject represents a single VEVENT or VTODO within a Calendar. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class CalendarObject extends \Sabre\DAV\File implements ICalendarObject, \Sabre\DAVACL\IACL { + + /** + * Sabre\CalDAV\Backend\BackendInterface + * + * @var Sabre\CalDAV\Backend\AbstractBackend + */ + protected $caldavBackend; + + /** + * Array with information about this CalendarObject + * + * @var array + */ + protected $objectData; + + /** + * Array with information about the containing calendar + * + * @var array + */ + protected $calendarInfo; + + /** + * Constructor + * + * The following properties may be passed within $objectData: + * + * * calendarid - This must refer to a calendarid from a caldavBackend + * * uri - A unique uri. Only the 'basename' must be passed. + * * calendardata (optional) - The iCalendar data + * * etag - (optional) The etag for this object, MUST be encloded with + * double-quotes. + * * size - (optional) The size of the data in bytes. + * * lastmodified - (optional) format as a unix timestamp. + * * acl - (optional) Use this to override the default ACL for the node. + * + * @param Backend\BackendInterface $caldavBackend + * @param array $calendarInfo + * @param array $objectData + */ + function __construct(Backend\BackendInterface $caldavBackend, array $calendarInfo, array $objectData) { + + $this->caldavBackend = $caldavBackend; + + if (!isset($objectData['uri'])) { + throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property'); + } + + $this->calendarInfo = $calendarInfo; + $this->objectData = $objectData; + + } + + /** + * Returns the uri for this object + * + * @return string + */ + function getName() { + + return $this->objectData['uri']; + + } + + /** + * Returns the ICalendar-formatted object + * + * @return string + */ + function get() { + + // Pre-populating the 'calendardata' is optional, if we don't have it + // already we fetch it from the backend. + if (!isset($this->objectData['calendardata'])) { + $this->objectData = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $this->objectData['uri']); + } + return $this->objectData['calendardata']; + + } + + /** + * Updates the ICalendar-formatted object + * + * @param string|resource $calendarData + * @return string + */ + function put($calendarData) { + + if (is_resource($calendarData)) { + $calendarData = stream_get_contents($calendarData); + } + $etag = $this->caldavBackend->updateCalendarObject($this->calendarInfo['id'], $this->objectData['uri'], $calendarData); + $this->objectData['calendardata'] = $calendarData; + $this->objectData['etag'] = $etag; + + return $etag; + + } + + /** + * Deletes the calendar object + * + * @return void + */ + function delete() { + + $this->caldavBackend->deleteCalendarObject($this->calendarInfo['id'], $this->objectData['uri']); + + } + + /** + * Returns the mime content-type + * + * @return string + */ + function getContentType() { + + $mime = 'text/calendar; charset=utf-8'; + if (isset($this->objectData['component']) && $this->objectData['component']) { + $mime .= '; component=' . $this->objectData['component']; + } + return $mime; + + } + + /** + * Returns an ETag for this object. + * + * The ETag is an arbitrary string, but MUST be surrounded by double-quotes. + * + * @return string + */ + function getETag() { + + if (isset($this->objectData['etag'])) { + return $this->objectData['etag']; + } else { + return '"' . md5($this->get()) . '"'; + } + + } + + /** + * Returns the last modification date as a unix timestamp + * + * @return int + */ + function getLastModified() { + + return $this->objectData['lastmodified']; + + } + + /** + * Returns the size of this object in bytes + * + * @return int + */ + function getSize() { + + if (array_key_exists('size', $this->objectData)) { + return $this->objectData['size']; + } else { + return strlen($this->get()); + } + + } + + /** + * Returns the owner principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + function getOwner() { + + return $this->calendarInfo['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. 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 + */ + function getACL() { + + // An alternative acl may be specified in the object data. + if (isset($this->objectData['acl'])) { + return $this->objectData['acl']; + } + + // The default ACL + return [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-read', + 'protected' => true, + ], + + ]; + + } + + /** + * Updates the ACL + * + * This method will receive a list of new ACE's. + * + * @param array $acl + * @return void + */ + function setACL(array $acl) { + + throw new \Sabre\DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported'); + + } + + /** + * Returns the list of supported privileges for this node. + * + * The returned data structure is a list of nested privileges. + * See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple + * standard structure. + * + * If null is returned from this method, the default privilege set is used, + * which is fine for most common usecases. + * + * @return array|null + */ + function getSupportedPrivilegeSet() { + + return null; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php b/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php new file mode 100644 index 000000000..f3c7524d2 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/CalendarQueryValidator.php @@ -0,0 +1,375 @@ +<?php + +namespace Sabre\CalDAV; + +use Sabre\VObject; +use DateTime; + +/** + * CalendarQuery Validator + * + * This class is responsible for checking if an iCalendar object matches a set + * of filters. The main function to do this is 'validate'. + * + * This is used to determine which icalendar objects should be returned for a + * calendar-query REPORT request. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class CalendarQueryValidator { + + /** + * Verify if a list of filters applies to the calendar data object + * + * The list of filters must be formatted as parsed by \Sabre\CalDAV\CalendarQueryParser + * + * @param VObject\Component $vObject + * @param array $filters + * @return bool + */ + function validate(VObject\Component\VCalendar $vObject, array $filters) { + + // The top level object is always a component filter. + // We'll parse it manually, as it's pretty simple. + if ($vObject->name !== $filters['name']) { + return false; + } + + return + $this->validateCompFilters($vObject, $filters['comp-filters']) && + $this->validatePropFilters($vObject, $filters['prop-filters']); + + + } + + /** + * This method checks the validity of comp-filters. + * + * A list of comp-filters needs to be specified. Also the parent of the + * component we're checking should be specified, not the component to check + * itself. + * + * @param VObject\Component $parent + * @param array $filters + * @return bool + */ + protected function validateCompFilters(VObject\Component $parent, array $filters) { + + foreach ($filters as $filter) { + + $isDefined = isset($parent->{$filter['name']}); + + if ($filter['is-not-defined']) { + + if ($isDefined) { + return false; + } else { + continue; + } + + } + if (!$isDefined) { + return false; + } + + if ($filter['time-range']) { + foreach ($parent->{$filter['name']} as $subComponent) { + if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) { + continue 2; + } + } + return false; + } + + if (!$filter['comp-filters'] && !$filter['prop-filters']) { + continue; + } + + // If there are sub-filters, we need to find at least one component + // for which the subfilters hold true. + foreach ($parent->{$filter['name']} as $subComponent) { + + if ( + $this->validateCompFilters($subComponent, $filter['comp-filters']) && + $this->validatePropFilters($subComponent, $filter['prop-filters'])) { + // We had a match, so this comp-filter succeeds + continue 2; + } + + } + + // If we got here it means there were sub-comp-filters or + // sub-prop-filters and there was no match. This means this filter + // needs to return false. + return false; + + } + + // If we got here it means we got through all comp-filters alive so the + // filters were all true. + return true; + + } + + /** + * This method checks the validity of prop-filters. + * + * A list of prop-filters needs to be specified. Also the parent of the + * property we're checking should be specified, not the property to check + * itself. + * + * @param VObject\Component $parent + * @param array $filters + * @return bool + */ + protected function validatePropFilters(VObject\Component $parent, array $filters) { + + foreach ($filters as $filter) { + + $isDefined = isset($parent->{$filter['name']}); + + if ($filter['is-not-defined']) { + + if ($isDefined) { + return false; + } else { + continue; + } + + } + if (!$isDefined) { + return false; + } + + if ($filter['time-range']) { + foreach ($parent->{$filter['name']} as $subComponent) { + if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) { + continue 2; + } + } + return false; + } + + if (!$filter['param-filters'] && !$filter['text-match']) { + continue; + } + + // If there are sub-filters, we need to find at least one property + // for which the subfilters hold true. + foreach ($parent->{$filter['name']} as $subComponent) { + + if ( + $this->validateParamFilters($subComponent, $filter['param-filters']) && + (!$filter['text-match'] || $this->validateTextMatch($subComponent, $filter['text-match'])) + ) { + // We had a match, so this prop-filter succeeds + continue 2; + } + + } + + // If we got here it means there were sub-param-filters or + // text-match filters and there was no match. This means the + // filter needs to return false. + return false; + + } + + // If we got here it means we got through all prop-filters alive so the + // filters were all true. + return true; + + } + + /** + * This method checks the validity of param-filters. + * + * A list of param-filters needs to be specified. Also the parent of the + * parameter we're checking should be specified, not the parameter to check + * itself. + * + * @param VObject\Property $parent + * @param array $filters + * @return bool + */ + protected function validateParamFilters(VObject\Property $parent, array $filters) { + + foreach ($filters as $filter) { + + $isDefined = isset($parent[$filter['name']]); + + if ($filter['is-not-defined']) { + + if ($isDefined) { + return false; + } else { + continue; + } + + } + if (!$isDefined) { + return false; + } + + if (!$filter['text-match']) { + continue; + } + + // If there are sub-filters, we need to find at least one parameter + // for which the subfilters hold true. + foreach ($parent[$filter['name']]->getParts() as $paramPart) { + + if ($this->validateTextMatch($paramPart, $filter['text-match'])) { + // We had a match, so this param-filter succeeds + continue 2; + } + + } + + // If we got here it means there was a text-match filter and there + // were no matches. This means the filter needs to return false. + return false; + + } + + // If we got here it means we got through all param-filters alive so the + // filters were all true. + return true; + + } + + /** + * This method checks the validity of a text-match. + * + * A single text-match should be specified as well as the specific property + * or parameter we need to validate. + * + * @param VObject\Node|string $check Value to check against. + * @param array $textMatch + * @return bool + */ + protected function validateTextMatch($check, array $textMatch) { + + if ($check instanceof VObject\Node) { + $check = $check->getValue(); + } + + $isMatching = \Sabre\DAV\StringUtil::textMatch($check, $textMatch['value'], $textMatch['collation']); + + return ($textMatch['negate-condition'] xor $isMatching); + + } + + /** + * Validates if a component matches the given time range. + * + * This is all based on the rules specified in rfc4791, which are quite + * complex. + * + * @param VObject\Node $component + * @param DateTime $start + * @param DateTime $end + * @return bool + */ + protected function validateTimeRange(VObject\Node $component, $start, $end) { + + if (is_null($start)) { + $start = new DateTime('1900-01-01'); + } + if (is_null($end)) { + $end = new DateTime('3000-01-01'); + } + + switch ($component->name) { + + case 'VEVENT' : + case 'VTODO' : + case 'VJOURNAL' : + + return $component->isInTimeRange($start, $end); + + case 'VALARM' : + + // If the valarm is wrapped in a recurring event, we need to + // expand the recursions, and validate each. + // + // Our datamodel doesn't easily allow us to do this straight + // in the VALARM component code, so this is a hack, and an + // expensive one too. + if ($component->parent->name === 'VEVENT' && $component->parent->RRULE) { + + // Fire up the iterator! + $it = new VObject\Recur\EventIterator($component->parent->parent, (string)$component->parent->UID); + while ($it->valid()) { + $expandedEvent = $it->getEventObject(); + + // We need to check from these expanded alarms, which + // one is the first to trigger. Based on this, we can + // determine if we can 'give up' expanding events. + $firstAlarm = null; + if ($expandedEvent->VALARM !== null) { + foreach ($expandedEvent->VALARM as $expandedAlarm) { + + $effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime(); + if ($expandedAlarm->isInTimeRange($start, $end)) { + return true; + } + + if ((string)$expandedAlarm->TRIGGER['VALUE'] === 'DATE-TIME') { + // This is an alarm with a non-relative trigger + // time, likely created by a buggy client. The + // implication is that every alarm in this + // recurring event trigger at the exact same + // time. It doesn't make sense to traverse + // further. + } else { + // We store the first alarm as a means to + // figure out when we can stop traversing. + if (!$firstAlarm || $effectiveTrigger < $firstAlarm) { + $firstAlarm = $effectiveTrigger; + } + } + } + } + if (is_null($firstAlarm)) { + // No alarm was found. + // + // Or technically: No alarm that will change for + // every instance of the recurrence was found, + // which means we can assume there was no match. + return false; + } + if ($firstAlarm > $end) { + return false; + } + $it->next(); + } + return false; + } else { + return $component->isInTimeRange($start, $end); + } + + case 'VFREEBUSY' : + throw new \Sabre\DAV\Exception\NotImplemented('time-range filters are currently not supported on ' . $component->name . ' components'); + + case 'COMPLETED' : + case 'CREATED' : + case 'DTEND' : + case 'DTSTAMP' : + case 'DTSTART' : + case 'DUE' : + case 'LAST-MODIFIED' : + return ($start <= $component->getDateTime() && $end >= $component->getDateTime()); + + + + default : + throw new \Sabre\DAV\Exception\BadRequest('You cannot create a time-range filter on a ' . $component->name . ' component'); + + } + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php b/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php new file mode 100644 index 000000000..0ac50e41d --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/CalendarRoot.php @@ -0,0 +1,80 @@ +<?php + +namespace Sabre\CalDAV; + +use Sabre\DAVACL\PrincipalBackend; + +/** + * Calendars collection + * + * This object is responsible for generating a list of calendar-homes for each + * user. + * + * This is the top-most node for the calendars tree. In most servers this class + * represents the "/calendars" path. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class CalendarRoot extends \Sabre\DAVACL\AbstractPrincipalCollection { + + /** + * CalDAV backend + * + * @var Sabre\CalDAV\Backend\BackendInterface + */ + protected $caldavBackend; + + /** + * Constructor + * + * This constructor needs both an authentication and a caldav backend. + * + * By default this class will show a list of calendar collections for + * principals in the 'principals' collection. If your main principals are + * actually located in a different path, use the $principalPrefix argument + * to override this. + * + * @param PrincipalBackend\BackendInterface $principalBackend + * @param Backend\BackendInterface $caldavBackend + * @param string $principalPrefix + */ + function __construct(PrincipalBackend\BackendInterface $principalBackend, 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 + */ + function getName() { + + return 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 + */ + function getChildForPrincipal(array $principal) { + + return new CalendarHome($this->caldavBackend, $principal); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php b/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php new file mode 100644 index 000000000..5ce8a93f5 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php @@ -0,0 +1,35 @@ +<?php + +namespace Sabre\CalDAV\Exception; + +use Sabre\DAV; +use Sabre\CalDAV; + +/** + * InvalidComponentType + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class InvalidComponentType extends DAV\Exception\Forbidden { + + /** + * Adds in extra information in the xml response. + * + * This method adds the {CALDAV:}supported-calendar-component as defined in rfc4791 + * + * @param DAV\Server $server + * @param \DOMElement $errorNode + * @return void + */ + function serialize(DAV\Server $server, \DOMElement $errorNode) { + + $doc = $errorNode->ownerDocument; + + $np = $doc->createElementNS(CalDAV\Plugin::NS_CALDAV, 'cal:supported-calendar-component'); + $errorNode->appendChild($np); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php b/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php new file mode 100644 index 000000000..8c296d50f --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php @@ -0,0 +1,366 @@ +<?php + +namespace Sabre\CalDAV; + +use DateTimeZone; +use Sabre\DAV; +use Sabre\VObject; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Sabre\DAV\Exception\BadRequest; +use DateTime; + +/** + * ICS Exporter + * + * This plugin adds the ability to export entire calendars as .ics files. + * This is useful for clients that don't support CalDAV yet. They often do + * support ics files. + * + * To use this, point a http client to a caldav calendar, and add ?expand to + * the url. + * + * Further options that can be added to the url: + * start=123456789 - Only return events after the given unix timestamp + * end=123245679 - Only return events from before the given unix timestamp + * expand=1 - Strip timezone information and expand recurring events. + * If you'd like to expand, you _must_ also specify start + * and end. + * + * By default this plugin returns data in the text/calendar format (iCalendar + * 2.0). If you'd like to receive jCal data instead, you can use an Accept + * header: + * + * Accept: application/calendar+json + * + * Alternatively, you can also specify this in the url using + * accept=application/calendar+json, or accept=jcal for short. If the url + * parameter and Accept header is specified, the url parameter wins. + * + * Note that specifying a start or end data implies that only events will be + * returned. VTODO and VJOURNAL will be stripped. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ICSExportPlugin extends DAV\ServerPlugin { + + /** + * Reference to Server class + * + * @var \Sabre\DAV\Server + */ + protected $server; + + /** + * Initializes the plugin and registers event handlers + * + * @param \Sabre\DAV\Server $server + * @return void + */ + function initialize(DAV\Server $server) { + + $this->server = $server; + $server->on('method:GET', [$this, 'httpGet'], 90); + $server->on('browserButtonActions', function($path, $node, &$actions) { + if ($node instanceof ICalendar) { + $actions .= '<a href="' . htmlspecialchars($path, ENT_QUOTES, 'UTF-8') . '?export"><span class="oi" data-glyph="calendar"></span></a>'; + } + }); + + } + + /** + * Intercepts GET requests on calendar urls ending with ?export. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + function httpGet(RequestInterface $request, ResponseInterface $response) { + + $queryParams = $request->getQueryParameters(); + if (!array_key_exists('export', $queryParams)) return; + + $path = $request->getPath(); + + $node = $this->server->getProperties($path, [ + '{DAV:}resourcetype', + '{DAV:}displayname', + '{http://sabredav.org/ns}sync-token', + '{DAV:}sync-token', + '{http://apple.com/ns/ical/}calendar-color', + ]); + + if (!isset($node['{DAV:}resourcetype']) || !$node['{DAV:}resourcetype']->is('{' . Plugin::NS_CALDAV . '}calendar')) { + return; + } + // Marking the transactionType, for logging purposes. + $this->server->transactionType = 'get-calendar-export'; + + $properties = $node; + + $start = null; + $end = null; + $expand = false; + $componentType = false; + if (isset($queryParams['start'])) { + if (!ctype_digit($queryParams['start'])) { + throw new BadRequest('The start= parameter must contain a unix timestamp'); + } + $start = DateTime::createFromFormat('U', $queryParams['start']); + } + if (isset($queryParams['end'])) { + if (!ctype_digit($queryParams['end'])) { + throw new BadRequest('The end= parameter must contain a unix timestamp'); + } + $end = DateTime::createFromFormat('U', $queryParams['end']); + } + if (isset($queryParams['expand']) && !!$queryParams['expand']) { + if (!$start || !$end) { + throw new BadRequest('If you\'d like to expand recurrences, you must specify both a start= and end= parameter.'); + } + $expand = true; + $componentType = 'VEVENT'; + } + if (isset($queryParams['componentType'])) { + if (!in_array($queryParams['componentType'], ['VEVENT', 'VTODO', 'VJOURNAL'])) { + throw new BadRequest('You are not allowed to search for components of type: ' . $queryParams['componentType'] . ' here'); + } + $componentType = $queryParams['componentType']; + } + + $format = \Sabre\HTTP\Util::Negotiate( + $request->getHeader('Accept'), + [ + 'text/calendar', + 'application/calendar+json', + ] + ); + + if (isset($queryParams['accept'])) { + if ($queryParams['accept'] === 'application/calendar+json' || $queryParams['accept'] === 'jcal') { + $format = 'application/calendar+json'; + } + } + if (!$format) { + $format = 'text/calendar'; + } + + $this->generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, $response); + + // Returning false to break the event chain + return false; + + } + + /** + * This method is responsible for generating the actual, full response. + * + * @param string $path + * @param DateTime|null $start + * @param DateTime|null $end + * @param bool $expand + * @param string $componentType + * @param string $format + * @param array $properties + * @param ResponseInterface $response + */ + protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response) { + + $calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data'; + + $blobs = []; + if ($start || $end || $componentType) { + + // If there was a start or end filter, we need to enlist + // calendarQuery for speed. + $calendarNode = $this->server->tree->getNodeForPath($path); + $queryResult = $calendarNode->calendarQuery([ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => $componentType, + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $start, + 'end' => $end, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + + // queryResult is just a list of base urls. We need to prefix the + // calendar path. + $queryResult = array_map( + function($item) use ($path) { + return $path . '/' . $item; + }, + $queryResult + ); + $nodes = $this->server->getPropertiesForMultiplePaths($queryResult, [$calDataProp]); + unset($queryResult); + + } else { + $nodes = $this->server->getPropertiesForPath($path, [$calDataProp], 1); + } + + // Flattening the arrays + foreach ($nodes as $node) { + if (isset($node[200][$calDataProp])) { + $blobs[$node['href']] = $node[200][$calDataProp]; + } + } + unset($nodes); + + $mergedCalendar = $this->mergeObjects( + $properties, + $blobs + ); + + if ($expand) { + $calendarTimeZone = null; + // We're expanding, and for that we need to figure out the + // calendar's timezone. + $tzProp = '{' . Plugin::NS_CALDAV . '}calendar-timezone'; + $tzResult = $this->server->getProperties($path, [$tzProp]); + if (isset($tzResult[$tzProp])) { + // This property contains a VCALENDAR with a single + // VTIMEZONE. + $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + // Destroy circular references to PHP will GC the object. + $vtimezoneObj->destroy(); + unset($vtimezoneObj); + } else { + // Defaulting to UTC. + $calendarTimeZone = new DateTimeZone('UTC'); + } + + $mergedCalendar = $mergedCalendar->expand($start, $end, $calendarTimeZone); + } + + $response->setHeader('Content-Type', $format); + + switch ($format) { + case 'text/calendar' : + $mergedCalendar = $mergedCalendar->serialize(); + break; + case 'application/calendar+json' : + $mergedCalendar = json_encode($mergedCalendar->jsonSerialize()); + break; + } + + $response->setStatus(200); + $response->setBody($mergedCalendar); + + } + + /** + * Merges all calendar objects, and builds one big iCalendar blob. + * + * @param array $properties Some CalDAV properties + * @param array $inputObjects + * @return VObject\Component\VCalendar + */ + function mergeObjects(array $properties, array $inputObjects) { + + $calendar = new VObject\Component\VCalendar(); + $calendar->version = '2.0'; + if (DAV\Server::$exposeVersion) { + $calendar->prodid = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN'; + } else { + $calendar->prodid = '-//SabreDAV//SabreDAV//EN'; + } + if (isset($properties['{DAV:}displayname'])) { + $calendar->{'X-WR-CALNAME'} = $properties['{DAV:}displayname']; + } + if (isset($properties['{http://apple.com/ns/ical/}calendar-color'])) { + $calendar->{'X-APPLE-CALENDAR-COLOR'} = $properties['{http://apple.com/ns/ical/}calendar-color']; + } + + $collectedTimezones = []; + + $timezones = []; + $objects = []; + + foreach ($inputObjects as $href => $inputObject) { + + $nodeComp = VObject\Reader::read($inputObject); + + foreach ($nodeComp->children() as $child) { + + switch ($child->name) { + case 'VEVENT' : + case 'VTODO' : + case 'VJOURNAL' : + $objects[] = clone $child; + break; + + // VTIMEZONE is special, because we need to filter out the duplicates + case 'VTIMEZONE' : + // Naively just checking tzid. + if (in_array((string)$child->TZID, $collectedTimezones)) continue; + + $timezones[] = clone $child; + $collectedTimezones[] = $child->TZID; + break; + + } + + } + // Destroy circular references to PHP will GC the object. + $nodeComp->destroy(); + unset($nodeComp); + + } + + foreach ($timezones as $tz) $calendar->add($tz); + foreach ($objects as $obj) $calendar->add($obj); + + return $calendar; + + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + function getPluginName() { + + return 'ics-export'; + + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds the ability to export CalDAV calendars as a single iCalendar file.', + 'link' => 'http://sabre.io/dav/ics-export-plugin/', + ]; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/ICalendar.php b/vendor/sabre/dav/lib/CalDAV/ICalendar.php new file mode 100644 index 000000000..7cf4b1256 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/ICalendar.php @@ -0,0 +1,18 @@ +<?php + +namespace Sabre\CalDAV; + +use Sabre\DAVACL; + +/** + * Calendar interface + * + * Implement this interface to allow a node to be recognized as an calendar. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface ICalendar extends ICalendarObjectContainer, DAVACL\IACL { + +} diff --git a/vendor/sabre/dav/lib/CalDAV/ICalendarObject.php b/vendor/sabre/dav/lib/CalDAV/ICalendarObject.php new file mode 100644 index 000000000..b3a767b74 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/ICalendarObject.php @@ -0,0 +1,21 @@ +<?php + +namespace Sabre\CalDAV; + +use Sabre\DAV; + +/** + * CalendarObject interface + * + * Extend the ICalendarObject interface to allow your custom nodes to be picked up as + * CalendarObjects. + * + * Calendar objects are resources such as Events, Todo's or Journals. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface ICalendarObject extends DAV\IFile { + +} diff --git a/vendor/sabre/dav/lib/CalDAV/ICalendarObjectContainer.php b/vendor/sabre/dav/lib/CalDAV/ICalendarObjectContainer.php new file mode 100644 index 000000000..0308b8a55 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/ICalendarObjectContainer.php @@ -0,0 +1,39 @@ +<?php + +namespace Sabre\CalDAV; + +/** + * This interface represents a node that may contain calendar objects. + * + * This is the shared parent for both the Inbox collection and calendars + * resources. + * + * In most cases you will likely want to look at ICalendar instead of this + * interface. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface ICalendarObjectContainer extends \Sabre\DAV\ICollection { + + /** + * 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. + * + * @param array $filters + * @return array + */ + function calendarQuery(array $filters); + +} diff --git a/vendor/sabre/dav/lib/CalDAV/IShareableCalendar.php b/vendor/sabre/dav/lib/CalDAV/IShareableCalendar.php new file mode 100644 index 000000000..cfda91a55 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/IShareableCalendar.php @@ -0,0 +1,48 @@ +<?php + +namespace Sabre\CalDAV; + +/** + * This interface represents a Calendar that can be shared with other users. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface IShareableCalendar extends ICalendar { + + /** + * Updates the list of shares. + * + * The first array is a list of people that are to be added to the + * calendar. + * + * Every element in the add array has the following properties: + * * href - A url. Usually a mailto: address + * * commonName - Usually a first and last name, or false + * * summary - A description of the share, can also be false + * * readOnly - A boolean value + * + * Every element in the remove array is just the address string. + * + * @param array $add + * @param array $remove + * @return void + */ + function updateShares(array $add, array $remove); + + /** + * Returns the list of people whom this calendar is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * @return array + */ + function getShares(); + +} diff --git a/vendor/sabre/dav/lib/CalDAV/ISharedCalendar.php b/vendor/sabre/dav/lib/CalDAV/ISharedCalendar.php new file mode 100644 index 000000000..84442ac21 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/ISharedCalendar.php @@ -0,0 +1,36 @@ +<?php + +namespace Sabre\CalDAV; + +/** + * This interface represents a Calendar that is shared by a different user. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface ISharedCalendar extends ICalendar { + + /** + * This method should return the url of the owners' copy of the shared + * calendar. + * + * @return string + */ + function getSharedUrl(); + + /** + * Returns the list of people whom this calendar is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * @return array + */ + function getShares(); + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Notifications/Collection.php b/vendor/sabre/dav/lib/CalDAV/Notifications/Collection.php new file mode 100644 index 000000000..1fcc1171c --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Notifications/Collection.php @@ -0,0 +1,173 @@ +<?php + +namespace Sabre\CalDAV\Notifications; + +use Sabre\DAV; +use Sabre\CalDAV; +use Sabre\DAVACL; + +/** + * This node represents a list of notifications. + * + * It provides no additional functionality, but you must implement this + * interface to allow the Notifications plugin to mark the collection + * as a notifications collection. + * + * This collection should only return Sabre\CalDAV\Notifications\INode nodes as + * its children. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Collection extends DAV\Collection implements ICollection, DAVACL\IACL { + + /** + * The notification backend + * + * @var Sabre\CalDAV\Backend\NotificationSupport + */ + protected $caldavBackend; + + /** + * Principal uri + * + * @var string + */ + protected $principalUri; + + /** + * Constructor + * + * @param CalDAV\Backend\NotificationSupport $caldavBackend + * @param string $principalUri + */ + function __construct(CalDAV\Backend\NotificationSupport $caldavBackend, $principalUri) { + + $this->caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + + } + + /** + * Returns all notifications for a principal + * + * @return array + */ + function getChildren() { + + $children = []; + $notifications = $this->caldavBackend->getNotificationsForPrincipal($this->principalUri); + + foreach ($notifications as $notification) { + + $children[] = new Node( + $this->caldavBackend, + $this->principalUri, + $notification + ); + } + + return $children; + + } + + /** + * Returns the name of this object + * + * @return string + */ + function getName() { + + return 'notifications'; + + } + + /** + * Returns the owner principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + 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. 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 + */ + function getACL() { + + return [ + [ + 'principal' => $this->getOwner(), + 'privilege' => '{DAV:}read', + 'protected' => true, + ], + [ + 'principal' => $this->getOwner(), + 'privilege' => '{DAV:}write', + 'protected' => true, + ] + ]; + + } + + /** + * Updates the ACL + * + * This method will receive a list of new ACE's as an array argument. + * + * @param array $acl + * @return void + */ + function setACL(array $acl) { + + throw new DAV\Exception\NotImplemented('Updating ACLs is not implemented here'); + + } + + /** + * Returns the list of supported privileges for this node. + * + * The returned data structure is a list of nested privileges. + * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple + * standard structure. + * + * If null is returned from this method, the default privilege set is used, + * which is fine for most common usecases. + * + * @return array|null + */ + function getSupportedPrivilegeSet() { + + return null; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php b/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php new file mode 100644 index 000000000..008e87435 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Notifications/ICollection.php @@ -0,0 +1,23 @@ +<?php + +namespace Sabre\CalDAV\Notifications; + +use Sabre\DAV; + +/** + * This node represents a list of notifications. + * + * It provides no additional functionality, but you must implement this + * interface to allow the Notifications plugin to mark the collection + * as a notifications collection. + * + * This collection should only return Sabre\CalDAV\Notifications\INode nodes as + * its children. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface ICollection extends DAV\ICollection { + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Notifications/INode.php b/vendor/sabre/dav/lib/CalDAV/Notifications/INode.php new file mode 100644 index 000000000..f9986b714 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Notifications/INode.php @@ -0,0 +1,38 @@ +<?php + +namespace Sabre\CalDAV\Notifications; + +/** + * This node represents a single notification. + * + * The signature is mostly identical to that of Sabre\DAV\IFile, but the get() method + * MUST return an xml document that matches the requirements of the + * 'caldav-notifications.txt' spec. + * + * For a complete example, check out the Notification class, which contains + * some helper functions. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface INode { + + /** + * This method must return an xml element, using the + * Sabre\CalDAV\Notifications\INotificationType classes. + * + * @return INotificationType + */ + function getNotificationType(); + + /** + * Returns the etag for the notification. + * + * The etag must be surrounded by litteral double-quotes. + * + * @return string + */ + function getETag(); + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Notifications/Node.php b/vendor/sabre/dav/lib/CalDAV/Notifications/Node.php new file mode 100644 index 000000000..47e78d5de --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Notifications/Node.php @@ -0,0 +1,193 @@ +<?php + +namespace Sabre\CalDAV\Notifications; + +use Sabre\DAV; +use Sabre\CalDAV; +use Sabre\CalDAV\Xml\Notification\NotificationInterface; +use Sabre\DAVACL; + +/** + * This node represents a single notification. + * + * The signature is mostly identical to that of Sabre\DAV\IFile, but the get() method + * MUST return an xml document that matches the requirements of the + * 'caldav-notifications.txt' spec. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Node extends DAV\File implements INode, DAVACL\IACL { + + /** + * The notification backend + * + * @var Sabre\CalDAV\Backend\NotificationSupport + */ + protected $caldavBackend; + + /** + * The actual notification + * + * @var Sabre\CalDAV\Notifications\INotificationType + */ + protected $notification; + + /** + * Owner principal of the notification + * + * @var string + */ + protected $principalUri; + + /** + * Constructor + * + * @param CalDAV\Backend\NotificationSupport $caldavBackend + * @param string $principalUri + * @param NotificationInterface $notification + */ + function __construct(CalDAV\Backend\NotificationSupport $caldavBackend, $principalUri, NotificationInterface $notification) { + + $this->caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + $this->notification = $notification; + + } + + /** + * Returns the path name for this notification + * + * @return id + */ + function getName() { + + return $this->notification->getId() . '.xml'; + + } + + /** + * Returns the etag for the notification. + * + * The etag must be surrounded by litteral double-quotes. + * + * @return string + */ + function getETag() { + + return $this->notification->getETag(); + + } + + /** + * This method must return an xml element, using the + * Sabre\CalDAV\Notifications\INotificationType classes. + * + * @return INotificationType + */ + function getNotificationType() { + + return $this->notification; + + } + + /** + * Deletes this notification + * + * @return void + */ + function delete() { + + $this->caldavBackend->deleteNotification($this->getOwner(), $this->notification); + + } + + /** + * Returns the owner principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + 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. 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 + */ + function getACL() { + + return [ + [ + 'principal' => $this->getOwner(), + 'privilege' => '{DAV:}read', + 'protected' => true, + ], + [ + 'principal' => $this->getOwner(), + 'privilege' => '{DAV:}write', + 'protected' => true, + ] + ]; + + } + + /** + * Updates the ACL + * + * This method will receive a list of new ACE's as an array argument. + * + * @param array $acl + * @return void + */ + function setACL(array $acl) { + + throw new DAV\Exception\NotImplemented('Updating ACLs is not implemented here'); + + } + + /** + * Returns the list of supported privileges for this node. + * + * The returned data structure is a list of nested privileges. + * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple + * standard structure. + * + * If null is returned from this method, the default privilege set is used, + * which is fine for most common usecases. + * + * @return array|null + */ + function getSupportedPrivilegeSet() { + + return null; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php b/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php new file mode 100644 index 000000000..546bf927f --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Notifications/Plugin.php @@ -0,0 +1,180 @@ +<?php + +namespace Sabre\CalDAV\Notifications; + +use Sabre\DAV; +use Sabre\DAV\PropFind; +use Sabre\DAV\INode as BaseINode; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Server; +use Sabre\DAVACL; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +/** + * Notifications plugin + * + * This plugin implements several features required by the caldav-notification + * draft specification. + * + * Before version 2.1.0 this functionality was part of Sabre\CalDAV\Plugin but + * this has since been split up. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Plugin extends ServerPlugin { + + /** + * This is the namespace for the proprietary calendarserver extensions + */ + const NS_CALENDARSERVER = 'http://calendarserver.org/ns/'; + + /** + * Reference to the main server object. + * + * @var Server + */ + protected $server; + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + function getPluginName() { + + return 'notifications'; + + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param Server $server + * @return void + */ + function initialize(Server $server) { + + $this->server = $server; + $server->on('method:GET', [$this, 'httpGet'], 90); + $server->on('propFind', [$this, 'propFind']); + + $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs'; + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Notifications\\ICollection'] = '{' . self::NS_CALENDARSERVER . '}notification'; + + array_push($server->protectedProperties, + '{' . self::NS_CALENDARSERVER . '}notification-URL', + '{' . self::NS_CALENDARSERVER . '}notificationtype' + ); + + } + + /** + * PropFind + * + * @param PropFind $propFind + * @param BaseINode $node + * @return void + */ + function propFind(PropFind $propFind, BaseINode $node) { + + $caldavPlugin = $this->server->getPlugin('caldav'); + + if ($node instanceof DAVACL\IPrincipal) { + + $principalUrl = $node->getPrincipalUrl(); + + // notification-URL property + $propFind->handle('{' . self::NS_CALENDARSERVER . '}notification-URL', function() use ($principalUrl, $caldavPlugin) { + + $notificationPath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl) . '/notifications/'; + return new DAV\Xml\Property\Href($notificationPath); + + }); + + } + + if ($node instanceof INode) { + + $propFind->handle( + '{' . self::NS_CALENDARSERVER . '}notificationtype', + [$node, 'getNotificationType'] + ); + + } + + } + + /** + * This event is triggered before the usual GET request handler. + * + * We use this to intercept GET calls to notification nodes, and return the + * proper response. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + function httpGet(RequestInterface $request, ResponseInterface $response) { + + $path = $request->getPath(); + + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (DAV\Exception\NotFound $e) { + return; + } + + if (!$node instanceof INode) + return; + + $writer = $this->server->xml->getWriter(); + $writer->contextUri = $this->server->getBaseUri(); + $writer->openMemory(); + $writer->startDocument('1.0', 'UTF-8'); + $writer->startElement('{http://calendarserver.org/ns/}notification'); + $node->getNotificationType()->xmlSerializeFull($writer); + $writer->endElement(); + + $response->setHeader('Content-Type', 'application/xml'); + $response->setHeader('ETag', $node->getETag()); + $response->setStatus(200); + $response->setBody($writer->outputMemory()); + + // Return false to break the event chain. + return false; + + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for caldav-notifications, which is required to enable caldav-sharing.', + 'link' => 'http://sabre.io/dav/caldav-sharing/', + ]; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Plugin.php b/vendor/sabre/dav/lib/CalDAV/Plugin.php new file mode 100644 index 000000000..663490023 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Plugin.php @@ -0,0 +1,1025 @@ +<?php + +namespace Sabre\CalDAV; + +use DateTimeZone; +use Sabre\DAV; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\MkCol; +use Sabre\DAV\Xml\Property\Href; +use Sabre\DAVACL; +use Sabre\VObject; +use Sabre\HTTP; +use Sabre\Uri; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +/** + * CalDAV plugin + * + * This plugin provides functionality added by CalDAV (RFC 4791) + * It implements new reports, and the MKCALENDAR method. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Plugin extends DAV\ServerPlugin { + + /** + * This is the official CalDAV namespace + */ + const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav'; + + /** + * This is the namespace for the proprietary calendarserver extensions + */ + const NS_CALENDARSERVER = 'http://calendarserver.org/ns/'; + + /** + * The hardcoded root for calendar objects. It is unfortunate + * that we're stuck with it, but it will have to do for now + */ + const CALENDAR_ROOT = 'calendars'; + + /** + * Reference to server object + * + * @var DAV\Server + */ + protected $server; + + /** + * The default PDO storage uses a MySQL MEDIUMBLOB for iCalendar data, + * which can hold up to 2^24 = 16777216 bytes. This is plenty. We're + * capping it to 10M here. + */ + protected $maxResourceSize = 10000000; + + /** + * Use this method to tell the server this plugin defines additional + * HTTP methods. + * + * This method is passed a uri. It should only return HTTP methods that are + * available for the specified uri. + * + * @param string $uri + * @return array + */ + function getHTTPMethods($uri) { + + // The MKCALENDAR is only available on unmapped uri's, whose + // parents extend IExtendedCollection + list($parent, $name) = Uri\split($uri); + + $node = $this->server->tree->getNodeForPath($parent); + + if ($node instanceof DAV\IExtendedCollection) { + try { + $node->getChild($name); + } catch (DAV\Exception\NotFound $e) { + return ['MKCALENDAR']; + } + } + return []; + + } + + /** + * Returns the path to a principal's calendar home. + * + * The return url must not end with a slash. + * This function should return null in case a principal did not have + * a calendar home. + * + * @param string $principalUrl + * @return string + */ + function getCalendarHomeForPrincipal($principalUrl) { + + // The default behavior for most sabre/dav servers is that there is a + // principals root node, which contains users directly under it. + // + // This function assumes that there are two components in a principal + // path. If there's more, we don't return a calendar home. This + // excludes things like the calendar-proxy-read principal (which it + // should). + $parts = explode('/', trim($principalUrl, '/')); + if (count($parts) !== 2) return; + if ($parts[0] !== 'principals') return; + + return self::CALENDAR_ROOT . '/' . $parts[1]; + + } + + /** + * Returns a list of features for the DAV: HTTP header. + * + * @return array + */ + function getFeatures() { + + return ['calendar-access', 'calendar-proxy']; + + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + function getPluginName() { + + return 'caldav'; + + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * Note that you still need to subscribe to the 'report' event to actually + * implement them + * + * @param string $uri + * @return array + */ + function getSupportedReportSet($uri) { + + $node = $this->server->tree->getNodeForPath($uri); + + $reports = []; + if ($node instanceof ICalendarObjectContainer || $node instanceof ICalendarObject) { + $reports[] = '{' . self::NS_CALDAV . '}calendar-multiget'; + $reports[] = '{' . self::NS_CALDAV . '}calendar-query'; + } + if ($node instanceof ICalendar) { + $reports[] = '{' . self::NS_CALDAV . '}free-busy-query'; + } + // iCal has a bug where it assumes that sync support is enabled, only + // if we say we support it on the calendar-home, even though this is + // not actually the case. + if ($node instanceof CalendarHome && $this->server->getPlugin('sync')) { + $reports[] = '{DAV:}sync-collection'; + } + return $reports; + + } + + /** + * Initializes the plugin + * + * @param DAV\Server $server + * @return void + */ + function initialize(DAV\Server $server) { + + $this->server = $server; + + $server->on('method:MKCALENDAR', [$this, 'httpMkCalendar']); + $server->on('report', [$this, 'report']); + $server->on('propFind', [$this, 'propFind']); + $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); + $server->on('beforeWriteContent', [$this, 'beforeWriteContent']); + $server->on('afterMethod:GET', [$this, 'httpAfterGET']); + + $server->xml->namespaceMap[self::NS_CALDAV] = 'cal'; + $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs'; + + $server->xml->elementMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet'; + $server->xml->elementMap['{' . self::NS_CALDAV . '}calendar-query'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport'; + $server->xml->elementMap['{' . self::NS_CALDAV . '}calendar-multiget'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport'; + $server->xml->elementMap['{' . self::NS_CALDAV . '}free-busy-query'] = 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport'; + $server->xml->elementMap['{' . self::NS_CALDAV . '}mkcalendar'] = 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar'; + $server->xml->elementMap['{' . self::NS_CALDAV . '}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp'; + $server->xml->elementMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet'; + + $server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar'; + + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read'; + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write'; + + array_push($server->protectedProperties, + + '{' . self::NS_CALDAV . '}supported-calendar-component-set', + '{' . self::NS_CALDAV . '}supported-calendar-data', + '{' . self::NS_CALDAV . '}max-resource-size', + '{' . self::NS_CALDAV . '}min-date-time', + '{' . self::NS_CALDAV . '}max-date-time', + '{' . self::NS_CALDAV . '}max-instances', + '{' . self::NS_CALDAV . '}max-attendees-per-instance', + '{' . self::NS_CALDAV . '}calendar-home-set', + '{' . self::NS_CALDAV . '}supported-collation-set', + '{' . self::NS_CALDAV . '}calendar-data', + + // CalendarServer extensions + '{' . self::NS_CALENDARSERVER . '}getctag', + '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for', + '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for' + + ); + + if ($aclPlugin = $server->getPlugin('acl')) { + $aclPlugin->principalSearchPropertySet['{' . self::NS_CALDAV . '}calendar-user-address-set'] = 'Calendar address'; + } + } + + /** + * This functions handles REPORT requests specific to CalDAV + * + * @param string $reportName + * @param mixed $report + * @return bool + */ + function report($reportName, $report) { + + switch ($reportName) { + case '{' . self::NS_CALDAV . '}calendar-multiget' : + $this->server->transactionType = 'report-calendar-multiget'; + $this->calendarMultiGetReport($report); + return false; + case '{' . self::NS_CALDAV . '}calendar-query' : + $this->server->transactionType = 'report-calendar-query'; + $this->calendarQueryReport($report); + return false; + case '{' . self::NS_CALDAV . '}free-busy-query' : + $this->server->transactionType = 'report-free-busy-query'; + $this->freeBusyQueryReport($report); + return false; + + } + + + } + + /** + * This function handles the MKCALENDAR HTTP method, which creates + * a new calendar. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + function httpMkCalendar(RequestInterface $request, ResponseInterface $response) { + + $body = $request->getBodyAsString(); + $path = $request->getPath(); + + $properties = []; + + if ($body) { + + try { + $mkcalendar = $this->server->xml->expect( + '{urn:ietf:params:xml:ns:caldav}mkcalendar', + $body + ); + } catch (\Sabre\Xml\ParseException $e) { + throw new BadRequest($e->getMessage(), null, $e); + } + $properties = $mkcalendar->getProperties(); + + } + + // iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored + // subscriptions. Before that it used MKCOL which was the correct way + // to do this. + // + // If the body had a {DAV:}resourcetype, it means we stumbled upon this + // request, and we simply use it instead of the pre-defined list. + if (isset($properties['{DAV:}resourcetype'])) { + $resourceType = $properties['{DAV:}resourcetype']->getValue(); + } else { + $resourceType = ['{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar']; + } + + $this->server->createCollection($path, new MkCol($resourceType, $properties)); + + $this->server->httpResponse->setStatus(201); + $this->server->httpResponse->setHeader('Content-Length', 0); + + // This breaks the method chain. + return false; + } + + /** + * PropFind + * + * This method handler is invoked before any after properties for a + * resource are fetched. This allows us to add in any CalDAV specific + * properties. + * + * @param DAV\PropFind $propFind + * @param DAV\INode $node + * @return void + */ + function propFind(DAV\PropFind $propFind, DAV\INode $node) { + + $ns = '{' . self::NS_CALDAV . '}'; + + if ($node instanceof ICalendarObjectContainer) { + + $propFind->handle($ns . 'max-resource-size', $this->maxResourceSize); + $propFind->handle($ns . 'supported-calendar-data', function() { + return new Xml\Property\SupportedCalendarData(); + }); + $propFind->handle($ns . 'supported-collation-set', function() { + return new Xml\Property\SupportedCollationSet(); + }); + + } + + if ($node instanceof DAVACL\IPrincipal) { + + $principalUrl = $node->getPrincipalUrl(); + + $propFind->handle('{' . self::NS_CALDAV . '}calendar-home-set', function() use ($principalUrl) { + + $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl); + if (is_null($calendarHomePath)) return null; + return new Href($calendarHomePath . '/'); + + }); + // The calendar-user-address-set property is basically mapped to + // the {DAV:}alternate-URI-set property. + $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-address-set', function() use ($node) { + $addresses = $node->getAlternateUriSet(); + $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl() . '/'; + return new Href($addresses, false); + }); + // For some reason somebody thought it was a good idea to add + // another one of these properties. We're supporting it too. + $propFind->handle('{' . self::NS_CALENDARSERVER . '}email-address-set', function() use ($node) { + $addresses = $node->getAlternateUriSet(); + $emails = []; + foreach ($addresses as $address) { + if (substr($address, 0, 7) === 'mailto:') { + $emails[] = substr($address, 7); + } + } + return new Xml\Property\EmailAddressSet($emails); + }); + + // These two properties are shortcuts for ical to easily find + // other principals this principal has access to. + $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for'; + $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for'; + + if ($propFind->getStatus($propRead) === 404 || $propFind->getStatus($propWrite) === 404) { + + $aclPlugin = $this->server->getPlugin('acl'); + $membership = $aclPlugin->getPrincipalMembership($propFind->getPath()); + $readList = []; + $writeList = []; + + foreach ($membership as $group) { + + $groupNode = $this->server->tree->getNodeForPath($group); + + $listItem = Uri\split($group)[0] . '/'; + + // If the node is either ap proxy-read or proxy-write + // group, we grab the parent principal and add it to the + // list. + if ($groupNode instanceof Principal\IProxyRead) { + $readList[] = $listItem; + } + if ($groupNode instanceof Principal\IProxyWrite) { + $writeList[] = $listItem; + } + + } + + $propFind->set($propRead, new Href($readList)); + $propFind->set($propWrite, new Href($writeList)); + + } + + } // instanceof IPrincipal + + if ($node instanceof ICalendarObject) { + + // The calendar-data property is not supposed to be a 'real' + // property, but in large chunks of the spec it does act as such. + // Therefore we simply expose it as a property. + $propFind->handle('{' . self::NS_CALDAV . '}calendar-data', function() use ($node) { + $val = $node->get(); + if (is_resource($val)) + $val = stream_get_contents($val); + + // Taking out \r to not screw up the xml output + return str_replace("\r", "", $val); + + }); + + } + + } + + /** + * This function handles the calendar-multiget REPORT. + * + * This report is used by the client to fetch the content of a series + * of urls. Effectively avoiding a lot of redundant requests. + * + * @param CalendarMultiGetReport $report + * @return void + */ + function calendarMultiGetReport($report) { + + $needsJson = $report->contentType === 'application/calendar+json'; + + $timeZones = []; + $propertyList = []; + + $paths = array_map( + [$this->server, 'calculateUri'], + $report->hrefs + ); + + foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $uri => $objProps) { + + if (($needsJson || $report->expand) && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) { + $vObject = VObject\Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']); + + if ($report->expand) { + // We're expanding, and for that we need to figure out the + // calendar's timezone. + list($calendarPath) = Uri\split($uri); + if (!isset($timeZones[$calendarPath])) { + // Checking the calendar-timezone property. + $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone'; + $tzResult = $this->server->getProperties($calendarPath, [$tzProp]); + if (isset($tzResult[$tzProp])) { + // This property contains a VCALENDAR with a single + // VTIMEZONE. + $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); + $timeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + } else { + // Defaulting to UTC. + $timeZone = new DateTimeZone('UTC'); + } + $timeZones[$calendarPath] = $timeZone; + } + + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $timeZones[$calendarPath]); + } + if ($needsJson) { + $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize()); + } else { + $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize(); + } + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + + $propertyList[] = $objProps; + + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, $prefer['return'] === 'minimal')); + + } + + /** + * This function handles the calendar-query REPORT + * + * This report is used by clients to request calendar objects based on + * complex conditions. + * + * @param Xml\Request\CalendarQueryReport $report + * @return void + */ + function calendarQueryReport($report) { + + $path = $this->server->getRequestUri(); + + $needsJson = $report->contentType === 'application/calendar+json'; + + $node = $this->server->tree->getNodeForPath($this->server->getRequestUri()); + $depth = $this->server->getHTTPDepth(0); + + // The default result is an empty array + $result = []; + + $calendarTimeZone = null; + if ($report->expand) { + // We're expanding, and for that we need to figure out the + // calendar's timezone. + $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone'; + $tzResult = $this->server->getProperties($path, [$tzProp]); + if (isset($tzResult[$tzProp])) { + // This property contains a VCALENDAR with a single + // VTIMEZONE. + $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + + // Destroy circular references so PHP will garbage collect the + // object. + $vtimezoneObj->destroy(); + } else { + // Defaulting to UTC. + $calendarTimeZone = new DateTimeZone('UTC'); + } + } + + // The calendarobject was requested directly. In this case we handle + // this locally. + if ($depth == 0 && $node instanceof ICalendarObject) { + + $requestedCalendarData = true; + $requestedProperties = $report->properties; + + if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) { + + // We always retrieve calendar-data, as we need it for filtering. + $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data'; + + // If calendar-data wasn't explicitly requested, we need to remove + // it after processing. + $requestedCalendarData = false; + } + + $properties = $this->server->getPropertiesForPath( + $path, + $requestedProperties, + 0 + ); + + // This array should have only 1 element, the first calendar + // object. + $properties = current($properties); + + // If there wasn't any calendar-data returned somehow, we ignore + // this. + if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) { + + $validator = new CalendarQueryValidator(); + + $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); + if ($validator->validate($vObject, $report->filters)) { + + // If the client didn't require the calendar-data property, + // we won't give it back. + if (!$requestedCalendarData) { + unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); + } else { + + + if ($report->expand) { + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone); + } + if ($needsJson) { + $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize()); + } elseif ($report->expand) { + $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize(); + } + } + + $result = [$properties]; + + } + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + + } + + } + + if ($node instanceof ICalendarObjectContainer && $depth === 0) { + + if (strpos($this->server->httpRequest->getHeader('User-Agent'), 'MSFT-') === 0) { + // Microsoft clients incorrectly supplied depth as 0, when it actually + // should have set depth to 1. We're implementing a workaround here + // to deal with this. + // + // This targets at least the following clients: + // Windows 10 + // Windows Phone 8, 10 + $depth = 1; + } else { + throw new BadRequest('A calendar-query REPORT on a calendar with a Depth: 0 is undefined. Set Depth to 1'); + } + + } + + // If we're dealing with a calendar, the calendar itself is responsible + // for the calendar-query. + if ($node instanceof ICalendarObjectContainer && $depth == 1) { + + $nodePaths = $node->calendarQuery($report->filters); + + foreach ($nodePaths as $path) { + + list($properties) = + $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $report->properties); + + if (($needsJson || $report->expand)) { + $vObject = VObject\Reader::read($properties[200]['{' . self::NS_CALDAV . '}calendar-data']); + + if ($report->expand) { + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone); + } + + if ($needsJson) { + $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize()); + } else { + $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize(); + } + + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + $result[] = $properties; + + } + + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, $prefer['return'] === 'minimal')); + + } + + /** + * This method is responsible for parsing the request and generating the + * response for the CALDAV:free-busy-query REPORT. + * + * @param Xml\Request\FreeBusyQueryReport $report + * @return void + */ + protected function freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report) { + + $uri = $this->server->getRequestUri(); + + $acl = $this->server->getPlugin('acl'); + if ($acl) { + $acl->checkPrivileges($uri, '{' . self::NS_CALDAV . '}read-free-busy'); + } + + $calendar = $this->server->tree->getNodeForPath($uri); + if (!$calendar instanceof ICalendar) { + throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars'); + } + + $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone'; + + // Figuring out the default timezone for the calendar, for floating + // times. + $calendarProps = $this->server->getProperties($uri, [$tzProp]); + + if (isset($calendarProps[$tzProp])) { + $vtimezoneObj = VObject\Reader::read($calendarProps[$tzProp]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + // Destroy circular references so PHP will garbage collect the object. + $vtimezoneObj->destroy(); + } else { + $calendarTimeZone = new DateTimeZone('UTC'); + } + + // Doing a calendar-query first, to make sure we get the most + // performance. + $urls = $calendar->calendarQuery([ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $report->start, + 'end' => $report->end, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + + $objects = array_map(function($url) use ($calendar) { + $obj = $calendar->getChild($url)->get(); + return $obj; + }, $urls); + + $generator = new VObject\FreeBusyGenerator(); + $generator->setObjects($objects); + $generator->setTimeRange($report->start, $report->end); + $generator->setTimeZone($calendarTimeZone); + $result = $generator->getResult(); + $result = $result->serialize(); + + $this->server->httpResponse->setStatus(200); + $this->server->httpResponse->setHeader('Content-Type', 'text/calendar'); + $this->server->httpResponse->setHeader('Content-Length', strlen($result)); + $this->server->httpResponse->setBody($result); + + } + + /** + * This method is triggered before a file gets updated with new content. + * + * This plugin uses this method to ensure that CalDAV objects receive + * valid calendar data. + * + * @param string $path + * @param DAV\IFile $node + * @param resource $data + * @param bool $modified Should be set to true, if this event handler + * changed &$data. + * @return void + */ + function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) { + + if (!$node instanceof ICalendarObject) + return; + + // We're onyl interested in ICalendarObject nodes that are inside of a + // real calendar. This is to avoid triggering validation and scheduling + // for non-calendars (such as an inbox). + list($parent) = Uri\split($path); + $parentNode = $this->server->tree->getNodeForPath($parent); + + if (!$parentNode instanceof ICalendar) + return; + + $this->validateICalendar( + $data, + $path, + $modified, + $this->server->httpRequest, + $this->server->httpResponse, + false + ); + + } + + /** + * This method is triggered before a new file is created. + * + * This plugin uses this method to ensure that newly created calendar + * objects contain valid calendar data. + * + * @param string $path + * @param resource $data + * @param DAV\ICollection $parentNode + * @param bool $modified Should be set to true, if this event handler + * changed &$data. + * @return void + */ + function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) { + + if (!$parentNode instanceof ICalendar) + return; + + $this->validateICalendar( + $data, + $path, + $modified, + $this->server->httpRequest, + $this->server->httpResponse, + true + ); + + } + + /** + * 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, &$modified, RequestInterface $request, 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 { + + // 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 { + $vobj = VObject\Reader::read($data); + } + + } catch (VObject\ParseException $e) { + + 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) = Uri\split($path); + $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]); + + if (isset($calendarProperties[$sCCS])) { + $supportedComponents = $calendarProperties[$sCCS]->getValue(); + } else { + $supportedComponents = ['VJOURNAL', '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 Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType); + } + if (!isset($component->UID)) { + throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID'); + } + $foundUID = (string)$component->UID; + } else { + if ($foundType !== $component->name) { + throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType); + } + if ($foundUID !== (string)$component->UID) { + throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs'); + } + } + break; + default : + throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here'); + + } + } + if (!$foundType) + throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL'); + + // 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; + } + + } + + // Destroy circular references so PHP will garbage collect the object. + $vobj->destroy(); + + } + + + /** + * This method is used to generate HTML output for the + * DAV\Browser\Plugin. This allows us to generate an interface users + * can use to create new calendars. + * + * @param DAV\INode $node + * @param string $output + * @return bool + */ + function htmlActionsPanel(DAV\INode $node, &$output) { + + if (!$node instanceof CalendarHome) + return; + + $output .= '<tr><td colspan="2"><form method="post" action=""> + <h3>Create new calendar</h3> + <input type="hidden" name="sabreAction" value="mkcol" /> + <input type="hidden" name="resourceType" value="{DAV:}collection,{' . self::NS_CALDAV . '}calendar" /> + <label>Name (uri):</label> <input type="text" name="name" /><br /> + <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br /> + <input type="submit" value="create" /> + </form> + </td></tr>'; + + return false; + + } + + /** + * This event is triggered after GET requests. + * + * This is used to transform data into jCal, if this was requested. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + function httpAfterGet(RequestInterface $request, ResponseInterface $response) { + + if (strpos($response->getHeader('Content-Type'), 'text/calendar') === false) { + return; + } + + $result = HTTP\Util::negotiate( + $request->getHeader('Accept'), + ['text/calendar', 'application/calendar+json'] + ); + + if ($result !== 'application/calendar+json') { + // Do nothing + return; + } + + // Transforming. + $vobj = VObject\Reader::read($response->getBody()); + + $jsonBody = json_encode($vobj->jsonSerialize()); + $response->setBody($jsonBody); + + // Destroy circular references so PHP will garbage collect the object. + $vobj->destroy(); + + $response->setHeader('Content-Type', 'application/calendar+json'); + $response->setHeader('Content-Length', strlen($jsonBody)); + + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for CalDAV (rfc4791)', + 'link' => 'http://sabre.io/dav/caldav/', + ]; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php b/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php new file mode 100644 index 000000000..e19719a76 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Principal/Collection.php @@ -0,0 +1,33 @@ +<?php + +namespace Sabre\CalDAV\Principal; + +use Sabre\DAVACL; + +/** + * Principal collection + * + * This is an alternative collection to the standard ACL principal collection. + * This collection adds support for the calendar-proxy-read and + * calendar-proxy-write sub-principals, as defined by the caldav-proxy + * specification. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Collection extends DAVACL\PrincipalCollection { + + /** + * Returns a child object based on principal information + * + * @param array $principalInfo + * @return User + */ + function getChildForPrincipal(array $principalInfo) { + + return new User($this->principalBackend, $principalInfo); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Principal/IProxyRead.php b/vendor/sabre/dav/lib/CalDAV/Principal/IProxyRead.php new file mode 100644 index 000000000..7dd375932 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Principal/IProxyRead.php @@ -0,0 +1,19 @@ +<?php + +namespace Sabre\CalDAV\Principal; + +use Sabre\DAVACL; + +/** + * ProxyRead principal interface + * + * Any principal node implementing this interface will be picked up as a 'proxy + * principal group'. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface IProxyRead extends DAVACL\IPrincipal { + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Principal/IProxyWrite.php b/vendor/sabre/dav/lib/CalDAV/Principal/IProxyWrite.php new file mode 100644 index 000000000..eda87a4ff --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Principal/IProxyWrite.php @@ -0,0 +1,19 @@ +<?php + +namespace Sabre\CalDAV\Principal; + +use Sabre\DAVACL; + +/** + * ProxyWrite principal interface + * + * Any principal node implementing this interface will be picked up as a 'proxy + * principal group'. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface IProxyWrite extends DAVACL\IPrincipal { + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Principal/ProxyRead.php b/vendor/sabre/dav/lib/CalDAV/Principal/ProxyRead.php new file mode 100644 index 000000000..93f0fe095 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Principal/ProxyRead.php @@ -0,0 +1,181 @@ +<?php + +namespace Sabre\CalDAV\Principal; + +use Sabre\DAVACL; +use Sabre\DAV; + +/** + * ProxyRead principal + * + * This class represents a principal group, hosted under the main principal. + * This is needed to implement 'Calendar delegation' support. This class is + * instantiated by User. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ProxyRead implements IProxyRead { + + /** + * Principal information from the parent principal. + * + * @var array + */ + protected $principalInfo; + + /** + * Principal backend + * + * @var DAVACL\PrincipalBackend\BackendInterface + */ + protected $principalBackend; + + /** + * Creates the object. + * + * Note that you MUST supply the parent principal information. + * + * @param DAVACL\PrincipalBackend\BackendInterface $principalBackend + * @param array $principalInfo + */ + function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo) { + + $this->principalInfo = $principalInfo; + $this->principalBackend = $principalBackend; + + } + + /** + * Returns this principals name. + * + * @return string + */ + function getName() { + + return 'calendar-proxy-read'; + + } + + /** + * Returns the last modification time + * + * @return null + */ + function getLastModified() { + + return null; + + } + + /** + * Deletes the current node + * + * @throws DAV\Exception\Forbidden + * @return void + */ + function delete() { + + throw new DAV\Exception\Forbidden('Permission denied to delete node'); + + } + + /** + * Renames the node + * + * @throws DAV\Exception\Forbidden + * @param string $name The new name + * @return void + */ + function setName($name) { + + throw new DAV\Exception\Forbidden('Permission denied to rename file'); + + } + + + /** + * Returns a list of alternative urls for a principal + * + * This can for example be an email address, or ldap url. + * + * @return array + */ + function getAlternateUriSet() { + + return []; + + } + + /** + * Returns the full principal url + * + * @return string + */ + function getPrincipalUrl() { + + return $this->principalInfo['uri'] . '/' . $this->getName(); + + } + + /** + * Returns the list of group members + * + * If this principal is a group, this function should return + * all member principal uri's for the group. + * + * @return array + */ + function getGroupMemberSet() { + + return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl()); + + } + + /** + * Returns the list of groups this principal is member of + * + * If this principal is a member of a (list of) groups, this function + * should return a list of principal uri's for it's members. + * + * @return array + */ + function getGroupMembership() { + + return $this->principalBackend->getGroupMembership($this->getPrincipalUrl()); + + } + + /** + * Sets a list of group members + * + * If this principal is a group, this method sets all the group members. + * The list of members is always overwritten, never appended to. + * + * This method should throw an exception if the members could not be set. + * + * @param array $principals + * @return void + */ + function setGroupMemberSet(array $principals) { + + $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals); + + } + + /** + * Returns the displayname + * + * This should be a human readable name for the principal. + * If none is available, return the nodename. + * + * @return string + */ + function getDisplayName() { + + return $this->getName(); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php b/vendor/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php new file mode 100644 index 000000000..8124c05e0 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php @@ -0,0 +1,181 @@ +<?php + +namespace Sabre\CalDAV\Principal; + +use Sabre\DAVACL; +use Sabre\DAV; + +/** + * ProxyWrite principal + * + * This class represents a principal group, hosted under the main principal. + * This is needed to implement 'Calendar delegation' support. This class is + * instantiated by User. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ProxyWrite implements IProxyWrite { + + /** + * Parent principal information + * + * @var array + */ + protected $principalInfo; + + /** + * Principal Backend + * + * @var DAVACL\PrincipalBackend\BackendInterface + */ + protected $principalBackend; + + /** + * Creates the object + * + * Note that you MUST supply the parent principal information. + * + * @param DAVACL\PrincipalBackend\BackendInterface $principalBackend + * @param array $principalInfo + */ + function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo) { + + $this->principalInfo = $principalInfo; + $this->principalBackend = $principalBackend; + + } + + /** + * Returns this principals name. + * + * @return string + */ + function getName() { + + return 'calendar-proxy-write'; + + } + + /** + * Returns the last modification time + * + * @return null + */ + function getLastModified() { + + return null; + + } + + /** + * Deletes the current node + * + * @throws DAV\Exception\Forbidden + * @return void + */ + function delete() { + + throw new DAV\Exception\Forbidden('Permission denied to delete node'); + + } + + /** + * Renames the node + * + * @throws DAV\Exception\Forbidden + * @param string $name The new name + * @return void + */ + function setName($name) { + + throw new DAV\Exception\Forbidden('Permission denied to rename file'); + + } + + + /** + * Returns a list of alternative urls for a principal + * + * This can for example be an email address, or ldap url. + * + * @return array + */ + function getAlternateUriSet() { + + return []; + + } + + /** + * Returns the full principal url + * + * @return string + */ + function getPrincipalUrl() { + + return $this->principalInfo['uri'] . '/' . $this->getName(); + + } + + /** + * Returns the list of group members + * + * If this principal is a group, this function should return + * all member principal uri's for the group. + * + * @return array + */ + function getGroupMemberSet() { + + return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl()); + + } + + /** + * Returns the list of groups this principal is member of + * + * If this principal is a member of a (list of) groups, this function + * should return a list of principal uri's for it's members. + * + * @return array + */ + function getGroupMembership() { + + return $this->principalBackend->getGroupMembership($this->getPrincipalUrl()); + + } + + /** + * Sets a list of group members + * + * If this principal is a group, this method sets all the group members. + * The list of members is always overwritten, never appended to. + * + * This method should throw an exception if the members could not be set. + * + * @param array $principals + * @return void + */ + function setGroupMemberSet(array $principals) { + + $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals); + + } + + /** + * Returns the displayname + * + * This should be a human readable name for the principal. + * If none is available, return the nodename. + * + * @return string + */ + function getDisplayName() { + + return $this->getName(); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Principal/User.php b/vendor/sabre/dav/lib/CalDAV/Principal/User.php new file mode 100644 index 000000000..6e97e7cca --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Principal/User.php @@ -0,0 +1,135 @@ +<?php + +namespace Sabre\CalDAV\Principal; + +use Sabre\DAV; +use Sabre\DAVACL; + +/** + * CalDAV principal + * + * This is a standard user-principal for CalDAV. This principal is also a + * collection and returns the caldav-proxy-read and caldav-proxy-write child + * principals. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class User extends DAVACL\Principal implements DAV\ICollection { + + /** + * Creates a new file in the directory + * + * @param string $name Name of the file + * @param resource $data Initial payload, passed as a readable stream resource. + * @throws DAV\Exception\Forbidden + * @return void + */ + function createFile($name, $data = null) { + + throw new DAV\Exception\Forbidden('Permission denied to create file (filename ' . $name . ')'); + + } + + /** + * Creates a new subdirectory + * + * @param string $name + * @throws DAV\Exception\Forbidden + * @return void + */ + function createDirectory($name) { + + throw new DAV\Exception\Forbidden('Permission denied to create directory'); + + } + + /** + * Returns a specific child node, referenced by its name + * + * @param string $name + * @return DAV\INode + */ + function getChild($name) { + + $principal = $this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/' . $name); + if (!$principal) { + throw new DAV\Exception\NotFound('Node with name ' . $name . ' was not found'); + } + if ($name === 'calendar-proxy-read') + return new ProxyRead($this->principalBackend, $this->principalProperties); + + if ($name === 'calendar-proxy-write') + return new ProxyWrite($this->principalBackend, $this->principalProperties); + + throw new DAV\Exception\NotFound('Node with name ' . $name . ' was not found'); + + } + + /** + * Returns an array with all the child nodes + * + * @return DAV\INode[] + */ + function getChildren() { + + $r = []; + if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/calendar-proxy-read')) { + $r[] = new ProxyRead($this->principalBackend, $this->principalProperties); + } + if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/calendar-proxy-write')) { + $r[] = new ProxyWrite($this->principalBackend, $this->principalProperties); + } + + return $r; + + } + + /** + * Returns whether or not the child node exists + * + * @param string $name + * @return bool + */ + function childExists($name) { + + try { + $this->getChild($name); + return true; + } catch (DAV\Exception\NotFound $e) { + 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 + */ + function getACL() { + + $acl = parent::getACL(); + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalProperties['uri'] . '/calendar-proxy-read', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalProperties['uri'] . '/calendar-proxy-write', + 'protected' => true, + ]; + return $acl; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php b/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php new file mode 100644 index 000000000..c9fd77d93 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Schedule/IInbox.php @@ -0,0 +1,15 @@ +<?php + +namespace Sabre\CalDAV\Schedule; + +/** + * Implement this interface to have a node be recognized as a CalDAV scheduling + * inbox. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface IInbox extends \Sabre\CalDAV\ICalendarObjectContainer, \Sabre\DAVACL\IACL { + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php b/vendor/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php new file mode 100644 index 000000000..ffb1fe45b --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -0,0 +1,190 @@ +<?php + +namespace Sabre\CalDAV\Schedule; + +use Sabre\DAV; +use Sabre\VObject\ITip; + +/** + * 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) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class IMipPlugin extends DAV\ServerPlugin { + + /** + * Email address used in From: header. + * + * @var string + */ + protected $senderEmail; + + /** + * ITipMessage + * + * @var ITip\Message + */ + protected $itipMessage; + + /** + * Creates the email handler. + * + * @param string $senderEmail. The 'senderEmail' is the email that shows up + * in the 'From:' address. This should + * generally be some kind of no-reply email + * address you own. + */ + function __construct($senderEmail) { + + $this->senderEmail = $senderEmail; + + } + + /* + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param DAV\Server $server + * @return void + */ + function initialize(DAV\Server $server) { + + $server->on('schedule', [$this, 'schedule'], 120); + + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + function getPluginName() { + + return 'imip'; + + } + + /** + * Event handler for the 'schedule' event. + * + * @param ITip\Message $iTipMessage + * @return void + */ + function schedule(ITip\Message $iTipMessage) { + + // 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; + } + + $summary = $iTipMessage->message->VEVENT->SUMMARY; + + if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') + return; + + if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') + return; + + $sender = substr($iTipMessage->sender, 7); + $recipient = substr($iTipMessage->recipient, 7); + + if ($iTipMessage->senderName) { + $sender = $iTipMessage->senderName . ' <' . $sender . '>'; + } + if ($iTipMessage->recipientName) { + $recipient = $iTipMessage->recipientName . ' <' . $recipient . '>'; + } + + $subject = 'SabreDAV iTIP message'; + switch (strtoupper($iTipMessage->method)) { + case 'REPLY' : + $subject = 'Re: ' . $summary; + break; + case 'REQUEST' : + $subject = $summary; + break; + case 'CANCEL' : + $subject = 'Cancelled: ' . $summary; + break; + } + + $headers = [ + 'Reply-To: ' . $sender, + 'From: ' . $this->senderEmail, + 'Content-Type: text/calendar; charset=UTF-8; method=' . $iTipMessage->method, + ]; + if (DAV\Server::$exposeVersion) { + $headers[] = 'X-Sabre-Version: ' . DAV\Version::VERSION; + } + $this->mail( + $recipient, + $subject, + $iTipMessage->message->serialize(), + $headers + ); + $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip'; + + } + + // @codeCoverageIgnoreStart + // This is deemed untestable in a reasonable manner + + /** + * This function is responsible 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)); + + } + + // @codeCoverageIgnoreEnd + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Email delivery (rfc6037) for CalDAV scheduling', + 'link' => 'http://sabre.io/dav/scheduling/', + ]; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php b/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php new file mode 100644 index 000000000..88fbdc411 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Schedule/IOutbox.php @@ -0,0 +1,15 @@ +<?php + +namespace Sabre\CalDAV\Schedule; + +/** + * Implement this interface to have a node be recognized as a CalDAV scheduling + * outbox. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface IOutbox extends \Sabre\DAV\ICollection, \Sabre\DAVACL\IACL { + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php b/vendor/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php new file mode 100644 index 000000000..b37cb40a1 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php @@ -0,0 +1,13 @@ +<?php + +namespace Sabre\CalDAV\Schedule; + +/** + * The SchedulingObject represents a scheduling object in the Inbox collection + * + * @license http://sabre.io/license/ Modified BSD License + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + */ +interface ISchedulingObject extends \Sabre\CalDAV\ICalendarObject { + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/Inbox.php b/vendor/sabre/dav/lib/CalDAV/Schedule/Inbox.php new file mode 100644 index 000000000..13212565e --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Schedule/Inbox.php @@ -0,0 +1,261 @@ +<?php + +namespace Sabre\CalDAV\Schedule; + +use Sabre\DAV; +use Sabre\CalDAV; +use Sabre\DAVACL; +use Sabre\CalDAV\Backend; +use Sabre\VObject; + +/** + * The CalDAV scheduling inbox + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Inbox extends DAV\Collection implements IInbox { + + /** + * CalDAV backend + * + * @var Backend\BackendInterface + */ + protected $caldavBackend; + + /** + * The principal Uri + * + * @var string + */ + protected $principalUri; + + /** + * Constructor + * + * @param Backend\SchedulingSupport $caldavBackend + * @param string $principalUri + */ + function __construct(Backend\SchedulingSupport $caldavBackend, $principalUri) { + + $this->caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + function getName() { + + return 'inbox'; + + } + + /** + * Returns an array with all the child nodes + * + * @return \Sabre\DAV\INode[] + */ + function getChildren() { + + $objs = $this->caldavBackend->getSchedulingObjects($this->principalUri); + $children = []; + foreach ($objs as $obj) { + //$obj['acl'] = $this->getACL(); + $obj['principaluri'] = $this->principalUri; + $children[] = new SchedulingObject($this->caldavBackend, $obj); + } + return $children; + + } + + /** + * Creates a new file in the directory + * + * Data will either be supplied as a stream resource, or in certain cases + * as a string. Keep in mind that you may have to support either. + * + * After succesful creation of the file, you may choose to return the ETag + * of the new file here. + * + * The returned ETag must be surrounded by double-quotes (The quotes should + * be part of the actual string). + * + * If you cannot accurately determine the ETag, you should not return it. + * If you don't store the file exactly as-is (you're transforming it + * somehow) you should also not return an ETag. + * + * This means that if a subsequent GET to this new file does not exactly + * return the same contents of what was submitted here, you are strongly + * recommended to omit the ETag. + * + * @param string $name Name of the file + * @param resource|string $data Initial payload + * @return null|string + */ + function createFile($name, $data = null) { + + $this->caldavBackend->createSchedulingObject($this->principalUri, $name, $data); + + } + + /** + * Returns the owner principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + 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. 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 + */ + function getACL() { + + return [ + [ + 'privilege' => '{DAV:}read', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}unbind', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}unbind', + 'principal' => $this->getOwner() . '/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-deliver-invite', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + [ + 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-deliver-reply', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + ]; + + } + + /** + * Updates the ACL + * + * This method will receive a list of new ACE's. + * + * @param array $acl + * @return void + */ + function setACL(array $acl) { + + throw new DAV\Exception\MethodNotAllowed('You\'re not allowed to update the ACL'); + + } + + /** + * Returns the list of supported privileges for this node. + * + * The returned data structure is a list of nested privileges. + * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple + * standard structure. + * + * If null is returned from this method, the default privilege set is used, + * which is fine for most common usecases. + * + * @return array|null + */ + function getSupportedPrivilegeSet() { + + $ns = '{' . CalDAV\Plugin::NS_CALDAV . '}'; + + $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet(); + $default['aggregates'][] = [ + 'privilege' => $ns . 'schedule-deliver', + 'aggregates' => [ + ['privilege' => $ns . 'schedule-deliver-invite'], + ['privilege' => $ns . 'schedule-deliver-reply'], + ], + ]; + return $default; + + } + + /** + * 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. + * + * @param array $filters + * @return array + */ + function calendarQuery(array $filters) { + + $result = []; + $validator = new CalDAV\CalendarQueryValidator(); + + $objects = $this->caldavBackend->getSchedulingObjects($this->principalUri); + foreach ($objects as $object) { + $vObject = VObject\Reader::read($object['calendardata']); + if ($validator->validate($vObject, $filters)) { + $result[] = $object['uri']; + } + + // Destroy circular references to PHP will GC the object. + $vObject->destroy(); + } + return $result; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php b/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php new file mode 100644 index 000000000..dabaee2ca --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php @@ -0,0 +1,184 @@ +<?php + +namespace Sabre\CalDAV\Schedule; + +use Sabre\DAV; +use Sabre\CalDAV; +use Sabre\DAVACL; + +/** + * The CalDAV scheduling outbox + * + * The outbox is mainly used as an endpoint in the tree for a client to do + * free-busy requests. This functionality is completely handled by the + * Scheduling plugin, so this object is actually mostly static. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Outbox extends DAV\Collection implements IOutbox { + + /** + * The principal Uri + * + * @var string + */ + protected $principalUri; + + /** + * Constructor + * + * @param string $principalUri + */ + function __construct($principalUri) { + + $this->principalUri = $principalUri; + + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + function getName() { + + return 'outbox'; + + } + + /** + * Returns an array with all the child nodes + * + * @return \Sabre\DAV\INode[] + */ + function getChildren() { + + return []; + + } + + /** + * Returns the owner principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + 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. 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 + */ + function getACL() { + + return [ + [ + 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy', + 'principal' => $this->getOwner() . '/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent', + 'principal' => $this->getOwner() . '/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner() . '/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner() . '/calendar-proxy-write', + 'protected' => true, + ], + ]; + + } + + /** + * Updates the ACL + * + * This method will receive a list of new ACE's. + * + * @param array $acl + * @return void + */ + function setACL(array $acl) { + + throw new DAV\Exception\MethodNotAllowed('You\'re not allowed to update the ACL'); + + } + + /** + * Returns the list of supported privileges for this node. + * + * The returned data structure is a list of nested privileges. + * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple + * standard structure. + * + * If null is returned from this method, the default privilege set is used, + * which is fine for most common usecases. + * + * @return array|null + */ + function getSupportedPrivilegeSet() { + + $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet(); + $default['aggregates'][] = [ + 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy', + ]; + $default['aggregates'][] = [ + 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent', + ]; + + return $default; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php b/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php new file mode 100644 index 000000000..827d6209b --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php @@ -0,0 +1,994 @@ +<?php + +namespace Sabre\CalDAV\Schedule; + +use DateTimeZone; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\PropFind; +use Sabre\DAV\PropPatch; +use Sabre\DAV\INode; +use Sabre\DAV\Xml\Property\Href; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Sabre\VObject; +use Sabre\VObject\Reader; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\ITip; +use Sabre\VObject\ITip\Message; +use Sabre\DAVACL; +use Sabre\CalDAV\ICalendar; +use Sabre\CalDAV\ICalendarObject; +use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Exception\NotImplemented; + +/** + * CalDAV scheduling plugin. + * ========================= + * + * This plugin provides the functionality added by the "Scheduling Extensions + * to CalDAV" standard, as defined in RFC6638. + * + * calendar-auto-schedule largely works by intercepting a users request to + * update their local calendar. If a user creates a new event with attendees, + * this plugin is supposed to grab the information from that event, and notify + * the attendees of this. + * + * There's 3 possible transports for this: + * * local delivery + * * delivery through email (iMip) + * * server-to-server delivery (iSchedule) + * + * iMip is simply, because we just need to add the iTip message as an email + * attachment. Local delivery is harder, because we both need to add this same + * message to a local DAV inbox, as well as live-update the relevant events. + * + * iSchedule is something for later. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Plugin extends ServerPlugin { + + /** + * This is the official CalDAV namespace + */ + const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav'; + + /** + * Reference to main Server object. + * + * @var Server + */ + protected $server; + + /** + * Returns a list of features for the DAV: HTTP header. + * + * @return array + */ + function getFeatures() { + + return ['calendar-auto-schedule', 'calendar-availability']; + + } + + /** + * Returns the name of the plugin. + * + * Using this name other plugins will be able to access other plugins + * using Server::getPlugin + * + * @return string + */ + function getPluginName() { + + return 'caldav-schedule'; + + } + + /** + * Initializes the plugin + * + * @param Server $server + * @return void + */ + function initialize(Server $server) { + + $this->server = $server; + $server->on('method:POST', [$this, 'httpPost']); + $server->on('propFind', [$this, 'propFind']); + $server->on('propPatch', [$this, 'propPatch']); + $server->on('calendarObjectChange', [$this, 'calendarObjectChange']); + $server->on('beforeUnbind', [$this, 'beforeUnbind']); + $server->on('schedule', [$this, 'scheduleLocalDelivery']); + + $ns = '{' . self::NS_CALDAV . '}'; + + /** + * This information ensures that the {DAV:}resourcetype property has + * the correct values. + */ + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = $ns . 'schedule-outbox'; + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IInbox'] = $ns . 'schedule-inbox'; + + /** + * Properties we protect are made read-only by the server. + */ + array_push($server->protectedProperties, + $ns . 'schedule-inbox-URL', + $ns . 'schedule-outbox-URL', + $ns . 'calendar-user-address-set', + $ns . 'calendar-user-type', + $ns . 'schedule-default-calendar-URL' + ); + + } + + /** + * Use this method to tell the server this plugin defines additional + * HTTP methods. + * + * This method is passed a uri. It should only return HTTP methods that are + * available for the specified uri. + * + * @param string $uri + * @return array + */ + function getHTTPMethods($uri) { + + try { + $node = $this->server->tree->getNodeForPath($uri); + } catch (NotFound $e) { + return []; + } + + if ($node instanceof IOutbox) { + return ['POST']; + } + + return []; + + } + + /** + * This method handles POST request for the outbox. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + function httpPost(RequestInterface $request, ResponseInterface $response) { + + // Checking if this is a text/calendar content type + $contentType = $request->getHeader('Content-Type'); + if (strpos($contentType, 'text/calendar') !== 0) { + return; + } + + $path = $request->getPath(); + + // Checking if we're talking to an outbox + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (NotFound $e) { + return; + } + if (!$node instanceof IOutbox) + return; + + $this->server->transactionType = 'post-caldav-outbox'; + $this->outboxRequest($node, $request, $response); + + // Returning false breaks the event chain and tells the server we've + // handled the request. + return false; + + } + + /** + * This method handler is invoked during fetching of properties. + * + * We use this event to add calendar-auto-schedule-specific properties. + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + function propFind(PropFind $propFind, INode $node) { + + if ($node instanceof DAVACL\IPrincipal) { + + $caldavPlugin = $this->server->getPlugin('caldav'); + $principalUrl = $node->getPrincipalUrl(); + + // schedule-outbox-URL property + $propFind->handle('{' . self::NS_CALDAV . '}schedule-outbox-URL', function() use ($principalUrl, $caldavPlugin) { + + $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); + if (!$calendarHomePath) { + return null; + } + $outboxPath = $calendarHomePath . '/outbox/'; + + return new Href($outboxPath); + + }); + // schedule-inbox-URL property + $propFind->handle('{' . self::NS_CALDAV . '}schedule-inbox-URL', function() use ($principalUrl, $caldavPlugin) { + + $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); + if (!$calendarHomePath) { + return null; + } + $inboxPath = $calendarHomePath . '/inbox/'; + + return new Href($inboxPath); + + }); + + $propFind->handle('{' . self::NS_CALDAV . '}schedule-default-calendar-URL', function() use ($principalUrl, $caldavPlugin) { + + // We don't support customizing this property yet, so in the + // meantime we just grab the first calendar in the home-set. + $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); + + if (!$calendarHomePath) { + return null; + } + + $sccs = '{' . self::NS_CALDAV . '}supported-calendar-component-set'; + + $result = $this->server->getPropertiesForPath($calendarHomePath, [ + '{DAV:}resourcetype', + $sccs, + ], 1); + + foreach ($result as $child) { + if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{' . self::NS_CALDAV . '}calendar') || $child[200]['{DAV:}resourcetype']->is('{http://calendarserver.org/ns/}shared')) { + // Node is either not a calendar or a shared instance. + continue; + } + if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) { + // Either there is no supported-calendar-component-set + // (which is fine) or we found one that supports VEVENT. + return new Href($child['href']); + } + } + + }); + + // The server currently reports every principal to be of type + // 'INDIVIDUAL' + $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-type', function() { + + return 'INDIVIDUAL'; + + }); + + } + + // Mapping the old property to the new property. + $propFind->handle('{http://calendarserver.org/ns/}calendar-availability', function() use ($propFind, $node) { + + // In case it wasn't clear, the only difference is that we map the + // old property to a different namespace. + $availProp = '{' . self::NS_CALDAV . '}calendar-availability'; + $subPropFind = new PropFind( + $propFind->getPath(), + [$availProp] + ); + + $this->server->getPropertiesByNode( + $subPropFind, + $node + ); + + $propFind->set( + '{http://calendarserver.org/ns/}calendar-availability', + $subPropFind->get($availProp), + $subPropFind->getStatus($availProp) + ); + + }); + + } + + /** + * This method is called during property updates. + * + * @param string $path + * @param PropPatch $propPatch + * @return void + */ + function propPatch($path, PropPatch $propPatch) { + + // Mapping the old property to the new property. + $propPatch->handle('{http://calendarserver.org/ns/}calendar-availability', function($value) use ($path) { + + $availProp = '{' . self::NS_CALDAV . '}calendar-availability'; + $subPropPatch = new PropPatch([$availProp => $value]); + $this->server->emit('propPatch', [$path, $subPropPatch]); + $subPropPatch->commit(); + + return $subPropPatch->getResult()[$availProp]; + + }); + + } + + /** + * This method is triggered whenever there was a calendar object gets + * created or updated. + * + * @param RequestInterface $request HTTP request + * @param ResponseInterface $response HTTP Response + * @param VCalendar $vCal Parsed iCalendar object + * @param mixed $calendarPath Path to calendar collection + * @param mixed $modified The iCalendar object has been touched. + * @param mixed $isNew Whether this was a new item or we're updating one + * @return void + */ + function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) { + + if (!$this->scheduleReply($this->server->httpRequest)) { + return; + } + + $calendarNode = $this->server->tree->getNodeForPath($calendarPath); + + $addresses = $this->getAddressesForPrincipal( + $calendarNode->getOwner() + ); + + if (!$isNew) { + $node = $this->server->tree->getNodeForPath($request->getPath()); + $oldObj = Reader::read($node->get()); + } else { + $oldObj = null; + } + + $this->processICalendarChange($oldObj, $vCal, $addresses, [], $modified); + + if ($oldObj) { + // Destroy circular references so PHP will GC the object. + $oldObj->destroy(); + } + + } + + /** + * This method is responsible for delivering the ITip message. + * + * @param ITip\Message $itipMessage + * @return void + */ + function deliver(ITip\Message $iTipMessage) { + + $this->server->emit('schedule', [$iTipMessage]); + if (!$iTipMessage->scheduleStatus) { + $iTipMessage->scheduleStatus = '5.2;There was no system capable of delivering the scheduling message'; + } + // In case the change was considered 'insignificant', we are going to + // remove any error statuses, if any. See ticket #525. + list($baseCode) = explode('.', $iTipMessage->scheduleStatus); + if (!$iTipMessage->significantChange && in_array($baseCode, ['3', '5'])) { + $iTipMessage->scheduleStatus = null; + } + + } + + /** + * This method is triggered before a file gets deleted. + * + * We use this event to make sure that when this happens, attendees get + * cancellations, and organizers get 'DECLINED' statuses. + * + * @param string $path + * @return void + */ + function beforeUnbind($path) { + + // FIXME: We shouldn't trigger this functionality when we're issuing a + // MOVE. This is a hack. + if ($this->server->httpRequest->getMethod() === 'MOVE') return; + + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) { + return; + } + + if (!$this->scheduleReply($this->server->httpRequest)) { + return; + } + + $addresses = $this->getAddressesForPrincipal( + $node->getOwner() + ); + + $broker = new ITip\Broker(); + $messages = $broker->parseEvent(null, $addresses, $node->get()); + + foreach ($messages as $message) { + $this->deliver($message); + } + + } + + /** + * Event handler for the 'schedule' event. + * + * This handler attempts to look at local accounts to deliver the + * scheduling object. + * + * @param ITip\Message $iTipMessage + * @return void + */ + function scheduleLocalDelivery(ITip\Message $iTipMessage) { + + $aclPlugin = $this->server->getPlugin('acl'); + + // Local delivery is not available if the ACL plugin is not loaded. + if (!$aclPlugin) { + return; + } + + $caldavNS = '{' . self::NS_CALDAV . '}'; + + $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient); + if (!$principalUri) { + $iTipMessage->scheduleStatus = '3.7;Could not find principal.'; + return; + } + + // We found a principal URL, now we need to find its inbox. + // Unfortunately we may not have sufficient privileges to find this, so + // we are temporarily turning off ACL to let this come through. + // + // Once we support PHP 5.5, this should be wrapped in a try..finally + // block so we can ensure that this privilege gets added again after. + $this->server->removeListener('propFind', [$aclPlugin, 'propFind']); + + $result = $this->server->getProperties( + $principalUri, + [ + '{DAV:}principal-URL', + $caldavNS . 'calendar-home-set', + $caldavNS . 'schedule-inbox-URL', + $caldavNS . 'schedule-default-calendar-URL', + '{http://sabredav.org/ns}email-address', + ] + ); + + // Re-registering the ACL event + $this->server->on('propFind', [$aclPlugin, 'propFind'], 20); + + if (!isset($result[$caldavNS . 'schedule-inbox-URL'])) { + $iTipMessage->scheduleStatus = '5.2;Could not find local inbox'; + return; + } + if (!isset($result[$caldavNS . 'calendar-home-set'])) { + $iTipMessage->scheduleStatus = '5.2;Could not locate a calendar-home-set'; + return; + } + if (!isset($result[$caldavNS . 'schedule-default-calendar-URL'])) { + $iTipMessage->scheduleStatus = '5.2;Could not find a schedule-default-calendar-URL property'; + return; + } + + $calendarPath = $result[$caldavNS . 'schedule-default-calendar-URL']->getHref(); + $homePath = $result[$caldavNS . 'calendar-home-set']->getHref(); + $inboxPath = $result[$caldavNS . 'schedule-inbox-URL']->getHref(); + + if ($iTipMessage->method === 'REPLY') { + $privilege = 'schedule-deliver-reply'; + } else { + $privilege = 'schedule-deliver-invite'; + } + + if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS . $privilege, DAVACL\Plugin::R_PARENT, false)) { + $iTipMessage->scheduleStatus = '3.8;organizer did not have the ' . $privilege . ' privilege on the attendees inbox'; + return; + } + + // Next, we're going to find out if the item already exits in one of + // the users' calendars. + $uid = $iTipMessage->uid; + + $newFileName = 'sabredav-' . \Sabre\DAV\UUIDUtil::getUUID() . '.ics'; + + $home = $this->server->tree->getNodeForPath($homePath); + $inbox = $this->server->tree->getNodeForPath($inboxPath); + + $currentObject = null; + $objectNode = null; + $isNewNode = false; + + $result = $home->getCalendarObjectByUID($uid); + if ($result) { + // There was an existing object, we need to update probably. + $objectPath = $homePath . '/' . $result; + $objectNode = $this->server->tree->getNodeForPath($objectPath); + $oldICalendarData = $objectNode->get(); + $currentObject = Reader::read($oldICalendarData); + } else { + $isNewNode = true; + } + + $broker = new ITip\Broker(); + $newObject = $broker->processMessage($iTipMessage, $currentObject); + + $inbox->createFile($newFileName, $iTipMessage->message->serialize()); + + if (!$newObject) { + // We received an iTip message referring to a UID that we don't + // have in any calendars yet, and processMessage did not give us a + // calendarobject back. + // + // The implication is that processMessage did not understand the + // iTip message. + $iTipMessage->scheduleStatus = '5.0;iTip message was not processed by the server, likely because we didn\'t understand it.'; + return; + } + + // Note that we are bypassing ACL on purpose by calling this directly. + // We may need to look a bit deeper into this later. Supporting ACL + // here would be nice. + if ($isNewNode) { + $calendar = $this->server->tree->getNodeForPath($calendarPath); + $calendar->createFile($newFileName, $newObject->serialize()); + } else { + // If the message was a reply, we may have to inform other + // attendees of this attendees status. Therefore we're shooting off + // another itipMessage. + if ($iTipMessage->method === 'REPLY') { + $this->processICalendarChange( + $oldICalendarData, + $newObject, + [$iTipMessage->recipient], + [$iTipMessage->sender] + ); + } + $objectNode->put($newObject->serialize()); + } + $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; + + } + + /** + * This method looks at an old iCalendar object, a new iCalendar object and + * starts sending scheduling messages based on the changes. + * + * A list of addresses needs to be specified, so the system knows who made + * the update, because the behavior may be different based on if it's an + * attendee or an organizer. + * + * This method may update $newObject to add any status changes. + * + * @param VCalendar|string $oldObject + * @param VCalendar $newObject + * @param array $addresses + * @param array $ignore Any addresses to not send messages to. + * @param bool $modified A marker to indicate that the original object + * modified by this process. + * @return void + */ + protected function processICalendarChange($oldObject = null, VCalendar $newObject, array $addresses, array $ignore = [], &$modified = false) { + + $broker = new ITip\Broker(); + $messages = $broker->parseEvent($newObject, $addresses, $oldObject); + + if ($messages) $modified = true; + + foreach ($messages as $message) { + + if (in_array($message->recipient, $ignore)) { + continue; + } + + $this->deliver($message); + + if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() === $message->recipient)) { + if ($message->scheduleStatus) { + $newObject->VEVENT->ORGANIZER['SCHEDULE-STATUS'] = $message->getScheduleStatus(); + } + unset($newObject->VEVENT->ORGANIZER['SCHEDULE-FORCE-SEND']); + + } else { + + if (isset($newObject->VEVENT->ATTENDEE)) foreach ($newObject->VEVENT->ATTENDEE as $attendee) { + + if ($attendee->getNormalizedValue() === $message->recipient) { + if ($message->scheduleStatus) { + $attendee['SCHEDULE-STATUS'] = $message->getScheduleStatus(); + } + unset($attendee['SCHEDULE-FORCE-SEND']); + break; + } + + } + + } + + } + + } + + /** + * Returns a list of addresses that are associated with a principal. + * + * @param string $principal + * @return array + */ + protected function getAddressesForPrincipal($principal) { + + $CUAS = '{' . self::NS_CALDAV . '}calendar-user-address-set'; + + $properties = $this->server->getProperties( + $principal, + [$CUAS] + ); + + // If we can't find this information, we'll stop processing + if (!isset($properties[$CUAS])) { + return; + } + + $addresses = $properties[$CUAS]->getHrefs(); + return $addresses; + + } + + /** + * This method handles POST requests to the schedule-outbox. + * + * Currently, two types of requests are support: + * * FREEBUSY requests from RFC 6638 + * * Simple iTIP messages from draft-desruisseaux-caldav-sched-04 + * + * The latter is from an expired early draft of the CalDAV scheduling + * extensions, but iCal depends on a feature from that spec, so we + * implement it. + * + * @param IOutbox $outboxNode + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + function outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response) { + + $outboxPath = $request->getPath(); + + // Parsing the request body + try { + $vObject = VObject\Reader::read($request->getBody()); + } catch (VObject\ParseException $e) { + throw new BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage()); + } + + // The incoming iCalendar object must have a METHOD property, and a + // component. The combination of both determines what type of request + // this is. + $componentType = null; + foreach ($vObject->getComponents() as $component) { + if ($component->name !== 'VTIMEZONE') { + $componentType = $component->name; + break; + } + } + if (is_null($componentType)) { + throw new BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component'); + } + + // Validating the METHOD + $method = strtoupper((string)$vObject->METHOD); + if (!$method) { + throw new BadRequest('A METHOD property must be specified in iTIP messages'); + } + + // So we support one type of request: + // + // REQUEST with a VFREEBUSY component + + $acl = $this->server->getPlugin('acl'); + + if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') { + + $acl && $acl->checkPrivileges($outboxPath, '{' . self::NS_CALDAV . '}schedule-query-freebusy'); + $this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response); + + // Destroy circular references so PHP can GC the object. + $vObject->destroy(); + unset($vObject); + + } else { + + throw new NotImplemented('We only support VFREEBUSY (REQUEST) on this endpoint'); + + } + + } + + /** + * This method is responsible for parsing a free-busy query request and + * returning it's result. + * + * @param IOutbox $outbox + * @param VObject\Component $vObject + * @param RequestInterface $request + * @param ResponseInterface $response + * @return string + */ + protected function handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response) { + + $vFreeBusy = $vObject->VFREEBUSY; + $organizer = $vFreeBusy->organizer; + + $organizer = (string)$organizer; + + // Validating if the organizer matches the owner of the inbox. + $owner = $outbox->getOwner(); + + $caldavNS = '{' . self::NS_CALDAV . '}'; + + $uas = $caldavNS . 'calendar-user-address-set'; + $props = $this->server->getProperties($owner, [$uas]); + + if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) { + throw new Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox'); + } + + if (!isset($vFreeBusy->ATTENDEE)) { + throw new BadRequest('You must at least specify 1 attendee'); + } + + $attendees = []; + foreach ($vFreeBusy->ATTENDEE as $attendee) { + $attendees[] = (string)$attendee; + } + + + if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) { + throw new BadRequest('DTSTART and DTEND must both be specified'); + } + + $startRange = $vFreeBusy->DTSTART->getDateTime(); + $endRange = $vFreeBusy->DTEND->getDateTime(); + + $results = []; + foreach ($attendees as $attendee) { + $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject); + } + + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->formatOutput = true; + $scheduleResponse = $dom->createElement('cal:schedule-response'); + foreach ($this->server->xml->namespaceMap as $namespace => $prefix) { + + $scheduleResponse->setAttribute('xmlns:' . $prefix, $namespace); + + } + $dom->appendChild($scheduleResponse); + + foreach ($results as $result) { + $xresponse = $dom->createElement('cal:response'); + + $recipient = $dom->createElement('cal:recipient'); + $recipientHref = $dom->createElement('d:href'); + + $recipientHref->appendChild($dom->createTextNode($result['href'])); + $recipient->appendChild($recipientHref); + $xresponse->appendChild($recipient); + + $reqStatus = $dom->createElement('cal:request-status'); + $reqStatus->appendChild($dom->createTextNode($result['request-status'])); + $xresponse->appendChild($reqStatus); + + if (isset($result['calendar-data'])) { + + $calendardata = $dom->createElement('cal:calendar-data'); + $calendardata->appendChild($dom->createTextNode(str_replace("\r\n", "\n", $result['calendar-data']->serialize()))); + $xresponse->appendChild($calendardata); + + } + $scheduleResponse->appendChild($xresponse); + } + + $response->setStatus(200); + $response->setHeader('Content-Type', 'application/xml'); + $response->setBody($dom->saveXML()); + + } + + /** + * 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 + * + * The following request status codes may be returned: + * * 2.0;description + * * 3.7;description + * + * @param string $email address + * @param DateTimeInterface $start + * @param DateTimeInterface $end + * @param VObject\Component $request + * @return array + */ + protected function getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request) { + + $caldavNS = '{' . self::NS_CALDAV . '}'; + + $aclPlugin = $this->server->getPlugin('acl'); + if (substr($email, 0, 7) === 'mailto:') $email = substr($email, 7); + + $result = $aclPlugin->principalSearch( + ['{http://sabredav.org/ns}email-address' => $email], + [ + '{DAV:}principal-URL', + $caldavNS . 'calendar-home-set', + $caldavNS . 'schedule-inbox-URL', + '{http://sabredav.org/ns}email-address', + + ] + ); + + if (!count($result)) { + return [ + 'request-status' => '3.7;Could not find principal', + 'href' => 'mailto:' . $email, + ]; + } + + if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) { + return [ + 'request-status' => '3.7;No calendar-home-set property found', + 'href' => 'mailto:' . $email, + ]; + } + if (!isset($result[0][200][$caldavNS . 'schedule-inbox-URL'])) { + return [ + 'request-status' => '3.7;No schedule-inbox-URL property found', + 'href' => 'mailto:' . $email, + ]; + } + $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref(); + $inboxUrl = $result[0][200][$caldavNS . 'schedule-inbox-URL']->getHref(); + + // Grabbing the calendar list + $objects = []; + $calendarTimeZone = new DateTimeZone('UTC'); + + foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) { + if (!$node instanceof ICalendar) { + continue; + } + + $sct = $caldavNS . 'schedule-calendar-transp'; + $ctz = $caldavNS . 'calendar-timezone'; + $props = $node->getProperties([$sct, $ctz]); + + if (isset($props[$sct]) && $props[$sct]->getValue() == ScheduleCalendarTransp::TRANSPARENT) { + // If a calendar is marked as 'transparent', it means we must + // ignore it for free-busy purposes. + continue; + } + + $aclPlugin->checkPrivileges($homeSet . $node->getName(), $caldavNS . 'read-free-busy'); + + if (isset($props[$ctz])) { + $vtimezoneObj = VObject\Reader::read($props[$ctz]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + + // Destroy circular references so PHP can garbage collect the object. + $vtimezoneObj->destroy(); + + } + + // Getting the list of object uris within the time-range + $urls = $node->calendarQuery([ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $start, + 'end' => $end, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + + $calObjects = array_map(function($url) use ($node) { + $obj = $node->getChild($url)->get(); + return $obj; + }, $urls); + + $objects = array_merge($objects, $calObjects); + + } + + $inboxProps = $this->server->getProperties( + $inboxUrl, + $caldavNS . 'calendar-availability' + ); + + $vcalendar = new VObject\Component\VCalendar(); + $vcalendar->METHOD = 'REPLY'; + + $generator = new VObject\FreeBusyGenerator(); + $generator->setObjects($objects); + $generator->setTimeRange($start, $end); + $generator->setBaseObject($vcalendar); + $generator->setTimeZone($calendarTimeZone); + + if ($inboxProps) { + $generator->setVAvailability( + VObject\Reader::read( + $inboxProps[$caldavNS . 'calendar-availability'] + ) + ); + } + + $result = $generator->getResult(); + + $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email; + $vcalendar->VFREEBUSY->UID = (string)$request->VFREEBUSY->UID; + $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER; + + return [ + 'calendar-data' => $result, + 'request-status' => '2.0;Success', + 'href' => 'mailto:' . $email, + ]; + } + + /** + * This method checks the 'Schedule-Reply' header + * and returns false if it's 'F', otherwise true. + * + * @param RequestInterface $request + * @return bool + */ + private function scheduleReply(RequestInterface $request) { + + $scheduleReply = $request->getHeader('Schedule-Reply'); + return $scheduleReply !== 'F'; + + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds calendar-auto-schedule, as defined in rf6868', + 'link' => 'http://sabre.io/dav/scheduling/', + ]; + + } +} diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php b/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php new file mode 100644 index 000000000..a36646e6c --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php @@ -0,0 +1,165 @@ +<?php + +namespace Sabre\CalDAV\Schedule; + +use Sabre\CalDAV\Backend; +use Sabre\DAV\Exception\MethodNotAllowed; + +/** + * The SchedulingObject represents a scheduling object in the Inbox collection + * + * @author Brett (https://github.com/bretten) + * @license http://sabre.io/license/ Modified BSD License + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + */ +class SchedulingObject extends \Sabre\CalDAV\CalendarObject implements ISchedulingObject { + + /** + /* The CalDAV backend + * + * @var Backend\SchedulingSupport + */ + protected $caldavBackend; + + /** + * Array with information about this SchedulingObject + * + * @var array + */ + protected $objectData; + + /** + * Constructor + * + * The following properties may be passed within $objectData: + * + * * uri - A unique uri. Only the 'basename' must be passed. + * * principaluri - the principal that owns the object. + * * calendardata (optional) - The iCalendar data + * * etag - (optional) The etag for this object, MUST be encloded with + * double-quotes. + * * size - (optional) The size of the data in bytes. + * * lastmodified - (optional) format as a unix timestamp. + * * acl - (optional) Use this to override the default ACL for the node. + * + * @param Backend\BackendInterface $caldavBackend + * @param array $objectData + */ + function __construct(Backend\SchedulingSupport $caldavBackend, array $objectData) { + + $this->caldavBackend = $caldavBackend; + + if (!isset($objectData['uri'])) { + throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property'); + } + + $this->objectData = $objectData; + + } + + /** + * Returns the ICalendar-formatted object + * + * @return string + */ + function get() { + + // Pre-populating the 'calendardata' is optional, if we don't have it + // already we fetch it from the backend. + if (!isset($this->objectData['calendardata'])) { + $this->objectData = $this->caldavBackend->getSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']); + } + return $this->objectData['calendardata']; + + } + + /** + * Updates the ICalendar-formatted object + * + * @param string|resource $calendarData + * @return string + */ + function put($calendarData) { + + throw new MethodNotAllowed('Updating scheduling objects is not supported'); + + } + + /** + * Deletes the scheduling message + * + * @return void + */ + function delete() { + + $this->caldavBackend->deleteSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']); + + } + + /** + * Returns the owner principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + function getOwner() { + + return $this->objectData['principaluri']; + + } + + + /** + * 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 + */ + function getACL() { + + // An alternative acl may be specified in the object data. + // + + if (isset($this->objectData['acl'])) { + return $this->objectData['acl']; + } + + // The default ACL + return [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->objectData['principaluri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->objectData['principaluri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->objectData['principaluri'] . '/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->objectData['principaluri'] . '/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->objectData['principaluri'] . '/calendar-proxy-read', + 'protected' => true, + ], + ]; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/ShareableCalendar.php b/vendor/sabre/dav/lib/CalDAV/ShareableCalendar.php new file mode 100644 index 000000000..c11695d5f --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/ShareableCalendar.php @@ -0,0 +1,72 @@ +<?php + +namespace Sabre\CalDAV; + +/** + * This object represents a CalDAV calendar that can be shared with other + * users. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ShareableCalendar extends Calendar implements IShareableCalendar { + + /** + * Updates the list of shares. + * + * The first array is a list of people that are to be added to the + * calendar. + * + * Every element in the add array has the following properties: + * * href - A url. Usually a mailto: address + * * commonName - Usually a first and last name, or false + * * summary - A description of the share, can also be false + * * readOnly - A boolean value + * + * Every element in the remove array is just the address string. + * + * @param array $add + * @param array $remove + * @return void + */ + function updateShares(array $add, array $remove) { + + $this->caldavBackend->updateShares($this->calendarInfo['id'], $add, $remove); + + } + + /** + * Returns the list of people whom this calendar is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * @return array + */ + function getShares() { + + return $this->caldavBackend->getShares($this->calendarInfo['id']); + + } + + /** + * Marks this calendar as published. + * + * Publishing a calendar should automatically create a read-only, public, + * subscribable calendar. + * + * @param bool $value + * @return void + */ + function setPublishStatus($value) { + + $this->caldavBackend->setPublishStatus($this->calendarInfo['id'], $value); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php b/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php new file mode 100644 index 000000000..7973eff9c --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php @@ -0,0 +1,148 @@ +<?php + +namespace Sabre\CalDAV; + +/** + * This object represents a CalDAV calendar that is shared by a different user. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class SharedCalendar extends Calendar implements ISharedCalendar { + + /** + * Constructor + * + * @param Backend\BackendInterface $caldavBackend + * @param array $calendarInfo + */ + function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo) { + + $required = [ + '{http://calendarserver.org/ns/}shared-url', + '{http://sabredav.org/ns}owner-principal', + '{http://sabredav.org/ns}read-only', + ]; + foreach ($required as $r) { + if (!isset($calendarInfo[$r])) { + throw new \InvalidArgumentException('The ' . $r . ' property must be specified for SharedCalendar(s)'); + } + } + + parent::__construct($caldavBackend, $calendarInfo); + + } + + /** + * This method should return the url of the owners' copy of the shared + * calendar. + * + * @return string + */ + function getSharedUrl() { + + return $this->calendarInfo['{http://calendarserver.org/ns/}shared-url']; + + } + + /** + * Returns the owner principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + function getOwner() { + + return $this->calendarInfo['{http://sabredav.org/ns}owner-principal']; + + } + + /** + * 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 + */ + function getACL() { + + // The top-level ACL only contains access information for the true + // owner of the calendar, so we need to add the information for the + // sharee. + $acl = parent::getACL(); + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + if ($this->calendarInfo['{http://sabredav.org/ns}read-only']) { + $acl[] = [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + } else { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + } + return $acl; + + } + + /** + * This method returns the ACL's for calendar objects in this calendar. + * The result of this method automatically gets passed to the + * calendar-object nodes in the calendar. + * + * @return array + */ + function getChildACL() { + + $acl = parent::getChildACL(); + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + + if (!$this->calendarInfo['{http://sabredav.org/ns}read-only']) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + } + return $acl; + + } + + + /** + * Returns the list of people whom this calendar is shared with. + * + * Every element in this array should have the following properties: + * * href - Often a mailto: address + * * commonName - Optional, for example a first + last name + * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. + * * readOnly - boolean + * * summary - Optional, a description for the share + * + * @return array + */ + function getShares() { + + return $this->caldavBackend->getShares($this->calendarInfo['id']); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php b/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php new file mode 100644 index 000000000..5154fb1de --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php @@ -0,0 +1,426 @@ +<?php + +namespace Sabre\CalDAV; + +use Sabre\DAV; +use Sabre\DAV\Xml\Property\Href; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +/** + * This plugin implements support for caldav sharing. + * + * This spec is defined at: + * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt + * + * See: + * Sabre\CalDAV\Backend\SharingSupport for all the documentation. + * + * Note: This feature is experimental, and may change in between different + * SabreDAV versions. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class SharingPlugin extends DAV\ServerPlugin { + + /** + * These are the various status constants used by sharing-messages. + */ + const STATUS_ACCEPTED = 1; + const STATUS_DECLINED = 2; + const STATUS_DELETED = 3; + const STATUS_NORESPONSE = 4; + const STATUS_INVALID = 5; + + /** + * Reference to SabreDAV server object. + * + * @var Sabre\DAV\Server + */ + protected $server; + + /** + * This method should return a list of server-features. + * + * This is for example 'versioning' and is added to the DAV: header + * in an OPTIONS response. + * + * @return array + */ + function getFeatures() { + + return ['calendarserver-sharing']; + + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using Sabre\DAV\Server::getPlugin + * + * @return string + */ + function getPluginName() { + + return 'caldav-sharing'; + + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param DAV\Server $server + * @return void + */ + function initialize(DAV\Server $server) { + + $this->server = $server; + $server->resourceTypeMapping['Sabre\\CalDAV\\ISharedCalendar'] = '{' . Plugin::NS_CALENDARSERVER . '}shared'; + + array_push( + $this->server->protectedProperties, + '{' . Plugin::NS_CALENDARSERVER . '}invite', + '{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes', + '{' . Plugin::NS_CALENDARSERVER . '}shared-url' + ); + + $this->server->xml->elementMap['{' . Plugin::NS_CALENDARSERVER . '}share'] = 'Sabre\\CalDAV\\Xml\\Request\\Share'; + $this->server->xml->elementMap['{' . Plugin::NS_CALENDARSERVER . '}invite-reply'] = 'Sabre\\CalDAV\\Xml\\Request\\InviteReply'; + + $this->server->on('propFind', [$this, 'propFindEarly']); + $this->server->on('propFind', [$this, 'propFindLate'], 150); + $this->server->on('propPatch', [$this, 'propPatch'], 40); + $this->server->on('method:POST', [$this, 'httpPost']); + + } + + /** + * This event is triggered when properties are requested for a certain + * node. + * + * This allows us to inject any properties early. + * + * @param DAV\PropFind $propFind + * @param DAV\INode $node + * @return void + */ + function propFindEarly(DAV\PropFind $propFind, DAV\INode $node) { + + if ($node instanceof IShareableCalendar) { + + $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}invite', function() use ($node) { + return new Xml\Property\Invite( + $node->getShares() + ); + }); + + } + + if ($node instanceof ISharedCalendar) { + + $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}shared-url', function() use ($node) { + return new Href( + $node->getSharedUrl() + ); + }); + + $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}invite', function() use ($node) { + + // Fetching owner information + $props = $this->server->getPropertiesForPath($node->getOwner(), [ + '{http://sabredav.org/ns}email-address', + '{DAV:}displayname', + ], 0); + + $ownerInfo = [ + 'href' => $node->getOwner(), + ]; + + if (isset($props[0][200])) { + + // We're mapping the internal webdav properties to the + // elements caldav-sharing expects. + if (isset($props[0][200]['{http://sabredav.org/ns}email-address'])) { + $ownerInfo['href'] = 'mailto:' . $props[0][200]['{http://sabredav.org/ns}email-address']; + } + if (isset($props[0][200]['{DAV:}displayname'])) { + $ownerInfo['commonName'] = $props[0][200]['{DAV:}displayname']; + } + + } + + return new Xml\Property\Invite( + $node->getShares(), + $ownerInfo + ); + + }); + + } + + } + + /** + * This method is triggered *after* all properties have been retrieved. + * This allows us to inject the correct resourcetype for calendars that + * have been shared. + * + * @param DAV\PropFind $propFind + * @param DAV\INode $node + * @return void + */ + function propFindLate(DAV\PropFind $propFind, DAV\INode $node) { + + if ($node instanceof IShareableCalendar) { + if ($rt = $propFind->get('{DAV:}resourcetype')) { + if (count($node->getShares()) > 0) { + $rt->add('{' . Plugin::NS_CALENDARSERVER . '}shared-owner'); + } + } + $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes', function() { + return new Xml\Property\AllowedSharingModes(true, false); + }); + + } + + } + + /** + * This method is trigged when a user attempts to update a node's + * properties. + * + * A previous draft of the sharing spec stated that it was possible to use + * PROPPATCH to remove 'shared-owner' from the resourcetype, thus unsharing + * the calendar. + * + * Even though this is no longer in the current spec, we keep this around + * because OS X 10.7 may still make use of this feature. + * + * @param string $path + * @param DAV\PropPatch $propPatch + * @return void + */ + function propPatch($path, DAV\PropPatch $propPatch) { + + $node = $this->server->tree->getNodeForPath($path); + if (!$node instanceof IShareableCalendar) + return; + + $propPatch->handle('{DAV:}resourcetype', function($value) use ($node) { + if ($value->is('{' . Plugin::NS_CALENDARSERVER . '}shared-owner')) return false; + $shares = $node->getShares(); + $remove = []; + foreach ($shares as $share) { + $remove[] = $share['href']; + } + $node->updateShares([], $remove); + + return true; + + }); + + } + + /** + * We intercept this to handle POST requests on calendars. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return null|bool + */ + function httpPost(RequestInterface $request, ResponseInterface $response) { + + $path = $request->getPath(); + + // Only handling xml + $contentType = $request->getHeader('Content-Type'); + if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) + return; + + // Making sure the node exists + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (DAV\Exception\NotFound $e) { + return; + } + + $requestBody = $request->getBodyAsString(); + + // If this request handler could not deal with this POST request, it + // will return 'null' and other plugins get a chance to handle the + // request. + // + // However, we already requested the full body. This is a problem, + // because a body can only be read once. This is why we preemptively + // re-populated the request body with the existing data. + $request->setBody($requestBody); + + $message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType); + + switch ($documentType) { + + // Dealing with the 'share' document, which modified invitees on a + // calendar. + case '{' . Plugin::NS_CALENDARSERVER . '}share' : + + // We can only deal with IShareableCalendar objects + if (!$node instanceof IShareableCalendar) { + return; + } + + $this->server->transactionType = 'post-calendar-share'; + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}write'); + } + + $node->updateShares($message->set, $message->remove); + + $response->setStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + + // The invite-reply document is sent when the user replies to an + // invitation of a calendar share. + case '{' . Plugin::NS_CALENDARSERVER . '}invite-reply' : + + // This only works on the calendar-home-root node. + if (!$node instanceof CalendarHome) { + return; + } + $this->server->transactionType = 'post-invite-reply'; + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}write'); + } + + $url = $node->shareReply( + $message->href, + $message->status, + $message->calendarUri, + $message->inReplyTo, + $message->summary + ); + + $response->setStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + if ($url) { + $writer = $this->server->xml->getWriter($this->server->getBaseUri()); + $writer->openMemory(); + $writer->startDocument(); + $writer->startElement('{' . Plugin::NS_CALENDARSERVER . '}shared-as'); + $writer->write(new Href($url)); + $writer->endElement(); + $response->setHeader('Content-Type', 'application/xml'); + $response->setBody($writer->outputMemory()); + + } + + // Breaking the event chain + return false; + + case '{' . Plugin::NS_CALENDARSERVER . '}publish-calendar' : + + // We can only deal with IShareableCalendar objects + if (!$node instanceof IShareableCalendar) { + return; + } + $this->server->transactionType = 'post-publish-calendar'; + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}write'); + } + + $node->setPublishStatus(true); + + // iCloud sends back the 202, so we will too. + $response->setStatus(202); + + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + + case '{' . Plugin::NS_CALENDARSERVER . '}unpublish-calendar' : + + // We can only deal with IShareableCalendar objects + if (!$node instanceof IShareableCalendar) { + return; + } + $this->server->transactionType = 'post-unpublish-calendar'; + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}write'); + } + + $node->setPublishStatus(false); + + $response->setStatus(200); + + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + + } + + + + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for caldav-sharing.', + 'link' => 'http://sabre.io/dav/caldav-sharing/', + ]; + + } +} diff --git a/vendor/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php b/vendor/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php new file mode 100644 index 000000000..7ba259c7b --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php @@ -0,0 +1,40 @@ +<?php + +namespace Sabre\CalDAV\Subscriptions; + +use Sabre\DAV\ICollection; +use Sabre\DAV\IProperties; + +/** + * ISubscription + * + * Nodes implementing this interface represent calendar subscriptions. + * + * The subscription node doesn't do much, other than returning and updating + * subscription-related properties. + * + * The following properties should be supported: + * + * 1. {DAV:}displayname + * 2. {http://apple.com/ns/ical/}refreshrate + * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos + * should not be stripped). + * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms + * should not be stripped). + * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if + * attachments should not be stripped). + * 6. {http://calendarserver.org/ns/}source (Must be a + * Sabre\DAV\Property\Href). + * 7. {http://apple.com/ns/ical/}calendar-color + * 8. {http://apple.com/ns/ical/}calendar-order + * + * It is recommended to support every property. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface ISubscription extends ICollection, IProperties { + + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Subscriptions/Plugin.php b/vendor/sabre/dav/lib/CalDAV/Subscriptions/Plugin.php new file mode 100644 index 000000000..7abbfb1f9 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Subscriptions/Plugin.php @@ -0,0 +1,120 @@ +<?php + +namespace Sabre\CalDAV\Subscriptions; + +use Sabre\DAV\INode; +use Sabre\DAV\PropFind; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Server; + +/** + * This plugin adds calendar-subscription support to your CalDAV server. + * + * Some clients support 'managed subscriptions' server-side. This is basically + * a list of subscription urls a user is using. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Plugin extends ServerPlugin { + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param Server $server + * @return void + */ + function initialize(Server $server) { + + $server->resourceTypeMapping['Sabre\\CalDAV\\Subscriptions\\ISubscription'] = + '{http://calendarserver.org/ns/}subscribed'; + + $server->xml->elementMap['{http://calendarserver.org/ns/}source'] = + 'Sabre\\DAV\\Xml\\Property\\Href'; + + $server->on('propFind', [$this, 'propFind'], 150); + + } + + /** + * This method should return a list of server-features. + * + * This is for example 'versioning' and is added to the DAV: header + * in an OPTIONS response. + * + * @return array + */ + function getFeatures() { + + return ['calendarserver-subscribed']; + + } + + /** + * Triggered after properties have been fetched. + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + function propFind(PropFind $propFind, INode $node) { + + // There's a bunch of properties that must appear as a self-closing + // xml-element. This event handler ensures that this will be the case. + $props = [ + '{http://calendarserver.org/ns/}subscribed-strip-alarms', + '{http://calendarserver.org/ns/}subscribed-strip-attachments', + '{http://calendarserver.org/ns/}subscribed-strip-todos', + ]; + + foreach ($props as $prop) { + + if ($propFind->getStatus($prop) === 200) { + $propFind->set($prop, '', 200); + } + + } + + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + function getPluginName() { + + return 'subscriptions'; + + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'This plugin allows users to store iCalendar subscriptions in their calendar-home.', + 'link' => null, + ]; + + } +} diff --git a/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php b/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php new file mode 100644 index 000000000..ee53da2c6 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php @@ -0,0 +1,274 @@ +<?php + +namespace Sabre\CalDAV\Subscriptions; + +use Sabre\DAV\Collection; +use Sabre\DAV\Xml\Property\Href; +use Sabre\DAV\PropPatch; +use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAVACL\IACL; +use Sabre\CalDAV\Backend\SubscriptionSupport; + +/** + * Subscription Node + * + * This node represents a subscription. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Subscription extends Collection implements ISubscription, IACL { + + /** + * caldavBackend + * + * @var SupportsSubscriptions + */ + protected $caldavBackend; + + /** + * subscriptionInfo + * + * @var array + */ + protected $subscriptionInfo; + + /** + * Constructor + * + * @param SubscriptionSupport $caldavBackend + * @param array $calendarInfo + */ + function __construct(SubscriptionSupport $caldavBackend, array $subscriptionInfo) { + + $this->caldavBackend = $caldavBackend; + $this->subscriptionInfo = $subscriptionInfo; + + $required = [ + 'id', + 'uri', + 'principaluri', + 'source', + ]; + + foreach ($required as $r) { + if (!isset($subscriptionInfo[$r])) { + throw new \InvalidArgumentException('The ' . $r . ' field is required when creating a subscription node'); + } + } + + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + function getName() { + + return $this->subscriptionInfo['uri']; + + } + + /** + * Returns the last modification time + * + * @return int + */ + function getLastModified() { + + if (isset($this->subscriptionInfo['lastmodified'])) { + return $this->subscriptionInfo['lastmodified']; + } + + } + + /** + * Deletes the current node + * + * @return void + */ + function delete() { + + $this->caldavBackend->deleteSubscription( + $this->subscriptionInfo['id'] + ); + + } + + /** + * Returns an array with all the child nodes + * + * @return DAV\INode[] + */ + function getChildren() { + + return []; + + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + * + * @param PropPatch $propPatch + * @return void + */ + function propPatch(PropPatch $propPatch) { + + return $this->caldavBackend->updateSubscription( + $this->subscriptionInfo['id'], + $propPatch + ); + + } + + /** + * Returns a list of properties for this nodes. + * + * 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. + * + * Note that it's fine to liberally give properties back, instead of + * conforming to the list of requested properties. + * The Server class will filter out the extra. + * + * @param array $properties + * @return void + */ + function getProperties($properties) { + + $r = []; + + foreach ($properties as $prop) { + + switch ($prop) { + case '{http://calendarserver.org/ns/}source' : + $r[$prop] = new Href($this->subscriptionInfo['source'], false); + break; + default : + if (array_key_exists($prop, $this->subscriptionInfo)) { + $r[$prop] = $this->subscriptionInfo[$prop]; + } + break; + } + + } + + return $r; + + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + function getOwner() { + + return $this->subscriptionInfo['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. 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 + */ + function getACL() { + + return [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner() . '/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner() . '/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner() . '/calendar-proxy-read', + 'protected' => true, + ] + ]; + + } + + /** + * Updates the ACL. + * + * This method will receive a list of new ACE's. + * + * @param array $acl + * @return void + */ + function setACL(array $acl) { + + throw new MethodNotAllowed('Changing ACL is not yet supported'); + + } + + /** + * Returns the list of supported privileges for this node. + * + * The returned data structure is a list of nested privileges. + * See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple + * standard structure. + * + * If null is returned from this method, the default privilege set is used, + * which is fine for most common usecases. + * + * @return array|null + */ + function getSupportedPrivilegeSet() { + + return null; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php b/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php new file mode 100644 index 000000000..9babcf15c --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php @@ -0,0 +1,84 @@ +<?php + +namespace Sabre\CalDAV\Xml\Filter; + +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use Sabre\DAV\Exception\BadRequest; +use Sabre\CalDAV\Plugin; +use Sabre\VObject\DateTimeParser; + +/** + * CalendarData parser. + * + * This class parses the {urn:ietf:params:xml:ns:caldav}calendar-data XML + * element, as defined in: + * + * https://tools.ietf.org/html/rfc4791#section-9.6 + * + * This element is used in three distinct places in the caldav spec, but in + * this case, this element class only implements the calendar-data element as + * it appears in a DAV:prop element, in a calendar-query or calendar-multiget + * REPORT request. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @license http://sabre.io/license/ Modified BSD License + */ +class CalendarData implements XmlDeserializable { + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $result = [ + 'contentType' => $reader->getAttribute('content-type') ?: 'text/calendar', + 'version' => $reader->getAttribute('version') ?: '2.0', + ]; + + $elems = (array)$reader->parseInnerTree(); + foreach ($elems as $elem) { + + switch ($elem['name']) { + case '{' . Plugin::NS_CALDAV . '}expand' : + + $result['expand'] = [ + 'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null, + 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null, + ]; + + if (!$result['expand']['start'] || !$result['expand']['end']) { + throw new BadRequest('The "start" and "end" attributes are required when expanding calendar-data'); + } + if ($result['expand']['end'] <= $result['expand']['start']) { + throw new BadRequest('The end-date must be larger than the start-date when expanding calendar-data'); + } + break; + } + + } + + return $result; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php b/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php new file mode 100644 index 000000000..c9b27dbfd --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php @@ -0,0 +1,97 @@ +<?php + +namespace Sabre\CalDAV\Xml\Filter; + +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use Sabre\DAV\Exception\BadRequest; +use Sabre\CalDAV\Plugin; +use Sabre\VObject\DateTimeParser; + +/** + * CompFilter parser. + * + * This class parses the {urn:ietf:params:xml:ns:caldav}comp-filter XML + * element, as defined in: + * + * https://tools.ietf.org/html/rfc4791#section-9.6 + * + * The result will be spit out as an array. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @license http://sabre.io/license/ Modified BSD License + */ +class CompFilter implements XmlDeserializable { + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $result = [ + 'name' => null, + 'is-not-defined' => false, + 'comp-filters' => [], + 'prop-filters' => [], + 'time-range' => false, + ]; + + $att = $reader->parseAttributes(); + $result['name'] = $att['name']; + + $elems = $reader->parseInnerTree(); + + if (is_array($elems)) foreach ($elems as $elem) { + + switch ($elem['name']) { + + case '{' . Plugin::NS_CALDAV . '}comp-filter' : + $result['comp-filters'][] = $elem['value']; + break; + case '{' . Plugin::NS_CALDAV . '}prop-filter' : + $result['prop-filters'][] = $elem['value']; + break; + case '{' . Plugin::NS_CALDAV . '}is-not-defined' : + $result['is-not-defined'] = true; + break; + case '{' . Plugin::NS_CALDAV . '}time-range' : + if ($result['name'] === 'VCALENDAR') { + throw new BadRequest('You cannot add time-range filters on the VCALENDAR component'); + } + $result['time-range'] = [ + 'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null, + 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null, + ]; + if ($result['time-range']['start'] && $result['time-range']['end'] && $result['time-range']['end'] <= $result['time-range']['start']) { + throw new BadRequest('The end-date must be larger than the start-date'); + } + break; + + } + + } + + return $result; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php b/vendor/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php new file mode 100644 index 000000000..eb7f564df --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php @@ -0,0 +1,82 @@ +<?php + +namespace Sabre\CalDAV\Xml\Filter; + +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use Sabre\CalDAV\Plugin; + +/** + * PropFilter parser. + * + * This class parses the {urn:ietf:params:xml:ns:caldav}param-filter XML + * element, as defined in: + * + * https://tools.ietf.org/html/rfc4791#section-9.7.3 + * + * The result will be spit out as an array. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ParamFilter implements XmlDeserializable { + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $result = [ + 'name' => null, + 'is-not-defined' => false, + 'text-match' => null, + ]; + + $att = $reader->parseAttributes(); + $result['name'] = $att['name']; + + $elems = $reader->parseInnerTree(); + + if (is_array($elems)) foreach ($elems as $elem) { + + switch ($elem['name']) { + + case '{' . Plugin::NS_CALDAV . '}is-not-defined' : + $result['is-not-defined'] = true; + break; + case '{' . Plugin::NS_CALDAV . '}text-match' : + $result['text-match'] = [ + 'negate-condition' => isset($elem['attributes']['negate-condition']) && $elem['attributes']['negate-condition'] === 'yes', + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;ascii-casemap', + 'value' => $elem['value'], + ]; + break; + + } + + } + + return $result; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php b/vendor/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php new file mode 100644 index 000000000..4c2e1b172 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php @@ -0,0 +1,98 @@ +<?php + +namespace Sabre\CalDAV\Xml\Filter; + +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use Sabre\DAV\Exception\BadRequest; +use Sabre\CalDAV\Plugin; +use Sabre\VObject\DateTimeParser; + +/** + * PropFilter parser. + * + * This class parses the {urn:ietf:params:xml:ns:caldav}prop-filter XML + * element, as defined in: + * + * https://tools.ietf.org/html/rfc4791#section-9.7.2 + * + * The result will be spit out as an array. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @license http://sabre.io/license/ Modified BSD License + */ +class PropFilter implements XmlDeserializable { + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $result = [ + 'name' => null, + 'is-not-defined' => false, + 'param-filters' => [], + 'text-match' => null, + 'time-range' => false, + ]; + + $att = $reader->parseAttributes(); + $result['name'] = $att['name']; + + $elems = $reader->parseInnerTree(); + + if (is_array($elems)) foreach ($elems as $elem) { + + switch ($elem['name']) { + + case '{' . Plugin::NS_CALDAV . '}param-filter' : + $result['param-filters'][] = $elem['value']; + break; + case '{' . Plugin::NS_CALDAV . '}is-not-defined' : + $result['is-not-defined'] = true; + break; + case '{' . Plugin::NS_CALDAV . '}time-range' : + $result['time-range'] = [ + 'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null, + 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null, + ]; + if ($result['time-range']['start'] && $result['time-range']['end'] && $result['time-range']['end'] <= $result['time-range']['start']) { + throw new BadRequest('The end-date must be larger than the start-date'); + } + break; + case '{' . Plugin::NS_CALDAV . '}text-match' : + $result['text-match'] = [ + 'negate-condition' => isset($elem['attributes']['negate-condition']) && $elem['attributes']['negate-condition'] === 'yes', + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;ascii-casemap', + 'value' => $elem['value'], + ]; + break; + + } + + } + + return $result; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php b/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php new file mode 100644 index 000000000..7fb022e33 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php @@ -0,0 +1,307 @@ +<?php + +namespace Sabre\CalDAV\Xml\Notification; + +use Sabre\Xml\Writer; +use Sabre\CalDAV\SharingPlugin as SharingPlugin; +use Sabre\CalDAV; + +/** + * This class represents the cs:invite-notification notification element. + * + * This element is defined here: + * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Invite implements NotificationInterface { + + /** + * A unique id for the message + * + * @var string + */ + protected $id; + + /** + * Timestamp of the notification + * + * @var DateTime + */ + protected $dtStamp; + + /** + * A url to the recipient of the notification. This can be an email + * address (mailto:), or a principal url. + * + * @var string + */ + protected $href; + + /** + * The type of message, see the SharingPlugin::STATUS_* constants. + * + * @var int + */ + protected $type; + + /** + * True if access to a calendar is read-only. + * + * @var bool + */ + protected $readOnly; + + /** + * A url to the shared calendar. + * + * @var string + */ + protected $hostUrl; + + /** + * Url to the sharer of the calendar + * + * @var string + */ + protected $organizer; + + /** + * The name of the sharer. + * + * @var string + */ + protected $commonName; + + /** + * The name of the sharer. + * + * @var string + */ + protected $firstName; + + /** + * The name of the sharer. + * + * @var string + */ + protected $lastName; + + /** + * A description of the share request + * + * @var string + */ + protected $summary; + + /** + * The Etag for the notification + * + * @var string + */ + protected $etag; + + /** + * The list of supported components + * + * @var Sabre\CalDAV\Property\SupportedCalendarComponentSet + */ + protected $supportedComponents; + + /** + * Creates the Invite notification. + * + * This constructor receives an array with the following elements: + * + * * id - A unique id + * * etag - The etag + * * dtStamp - A DateTime object with a timestamp for the notification. + * * type - The type of notification, see SharingPlugin::STATUS_* + * constants for details. + * * readOnly - This must be set to true, if this is an invite for + * read-only access to a calendar. + * * hostUrl - A url to the shared calendar. + * * organizer - Url to the sharer principal. + * * commonName - The real name of the sharer (optional). + * * firstName - The first name of the sharer (optional). + * * lastName - The last name of the sharer (optional). + * * summary - Description of the share, can be the same as the + * calendar, but may also be modified (optional). + * * supportedComponents - An instance of + * Sabre\CalDAV\Property\SupportedCalendarComponentSet. + * This allows the client to determine which components + * will be supported in the shared calendar. This is + * also optional. + * + * @param array $values All the options + */ + function __construct(array $values) { + + $required = [ + 'id', + 'etag', + 'href', + 'dtStamp', + 'type', + 'readOnly', + 'hostUrl', + 'organizer', + ]; + foreach ($required as $item) { + if (!isset($values[$item])) { + throw new \InvalidArgumentException($item . ' is a required constructor option'); + } + } + + foreach ($values as $key => $value) { + if (!property_exists($this, $key)) { + throw new \InvalidArgumentException('Unknown option: ' . $key); + } + $this->$key = $value; + } + + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + $writer->writeElement('{' . CalDAV\Plugin::NS_CALENDARSERVER . '}invite-notification'); + + } + + /** + * This method serializes the entire notification, as it is used in the + * response body. + * + * @param Writer $writer + * @return void + */ + function xmlSerializeFull(Writer $writer) { + + $cs = '{' . CalDAV\Plugin::NS_CALENDARSERVER . '}'; + + $this->dtStamp->setTimezone(new \DateTimezone('GMT')); + $writer->writeElement($cs . 'dtstamp', $this->dtStamp->format('Ymd\\THis\\Z')); + + $writer->startElement($cs . 'invite-notification'); + + $writer->writeElement($cs . 'uid', $this->id); + $writer->writeElement('{DAV:}href', $this->href); + + switch ($this->type) { + + case SharingPlugin::STATUS_ACCEPTED : + $writer->writeElement($cs . 'invite-accepted'); + break; + case SharingPlugin::STATUS_DECLINED : + $writer->writeElement($cs . 'invite-declined'); + break; + case SharingPlugin::STATUS_DELETED : + $writer->writeElement($cs . 'invite-deleted'); + break; + case SharingPlugin::STATUS_NORESPONSE : + $writer->writeElement($cs . 'invite-noresponse'); + break; + + } + + $writer->writeElement($cs . 'hosturl', [ + '{DAV:}href' => $writer->contextUri . $this->hostUrl + ]); + + if ($this->summary) { + $writer->writeElement($cs . 'summary', $this->summary); + } + + $writer->startElement($cs . 'access'); + if ($this->readOnly) { + $writer->writeElement($cs . 'read'); + } else { + $writer->writeElement($cs . 'read-write'); + } + $writer->endElement(); // access + + $writer->startElement($cs . 'organizer'); + // If the organizer contains a 'mailto:' part, it means it should be + // treated as absolute. + if (strtolower(substr($this->organizer, 0, 7)) === 'mailto:') { + $writer->writeElement('{DAV:}href', $this->organizer); + } else { + $writer->writeElement('{DAV:}href', $writer->contextUri . $this->organizer); + } + if ($this->commonName) { + $writer->writeElement($cs . 'common-name', $this->commonName); + } + if ($this->firstName) { + $writer->writeElement($cs . 'first-name', $this->firstName); + } + if ($this->lastName) { + $writer->writeElement($cs . 'last-name', $this->lastName); + } + $writer->endElement(); // organizer + + if ($this->commonName) { + $writer->writeElement($cs . 'organizer-cn', $this->commonName); + } + if ($this->firstName) { + $writer->writeElement($cs . 'organizer-first', $this->firstName); + } + if ($this->lastName) { + $writer->writeElement($cs . 'organizer-last', $this->lastName); + } + if ($this->supportedComponents) { + $writer->writeElement('{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set', $this->supportedComponents); + } + + $writer->endElement(); // invite-notification + + } + + /** + * Returns a unique id for this notification + * + * This is just the base url. This should generally be some kind of unique + * id. + * + * @return string + */ + function getId() { + + return $this->id; + + } + + /** + * Returns the ETag for this notification. + * + * The ETag must be surrounded by literal double-quotes. + * + * @return string + */ + function getETag() { + + return $this->etag; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php b/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php new file mode 100644 index 000000000..945323fed --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php @@ -0,0 +1,212 @@ +<?php + +namespace Sabre\CalDAV\Xml\Notification; + +use Sabre\Xml\Writer; +use Sabre\CalDAV; +use Sabre\CalDAV\SharingPlugin; + +/** + * This class represents the cs:invite-reply notification element. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class InviteReply implements NotificationInterface { + + /** + * A unique id for the message + * + * @var string + */ + protected $id; + + /** + * Timestamp of the notification + * + * @var DateTime + */ + protected $dtStamp; + + /** + * The unique id of the notification this was a reply to. + * + * @var string + */ + protected $inReplyTo; + + /** + * A url to the recipient of the original (!) notification. + * + * @var string + */ + protected $href; + + /** + * The type of message, see the SharingPlugin::STATUS_ constants. + * + * @var int + */ + protected $type; + + /** + * A url to the shared calendar. + * + * @var string + */ + protected $hostUrl; + + /** + * A description of the share request + * + * @var string + */ + protected $summary; + + /** + * Notification Etag + * + * @var string + */ + protected $etag; + + /** + * Creates the Invite Reply Notification. + * + * This constructor receives an array with the following elements: + * + * * id - A unique id + * * etag - The etag + * * dtStamp - A DateTime object with a timestamp for the notification. + * * inReplyTo - This should refer to the 'id' of the notification + * this is a reply to. + * * type - The type of notification, see SharingPlugin::STATUS_* + * constants for details. + * * hostUrl - A url to the shared calendar. + * * summary - Description of the share, can be the same as the + * calendar, but may also be modified (optional). + * + * @param array $values + */ + function __construct(array $values) { + + $required = [ + 'id', + 'etag', + 'href', + 'dtStamp', + 'inReplyTo', + 'type', + 'hostUrl', + ]; + foreach ($required as $item) { + if (!isset($values[$item])) { + throw new \InvalidArgumentException($item . ' is a required constructor option'); + } + } + + foreach ($values as $key => $value) { + if (!property_exists($this, $key)) { + throw new \InvalidArgumentException('Unknown option: ' . $key); + } + $this->$key = $value; + } + + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + $writer->writeElement('{' . CalDAV\Plugin::NS_CALENDARSERVER . '}invite-reply'); + + } + + /** + * This method serializes the entire notification, as it is used in the + * response body. + * + * @param Writer $writer + * @return void + */ + function xmlSerializeFull(Writer $writer) { + + $cs = '{' . CalDAV\Plugin::NS_CALENDARSERVER . '}'; + + $this->dtStamp->setTimezone(new \DateTimezone('GMT')); + $writer->writeElement($cs . 'dtstamp', $this->dtStamp->format('Ymd\\THis\\Z')); + + $writer->startElement($cs . 'invite-reply'); + + $writer->writeElement($cs . 'uid', $this->id); + $writer->writeElement($cs . 'in-reply-to', $this->inReplyTo); + $writer->writeElement('{DAV:}href', $this->href); + + switch ($this->type) { + + case SharingPlugin::STATUS_ACCEPTED : + $writer->writeElement($cs . 'invite-accepted'); + break; + case SharingPlugin::STATUS_DECLINED : + $writer->writeElement($cs . 'invite-declined'); + break; + + } + + $writer->writeElement($cs . 'hosturl', [ + '{DAV:}href' => $writer->contextUri . $this->hostUrl + ]); + + if ($this->summary) { + $writer->writeElement($cs . 'summary', $this->summary); + } + $writer->endElement(); // invite-reply + + } + + /** + * Returns a unique id for this notification + * + * This is just the base url. This should generally be some kind of unique + * id. + * + * @return string + */ + function getId() { + + return $this->id; + + } + + /** + * Returns the ETag for this notification. + * + * The ETag must be surrounded by literal double-quotes. + * + * @return string + */ + function getETag() { + + return $this->etag; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php b/vendor/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php new file mode 100644 index 000000000..1c08f12fd --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php @@ -0,0 +1,45 @@ +<?php + +namespace Sabre\CalDAV\Xml\Notification; + +use Sabre\Xml\XmlSerializable; +use Sabre\Xml\Writer; + +/** + * This interface reflects a single notification type. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface NotificationInterface extends XmlSerializable { + + /** + * This method serializes the entire notification, as it is used in the + * response body. + * + * @param Writer $writer + * @return void + */ + function xmlSerializeFull(Writer $writer); + + /** + * Returns a unique id for this notification + * + * This is just the base url. This should generally be some kind of unique + * id. + * + * @return string + */ + function getId(); + + /** + * Returns the ETag for this notification. + * + * The ETag must be surrounded by literal double-quotes. + * + * @return string + */ + function getETag(); + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Notification/SystemStatus.php b/vendor/sabre/dav/lib/CalDAV/Xml/Notification/SystemStatus.php new file mode 100644 index 000000000..d41702e07 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Notification/SystemStatus.php @@ -0,0 +1,182 @@ +<?php + +namespace Sabre\CalDAV\Xml\Notification; + +use Sabre\Xml\Writer; +use Sabre\CalDAV\Plugin; + +/** + * SystemStatus notification + * + * This notification can be used to indicate to the user that the system is + * down. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class SystemStatus implements NotificationInterface { + + const TYPE_LOW = 1; + const TYPE_MEDIUM = 2; + const TYPE_HIGH = 3; + + /** + * A unique id + * + * @var string + */ + protected $id; + + /** + * The type of alert. This should be one of the TYPE_ constants. + * + * @var int + */ + protected $type; + + /** + * A human-readable description of the problem. + * + * @var string + */ + protected $description; + + /** + * A url to a website with more information for the user. + * + * @var string + */ + protected $href; + + /** + * Notification Etag + * + * @var string + */ + protected $etag; + + /** + * Creates the notification. + * + * Some kind of unique id should be provided. This is used to generate a + * url. + * + * @param string $id + * @param string $etag + * @param int $type + * @param string $description + * @param string $href + */ + function __construct($id, $etag, $type = self::TYPE_HIGH, $description = null, $href = null) { + + $this->id = $id; + $this->type = $type; + $this->description = $description; + $this->href = $href; + $this->etag = $etag; + + } + + /** + * The serialize method is called during xml writing. + * + * It should use the $writer argument to encode this object into XML. + * + * Important note: it is not needed to create the parent element. The + * parent element is already created, and we only have to worry about + * attributes, child elements and text (if any). + * + * Important note 2: If you are writing any new elements, you are also + * responsible for closing them. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + switch ($this->type) { + case self::TYPE_LOW : + $type = 'low'; + break; + case self::TYPE_MEDIUM : + $type = 'medium'; + break; + default : + case self::TYPE_HIGH : + $type = 'high'; + break; + } + + $writer->startElement('{' . Plugin::NS_CALENDARSERVER . '}systemstatus'); + $writer->writeAttribute('type', $type); + $writer->endElement(); + + } + + /** + * This method serializes the entire notification, as it is used in the + * response body. + * + * @param Writer $writer + * @return void + */ + function xmlSerializeFull(Writer $writer) { + + $cs = '{' . Plugin::NS_CALENDARSERVER . '}'; + switch ($this->type) { + case self::TYPE_LOW : + $type = 'low'; + break; + case self::TYPE_MEDIUM : + $type = 'medium'; + break; + default : + case self::TYPE_HIGH : + $type = 'high'; + break; + } + + $writer->startElement($cs . 'systemstatus'); + $writer->writeAttribute('type', $type); + + + if ($this->description) { + $writer->writeElement($cs . 'description', $this->description); + } + if ($this->href) { + $writer->writeElement('{DAV:}href', $this->href); + } + + $writer->endElement(); // systemstatus + + } + + /** + * Returns a unique id for this notification + * + * This is just the base url. This should generally be some kind of unique + * id. + * + * @return string + */ + function getId() { + + return $this->id; + + } + + /* + * Returns the ETag for this notification. + * + * The ETag must be surrounded by literal double-quotes. + * + * @return string + */ + function getETag() { + + return $this->etag; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php b/vendor/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php new file mode 100644 index 000000000..c2a2d565e --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php @@ -0,0 +1,87 @@ +<?php + +namespace Sabre\CalDAV\Xml\Property; + +use Sabre\Xml\XmlSerializable; +use Sabre\Xml\Writer; +use Sabre\CalDAV\Plugin; + +/** + * AllowedSharingModes + * + * This property encodes the 'allowed-sharing-modes' property, as defined by + * the 'caldav-sharing-02' spec, in the http://calendarserver.org/ns/ + * namespace. + * + * This property is a representation of the supported-calendar_component-set + * property in the CalDAV namespace. It simply requires an array of components, + * such as VEVENT, VTODO + * + * @see https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing-02.txt + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @license http://sabre.io/license/ Modified BSD License + */ +class AllowedSharingModes implements XmlSerializable { + + /** + * Whether or not a calendar can be shared with another user + * + * @var bool + */ + protected $canBeShared; + + /** + * Whether or not the calendar can be placed on a public url. + * + * @var bool + */ + protected $canBePublished; + + /** + * Constructor + * + * @param bool $canBeShared + * @param bool $canBePublished + * @return void + */ + function __construct($canBeShared, $canBePublished) { + + $this->canBeShared = $canBeShared; + $this->canBePublished = $canBePublished; + + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + if ($this->canBeShared) { + $writer->writeElement('{' . Plugin::NS_CALENDARSERVER . '}can-be-shared'); + } + if ($this->canBePublished) { + $writer->writeElement('{' . Plugin::NS_CALENDARSERVER . '}can-be-published'); + } + + } + + + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php b/vendor/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php new file mode 100644 index 000000000..f577a9919 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php @@ -0,0 +1,80 @@ +<?php + +namespace Sabre\CalDAV\Xml\Property; + +use Sabre\Xml\Writer; +use Sabre\Xml\XmlSerializable; + +/** + * email-address-set property + * + * This property represents the email-address-set property in the + * http://calendarserver.org/ns/ namespace. + * + * It's a list of email addresses associated with a user. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class EmailAddressSet implements XmlSerializable { + + /** + * emails + * + * @var array + */ + private $emails; + + /** + * __construct + * + * @param array $emails + */ + function __construct(array $emails) { + + $this->emails = $emails; + + } + + /** + * Returns the email addresses + * + * @return array + */ + function getValue() { + + return $this->emails; + + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + foreach ($this->emails as $email) { + + $writer->writeElement('{http://calendarserver.org/ns/}email-address', $email); + + } + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php b/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php new file mode 100644 index 000000000..3ee053214 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php @@ -0,0 +1,252 @@ +<?php + +namespace Sabre\CalDAV\Xml\Property; + +use Sabre\Xml\Element; +use Sabre\Xml\Reader; +use Sabre\Xml\Writer; +use Sabre\CalDAV\Plugin; +use Sabre\CalDAV\SharingPlugin; + +/** + * Invite property + * + * This property encodes the 'invite' property, as defined by + * the 'caldav-sharing-02' spec, in the http://calendarserver.org/ns/ + * namespace. + * + * @see https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing-02.txt + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Invite implements Element { + + /** + * The list of users a calendar has been shared to. + * + * @var array + */ + protected $users; + + /** + * The organizer contains information about the person who shared the + * object. + * + * @var array + */ + protected $organizer; + + /** + * Creates the property. + * + * Users is an array. Each element of the array has the following + * properties: + * + * * href - Often a mailto: address + * * commonName - Optional, for example a first and lastname for a user. + * * status - One of the SharingPlugin::STATUS_* constants. + * * readOnly - true or false + * * summary - Optional, description of the share + * + * The organizer key is optional to specify. It's only useful when a + * 'sharee' requests the sharing information. + * + * The organizer may have the following properties: + * * href - Often a mailto: address. + * * commonName - Optional human-readable name. + * * firstName - Optional first name. + * * lastName - Optional last name. + * + * If you wonder why these two structures are so different, I guess a + * valid answer is that the current spec is still a draft. + * + * @param array $users + */ + function __construct(array $users, array $organizer = null) { + + $this->users = $users; + $this->organizer = $organizer; + + } + + /** + * Returns the list of users, as it was passed to the constructor. + * + * @return array + */ + function getValue() { + + return $this->users; + + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + $cs = '{' . Plugin::NS_CALENDARSERVER . '}'; + + if (!is_null($this->organizer)) { + + $writer->startElement($cs . 'organizer'); + $writer->writeElement('{DAV:}href', $this->organizer['href']); + + if (isset($this->organizer['commonName']) && $this->organizer['commonName']) { + $writer->writeElement($cs . 'common-name', $this->organizer['commonName']); + } + if (isset($this->organizer['firstName']) && $this->organizer['firstName']) { + $writer->writeElement($cs . 'first-name', $this->organizer['firstName']); + } + if (isset($this->organizer['lastName']) && $this->organizer['lastName']) { + $writer->writeElement($cs . 'last-name', $this->organizer['lastName']); + } + $writer->endElement(); // organizer + + } + + foreach ($this->users as $user) { + + $writer->startElement($cs . 'user'); + $writer->writeElement('{DAV:}href', $user['href']); + if (isset($user['commonName']) && $user['commonName']) { + $writer->writeElement($cs . 'common-name', $user['commonName']); + } + switch ($user['status']) { + + case SharingPlugin::STATUS_ACCEPTED : + $writer->writeElement($cs . 'invite-accepted'); + break; + case SharingPlugin::STATUS_DECLINED : + $writer->writeElement($cs . 'invite-declined'); + break; + case SharingPlugin::STATUS_NORESPONSE : + $writer->writeElement($cs . 'invite-noresponse'); + break; + case SharingPlugin::STATUS_INVALID : + $writer->writeElement($cs . 'invite-invalid'); + break; + } + + $writer->startElement($cs . 'access'); + if ($user['readOnly']) { + $writer->writeElement($cs . 'read'); + } else { + $writer->writeElement($cs . 'read-write'); + } + $writer->endElement(); // access + + if (isset($user['summary']) && $user['summary']) { + $writer->writeElement($cs . 'summary', $user['summary']); + } + + $writer->endElement(); //user + + } + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $cs = '{' . Plugin::NS_CALENDARSERVER . '}'; + + $users = []; + + foreach ($reader->parseInnerTree() as $elem) { + + if ($elem['name'] !== $cs . 'user') + continue; + + $user = [ + 'href' => null, + 'commonName' => null, + 'readOnly' => null, + 'summary' => null, + 'status' => null, + ]; + + foreach ($elem['value'] as $userElem) { + + switch ($userElem['name']) { + case $cs . 'invite-accepted' : + $user['status'] = SharingPlugin::STATUS_ACCEPTED; + break; + case $cs . 'invite-declined' : + $user['status'] = SharingPlugin::STATUS_DECLINED; + break; + case $cs . 'invite-noresponse' : + $user['status'] = SharingPlugin::STATUS_NORESPONSE; + break; + case $cs . 'invite-invalid' : + $user['status'] = SharingPlugin::STATUS_INVALID; + break; + case '{DAV:}href' : + $user['href'] = $userElem['value']; + break; + case $cs . 'common-name' : + $user['commonName'] = $userElem['value']; + break; + case $cs . 'access' : + foreach ($userElem['value'] as $accessHref) { + if ($accessHref['name'] === $cs . 'read') { + $user['readOnly'] = true; + } + } + break; + case $cs . 'summary' : + $user['summary'] = $userElem['value']; + break; + + } + + } + if (!$user['status']) { + throw new \InvalidArgumentException('Every user must have one of cs:invite-accepted, cs:invite-declined, cs:invite-noresponse or cs:invite-invalid'); + } + + $users[] = $user; + + } + + return new self($users); + + } +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php b/vendor/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php new file mode 100644 index 000000000..4a253e008 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php @@ -0,0 +1,140 @@ +<?php + +namespace Sabre\CalDAV\Xml\Property; + +use Sabre\Xml\Element; +use Sabre\Xml\Reader; +use Sabre\Xml\Writer; +use Sabre\Xml\Element\Elements; +use Sabre\CalDAV\Plugin; + +/** + * schedule-calendar-transp property. + * + * This property is a representation of the schedule-calendar-transp property. + * This property is defined in: + * + * http://tools.ietf.org/html/rfc6638#section-9.1 + * + * Its values are either 'transparent' or 'opaque'. If it's transparent, it + * means that this calendar will not be taken into consideration when a + * different user queries for free-busy information. If it's 'opaque', it will. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ScheduleCalendarTransp implements Element { + + const TRANSPARENT = 'transparent'; + const OPAQUE = 'opaque'; + + /** + * value + * + * @var string + */ + protected $value; + + /** + * Creates the property + * + * @param string $value + */ + function __construct($value) { + + if ($value !== self::TRANSPARENT && $value !== self::OPAQUE) { + throw new \InvalidArgumentException('The value must either be specified as "transparent" or "opaque"'); + } + $this->value = $value; + + } + + /** + * Returns the current value + * + * @return string + */ + function getValue() { + + return $this->value; + + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + switch ($this->value) { + case self::TRANSPARENT : + $writer->writeElement('{' . Plugin::NS_CALDAV . '}transparent'); + break; + case self::OPAQUE : + $writer->writeElement('{' . Plugin::NS_CALDAV . '}opaque'); + break; + } + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $elems = Elements::xmlDeserialize($reader); + + $value = null; + + foreach ($elems as $elem) { + switch ($elem) { + case '{' . Plugin::NS_CALDAV . '}opaque' : + $value = self::OPAQUE; + break; + case '{' . Plugin::NS_CALDAV . '}transparent' : + $value = self::TRANSPARENT; + break; + } + } + if (is_null($value)) + return null; + + return new self($value); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php b/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php new file mode 100644 index 000000000..7a26e767e --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php @@ -0,0 +1,129 @@ +<?php + +namespace Sabre\CalDAV\Xml\Property; + +use Sabre\Xml\Element; +use Sabre\Xml\ParseException; +use Sabre\Xml\Reader; +use Sabre\Xml\Writer; +use Sabre\CalDAV\Plugin; + +/** + * SupportedCalendarComponentSet property. + * + * This class represents the + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set property, as + * defined in: + * + * https://tools.ietf.org/html/rfc4791#section-5.2.3 + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class SupportedCalendarComponentSet implements Element { + + /** + * List of supported components. + * + * This array will contain values such as VEVENT, VTODO and VJOURNAL. + * + * @var array + */ + protected $components = []; + + /** + * Creates the property. + * + * @param array $components + */ + function __construct(array $components) { + + $this->components = $components; + + } + + /** + * Returns the list of supported components + * + * @return array + */ + function getValue() { + + return $this->components; + + } + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + foreach ($this->components as $component) { + + $writer->startElement('{' . Plugin::NS_CALDAV . '}comp'); + $writer->writeAttributes(['name' => $component]); + $writer->endElement(); + + } + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $elems = $reader->parseInnerTree(); + + $components = []; + + foreach ((array)$elems as $elem) { + if ($elem['name'] === '{' . Plugin::NS_CALDAV . '}comp') { + $components[] = $elem['attributes']['name']; + } + } + + if (!$components) { + throw new ParseException('supported-calendar-component-set must have at least one CALDAV:comp element'); + } + + return new self($components); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php b/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php new file mode 100644 index 000000000..b0c407fd6 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php @@ -0,0 +1,60 @@ +<?php + +namespace Sabre\CalDAV\Xml\Property; + +use Sabre\Xml\XmlSerializable; +use Sabre\Xml\Writer; +use Sabre\CalDAV\Plugin; + +/** + * Supported-calendar-data property + * + * This property is a representation of the supported-calendar-data property + * in the CalDAV namespace. SabreDAV only has support for text/calendar;2.0 + * so the value is currently hardcoded. + * + * This property is defined in: + * http://tools.ietf.org/html/rfc4791#section-5.2.4 + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @license http://sabre.io/license/ Modified BSD License + */ +class SupportedCalendarData implements XmlSerializable { + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + $writer->startElement('{' . Plugin::NS_CALDAV . '}calendar-data'); + $writer->writeAttributes([ + 'content-type' => 'text/calendar', + 'version' => '2.0', + ]); + $writer->endElement(); // calendar-data + $writer->startElement('{' . Plugin::NS_CALDAV . '}calendar-data'); + $writer->writeAttributes([ + 'content-type' => 'application/calendar+json', + ]); + $writer->endElement(); // calendar-data + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php b/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php new file mode 100644 index 000000000..71de25a62 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php @@ -0,0 +1,57 @@ +<?php + +namespace Sabre\CalDAV\Xml\Property; + +use Sabre\Xml\Writer; +use Sabre\Xml\XmlSerializable; +use Sabre\CalDAV\Plugin; + +/** + * supported-collation-set property + * + * This property is a representation of the supported-collation-set property + * in the CalDAV namespace. + * + * This property is defined in: + * http://tools.ietf.org/html/rfc4791#section-7.5.1 + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class SupportedCollationSet implements XmlSerializable { + + /** + * The xmlSerialize metod is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + $collations = [ + 'i;ascii-casemap', + 'i;octet', + 'i;unicode-casemap' + ]; + + foreach ($collations as $collation) { + $writer->writeElement('{' . Plugin::NS_CALDAV . '}supported-collation', $collation); + } + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php b/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php new file mode 100644 index 000000000..79b3bb3ac --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php @@ -0,0 +1,124 @@ +<?php + +namespace Sabre\CalDAV\Xml\Request; + +use Sabre\Xml\XmlDeserializable; +use Sabre\Xml\Reader; +use Sabre\CalDAV\Plugin; +use Sabre\Uri; + +/** + * CalendarMultiGetReport request parser. + * + * This class parses the {urn:ietf:params:xml:ns:caldav}calendar-multiget + * REPORT, as defined in: + * + * https://tools.ietf.org/html/rfc4791#section-7.9 + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @license http://sabre.io/license/ Modified BSD License + */ +class CalendarMultiGetReport implements XmlDeserializable { + + /** + * An array with requested properties. + * + * @var array + */ + public $properties; + + /** + * This is an array with the urls that are being requested. + * + * @var array + */ + public $hrefs; + + /** + * If the calendar data must be expanded, this will contain an array with 2 + * elements: start and end. + * + * Each may be a DateTime or null. + * + * @var array|null + */ + public $expand = null; + + /** + * The mimetype of the content that should be returend. Usually + * text/calendar. + * + * @var string + */ + public $contentType = null; + + /** + * The version of calendar-data that should be returned. Usually '2.0', + * referring to iCalendar 2.0. + * + * @var string + */ + public $version = null; + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $elems = $reader->parseInnerTree([ + '{urn:ietf:params:xml:ns:caldav}calendar-data' => 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $newProps = [ + 'hrefs' => [], + 'properties' => [], + ]; + + foreach ($elems as $elem) { + + switch ($elem['name']) { + + case '{DAV:}prop' : + $newProps['properties'] = array_keys($elem['value']); + if (isset($elem['value']['{' . Plugin::NS_CALDAV . '}calendar-data'])) { + $newProps += $elem['value']['{' . Plugin::NS_CALDAV . '}calendar-data']; + } + break; + case '{DAV:}href' : + $newProps['hrefs'][] = Uri\resolve($reader->contextUri, $elem['value']); + break; + + } + + } + + $obj = new self(); + foreach ($newProps as $key => $value) { + $obj->$key = $value; + } + + return $obj; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php b/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php new file mode 100644 index 000000000..848a4dc46 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php @@ -0,0 +1,139 @@ +<?php + +namespace Sabre\CalDAV\Xml\Request; + +use Sabre\Xml\XmlDeserializable; +use Sabre\Xml\Reader; +use Sabre\DAV\Exception\BadRequest; +use Sabre\CalDAV\Plugin; + +/** + * CalendarQueryReport request parser. + * + * This class parses the {urn:ietf:params:xml:ns:caldav}calendar-query + * REPORT, as defined in: + * + * https://tools.ietf.org/html/rfc4791#section-7.9 + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @license http://sabre.io/license/ Modified BSD License + */ +class CalendarQueryReport implements XmlDeserializable { + + /** + * An array with requested properties. + * + * @var array + */ + public $properties; + + /** + * List of property/component filters. + * + * @var array + */ + public $filters; + + /** + * If the calendar data must be expanded, this will contain an array with 2 + * elements: start and end. + * + * Each may be a DateTime or null. + * + * @var array|null + */ + public $expand = null; + + /** + * The mimetype of the content that should be returend. Usually + * text/calendar. + * + * @var string + */ + public $contentType = null; + + /** + * The version of calendar-data that should be returned. Usually '2.0', + * referring to iCalendar 2.0. + * + * @var string + */ + public $version = null; + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $elems = $reader->parseInnerTree([ + '{urn:ietf:params:xml:ns:caldav}comp-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\CompFilter', + '{urn:ietf:params:xml:ns:caldav}prop-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\PropFilter', + '{urn:ietf:params:xml:ns:caldav}param-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\ParamFilter', + '{urn:ietf:params:xml:ns:caldav}calendar-data' => 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $newProps = [ + 'filters' => null, + 'properties' => [], + ]; + + if (!is_array($elems)) $elems = []; + + foreach ($elems as $elem) { + + switch ($elem['name']) { + + case '{DAV:}prop' : + $newProps['properties'] = array_keys($elem['value']); + if (isset($elem['value']['{' . Plugin::NS_CALDAV . '}calendar-data'])) { + $newProps += $elem['value']['{' . Plugin::NS_CALDAV . '}calendar-data']; + } + break; + case '{' . Plugin::NS_CALDAV . '}filter' : + foreach ($elem['value'] as $subElem) { + if ($subElem['name'] === '{' . Plugin::NS_CALDAV . '}comp-filter') { + if (!is_null($newProps['filters'])) { + throw new BadRequest('Only one top-level comp-filter may be defined'); + } + $newProps['filters'] = $subElem['value']; + } + } + break; + + } + + } + + if (is_null($newProps['filters'])) { + throw new BadRequest('The {' . Plugin::NS_CALDAV . '}filter element is required for this request'); + } + + $obj = new self(); + foreach ($newProps as $key => $value) { + $obj->$key = $value; + } + return $obj; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php b/vendor/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php new file mode 100644 index 000000000..e3b27d08e --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php @@ -0,0 +1,91 @@ +<?php + +namespace Sabre\CalDAV\Xml\Request; + +use Sabre\CalDAV\Plugin; +use Sabre\DAV\Exception\BadRequest; +use Sabre\VObject\DateTimeParser; +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; + +/** + * FreeBusyQueryReport + * + * This class parses the {DAV:}free-busy-query REPORT, as defined in: + * + * http://tools.ietf.org/html/rfc3253#section-3.8 + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class FreeBusyQueryReport implements XmlDeserializable { + + /** + * Starttime of report + * + * @var DateTime|null + */ + public $start; + + /** + * End time of report + * + * @var DateTime|null + */ + public $end; + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $timeRange = '{' . Plugin::NS_CALDAV . '}time-range'; + + $start = null; + $end = null; + + foreach ((array)$reader->parseInnerTree([]) as $elem) { + + if ($elem['name'] !== $timeRange) continue; + + $start = empty($elem['attributes']['start']) ?: $elem['attributes']['start']; + $end = empty($elem['attributes']['end']) ?: $elem['attributes']['end']; + + } + if (!$start && !$end) { + throw new BadRequest('The freebusy report must have a time-range element'); + } + if ($start) { + $start = DateTimeParser::parseDateTime($start); + } + if ($end) { + $end = DateTimeParser::parseDateTime($end); + } + $result = new self(); + $result->start = $start; + $result->end = $end; + + return $result; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php b/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php new file mode 100644 index 000000000..ec627156f --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php @@ -0,0 +1,149 @@ +<?php + +namespace Sabre\CalDAV\Xml\Request; + +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use Sabre\Xml\Element\KeyValue; +use Sabre\DAV\Exception\BadRequest; +use Sabre\CalDAV\Plugin; +use Sabre\CalDAV\SharingPlugin; + +/** + * Invite-reply POST request parser + * + * This class parses the invite-reply POST request, as defined in: + * + * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class InviteReply implements XmlDeserializable { + + /** + * The sharee calendar user address. + * + * This is the address that the original invite was set to + * + * @var string + */ + public $href; + + /** + * The uri to the calendar that was being shared. + * + * @var string + */ + public $calendarUri; + + /** + * The id of the invite message that's being responded to + * + * @var string + */ + public $inReplyTo; + + /** + * An optional message + * + * @var string + */ + public $summary; + + /** + * Either SharingPlugin::STATUS_ACCEPTED or SharingPlugin::STATUS_DECLINED. + * + * @var int + */ + public $status; + + /** + * Constructor + * + * @param string $href + * @param string $calendarUri + * @param string $inReplyTo + * @param string $summary + * @param int $status + */ + function __construct($href, $calendarUri, $inReplyTo, $summary, $status) { + + $this->href = $href; + $this->calendarUri = $calendarUri; + $this->inReplyTo = $inReplyTo; + $this->summary = $summary; + $this->status = $status; + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $elems = KeyValue::xmlDeserialize($reader); + + $href = null; + $calendarUri = null; + $inReplyTo = null; + $summary = null; + $status = null; + + foreach ($elems as $name => $value) { + + switch ($name) { + + case '{' . Plugin::NS_CALENDARSERVER . '}hosturl' : + foreach ($value as $bla) { + if ($bla['name'] === '{DAV:}href') { + $calendarUri = $bla['value']; + } + } + break; + case '{' . Plugin::NS_CALENDARSERVER . '}invite-accepted' : + $status = SharingPlugin::STATUS_ACCEPTED; + break; + case '{' . Plugin::NS_CALENDARSERVER . '}invite-declined' : + $status = SharingPlugin::STATUS_DECLINED; + break; + case '{' . Plugin::NS_CALENDARSERVER . '}in-reply-to' : + $inReplyTo = $value; + break; + case '{' . Plugin::NS_CALENDARSERVER . '}summary' : + $summary = $value; + break; + case '{DAV:}href' : + $href = $value; + break; + } + + } + if (is_null($calendarUri)) { + throw new BadRequest('The {http://calendarserver.org/ns/}hosturl/{DAV:}href element must exist'); + } + + return new self($href, $calendarUri, $inReplyTo, $summary, $status); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php b/vendor/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php new file mode 100644 index 000000000..7b745db55 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php @@ -0,0 +1,79 @@ +<?php + +namespace Sabre\CalDAV\Xml\Request; + +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; + +/** + * MKCALENDAR parser. + * + * This class parses the MKCALENDAR request, as defined in: + * + * https://tools.ietf.org/html/rfc4791#section-5.3.1 + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class MkCalendar implements XmlDeserializable { + + /** + * The list of properties that will be set. + * + * @var array + */ + public $properties = []; + + /** + * Returns the list of properties the calendar will be initialized with. + * + * @return array + */ + function getProperties() { + + return $this->properties; + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $self = new self(); + + $elementMap = $reader->elementMap; + $elementMap['{DAV:}prop'] = 'Sabre\DAV\Xml\Element\Prop'; + $elementMap['{DAV:}set'] = 'Sabre\Xml\Element\KeyValue'; + $elems = $reader->parseInnerTree($elementMap); + + foreach ($elems as $elem) { + if ($elem['name'] === '{DAV:}set') { + $self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']); + } + } + + return $self; + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php b/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php new file mode 100644 index 000000000..dacc5dc94 --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php @@ -0,0 +1,116 @@ +<?php + +namespace Sabre\CalDAV\Xml\Request; + +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use Sabre\CalDAV\Plugin; + +/** + * Share POST request parser + * + * This class parses the share POST request, as defined in: + * + * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Share implements XmlDeserializable { + + /** + * The list of new people added or updated. + * + * Every element has the following keys: + * 1. href - An email address + * 2. commonName - Some name + * 3. summary - An optional description of the share + * 4. readOnly - true or false + * + * @var array + */ + public $set = []; + + /** + * List of people removed from the share list. + * + * The list is a flat list of email addresses (including mailto:). + * + * @var array + */ + public $remove = []; + + /** + * Constructor + * + * @param array $set + * @param array $remove + */ + function __construct(array $set, array $remove) { + + $this->set = $set; + $this->remove = $remove; + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $elems = $reader->parseInnerTree([ + '{' . Plugin::NS_CALENDARSERVER . '}set' => 'Sabre\\Xml\\Element\\KeyValue', + '{' . Plugin::NS_CALENDARSERVER . '}remove' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $set = []; + $remove = []; + + foreach ($elems as $elem) { + switch ($elem['name']) { + + case '{' . Plugin::NS_CALENDARSERVER . '}set' : + $sharee = $elem['value']; + + $sumElem = '{' . Plugin::NS_CALENDARSERVER . '}summary'; + $commonName = '{' . Plugin::NS_CALENDARSERVER . '}common-name'; + + $set[] = [ + 'href' => $sharee['{DAV:}href'], + 'commonName' => isset($sharee[$commonName]) ? $sharee[$commonName] : null, + 'summary' => isset($sharee[$sumElem]) ? $sharee[$sumElem] : null, + 'readOnly' => !array_key_exists('{' . Plugin::NS_CALENDARSERVER . '}read-write', $sharee), + ]; + break; + + case '{' . Plugin::NS_CALENDARSERVER . '}remove' : + $remove[] = $elem['value']['{DAV:}href']; + break; + + } + } + + return new self($set, $remove); + + } + +} |