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);
661 if (in_array($message->recipient,
$ignore)) {
667 if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() === $message->recipient)) {
668 if ($message->scheduleStatus) {
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) {
678 if ($message->scheduleStatus) {
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();
736 $outboxPath = $request->
getPath();
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');
875 $response->
setBody($dom->saveXML());
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');
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);
1007 $generator->setTimeRange($start, $end);
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/',
if($err=$client->getError()) $namespace
This interface represents a HTTP response.
The RequestInterface represents a HTTP request.
handle($propertyName, $valueOrCallBack)
Handles a specific property.
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...
beforeUnbind($path)
This method is triggered before a file gets deleted.
The baseclass for all server plugins.
The ITip class is a utility class that helps with processing so-called iTip messages.
getOwner()
Returns the owner principal.
setBody($body)
Updates the body resource with a new stream.
on($eventName, callable $callBack, $priority=100)
Subscribe to an event.
This class represents a set of properties that are going to be updated.
This is the abstract base class for both the Request and Response objects.
foreach($paths as $path) $request
outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response)
This method handles POST requests to the schedule-outbox.
Implement this interface to have a node be recognized as a CalDAV scheduling inbox.
getPath()
Returns the path this PROPFIND request is for.
deliver(ITip\Message $iTipMessage)
This method is responsible for delivering the ITip message.
This class helps with generating FREEBUSY reports based on existing sets of objects.
calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew)
This method is triggered whenever there was a calendar object gets created or updated.
This class holds all the information about a PROPFIND request.
scheduleLocalDelivery(ITip\Message $iTipMessage)
Event handler for the 'schedule' event.
set($propertyName, $value, $status=null)
Sets the value of the property.
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...
propFind(PropFind $propFind, INode $node)
This method handler is invoked during fetching of properties.
getPluginName()
Returns the name of the plugin.
The SchedulingObject represents a scheduling object in the Inbox collection.
getPluginName()
Returns a plugin name.
catch(Exception $e) $message
getPluginInfo()
Returns a bunch of meta-data about the plugin.
getFeatures()
Returns a list of features for the DAV: HTTP header.
getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request)
Returns free-busy information for a specific address.
setStatus($status)
Sets the HTTP status code.
getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet)
This method is triggered whenever a subsystem requests the privileges that are supported on a particu...
handle($properties, callable $callback)
Call this function if you wish to handle updating certain properties.
httpPost(RequestInterface $request, ResponseInterface $response)
This method handles POST request for the outbox.
getHeader($name)
Returns a specific HTTP header, based on it's name.
initialize(Server $server)
Initializes the plugin.
The INode interface is the base interface, and the parent class of both ICollection and IFile...
getHTTPMethods($uri)
Use this method to tell the server this plugin defines additional HTTP methods.
propPatch($path, PropPatch $propPatch)
This method is called during property updates.
CalendarObject interface.
getPath()
Returns the relative path.
static read($data, $options=0, $charset='UTF-8')
Parses a vCard or iCalendar object, and returns the top component.
static getUUID()
Returns a pseudo-random v4 UUID.
scheduleReply(RequestInterface $request)
This method checks the 'Schedule-Reply' header and returns false if it's 'F', otherwise true...
Exception thrown by Reader if an invalid object was attempted to be parsed.
if($path[strlen($path) - 1]==='/') if(is_dir($path)) if(!file_exists($path)) if(preg_match('#\.php$#D', mb_strtolower($path, 'UTF-8'))) $contentType
while(false !==($line=fgets($in))) if(! $columns) $ignore
const NS_CALDAV
This is the official CalDAV namespace.
Implement this interface to have a node be recognized as a CalDAV scheduling outbox.
setHeader($name, $value)
Updates a HTTP header.
getAddressesForPrincipal($principal)
Returns a list of addresses that are associated with a principal.
getBody()
Returns the message body, as it's internal representation.