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 */ public 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 (2 !== count($parts)) { return; } if ('principals' !== $parts[0]) { return; } return self::CALENDAR_ROOT.'/'.$parts[1]; } /** * Returns a list of features for the DAV: HTTP header. * * @return array */ public 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 */ public 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 */ public 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 */ public 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->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); $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 * @param mixed $path * * @return bool */ public function report($reportName, $report, $path) { 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 */ public 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(), 0, $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)); $response->setStatus(201); $response->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 */ public 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 LocalHref($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 LocalHref($addresses); }); // 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 ('mailto:' === substr($address, 0, 7)) { $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 (404 === $propFind->getStatus($propRead) || 404 === $propFind->getStatus($propWrite)) { $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 LocalHref($readList)); $propFind->set($propWrite, new LocalHref($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 */ public function calendarMultiGetReport($report) { $needsJson = 'application/calendar+json' === $report->contentType; $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, 'minimal' === $prefer['return'])); } /** * 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 */ public function calendarQueryReport($report) { $path = $this->server->getRequestUri(); $needsJson = 'application/calendar+json' === $report->contentType; $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 (0 == $depth && $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 && 0 === $depth) { if (0 === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'MSFT-')) { // 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 && 1 == $depth) { $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, 'minimal' === $prefer['return'])); } /** * This method is responsible for parsing the request and generating the * response for the CALDAV:free-busy-query REPORT. * * @param Xml\Request\FreeBusyQueryReport $report */ 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 */ public 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 */ public 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 */ 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 = $data; 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 ('VCALENDAR' !== $vobj->name) { 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; foreach ($vobj->getComponents() as $component) { switch ($component->name) { case 'VTIMEZONE': continue 2; case 'VEVENT': case 'VTODO': case 'VJOURNAL': $foundType = $component->name; break; } } if (!$foundType || !in_array($foundType, $supportedComponents)) { throw new Exception\InvalidComponentType('iCalendar objects must at least have a component of type '.implode(', ', $supportedComponents)); } $options = VObject\Node::PROFILE_CALDAV; $prefer = $this->server->getHTTPPrefer(); if ('strict' !== $prefer['handling']) { $options |= VObject\Node::REPAIR; } $messages = $vobj->validate($options); $highestLevel = 0; $warningMessage = null; // $messages contains a list of problems with the vcard, along with // their severity. foreach ($messages as $message) { if ($message['level'] > $highestLevel) { // Recording the highest reported error level. $highestLevel = $message['level']; $warningMessage = $message['message']; } switch ($message['level']) { case 1: // Level 1 means that there was a problem, but it was repaired. $modified = true; break; case 2: // Level 2 means a warning, but not critical break; case 3: // Level 3 means a critical error throw new DAV\Exception\UnsupportedMediaType('Validation error in iCalendar: '.$message['message']); } } if ($warningMessage) { $response->setHeader( 'X-Sabre-Ew-Gross', 'iCalendar validation warning: '.$warningMessage ); } // We use an extra variable to allow event handles to tell us whether // 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 ($modified || $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 && 0 !== strcmp($data, $before)) { $modified = true; } } // Destroy circular references so PHP will garbage collect the object. $vobj->destroy(); } /** * This method is triggered whenever a subsystem reqeuests the privileges * that are supported on a particular node. * * @param INode $node * @param array $supportedPrivilegeSet */ public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) { if ($node instanceof ICalendar) { $supportedPrivilegeSet['{DAV:}read']['aggregates']['{'.self::NS_CALDAV.'}read-free-busy'] = [ 'abstract' => false, 'aggregates' => [], ]; } } /** * 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 */ public function htmlActionsPanel(DAV\INode $node, &$output) { if (!$node instanceof CalendarHome) { return; } $output .= '

Create new calendar



'; 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 */ public function httpAfterGet(RequestInterface $request, ResponseInterface $response) { $contentType = $response->getHeader('Content-Type'); if (null === $contentType || false === strpos($contentType, 'text/calendar')) { return; } $result = HTTP\negotiateContentType( $request->getHeader('Accept'), ['text/calendar', 'application/calendar+json'] ); if ('application/calendar+json' !== $result) { // 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 */ public function getPluginInfo() { return [ 'name' => $this->getPluginName(), 'description' => 'Adds support for CalDAV (rfc4791)', 'link' => 'http://sabre.io/dav/caldav/', ]; } }