76 $node = $this->server->tree->getNodeForPath($parent);
80 $node->getChild(
$name);
82 return [
'MKCALENDAR'];
108 $parts = explode(
'/', trim($principalUrl,
'/'));
109 if (count($parts) !== 2)
return;
110 if ($parts[0] !==
'principals')
return;
112 return self::CALENDAR_ROOT .
'/' . $parts[1];
123 return [
'calendar-access',
'calendar-proxy'];
153 $node = $this->server->tree->getNodeForPath($uri);
157 $reports[] =
'{' . self::NS_CALDAV .
'}calendar-multiget';
158 $reports[] =
'{' . self::NS_CALDAV .
'}calendar-query';
161 $reports[] =
'{' . self::NS_CALDAV .
'}free-busy-query';
166 if ($node instanceof
CalendarHome && $this->server->getPlugin(
'sync')) {
167 $reports[] =
'{DAV:}sync-collection';
183 $server->on(
'method:MKCALENDAR', [$this,
'httpMkCalendar']);
184 $server->on(
'report', [$this,
'report']);
185 $server->on(
'propFind', [$this,
'propFind']);
186 $server->on(
'onHTMLActionsPanel', [$this,
'htmlActionsPanel']);
187 $server->on(
'beforeCreateFile', [$this,
'beforeCreateFile']);
188 $server->on(
'beforeWriteContent', [$this,
'beforeWriteContent']);
189 $server->on(
'afterMethod:GET', [$this,
'httpAfterGET']);
190 $server->on(
'getSupportedPrivilegeSet', [$this,
'getSupportedPrivilegeSet']);
195 $server->xml->elementMap[
'{' . self::NS_CALDAV .
'}supported-calendar-component-set'] =
'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet';
196 $server->xml->elementMap[
'{' . self::NS_CALDAV .
'}calendar-query'] =
'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport';
197 $server->xml->elementMap[
'{' . self::NS_CALDAV .
'}calendar-multiget'] =
'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport';
198 $server->xml->elementMap[
'{' . self::NS_CALDAV .
'}free-busy-query'] =
'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport';
199 $server->xml->elementMap[
'{' . self::NS_CALDAV .
'}mkcalendar'] =
'Sabre\\CalDAV\\Xml\\Request\\MkCalendar';
200 $server->xml->elementMap[
'{' . self::NS_CALDAV .
'}schedule-calendar-transp'] =
'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp';
201 $server->xml->elementMap[
'{' . self::NS_CALDAV .
'}supported-calendar-component-set'] =
'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet';
203 $server->resourceTypeMapping[
'\\Sabre\\CalDAV\\ICalendar'] =
'{urn:ietf:params:xml:ns:caldav}calendar';
205 $server->resourceTypeMapping[
'\\Sabre\\CalDAV\\Principal\\IProxyRead'] =
'{http://calendarserver.org/ns/}calendar-proxy-read';
206 $server->resourceTypeMapping[
'\\Sabre\\CalDAV\\Principal\\IProxyWrite'] =
'{http://calendarserver.org/ns/}calendar-proxy-write';
208 array_push(
$server->protectedProperties,
210 '{' . self::NS_CALDAV .
'}supported-calendar-component-set',
211 '{' . self::NS_CALDAV .
'}supported-calendar-data',
212 '{' . self::NS_CALDAV .
'}max-resource-size',
213 '{' . self::NS_CALDAV .
'}min-date-time',
214 '{' . self::NS_CALDAV .
'}max-date-time',
215 '{' . self::NS_CALDAV .
'}max-instances',
216 '{' . self::NS_CALDAV .
'}max-attendees-per-instance',
217 '{' . self::NS_CALDAV .
'}calendar-home-set',
218 '{' . self::NS_CALDAV .
'}supported-collation-set',
219 '{' . self::NS_CALDAV .
'}calendar-data',
222 '{' . self::NS_CALENDARSERVER .
'}getctag',
223 '{' . self::NS_CALENDARSERVER .
'}calendar-proxy-read-for',
224 '{' . self::NS_CALENDARSERVER .
'}calendar-proxy-write-for'
229 $aclPlugin->principalSearchPropertySet[
'{' . self::NS_CALDAV .
'}calendar-user-address-set'] =
'Calendar address';
243 switch ($reportName) {
244 case '{' . self::NS_CALDAV .
'}calendar-multiget' :
245 $this->server->transactionType =
'report-calendar-multiget';
248 case '{' . self::NS_CALDAV .
'}calendar-query' :
249 $this->server->transactionType =
'report-calendar-query';
252 case '{' . self::NS_CALDAV .
'}free-busy-query' :
253 $this->server->transactionType =
'report-free-busy-query';
272 $body =
$request->getBodyAsString();
280 $mkcalendar = $this->server->xml->expect(
281 '{urn:ietf:params:xml:ns:caldav}mkcalendar',
285 throw new BadRequest($e->getMessage(),
null, $e);
287 $properties = $mkcalendar->getProperties();
297 if (isset($properties[
'{DAV:}resourcetype'])) {
298 $resourceType = $properties[
'{DAV:}resourcetype']->getValue();
300 $resourceType = [
'{DAV:}collection',
'{urn:ietf:params:xml:ns:caldav}calendar'];
303 $this->server->createCollection(
$path,
new MkCol($resourceType, $properties));
306 $response->setHeader(
'Content-Length', 0);
325 $ns =
'{' . self::NS_CALDAV .
'}';
329 $propFind->handle($ns .
'max-resource-size', $this->maxResourceSize);
330 $propFind->handle($ns .
'supported-calendar-data',
function() {
333 $propFind->handle($ns .
'supported-collation-set',
function() {
341 $principalUrl = $node->getPrincipalUrl();
343 $propFind->handle(
'{' . self::NS_CALDAV .
'}calendar-home-set',
function() use ($principalUrl) {
346 if (is_null($calendarHomePath))
return null;
347 return new LocalHref($calendarHomePath .
'/');
352 $propFind->handle(
'{' . self::NS_CALDAV .
'}calendar-user-address-set',
function() use ($node) {
353 $addresses = $node->getAlternateUriSet();
354 $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl() .
'/';
359 $propFind->handle(
'{' . self::NS_CALENDARSERVER .
'}email-address-set',
function() use ($node) {
360 $addresses = $node->getAlternateUriSet();
362 foreach ($addresses as $address) {
363 if (substr($address, 0, 7) ===
'mailto:') {
364 $emails[] = substr($address, 7);
372 $propRead =
'{' . self::NS_CALENDARSERVER .
'}calendar-proxy-read-for';
373 $propWrite =
'{' . self::NS_CALENDARSERVER .
'}calendar-proxy-write-for';
375 if ($propFind->getStatus($propRead) === 404 || $propFind->getStatus($propWrite) === 404) {
378 $membership =
$aclPlugin->getPrincipalMembership($propFind->getPath());
382 foreach ($membership as $group) {
384 $groupNode = $this->server->tree->getNodeForPath($group);
391 if ($groupNode instanceof
Principal\IProxyRead) {
392 $readList[] = $listItem;
394 if ($groupNode instanceof
Principal\IProxyWrite) {
395 $writeList[] = $listItem;
400 $propFind->set($propRead,
new LocalHref($readList));
401 $propFind->set($propWrite,
new LocalHref($writeList));
412 $propFind->handle(
'{' . self::NS_CALDAV .
'}calendar-data',
function() use ($node) {
414 if (is_resource($val))
415 $val = stream_get_contents($val);
418 return str_replace(
"\r",
"", $val);
437 $needsJson = $report->contentType ===
'application/calendar+json';
443 [$this->server,
'calculateUri'],
447 foreach ($this->server->getPropertiesForMultiplePaths(
$paths, $report->properties) as $uri => $objProps) {
449 if (($needsJson || $report->expand) && isset($objProps[200][
'{' . self::NS_CALDAV .
'}calendar-data'])) {
452 if ($report->expand) {
456 if (!isset($timeZones[$calendarPath])) {
458 $tzProp =
'{' . self::NS_CALDAV .
'}calendar-timezone';
459 $tzResult = $this->server->getProperties($calendarPath, [$tzProp]);
460 if (isset($tzResult[$tzProp])) {
464 $timeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
472 $vObject = $vObject->expand($report->expand[
'start'], $report->expand[
'end'], $timeZones[$calendarPath]);
475 $objProps[200][
'{' . self::NS_CALDAV .
'}calendar-data'] = json_encode($vObject->jsonSerialize());
477 $objProps[200][
'{' . self::NS_CALDAV .
'}calendar-data'] = $vObject->serialize();
484 $propertyList[] = $objProps;
488 $prefer = $this->server->getHTTPPrefer();
490 $this->server->httpResponse->setStatus(207);
491 $this->server->httpResponse->setHeader(
'Content-Type',
'application/xml; charset=utf-8');
492 $this->server->httpResponse->setHeader(
'Vary',
'Brief,Prefer');
493 $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, $prefer[
'return'] ===
'minimal'));
508 $path = $this->server->getRequestUri();
510 $needsJson = $report->contentType ===
'application/calendar+json';
512 $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
513 $depth = $this->server->getHTTPDepth(0);
518 $calendarTimeZone =
null;
519 if ($report->expand) {
522 $tzProp =
'{' . self::NS_CALDAV .
'}calendar-timezone';
523 $tzResult = $this->server->getProperties(
$path, [$tzProp]);
524 if (isset($tzResult[$tzProp])) {
528 $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
532 $vtimezoneObj->destroy();
535 $calendarTimeZone =
new DateTimeZone(
'UTC');
543 $requestedCalendarData =
true;
544 $requestedProperties = $report->properties;
546 if (!in_array(
'{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
549 $requestedProperties[] =
'{urn:ietf:params:xml:ns:caldav}calendar-data';
553 $requestedCalendarData =
false;
556 $properties = $this->server->getPropertiesForPath(
558 $requestedProperties,
564 $properties = current($properties);
568 if (isset($properties[200][
'{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
572 $vObject =
VObject\Reader::read($properties[200][
'{urn:ietf:params:xml:ns:caldav}calendar-data']);
573 if ($validator->validate($vObject, $report->filters)) {
577 if (!$requestedCalendarData) {
578 unset($properties[200][
'{urn:ietf:params:xml:ns:caldav}calendar-data']);
582 if ($report->expand) {
583 $vObject = $vObject->expand($report->expand[
'start'], $report->expand[
'end'], $calendarTimeZone);
586 $properties[200][
'{' . self::NS_CALDAV .
'}calendar-data'] = json_encode($vObject->jsonSerialize());
587 } elseif ($report->expand) {
588 $properties[200][
'{' . self::NS_CALDAV .
'}calendar-data'] = $vObject->serialize();
605 if (strpos($this->server->httpRequest->getHeader(
'User-Agent'),
'MSFT-') === 0) {
615 throw new BadRequest(
'A calendar-query REPORT on a calendar with a Depth: 0 is undefined. Set Depth to 1');
624 $nodePaths = $node->calendarQuery($report->filters);
626 foreach ($nodePaths as
$path) {
629 $this->server->getPropertiesForPath($this->server->getRequestUri() .
'/' .
$path, $report->properties);
631 if (($needsJson || $report->expand)) {
634 if ($report->expand) {
635 $vObject = $vObject->expand($report->expand[
'start'], $report->expand[
'end'], $calendarTimeZone);
639 $properties[200][
'{' . self::NS_CALDAV .
'}calendar-data'] = json_encode($vObject->jsonSerialize());
641 $properties[200][
'{' . self::NS_CALDAV .
'}calendar-data'] = $vObject->serialize();
654 $prefer = $this->server->getHTTPPrefer();
656 $this->server->httpResponse->setStatus(207);
657 $this->server->httpResponse->setHeader(
'Content-Type',
'application/xml; charset=utf-8');
658 $this->server->httpResponse->setHeader(
'Vary',
'Brief,Prefer');
659 $this->server->httpResponse->setBody($this->server->generateMultiStatus(
$result, $prefer[
'return'] ===
'minimal'));
672 $uri = $this->server->getRequestUri();
674 $acl = $this->server->getPlugin(
'acl');
676 $acl->checkPrivileges($uri,
'{' . self::NS_CALDAV .
'}read-free-busy');
679 $calendar = $this->server->tree->getNodeForPath($uri);
684 $tzProp =
'{' . self::NS_CALDAV .
'}calendar-timezone';
688 $calendarProps = $this->server->getProperties($uri, [$tzProp]);
690 if (isset($calendarProps[$tzProp])) {
692 $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
694 $vtimezoneObj->destroy();
696 $calendarTimeZone =
new DateTimeZone(
'UTC');
702 'name' =>
'VCALENDAR',
706 'comp-filters' => [],
707 'prop-filters' => [],
708 'is-not-defined' =>
false,
710 'start' => $report->start,
711 'end' => $report->end,
715 'prop-filters' => [],
716 'is-not-defined' =>
false,
717 'time-range' =>
null,
726 $generator->setObjects($objects);
727 $generator->setTimeRange($report->start, $report->end);
728 $generator->setTimeZone($calendarTimeZone);
729 $result = $generator->getResult();
732 $this->server->httpResponse->setStatus(200);
733 $this->server->httpResponse->setHeader(
'Content-Type',
'text/calendar');
734 $this->server->httpResponse->setHeader(
'Content-Length', strlen(
$result));
735 $this->server->httpResponse->setBody(
$result);
761 $parentNode = $this->server->tree->getNodeForPath($parent);
770 $this->server->httpRequest,
771 $this->server->httpResponse,
799 $this->server->httpRequest,
800 $this->server->httpResponse,
823 if (is_resource(
$data)) {
833 if (substr(
$data, 0, 1) ===
'[') {
850 if (
$vobj->name !==
'VCALENDAR') {
854 $sCCS =
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
858 $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]);
860 if (isset($calendarProperties[$sCCS])) {
861 $supportedComponents = $calendarProperties[$sCCS]->getValue();
863 $supportedComponents = [
'VJOURNAL',
'VTODO',
'VEVENT'];
868 foreach (
$vobj->getComponents() as $component) {
869 switch ($component->name) {
875 $foundType = $component->name;
881 if (!$foundType || !in_array($foundType, $supportedComponents)) {
886 $prefer = $this->server->getHTTPPrefer();
888 if ($prefer[
'handling'] !==
'strict') {
895 $warningMessage =
null;
901 if (
$message[
'level'] > $highestLevel) {
904 $warningMessage =
$message[
'message'];
922 if ($warningMessage) {
925 'iCalendar validation warning: ' . $warningMessage
933 $subModified =
false;
936 'calendarObjectChange',
947 if ($modified || $subModified) {
952 if (!$modified && strcmp(
$data, $before) !== 0) {
973 $supportedPrivilegeSet[
'{DAV:}read'][
'aggregates'][
'{' . self::NS_CALDAV .
'}read-free-busy'] = [
994 $output .=
'<tr><td colspan="2"><form method="post" action="">
995 <h3>Create new calendar</h3>
996 <input type="hidden" name="sabreAction" value="mkcol" />
997 <input type="hidden" name="resourceType" value="{DAV:}collection,{' . self::NS_CALDAV .
'}calendar" />
998 <label>Name (uri):</label> <input type="text" name="name" /><br />
999 <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
1000 <input type="submit" value="create" />
1019 if (strpos(
$response->getHeader(
'Content-Type'),
'text/calendar') ===
false) {
1025 [
'text/calendar',
'application/calendar+json']
1028 if (
$result !==
'application/calendar+json') {
1036 $jsonBody = json_encode(
$vobj->jsonSerialize());
1042 $response->setHeader(
'Content-Type',
'application/calendar+json');
1043 $response->setHeader(
'Content-Length', strlen($jsonBody));
1062 'description' =>
'Adds support for CalDAV (rfc4791)',
1063 'link' =>
'http://sabre.io/dav/caldav/',
foreach($paths as $path) $request
An exception for terminatinating execution or to throw for unit testing.
The CalendarHome represents a node that is usually in a users' calendar-homeset.
report($reportName, $report, $path)
This functions handles REPORT requests specific to CalDAV.
freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report)
This method is responsible for parsing the request and generating the response for the CALDAV:free-bu...
calendarMultiGetReport($report)
This function handles the calendar-multiget REPORT.
htmlActionsPanel(DAV\INode $node, &$output)
This method is used to generate HTML output for the DAV\Browser\Plugin.
getCalendarHomeForPrincipal($principalUrl)
Returns the path to a principal's calendar home.
getHTTPMethods($uri)
Use this method to tell the server this plugin defines additional HTTP methods.
$maxResourceSize
The default PDO storage uses a MySQL MEDIUMBLOB for iCalendar data, which can hold up to 2^24 = 16777...
initialize(DAV\Server $server)
Initializes the plugin.
beforeWriteContent($path, DAV\IFile $node, &$data, &$modified)
This method is triggered before a file gets updated with new content.
propFind(DAV\PropFind $propFind, DAV\INode $node)
PropFind.
const NS_CALDAV
This is the official CalDAV namespace.
getPluginInfo()
Returns a bunch of meta-data about the plugin.
httpAfterGet(RequestInterface $request, ResponseInterface $response)
This event is triggered after GET requests.
getSupportedReportSet($uri)
Returns a list of reports this plugin supports.
getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet)
This method is triggered whenever a subsystem reqeuests the privileges that are supported on a partic...
httpMkCalendar(RequestInterface $request, ResponseInterface $response)
This function handles the MKCALENDAR HTTP method, which creates a new calendar.
const NS_CALENDARSERVER
This is the namespace for the proprietary calendarserver extensions.
getPluginName()
Returns a plugin name.
getFeatures()
Returns a list of features for the DAV: HTTP header.
beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified)
This method is triggered before a new file is created.
calendarQueryReport($report)
This function handles the calendar-query REPORT.
const CALENDAR_ROOT
The hardcoded root for calendar objects.
validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew)
Checks if the submitted iCalendar data is in fact, valid.
email-address-set property
Supported-calendar-data property.
supported-collation-set property
This class represents a MKCOL operation.
This class holds all the information about a PROPFIND request.
The baseclass for all server plugins.
The Request class represents a single HTTP request.
static negotiate($acceptHeaderValue, array $availableOptions)
Deprecated! Use negotiateContentType.
This class helps with generating FREEBUSY reports based on existing sets of objects.
const PROFILE_CALDAV
If this option is set, the validator will operate on iCalendar objects on the assumption that the vca...
const REPAIR
The following constants are used by the validate() method.
Exception thrown by Reader if an invalid object was attempted to be parsed.
static readJson($data, $options=0)
Parses a jCard or jCal object, and returns the top component.
static read($data, $options=0, $charset='UTF-8')
Parses a vCard or iCalendar object, and returns the top component.
This interface represents a node that may contain calendar objects.
CalendarObject interface.
The ICollection Interface.
The IExtendedCollection interface.
This interface represents a file in the directory tree.
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
split($path)
Returns the 'dirname' and 'basename' for a path.