aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php
diff options
context:
space:
mode:
authorredmatrix <git@macgirvin.com>2016-05-10 17:26:44 -0700
committerredmatrix <git@macgirvin.com>2016-05-10 17:26:44 -0700
commit0b02a6d123b2014705998c94ddf3d460948d3eac (patch)
tree78ff2cab9944a4f5ab3f80ec93cbe1120de90bb2 /vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php
parent40b5b6e9d2da7ab65c8b4d38cdceac83a4d78deb (diff)
downloadvolse-hubzilla-0b02a6d123b2014705998c94ddf3d460948d3eac.tar.gz
volse-hubzilla-0b02a6d123b2014705998c94ddf3d460948d3eac.tar.bz2
volse-hubzilla-0b02a6d123b2014705998c94ddf3d460948d3eac.zip
initial sabre upgrade (needs lots of work - to wit: authentication, redo the browser interface, and rework event export/import)
Diffstat (limited to 'vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php')
-rw-r--r--vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php994
1 files changed, 994 insertions, 0 deletions
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/',
+ ];
+
+ }
+}