server->tree->getNodeForPath($parent); if ($node instanceof Sabre_DAV_IExtendedCollection) { try { $node->getChild($name); } catch (Sabre_DAV_Exception_FileNotFound $e) { return array('MKCALENDAR'); } } return array(); } /** * Returns a list of features for the DAV: HTTP header. * * @return array */ public function getFeatures() { return array('calendar-access', 'calendar-proxy'); } /** * Returns a plugin name. * * Using this name other plugins will be able to access other plugins * using Sabre_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); if ($node instanceof Sabre_CalDAV_ICalendar || $node instanceof Sabre_CalDAV_ICalendarObject) { return array( '{' . self::NS_CALDAV . '}calendar-multiget', '{' . self::NS_CALDAV . '}calendar-query', ); } return array(); } /** * Initializes the plugin * * @param Sabre_DAV_Server $server * @return void */ public function initialize(Sabre_DAV_Server $server) { $this->server = $server; $server->subscribeEvent('unknownMethod',array($this,'unknownMethod')); //$server->subscribeEvent('unknownMethod',array($this,'unknownMethod2'),1000); $server->subscribeEvent('report',array($this,'report')); $server->subscribeEvent('beforeGetProperties',array($this,'beforeGetProperties')); $server->xmlNamespaces[self::NS_CALDAV] = 'cal'; $server->xmlNamespaces[self::NS_CALENDARSERVER] = 'cs'; $server->propertyMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre_CalDAV_Property_SupportedCalendarComponentSet'; $server->resourceTypeMapping['Sabre_CalDAV_ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar'; $server->resourceTypeMapping['Sabre_CalDAV_Principal_ProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read'; $server->resourceTypeMapping['Sabre_CalDAV_Principal_ProxyWrite'] = '{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', // scheduling extension '{' . self::NS_CALDAV . '}calendar-user-address-set', // CalendarServer extensions '{' . self::NS_CALENDARSERVER . '}getctag', '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for', '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for' ); } /** * This function handles support for the MKCALENDAR method * * @param string $method * @return bool */ public function unknownMethod($method, $uri) { if ($method!=='MKCALENDAR') return; $this->httpMkCalendar($uri); // false is returned to stop the unknownMethod event return false; } /** * This functions handles REPORT requests specific to CalDAV * * @param string $reportName * @param DOMNode $dom * @return bool */ public function report($reportName,$dom) { switch($reportName) { case '{'.self::NS_CALDAV.'}calendar-multiget' : $this->calendarMultiGetReport($dom); return false; case '{'.self::NS_CALDAV.'}calendar-query' : $this->calendarQueryReport($dom); return false; } } /** * This function handles the MKCALENDAR HTTP method, which creates * a new calendar. * * @param string $uri * @return void */ public function httpMkCalendar($uri) { // Due to unforgivable bugs in iCal, we're completely disabling MKCALENDAR support // for clients matching iCal in the user agent //$ua = $this->server->httpRequest->getHeader('User-Agent'); //if (strpos($ua,'iCal/')!==false) { // throw new Sabre_DAV_Exception_Forbidden('iCal has major bugs in it\'s RFC3744 support. Therefore we are left with no other choice but disabling this feature.'); //} $body = $this->server->httpRequest->getBody(true); $properties = array(); if ($body) { $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body); foreach($dom->firstChild->childNodes as $child) { if (Sabre_DAV_XMLUtil::toClarkNotation($child)!=='{DAV:}set') continue; foreach(Sabre_DAV_XMLUtil::parseProperties($child,$this->server->propertyMap) as $k=>$prop) { $properties[$k] = $prop; } } } $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar'); $this->server->createCollection($uri,$resourceType,$properties); $this->server->httpResponse->sendStatus(201); $this->server->httpResponse->setHeader('Content-Length',0); } /** * beforeGetProperties * * 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 string $path * @param Sabre_DAV_INode $node * @param array $requestedProperties * @param array $returnedProperties * @return void */ public function beforeGetProperties($path, Sabre_DAV_INode $node, &$requestedProperties, &$returnedProperties) { if ($node instanceof Sabre_DAVACL_IPrincipal) { // calendar-home-set property $calHome = '{' . self::NS_CALDAV . '}calendar-home-set'; if (in_array($calHome,$requestedProperties)) { $principalId = $node->getName(); $calendarHomePath = self::CALENDAR_ROOT . '/' . $principalId . '/'; unset($requestedProperties[$calHome]); $returnedProperties[200][$calHome] = new Sabre_DAV_Property_Href($calendarHomePath); } // calendar-user-address-set property $calProp = '{' . self::NS_CALDAV . '}calendar-user-address-set'; if (in_array($calProp,$requestedProperties)) { $addresses = $node->getAlternateUriSet(); $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl(); unset($requestedProperties[$calProp]); $returnedProperties[200][$calProp] = new Sabre_DAV_Property_HrefList($addresses, false); } // 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 (in_array($propRead,$requestedProperties) || in_array($propWrite,$requestedProperties)) { $membership = $node->getGroupMembership(); $readList = array(); $writeList = array(); foreach($membership as $group) { $groupNode = $this->server->tree->getNodeForPath($group); // 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 Sabre_CalDAV_Principal_ProxyRead) { list($readList[]) = Sabre_DAV_URLUtil::splitPath($group); } if ($groupNode instanceof Sabre_CalDAV_Principal_ProxyWrite) { list($writeList[]) = Sabre_DAV_URLUtil::splitPath($group); } } if (in_array($propRead,$requestedProperties)) { unset($requestedProperties[$propRead]); $returnedProperties[200][$propRead] = new Sabre_DAV_Property_HrefList($readList); } if (in_array($propWrite,$requestedProperties)) { unset($requestedProperties[$propWrite]); $returnedProperties[200][$propWrite] = new Sabre_DAV_Property_HrefList($writeList); } } } // instanceof IPrincipal if ($node instanceof Sabre_CalDAV_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. $calDataProp = '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}calendar-data'; if (in_array($calDataProp, $requestedProperties)) { unset($requestedProperties[$calDataProp]); $val = $node->get(); if (is_resource($val)) $val = stream_get_contents($val); // Taking out \r to not screw up the xml output $returnedProperties[200][$calDataProp] = 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 DOMNode $dom * @return void */ public function calendarMultiGetReport($dom) { $properties = array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild)); $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href'); foreach($hrefElems as $elem) { $uri = $this->server->calculateUri($elem->nodeValue); list($objProps) = $this->server->getPropertiesForPath($uri,$properties); $propertyList[]=$objProps; } $this->server->httpResponse->sendStatus(207); $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList)); } /** * This function handles the calendar-query REPORT * * This report is used by clients to request calendar objects based on * complex conditions. * * @param DOMNode $dom * @return void */ public function calendarQueryReport($dom) { $requestedProperties = array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild)); $filterNode = $dom->getElementsByTagNameNS('urn:ietf:params:xml:ns:caldav','filter'); if ($filterNode->length!==1) { throw new Sabre_DAV_Exception_BadRequest('The calendar-query report must have a filter element'); } $filters = Sabre_CalDAV_XMLUtil::parseCalendarQueryFilters($filterNode->item(0)); $requestedCalendarData = true; 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; } // These are the list of nodes that potentially match the requirement $candidateNodes = $this->server->getPropertiesForPath($this->server->getRequestUri(),$requestedProperties,$this->server->getHTTPDepth(0)); $verifiedNodes = array(); foreach($candidateNodes as $node) { // If the node didn't have a calendar-data property, it must not be a calendar object if (!isset($node[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) continue; if ($this->validateFilters($node[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'],$filters)) { if (!$requestedCalendarData) { unset($node[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); } $verifiedNodes[] = $node; } } $this->server->httpResponse->sendStatus(207); $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); $this->server->httpResponse->sendBody($this->server->generateMultiStatus($verifiedNodes)); } /** * Verify if a list of filters applies to the calendar data object * * The calendarData object must be a valid iCalendar blob. The list of * filters must be formatted as parsed by Sabre_CalDAV_Plugin::parseCalendarQueryFilters * * @param string $calendarData * @param array $filters * @return bool */ public function validateFilters($calendarData,$filters) { // We are converting the calendar object to an XML structure // This makes it far easier to parse $xCalendarData = Sabre_CalDAV_ICalendarUtil::toXCal($calendarData); $xml = simplexml_load_string($xCalendarData); $xml->registerXPathNamespace('c','urn:ietf:params:xml:ns:xcal'); foreach($filters as $xpath=>$filter) { // if-not-defined comes first if (isset($filter['is-not-defined'])) { if (!$xml->xpath($xpath)) continue; else return false; } $elem = $xml->xpath($xpath); if (!$elem) return false; $elem = $elem[0]; if (isset($filter['time-range'])) { switch($elem->getName()) { case 'vevent' : $result = $this->validateTimeRangeFilterForEvent($xml,$xpath,$filter); if ($result===false) return false; break; case 'vtodo' : $result = $this->validateTimeRangeFilterForTodo($xml,$xpath,$filter); if ($result===false) return false; break; case 'vjournal' : case 'vfreebusy' : case 'valarm' : // TODO: not implemented break; /* case 'vjournal' : $result = $this->validateTimeRangeFilterForJournal($xml,$xpath,$filter); if ($result===false) return false; break; case 'vfreebusy' : $result = $this->validateTimeRangeFilterForFreeBusy($xml,$xpath,$filter); if ($result===false) return false; break; case 'valarm' : $result = $this->validateTimeRangeFilterForAlarm($xml,$xpath,$filter); if ($result===false) return false; break; */ } } if (isset($filter['text-match'])) { $currentString = (string)$elem; $isMatching = Sabre_DAV_StringUtil::textMatch($currentString, $filter['text-match']['value'], $filter['text-match']['collation']); if ($filter['text-match']['negate-condition'] && $isMatching) return false; if (!$filter['text-match']['negate-condition'] && !$isMatching) return false; } } return true; } /** * Tries to find X-LIC-LOCATION in the VTIMEZONE object to be used instead as a TZID that DateTimeZone can interpret * * @param SimpleXMLElement $xml Event as xml object * @param string $tzid Timezone ID that needs to be transformed to an interpretable ID */ private function findTimeZoneID(SimpleXMLElement $xml,$tzid) { // FIXME: This is a quick hack. Maybe this can be formulated in a more general way $xvtimezone = $xml->xpath('/c:iCalendar/c:vcalendar/c:vtimezone'); if (!count($xvtimezone)) { throw new Sabre_DAV_Exception_BadRequest('No VTIMEZONE object found in calendar object'); } foreach($xvtimezone as $vtimezone) { if (((string)$vtimezone->{'tzid'}) == $tzid) { if (isset($vtimezone->{'x-lic-location'})) { $tzid = (string)$vtimezone->{'x-lic-location'}; } } } return $tzid; } /** * Checks whether a time-range filter matches an event. * * @param SimpleXMLElement $xml Event as xml object * @param string $currentXPath XPath to check * @param array $currentFilter Filter information * @return void */ private function validateTimeRangeFilterForEvent(SimpleXMLElement $xml,$currentXPath,array $currentFilter) { // Grabbing the DTSTART property $xdtstart = $xml->xpath($currentXPath.'/c:dtstart'); if (!count($xdtstart)) { throw new Sabre_DAV_Exception_BadRequest('DTSTART property missing from calendar object'); } // The dtstart can be both a date, or datetime property if ((string)$xdtstart[0]['value']==='DATE' || strlen((string)$xdtstart[0])===8) { $isDateTime = false; } else { $isDateTime = true; } // Determining the timezone if ($tzid = (string)$xdtstart[0]['tzid']) { try { $tz = new DateTimeZone($tzid); } catch (Exception $e) { $tzid = $this->findTimeZoneID($xml, $tzid); $tz = new DateTimeZone($tzid); } } else { $tz = null; } if ($isDateTime) { $dtstart = Sabre_CalDAV_XMLUtil::parseICalendarDateTime((string)$xdtstart[0],$tz); } else { $dtstart = Sabre_CalDAV_XMLUtil::parseICalendarDate((string)$xdtstart[0]); } // Grabbing the DTEND property $xdtend = $xml->xpath($currentXPath.'/c:dtend'); $dtend = null; if (count($xdtend)) { // Determining the timezone if ($tzid = (string)$xdtend[0]['tzid']) { try { $tz = new DateTimeZone($tzid); } catch (Exception $e) { $tzid = $this->findTimeZoneID($xml, $tzid); $tz = new DateTimeZone($tzid); } } else { $tz = null; } // Since the VALUE prameter of both DTSTART and DTEND must be the same // we can assume we don't need to check the VALUE paramter of DTEND. if ($isDateTime) { $dtend = Sabre_CalDAV_XMLUtil::parseICalendarDateTime((string)$xdtend[0],$tz); } else { $dtend = Sabre_CalDAV_XMLUtil::parseICalendarDate((string)$xdtend[0],$tz); } } if (is_null($dtend)) { // The DTEND property was not found. We will first see if the event has a duration // property $xduration = $xml->xpath($currentXPath.'/c:duration'); if (count($xduration)) { $duration = Sabre_CalDAV_XMLUtil::parseICalendarDuration((string)$xduration[0]); // Making sure that the duration is bigger than 0 seconds. $tempDT = clone $dtstart; $tempDT->modify($duration); if ($tempDT > $dtstart) { // use DTEND = DTSTART + DURATION $dtend = $tempDT; } else { // use DTEND = DTSTART $dtend = $dtstart; } } } if (is_null($dtend)) { if ($isDateTime) { // DTEND = DTSTART $dtend = $dtstart; } else { // DTEND = DTSTART + 1 DAY $dtend = clone $dtstart; $dtend->modify('+1 day'); } } // TODO: we need to properly parse RRULE's, but it's very difficult. // For now, we're always returning events if they have an RRULE at all. $rrule = $xml->xpath($currentXPath.'/c:rrule'); $hasRrule = (count($rrule))>0; if (!is_null($currentFilter['time-range']['start']) && $currentFilter['time-range']['start'] >= $dtend && !$hasRrule) return false; if (!is_null($currentFilter['time-range']['end']) && $currentFilter['time-range']['end'] <= $dtstart && !$hasRrule) return false; return true; } private function validateTimeRangeFilterForTodo(SimpleXMLElement $xml,$currentXPath,array $filter) { // Gathering all relevant elements $dtStart = null; $duration = null; $due = null; $completed = null; $created = null; $xdt = $xml->xpath($currentXPath.'/c:dtstart'); if (count($xdt)) { // The dtstart can be both a date, or datetime property if ((string)$xdt[0]['value']==='DATE') { $isDateTime = false; } else { $isDateTime = true; } // Determining the timezone if ($tzid = (string)$xdt[0]['tzid']) { $tz = new DateTimeZone($tzid); } else { $tz = null; } if ($isDateTime) { $dtStart = Sabre_CalDAV_XMLUtil::parseICalendarDateTime((string)$xdt[0],$tz); } else { $dtStart = Sabre_CalDAV_XMLUtil::parseICalendarDate((string)$xdt[0]); } } // Only need to grab duration if dtStart is set if (!is_null($dtStart)) { $xduration = $xml->xpath($currentXPath.'/c:duration'); if (count($xduration)) { $duration = Sabre_CalDAV_XMLUtil::parseICalendarDuration((string)$xduration[0]); } } if (!is_null($dtStart) && !is_null($duration)) { // Comparision from RFC 4791: // (start <= DTSTART+DURATION) AND ((end > DTSTART) OR (end >= DTSTART+DURATION)) $end = clone $dtStart; $end->modify($duration); if( (is_null($filter['time-range']['start']) || $filter['time-range']['start'] <= $end) && (is_null($filter['time-range']['end']) || $filter['time-range']['end'] > $dtStart || $filter['time-range']['end'] >= $end) ) { return true; } else { return false; } } // Need to grab the DUE property $xdt = $xml->xpath($currentXPath.'/c:due'); if (count($xdt)) { // The due property can be both a date, or datetime property if ((string)$xdt[0]['value']==='DATE') { $isDateTime = false; } else { $isDateTime = true; } // Determining the timezone if ($tzid = (string)$xdt[0]['tzid']) { $tz = new DateTimeZone($tzid); } else { $tz = null; } if ($isDateTime) { $due = Sabre_CalDAV_XMLUtil::parseICalendarDateTime((string)$xdt[0],$tz); } else { $due = Sabre_CalDAV_XMLUtil::parseICalendarDate((string)$xdt[0]); } } if (!is_null($dtStart) && !is_null($due)) { // Comparision from RFC 4791: // ((start < DUE) OR (start <= DTSTART)) AND ((end > DTSTART) OR (end >= DUE)) if( (is_null($filter['time-range']['start']) || $filter['time-range']['start'] < $due || $filter['time-range']['start'] < $dtstart) && (is_null($filter['time-range']['end']) || $filter['time-range']['end'] >= $due) ) { return true; } else { return false; } } if (!is_null($dtStart)) { // Comparision from RFC 4791 // (start <= DTSTART) AND (end > DTSTART) if ( (is_null($filter['time-range']['start']) || $filter['time-range']['start'] <= $dtStart) && (is_null($filter['time-range']['end']) || $filter['time-range']['end'] > $dtStart) ) { return true; } else { return false; } } if (!is_null($due)) { // Comparison from RFC 4791 // (start < DUE) AND (end >= DUE) if ( (is_null($filter['time-range']['start']) || $filter['time-range']['start'] < $due) && (is_null($filter['time-range']['end']) || $filter['time-range']['end'] >= $due) ) { return true; } else { return false; } } // Need to grab the COMPLETED property $xdt = $xml->xpath($currentXPath.'/c:completed'); if (count($xdt)) { $completed = Sabre_CalDAV_XMLUtil::parseICalendarDateTime((string)$xdt[0]); } // Need to grab the CREATED property $xdt = $xml->xpath($currentXPath.'/c:created'); if (count($xdt)) { $created = Sabre_CalDAV_XMLUtil::parseICalendarDateTime((string)$xdt[0]); } if (!is_null($completed) && !is_null($created)) { // Comparison from RFC 4791 // ((start <= CREATED) OR (start <= COMPLETED)) AND ((end >= CREATED) OR (end >= COMPLETED)) if( (is_null($filter['time-range']['start']) || $filter['time-range']['start'] <= $created || $filter['time-range']['start'] <= $completed) && (is_null($filter['time-range']['end']) || $filter['time-range']['end'] >= $created || $filter['time-range']['end'] >= $completed)) { return true; } else { return false; } } if (!is_null($completed)) { // Comparison from RFC 4791 // (start <= COMPLETED) AND (end >= COMPLETED) if( (is_null($filter['time-range']['start']) || $filter['time-range']['start'] <= $completed) && (is_null($filter['time-range']['end']) || $filter['time-range']['end'] >= $completed)) { return true; } else { return false; } } if (!is_null($created)) { // Comparison from RFC 4791 // (end > CREATED) if( (is_null($filter['time-range']['end']) || $filter['time-range']['end'] > $created) ) { return true; } else { return false; } } // Everything else is TRUE return true; } }