77 return [
'calendar-auto-schedule',
'calendar-availability'];
91 return 'caldav-schedule';
104 $server->on(
'method:POST', [$this,
'httpPost']);
105 $server->on(
'propFind', [$this,
'propFind']);
106 $server->on(
'propPatch', [$this,
'propPatch']);
107 $server->on(
'calendarObjectChange', [$this,
'calendarObjectChange']);
108 $server->on(
'beforeUnbind', [$this,
'beforeUnbind']);
109 $server->on(
'schedule', [$this,
'scheduleLocalDelivery']);
110 $server->on(
'getSupportedPrivilegeSet', [$this,
'getSupportedPrivilegeSet']);
112 $ns =
'{' . self::NS_CALDAV .
'}';
118 $server->resourceTypeMapping[
'\\Sabre\\CalDAV\\Schedule\\IOutbox'] = $ns .
'schedule-outbox';
119 $server->resourceTypeMapping[
'\\Sabre\\CalDAV\\Schedule\\IInbox'] = $ns .
'schedule-inbox';
124 array_push(
$server->protectedProperties,
125 $ns .
'schedule-inbox-URL',
126 $ns .
'schedule-outbox-URL',
127 $ns .
'calendar-user-address-set',
128 $ns .
'calendar-user-type',
129 $ns .
'schedule-default-calendar-URL'
147 $node = $this->server->tree->getNodeForPath($uri);
152 if ($node instanceof
IOutbox) {
179 $node = $this->server->tree->getNodeForPath(
$path);
186 $this->server->transactionType =
'post-caldav-outbox';
209 $principalUrl = $node->getPrincipalUrl();
212 $propFind->
handle(
'{' . self::NS_CALDAV .
'}schedule-outbox-URL',
function() use ($principalUrl,
$caldavPlugin) {
214 $calendarHomePath =
$caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
215 if (!$calendarHomePath) {
218 $outboxPath = $calendarHomePath .
'/outbox/';
224 $propFind->
handle(
'{' . self::NS_CALDAV .
'}schedule-inbox-URL',
function() use ($principalUrl,
$caldavPlugin) {
226 $calendarHomePath =
$caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
227 if (!$calendarHomePath) {
230 $inboxPath = $calendarHomePath .
'/inbox/';
236 $propFind->
handle(
'{' . self::NS_CALDAV .
'}schedule-default-calendar-URL',
function() use ($principalUrl,
$caldavPlugin) {
240 $calendarHomePath =
$caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
242 if (!$calendarHomePath) {
246 $sccs =
'{' . self::NS_CALDAV .
'}supported-calendar-component-set';
248 $result = $this->server->getPropertiesForPath($calendarHomePath, [
249 '{DAV:}resourcetype',
250 '{DAV:}share-access',
255 if (!isset($child[200][
'{DAV:}resourcetype']) || !$child[200][
'{DAV:}resourcetype']->is(
'{' . self::NS_CALDAV .
'}calendar')) {
259 if (isset($child[200][
'{DAV:}share-access'])) {
260 $shareAccess = $child[200][
'{DAV:}share-access']->getValue();
261 if ($shareAccess !== Sharing\Plugin::ACCESS_NOTSHARED && $shareAccess !== Sharing\Plugin::ACCESS_SHAREDOWNER) {
268 if (!isset($child[200][$sccs]) || in_array(
'VEVENT', $child[200][$sccs]->getValue())) {
279 $propFind->
handle(
'{' . self::NS_CALDAV .
'}calendar-user-type',
function() {
288 $propFind->
handle(
'{http://calendarserver.org/ns/}calendar-availability',
function() use ($propFind, $node) {
292 $availProp =
'{' . self::NS_CALDAV .
'}calendar-availability';
298 $this->server->getPropertiesByNode(
304 '{http://calendarserver.org/ns/}calendar-availability',
305 $subPropFind->get($availProp),
306 $subPropFind->getStatus($availProp)
323 $propPatch->
handle(
'{http://calendarserver.org/ns/}calendar-availability',
function($value) use (
$path) {
325 $availProp =
'{' . self::NS_CALDAV .
'}calendar-availability';
326 $subPropPatch =
new PropPatch([$availProp => $value]);
327 $this->server->emit(
'propPatch', [
$path, $subPropPatch]);
328 $subPropPatch->commit();
330 return $subPropPatch->getResult()[$availProp];
354 $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
357 $calendarNode->getOwner()
361 $node = $this->server->tree->getNodeForPath(
$request->getPath());
384 $this->server->emit(
'schedule', [$iTipMessage]);
385 if (!$iTipMessage->scheduleStatus) {
386 $iTipMessage->scheduleStatus =
'5.2;There was no system capable of delivering the scheduling message';
390 list($baseCode) = explode(
'.', $iTipMessage->scheduleStatus);
391 if (!$iTipMessage->significantChange && in_array($baseCode, [
'3',
'5'])) {
392 $iTipMessage->scheduleStatus =
null;
410 if ($this->server->httpRequest->getMethod() ===
'MOVE')
return;
412 $node = $this->server->tree->getNodeForPath(
$path);
427 $messages = $broker->parseEvent(
null, $addresses, $node->get());
453 $caldavNS =
'{' . self::NS_CALDAV .
'}';
455 $principalUri =
$aclPlugin->getPrincipalByUri($iTipMessage->recipient);
456 if (!$principalUri) {
457 $iTipMessage->scheduleStatus =
'3.7;Could not find principal.';
467 $this->server->removeListener(
'propFind', [
$aclPlugin,
'propFind']);
469 $result = $this->server->getProperties(
472 '{DAV:}principal-URL',
473 $caldavNS .
'calendar-home-set',
474 $caldavNS .
'schedule-inbox-URL',
475 $caldavNS .
'schedule-default-calendar-URL',
476 '{http://sabredav.org/ns}email-address',
481 $this->server->on(
'propFind', [
$aclPlugin,
'propFind'], 20);
483 if (!isset(
$result[$caldavNS .
'schedule-inbox-URL'])) {
484 $iTipMessage->scheduleStatus =
'5.2;Could not find local inbox';
487 if (!isset(
$result[$caldavNS .
'calendar-home-set'])) {
488 $iTipMessage->scheduleStatus =
'5.2;Could not locate a calendar-home-set';
491 if (!isset(
$result[$caldavNS .
'schedule-default-calendar-URL'])) {
492 $iTipMessage->scheduleStatus =
'5.2;Could not find a schedule-default-calendar-URL property';
496 $calendarPath =
$result[$caldavNS .
'schedule-default-calendar-URL']->getHref();
497 $homePath =
$result[$caldavNS .
'calendar-home-set']->getHref();
498 $inboxPath =
$result[$caldavNS .
'schedule-inbox-URL']->getHref();
500 if ($iTipMessage->method ===
'REPLY') {
501 $privilege =
'schedule-deliver-reply';
503 $privilege =
'schedule-deliver-invite';
506 if (!
$aclPlugin->checkPrivileges($inboxPath, $caldavNS . $privilege, DAVACL\Plugin::R_PARENT,
false)) {
507 $iTipMessage->scheduleStatus =
'3.8;insufficient privileges: ' . $privilege .
' is required on the recipient schedule inbox.';
513 $uid = $iTipMessage->uid;
517 $home = $this->server->tree->getNodeForPath($homePath);
518 $inbox = $this->server->tree->getNodeForPath($inboxPath);
520 $currentObject =
null;
524 $result = $home->getCalendarObjectByUID($uid);
527 $objectPath = $homePath .
'/' .
$result;
528 $objectNode = $this->server->tree->getNodeForPath($objectPath);
529 $oldICalendarData = $objectNode->get();
536 $newObject = $broker->processMessage($iTipMessage, $currentObject);
538 $inbox->createFile($newFileName, $iTipMessage->message->serialize());
547 $iTipMessage->scheduleStatus =
'5.0;iTip message was not processed by the server, likely because we didn\'t understand it.';
555 $calendar = $this->server->tree->getNodeForPath($calendarPath);
556 $calendar->createFile($newFileName, $newObject->serialize());
561 if ($iTipMessage->method ===
'REPLY') {
565 [$iTipMessage->recipient],
566 [$iTipMessage->sender]
569 $objectNode->put($newObject->serialize());
571 $iTipMessage->scheduleStatus =
'1.2;Message delivered locally';
586 $ns =
'{' . self::NS_CALDAV .
'}';
587 if ($node instanceof
IOutbox) {
588 $supportedPrivilegeSet[$ns .
'schedule-send'] = [
591 $ns .
'schedule-send-invite' => [
595 $ns .
'schedule-send-reply' => [
599 $ns .
'schedule-send-freebusy' => [
605 $ns .
'schedule-post-vevent' => [
612 if ($node instanceof
IInbox) {
613 $supportedPrivilegeSet[$ns .
'schedule-deliver'] = [
616 $ns .
'schedule-deliver-invite' => [
620 $ns .
'schedule-deliver-reply' => [
624 $ns .
'schedule-query-freebusy' => [
655 $messages = $broker->parseEvent($newObject, $addresses, $oldObject);
667 if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() ===
$message->recipient)) {
669 $newObject->VEVENT->ORGANIZER[
'SCHEDULE-STATUS'] =
$message->getScheduleStatus();
671 unset($newObject->VEVENT->ORGANIZER[
'SCHEDULE-FORCE-SEND']);
675 if (isset($newObject->VEVENT->ATTENDEE))
foreach ($newObject->VEVENT->ATTENDEE as $attendee) {
677 if ($attendee->getNormalizedValue() ===
$message->recipient) {
679 $attendee[
'SCHEDULE-STATUS'] =
$message->getScheduleStatus();
681 unset($attendee[
'SCHEDULE-FORCE-SEND']);
701 $CUAS =
'{' . self::NS_CALDAV .
'}calendar-user-address-set';
703 $properties = $this->server->getProperties(
709 if (!isset($properties[$CUAS])) {
713 $addresses = $properties[$CUAS]->getHrefs();
742 throw new BadRequest(
'The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage());
748 $componentType =
null;
749 foreach ($vObject->getComponents() as $component) {
750 if ($component->name !==
'VTIMEZONE') {
751 $componentType = $component->name;
755 if (is_null($componentType)) {
756 throw new BadRequest(
'We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
760 $method = strtoupper((
string)$vObject->METHOD);
762 throw new BadRequest(
'A METHOD property must be specified in iTIP messages');
769 $acl = $this->server->getPlugin(
'acl');
771 if ($componentType ===
'VFREEBUSY' && $method ===
'REQUEST') {
773 $acl && $acl->checkPrivileges($outboxPath,
'{' . self::NS_CALDAV .
'}schedule-send-freebusy');
782 throw new NotImplemented(
'We only support VFREEBUSY (REQUEST) on this endpoint');
800 $vFreeBusy = $vObject->VFREEBUSY;
801 $organizer = $vFreeBusy->ORGANIZER;
803 $organizer = (string)$organizer;
808 $caldavNS =
'{' . self::NS_CALDAV .
'}';
810 $uas = $caldavNS .
'calendar-user-address-set';
811 $props = $this->server->getProperties($owner, [$uas]);
813 if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
814 throw new Forbidden(
'The organizer in the request did not match any of the addresses for the owner of this inbox');
817 if (!isset($vFreeBusy->ATTENDEE)) {
818 throw new BadRequest(
'You must at least specify 1 attendee');
822 foreach ($vFreeBusy->ATTENDEE as $attendee) {
823 $attendees[] = (string)$attendee;
827 if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
828 throw new BadRequest(
'DTSTART and DTEND must both be specified');
831 $startRange = $vFreeBusy->DTSTART->getDateTime();
832 $endRange = $vFreeBusy->DTEND->getDateTime();
835 foreach ($attendees as $attendee) {
839 $dom = new \DOMDocument(
'1.0',
'utf-8');
840 $dom->formatOutput =
true;
841 $scheduleResponse = $dom->createElement(
'cal:schedule-response');
842 foreach ($this->server->xml->namespaceMap as
$namespace => $prefix) {
844 $scheduleResponse->setAttribute(
'xmlns:' . $prefix,
$namespace);
847 $dom->appendChild($scheduleResponse);
850 $xresponse = $dom->createElement(
'cal:response');
852 $recipient = $dom->createElement(
'cal:recipient');
853 $recipientHref = $dom->createElement(
'd:href');
855 $recipientHref->appendChild($dom->createTextNode(
$result[
'href']));
856 $recipient->appendChild($recipientHref);
857 $xresponse->appendChild($recipient);
859 $reqStatus = $dom->createElement(
'cal:request-status');
860 $reqStatus->appendChild($dom->createTextNode(
$result[
'request-status']));
861 $xresponse->appendChild($reqStatus);
863 if (isset(
$result[
'calendar-data'])) {
865 $calendardata = $dom->createElement(
'cal:calendar-data');
866 $calendardata->appendChild($dom->createTextNode(str_replace(
"\r\n",
"\n",
$result[
'calendar-data']->serialize())));
867 $xresponse->appendChild($calendardata);
870 $scheduleResponse->appendChild($xresponse);
874 $response->setHeader(
'Content-Type',
'application/xml');
899 $caldavNS =
'{' . self::NS_CALDAV .
'}';
905 [
'{http://sabredav.org/ns}email-address' =>
$email],
907 '{DAV:}principal-URL',
908 $caldavNS .
'calendar-home-set',
909 $caldavNS .
'schedule-inbox-URL',
910 '{http://sabredav.org/ns}email-address',
917 'request-status' =>
'3.7;Could not find principal',
918 'href' =>
'mailto:' .
$email,
922 if (!isset(
$result[0][200][$caldavNS .
'calendar-home-set'])) {
924 'request-status' =>
'3.7;No calendar-home-set property found',
925 'href' =>
'mailto:' .
$email,
928 if (!isset(
$result[0][200][$caldavNS .
'schedule-inbox-URL'])) {
930 'request-status' =>
'3.7;No schedule-inbox-URL property found',
931 'href' =>
'mailto:' .
$email,
934 $homeSet =
$result[0][200][$caldavNS .
'calendar-home-set']->getHref();
935 $inboxUrl =
$result[0][200][$caldavNS .
'schedule-inbox-URL']->getHref();
938 $aclPlugin->checkPrivileges($inboxUrl, $caldavNS .
'schedule-query-freebusy');
942 $calendarTimeZone =
new DateTimeZone(
'UTC');
944 foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
949 $sct = $caldavNS .
'schedule-calendar-transp';
950 $ctz = $caldavNS .
'calendar-timezone';
951 $props = $node->getProperties([$sct, $ctz]);
959 if (isset($props[$ctz])) {
961 $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
964 $vtimezoneObj->destroy();
969 $urls = $node->calendarQuery([
970 'name' =>
'VCALENDAR',
974 'comp-filters' => [],
975 'prop-filters' => [],
976 'is-not-defined' =>
false,
983 'prop-filters' => [],
984 'is-not-defined' =>
false,
985 'time-range' =>
null,
988 $calObjects = array_map(
function(
$url) use ($node) {
989 $obj = $node->getChild(
$url)->
get();
993 $objects = array_merge($objects, $calObjects);
997 $inboxProps = $this->server->getProperties(
999 $caldavNS .
'calendar-availability'
1003 $vcalendar->METHOD =
'REPLY';
1006 $generator->setObjects($objects);
1008 $generator->setBaseObject($vcalendar);
1009 $generator->setTimeZone($calendarTimeZone);
1012 $generator->setVAvailability(
1014 $inboxProps[$caldavNS .
'calendar-availability']
1019 $result = $generator->getResult();
1021 $vcalendar->VFREEBUSY->ATTENDEE =
'mailto:' .
$email;
1022 $vcalendar->VFREEBUSY->UID = (string)
$request->VFREEBUSY->UID;
1023 $vcalendar->VFREEBUSY->ORGANIZER = clone
$request->VFREEBUSY->ORGANIZER;
1027 'request-status' =>
'2.0;Success',
1028 'href' =>
'mailto:' .
$email,
1041 $scheduleReply =
$request->getHeader(
'Schedule-Reply');
1042 return $scheduleReply !==
'F';
1061 'description' =>
'Adds calendar-auto-schedule, as defined in rfc6638',
1062 'link' =>
'http://sabre.io/dav/scheduling/',
while(false !==( $line=fgets( $in))) if(! $columns) $ignore
foreach($paths as $path) $request
An exception for terminatinating execution or to throw for unit testing.
const NS_CALDAV
This is the official CalDAV namespace.
getPluginName()
Returns a plugin name.
initialize(Server $server)
Initializes the plugin.
deliver(ITip\Message $iTipMessage)
This method is responsible for delivering the ITip message.
getPluginName()
Returns the name of the plugin.
beforeUnbind($path)
This method is triggered before a file gets deleted.
scheduleReply(RequestInterface $request)
This method checks the 'Schedule-Reply' header and returns false if it's 'F', otherwise true.
getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet)
This method is triggered whenever a subsystem requests the privileges that are supported on a particu...
calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew)
This method is triggered whenever there was a calendar object gets created or updated.
scheduleLocalDelivery(ITip\Message $iTipMessage)
Event handler for the 'schedule' event.
getAddressesForPrincipal($principal)
Returns a list of addresses that are associated with a principal.
httpPost(RequestInterface $request, ResponseInterface $response)
This method handles POST request for the outbox.
getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request)
Returns free-busy information for a specific address.
propPatch($path, PropPatch $propPatch)
This method is called during property updates.
getPluginInfo()
Returns a bunch of meta-data about the plugin.
processICalendarChange($oldObject=null, VCalendar $newObject, array $addresses, array $ignore=[], &$modified=false)
This method looks at an old iCalendar object, a new iCalendar object and starts sending scheduling me...
propFind(PropFind $propFind, INode $node)
This method handler is invoked during fetching of properties.
outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response)
This method handles POST requests to the schedule-outbox.
getFeatures()
Returns a list of features for the DAV: HTTP header.
getHTTPMethods($uri)
Use this method to tell the server this plugin defines additional HTTP methods.
handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response)
This method is responsible for parsing a free-busy query request and returning it's result.
schedule-calendar-transp property.
This class holds all the information about a PROPFIND request.
set($propertyName, $value, $status=null)
Sets the value of the property.
getPath()
Returns the path this PROPFIND request is for.
handle($propertyName, $valueOrCallBack)
Handles a specific property.
This class represents a set of properties that are going to be updated.
handle($properties, callable $callback)
Call this function if you wish to handle updating certain properties.
The baseclass for all server plugins.
static getUUID()
Returns a pseudo-random v4 UUID.
This is the abstract base class for both the Request and Response objects.
This class helps with generating FREEBUSY reports based on existing sets of objects.
The ITip\Broker class is a utility class that helps with processing so-called iTip messages.
This class represents an iTip message.
Exception thrown by Reader if an invalid object was attempted to be parsed.
iCalendar/vCard/jCal/jCard/xCal/xCard reader object.
static read($data, $options=0, $charset='UTF-8')
Parses a vCard or iCalendar object, and returns the top component.
if($err=$client->getError()) $namespace
CalendarObject interface.
Implement this interface to have a node be recognized as a CalDAV scheduling inbox.
Implement this interface to have a node be recognized as a CalDAV scheduling outbox.
The SchedulingObject represents a scheduling object in the Inbox collection.
getOwner()
Returns the owner principal.
The INode interface is the base interface, and the parent class of both ICollection and IFile.
The RequestInterface represents a HTTP request.
This interface represents a HTTP response.
catch(Exception $e) $message
if( $path[strlen( $path) - 1]==='/') if(is_dir($path)) if(!file_exists( $path)) if(preg_match('#\.php$#D', mb_strtolower($path, 'UTF-8'))) $contentType