6 * This plugin provides functionality added by CalDAV (RFC 4791)
7 * It implements new reports, and the MKCALENDAR method.
11 * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved.
12 * @author Evert Pot (http://www.rooftopsolutions.nl/)
13 * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
15 class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin {
18 * This is the official CalDAV namespace
20 const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
23 * This is the namespace for the proprietary calendarserver extensions
25 const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
28 * The hardcoded root for calendar objects. It is unfortunate
29 * that we're stuck with it, but it will have to do for now
31 const CALENDAR_ROOT = 'calendars';
34 * Reference to server object
36 * @var Sabre_DAV_Server
41 * The email handler for invites and other scheduling messages.
43 * @var Sabre_CalDAV_Schedule_IMip
45 protected $imipHandler;
48 * Sets the iMIP handler.
50 * iMIP = The email transport of iCalendar scheduling messages. Setting
51 * this is optional, but if you want the server to allow invites to be sent
52 * out, you must set a handler.
54 * Specifically iCal will plain assume that the server supports this. If
55 * the server doesn't, iCal will display errors when inviting people to
58 * @param Sabre_CalDAV_Schedule_IMip $imipHandler
61 public function setIMipHandler(Sabre_CalDAV_Schedule_IMip $imipHandler) {
63 $this->imipHandler = $imipHandler;
68 * Use this method to tell the server this plugin defines additional
71 * This method is passed a uri. It should only return HTTP methods that are
72 * available for the specified uri.
77 public function getHTTPMethods($uri) {
79 // The MKCALENDAR is only available on unmapped uri's, whose
80 // parents extend IExtendedCollection
81 list($parent, $name) = Sabre_DAV_URLUtil::splitPath($uri);
83 $node = $this->server->tree->getNodeForPath($parent);
85 if ($node instanceof Sabre_DAV_IExtendedCollection) {
87 $node->getChild($name);
88 } catch (Sabre_DAV_Exception_NotFound $e) {
89 return array('MKCALENDAR');
97 * Returns a list of features for the DAV: HTTP header.
101 public function getFeatures() {
103 return array('calendar-access', 'calendar-proxy');
108 * Returns a plugin name.
110 * Using this name other plugins will be able to access other plugins
111 * using Sabre_DAV_Server::getPlugin
115 public function getPluginName() {
122 * Returns a list of reports this plugin supports.
124 * This will be used in the {DAV:}supported-report-set property.
125 * Note that you still need to subscribe to the 'report' event to actually
131 public function getSupportedReportSet($uri) {
133 $node = $this->server->tree->getNodeForPath($uri);
136 if ($node instanceof Sabre_CalDAV_ICalendar || $node instanceof Sabre_CalDAV_ICalendarObject) {
137 $reports[] = '{' . self::NS_CALDAV . '}calendar-multiget';
138 $reports[] = '{' . self::NS_CALDAV . '}calendar-query';
140 if ($node instanceof Sabre_CalDAV_ICalendar) {
141 $reports[] = '{' . self::NS_CALDAV . '}free-busy-query';
148 * Initializes the plugin
150 * @param Sabre_DAV_Server $server
153 public function initialize(Sabre_DAV_Server $server) {
155 $this->server = $server;
157 $server->subscribeEvent('unknownMethod',array($this,'unknownMethod'));
158 //$server->subscribeEvent('unknownMethod',array($this,'unknownMethod2'),1000);
159 $server->subscribeEvent('report',array($this,'report'));
160 $server->subscribeEvent('beforeGetProperties',array($this,'beforeGetProperties'));
161 $server->subscribeEvent('onHTMLActionsPanel', array($this,'htmlActionsPanel'));
162 $server->subscribeEvent('onBrowserPostAction', array($this,'browserPostAction'));
163 $server->subscribeEvent('beforeWriteContent', array($this, 'beforeWriteContent'));
164 $server->subscribeEvent('beforeCreateFile', array($this, 'beforeCreateFile'));
165 $server->subscribeEvent('beforeMethod', array($this,'beforeMethod'));
167 $server->xmlNamespaces[self::NS_CALDAV] = 'cal';
168 $server->xmlNamespaces[self::NS_CALENDARSERVER] = 'cs';
170 $server->propertyMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre_CalDAV_Property_SupportedCalendarComponentSet';
172 $server->resourceTypeMapping['Sabre_CalDAV_ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar';
173 $server->resourceTypeMapping['Sabre_CalDAV_Schedule_IOutbox'] = '{urn:ietf:params:xml:ns:caldav}schedule-outbox';
174 $server->resourceTypeMapping['Sabre_CalDAV_Principal_ProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read';
175 $server->resourceTypeMapping['Sabre_CalDAV_Principal_ProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write';
176 $server->resourceTypeMapping['Sabre_CalDAV_Notifications_ICollection'] = '{' . self::NS_CALENDARSERVER . '}notifications';
177 $server->resourceTypeMapping['Sabre_CalDAV_Notifications_INode'] = '{' . self::NS_CALENDARSERVER . '}notification';
179 array_push($server->protectedProperties,
181 '{' . self::NS_CALDAV . '}supported-calendar-component-set',
182 '{' . self::NS_CALDAV . '}supported-calendar-data',
183 '{' . self::NS_CALDAV . '}max-resource-size',
184 '{' . self::NS_CALDAV . '}min-date-time',
185 '{' . self::NS_CALDAV . '}max-date-time',
186 '{' . self::NS_CALDAV . '}max-instances',
187 '{' . self::NS_CALDAV . '}max-attendees-per-instance',
188 '{' . self::NS_CALDAV . '}calendar-home-set',
189 '{' . self::NS_CALDAV . '}supported-collation-set',
190 '{' . self::NS_CALDAV . '}calendar-data',
192 // scheduling extension
193 '{' . self::NS_CALDAV . '}schedule-inbox-URL',
194 '{' . self::NS_CALDAV . '}schedule-outbox-URL',
195 '{' . self::NS_CALDAV . '}calendar-user-address-set',
196 '{' . self::NS_CALDAV . '}calendar-user-type',
198 // CalendarServer extensions
199 '{' . self::NS_CALENDARSERVER . '}getctag',
200 '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for',
201 '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for',
202 '{' . self::NS_CALENDARSERVER . '}notification-URL',
203 '{' . self::NS_CALENDARSERVER . '}notificationtype'
209 * This function handles support for the MKCALENDAR method
211 * @param string $method
215 public function unknownMethod($method, $uri) {
219 $this->httpMkCalendar($uri);
220 // false is returned to stop the propagation of the
221 // unknownMethod event.
224 // Checking if we're talking to an outbox
226 $node = $this->server->tree->getNodeForPath($uri);
227 } catch (Sabre_DAV_Exception_NotFound $e) {
230 if (!$node instanceof Sabre_CalDAV_Schedule_IOutbox)
233 $this->outboxRequest($node);
241 * This functions handles REPORT requests specific to CalDAV
243 * @param string $reportName
244 * @param DOMNode $dom
247 public function report($reportName,$dom) {
249 switch($reportName) {
250 case '{'.self::NS_CALDAV.'}calendar-multiget' :
251 $this->calendarMultiGetReport($dom);
253 case '{'.self::NS_CALDAV.'}calendar-query' :
254 $this->calendarQueryReport($dom);
256 case '{'.self::NS_CALDAV.'}free-busy-query' :
257 $this->freeBusyQueryReport($dom);
266 * This function handles the MKCALENDAR HTTP method, which creates
272 public function httpMkCalendar($uri) {
274 // Due to unforgivable bugs in iCal, we're completely disabling MKCALENDAR support
275 // for clients matching iCal in the user agent
276 //$ua = $this->server->httpRequest->getHeader('User-Agent');
277 //if (strpos($ua,'iCal/')!==false) {
278 // 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.');
281 $body = $this->server->httpRequest->getBody(true);
282 $properties = array();
286 $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
288 foreach($dom->firstChild->childNodes as $child) {
290 if (Sabre_DAV_XMLUtil::toClarkNotation($child)!=='{DAV:}set') continue;
291 foreach(Sabre_DAV_XMLUtil::parseProperties($child,$this->server->propertyMap) as $k=>$prop) {
292 $properties[$k] = $prop;
298 $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
300 $this->server->createCollection($uri,$resourceType,$properties);
302 $this->server->httpResponse->sendStatus(201);
303 $this->server->httpResponse->setHeader('Content-Length',0);
307 * beforeGetProperties
309 * This method handler is invoked before any after properties for a
310 * resource are fetched. This allows us to add in any CalDAV specific
313 * @param string $path
314 * @param Sabre_DAV_INode $node
315 * @param array $requestedProperties
316 * @param array $returnedProperties
319 public function beforeGetProperties($path, Sabre_DAV_INode $node, &$requestedProperties, &$returnedProperties) {
321 if ($node instanceof Sabre_DAVACL_IPrincipal) {
323 // calendar-home-set property
324 $calHome = '{' . self::NS_CALDAV . '}calendar-home-set';
325 if (in_array($calHome,$requestedProperties)) {
326 $principalId = $node->getName();
327 $calendarHomePath = self::CALENDAR_ROOT . '/' . $principalId . '/';
328 unset($requestedProperties[$calHome]);
329 $returnedProperties[200][$calHome] = new Sabre_DAV_Property_Href($calendarHomePath);
332 // schedule-outbox-URL property
333 $scheduleProp = '{' . self::NS_CALDAV . '}schedule-outbox-URL';
334 if (in_array($scheduleProp,$requestedProperties)) {
335 $principalId = $node->getName();
336 $outboxPath = self::CALENDAR_ROOT . '/' . $principalId . '/outbox';
337 unset($requestedProperties[$scheduleProp]);
338 $returnedProperties[200][$scheduleProp] = new Sabre_DAV_Property_Href($outboxPath);
341 // calendar-user-address-set property
342 $calProp = '{' . self::NS_CALDAV . '}calendar-user-address-set';
343 if (in_array($calProp,$requestedProperties)) {
345 $addresses = $node->getAlternateUriSet();
346 $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl();
347 unset($requestedProperties[$calProp]);
348 $returnedProperties[200][$calProp] = new Sabre_DAV_Property_HrefList($addresses, false);
352 // These two properties are shortcuts for ical to easily find
353 // other principals this principal has access to.
354 $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for';
355 $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for';
356 if (in_array($propRead,$requestedProperties) || in_array($propWrite,$requestedProperties)) {
358 $membership = $node->getGroupMembership();
360 $writeList = array();
362 foreach($membership as $group) {
364 $groupNode = $this->server->tree->getNodeForPath($group);
366 // If the node is either ap proxy-read or proxy-write
367 // group, we grab the parent principal and add it to the
369 if ($groupNode instanceof Sabre_CalDAV_Principal_ProxyRead) {
370 list($readList[]) = Sabre_DAV_URLUtil::splitPath($group);
372 if ($groupNode instanceof Sabre_CalDAV_Principal_ProxyWrite) {
373 list($writeList[]) = Sabre_DAV_URLUtil::splitPath($group);
377 if (in_array($propRead,$requestedProperties)) {
378 unset($requestedProperties[$propRead]);
379 $returnedProperties[200][$propRead] = new Sabre_DAV_Property_HrefList($readList);
381 if (in_array($propWrite,$requestedProperties)) {
382 unset($requestedProperties[$propWrite]);
383 $returnedProperties[200][$propWrite] = new Sabre_DAV_Property_HrefList($writeList);
388 // notification-URL property
389 $notificationUrl = '{' . self::NS_CALENDARSERVER . '}notification-URL';
390 if (($index = array_search($notificationUrl, $requestedProperties)) !== false) {
391 $principalId = $node->getName();
392 $calendarHomePath = 'calendars/' . $principalId . '/notifications/';
393 unset($requestedProperties[$index]);
394 $returnedProperties[200][$notificationUrl] = new Sabre_DAV_Property_Href($calendarHomePath);
397 } // instanceof IPrincipal
399 if ($node instanceof Sabre_CalDAV_Notifications_INode) {
401 $propertyName = '{' . self::NS_CALENDARSERVER . '}notificationtype';
402 if (($index = array_search($propertyName, $requestedProperties)) !== false) {
404 $returnedProperties[200][$propertyName] =
405 $node->getNotificationType();
407 unset($requestedProperties[$index]);
411 } // instanceof Notifications_INode
414 if ($node instanceof Sabre_CalDAV_ICalendarObject) {
415 // The calendar-data property is not supposed to be a 'real'
416 // property, but in large chunks of the spec it does act as such.
417 // Therefore we simply expose it as a property.
418 $calDataProp = '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}calendar-data';
419 if (in_array($calDataProp, $requestedProperties)) {
420 unset($requestedProperties[$calDataProp]);
422 if (is_resource($val))
423 $val = stream_get_contents($val);
425 // Taking out \r to not screw up the xml output
426 $returnedProperties[200][$calDataProp] = str_replace("\r","", $val);
434 * This function handles the calendar-multiget REPORT.
436 * This report is used by the client to fetch the content of a series
437 * of urls. Effectively avoiding a lot of redundant requests.
439 * @param DOMNode $dom
442 public function calendarMultiGetReport($dom) {
444 $properties = array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild));
445 $hrefElems = $dom->getElementsByTagNameNS('DAV:','href');
447 $xpath = new DOMXPath($dom);
448 $xpath->registerNameSpace('cal',Sabre_CalDAV_Plugin::NS_CALDAV);
449 $xpath->registerNameSpace('dav','DAV:');
451 $expand = $xpath->query('/cal:calendar-multiget/dav:prop/cal:calendar-data/cal:expand');
452 if ($expand->length>0) {
453 $expandElem = $expand->item(0);
454 $start = $expandElem->getAttribute('start');
455 $end = $expandElem->getAttribute('end');
456 if(!$start || !$end) {
457 throw new Sabre_DAV_Exception_BadRequest('The "start" and "end" attributes are required for the CALDAV:expand element');
459 $start = Sabre_VObject_DateTimeParser::parseDateTime($start);
460 $end = Sabre_VObject_DateTimeParser::parseDateTime($end);
462 if ($end <= $start) {
463 throw new Sabre_DAV_Exception_BadRequest('The end-date must be larger than the start-date in the expand element.');
474 foreach($hrefElems as $elem) {
475 $uri = $this->server->calculateUri($elem->nodeValue);
476 list($objProps) = $this->server->getPropertiesForPath($uri,$properties);
478 if ($expand && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) {
479 $vObject = Sabre_VObject_Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']);
480 $vObject->expand($start, $end);
481 $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
484 $propertyList[]=$objProps;
488 $this->server->httpResponse->sendStatus(207);
489 $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
490 $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList));
495 * This function handles the calendar-query REPORT
497 * This report is used by clients to request calendar objects based on
498 * complex conditions.
500 * @param DOMNode $dom
503 public function calendarQueryReport($dom) {
505 $parser = new Sabre_CalDAV_CalendarQueryParser($dom);
508 $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
509 $depth = $this->server->getHTTPDepth(0);
511 // The default result is an empty array
514 // The calendarobject was requested directly. In this case we handle
516 if ($depth == 0 && $node instanceof Sabre_CalDAV_ICalendarObject) {
518 $requestedCalendarData = true;
519 $requestedProperties = $parser->requestedProperties;
521 if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
523 // We always retrieve calendar-data, as we need it for filtering.
524 $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data';
526 // If calendar-data wasn't explicitly requested, we need to remove
527 // it after processing.
528 $requestedCalendarData = false;
531 $properties = $this->server->getPropertiesForPath(
532 $this->server->getRequestUri(),
533 $requestedProperties,
537 // This array should have only 1 element, the first calendar
539 $properties = current($properties);
541 // If there wasn't any calendar-data returned somehow, we ignore
543 if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
545 $validator = new Sabre_CalDAV_CalendarQueryValidator();
546 $vObject = Sabre_VObject_Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
547 if ($validator->validate($vObject,$parser->filters)) {
549 // If the client didn't require the calendar-data property,
550 // we won't give it back.
551 if (!$requestedCalendarData) {
552 unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
554 if ($parser->expand) {
555 $vObject->expand($parser->expand['start'], $parser->expand['end']);
556 $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
560 $result = array($properties);
567 // If we're dealing with a calendar, the calendar itself is responsible
568 // for the calendar-query.
569 if ($node instanceof Sabre_CalDAV_ICalendar && $depth = 1) {
571 $nodePaths = $node->calendarQuery($parser->filters);
573 foreach($nodePaths as $path) {
576 $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $parser->requestedProperties);
578 if ($parser->expand) {
579 // We need to do some post-processing
580 $vObject = Sabre_VObject_Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
581 $vObject->expand($parser->expand['start'], $parser->expand['end']);
582 $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
585 $result[] = $properties;
591 $this->server->httpResponse->sendStatus(207);
592 $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
593 $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result));
598 * This method is responsible for parsing the request and generating the
599 * response for the CALDAV:free-busy-query REPORT.
601 * @param DOMNode $dom
604 protected function freeBusyQueryReport(DOMNode $dom) {
609 foreach($dom->firstChild->childNodes as $childNode) {
611 $clark = Sabre_DAV_XMLUtil::toClarkNotation($childNode);
612 if ($clark == '{' . self::NS_CALDAV . '}time-range') {
613 $start = $childNode->getAttribute('start');
614 $end = $childNode->getAttribute('end');
620 $start = Sabre_VObject_DateTimeParser::parseDateTime($start);
623 $end = Sabre_VObject_DateTimeParser::parseDateTime($end);
626 if (!$start && !$end) {
627 throw new Sabre_DAV_Exception_BadRequest('The freebusy report must have a time-range filter');
629 $acl = $this->server->getPlugin('acl');
632 throw new Sabre_DAV_Exception('The ACL plugin must be loaded for free-busy queries to work');
634 $uri = $this->server->getRequestUri();
635 $acl->checkPrivileges($uri,'{' . self::NS_CALDAV . '}read-free-busy');
637 $calendar = $this->server->tree->getNodeForPath($uri);
638 if (!$calendar instanceof Sabre_CalDAV_ICalendar) {
639 throw new Sabre_DAV_Exception_NotImplemented('The free-busy-query REPORT is only implemented on calendars');
642 $objects = array_map(function($child) {
643 $obj = $child->get();
644 if (is_resource($obj)) {
645 $obj = stream_get_contents($obj);
648 }, $calendar->getChildren());
650 $generator = new Sabre_VObject_FreeBusyGenerator();
651 $generator->setObjects($objects);
652 $generator->setTimeRange($start, $end);
653 $result = $generator->getResult();
654 $result = $result->serialize();
656 $this->server->httpResponse->sendStatus(200);
657 $this->server->httpResponse->setHeader('Content-Type', 'text/calendar');
658 $this->server->httpResponse->setHeader('Content-Length', strlen($result));
659 $this->server->httpResponse->sendBody($result);
664 * This method is triggered before a file gets updated with new content.
666 * This plugin uses this method to ensure that CalDAV objects receive
667 * valid calendar data.
669 * @param string $path
670 * @param Sabre_DAV_IFile $node
671 * @param resource $data
674 public function beforeWriteContent($path, Sabre_DAV_IFile $node, &$data) {
676 if (!$node instanceof Sabre_CalDAV_ICalendarObject)
679 $this->validateICalendar($data, $path);
684 * This method is triggered before a new file is created.
686 * This plugin uses this method to ensure that newly created calendar
687 * objects contain valid calendar data.
689 * @param string $path
690 * @param resource $data
691 * @param Sabre_DAV_ICollection $parentNode
694 public function beforeCreateFile($path, &$data, Sabre_DAV_ICollection $parentNode) {
696 if (!$parentNode instanceof Sabre_CalDAV_Calendar)
699 $this->validateICalendar($data, $path);
704 * This event is triggered before any HTTP request is handled.
706 * We use this to intercept GET calls to notification nodes, and return the
709 * @param string $method
710 * @param string $path
713 public function beforeMethod($method, $path) {
715 if ($method!=='GET') return;
718 $node = $this->server->tree->getNodeForPath($path);
719 } catch (Sabre_DAV_Exception_NotFound $e) {
723 if (!$node instanceof Sabre_CalDAV_Notifications_INode)
726 $dom = new DOMDocument('1.0', 'UTF-8');
727 $dom->formatOutput = true;
729 $root = $dom->createElement('cs:notification');
730 foreach($this->server->xmlNamespaces as $namespace => $prefix) {
731 $root->setAttribute('xmlns:' . $prefix, $namespace);
734 $dom->appendChild($root);
735 $node->getNotificationType()->serializeBody($this->server, $root);
737 $this->server->httpResponse->setHeader('Content-Type','application/xml');
738 $this->server->httpResponse->sendStatus(200);
739 $this->server->httpResponse->sendBody($dom->saveXML());
746 * Checks if the submitted iCalendar data is in fact, valid.
748 * An exception is thrown if it's not.
750 * @param resource|string $data
751 * @param string $path
754 protected function validateICalendar(&$data, $path) {
756 // If it's a stream, we convert it to a string first.
757 if (is_resource($data)) {
758 $data = stream_get_contents($data);
761 // Converting the data to unicode, if needed.
762 $data = Sabre_DAV_StringUtil::ensureUTF8($data);
766 $vobj = Sabre_VObject_Reader::read($data);
768 } catch (Sabre_VObject_ParseException $e) {
770 throw new Sabre_DAV_Exception_UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
774 if ($vobj->name !== 'VCALENDAR') {
775 throw new Sabre_DAV_Exception_UnsupportedMediaType('This collection can only support iCalendar objects.');
778 // Get the Supported Components for the target calendar
779 list($parentPath,$object) = Sabre_Dav_URLUtil::splitPath($path);
780 $calendarProperties = $this->server->getProperties($parentPath,array('{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'));
781 $supportedComponents = $calendarProperties['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']->getValue();
785 foreach($vobj->getComponents() as $component) {
786 switch($component->name) {
792 if (is_null($foundType)) {
793 $foundType = $component->name;
794 if (!in_array($foundType, $supportedComponents)) {
795 throw new Sabre_CalDAV_Exception_InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
797 if (!isset($component->UID)) {
798 throw new Sabre_DAV_Exception_BadRequest('Every ' . $component->name . ' component must have an UID');
800 $foundUID = (string)$component->UID;
802 if ($foundType !== $component->name) {
803 throw new Sabre_DAV_Exception_BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType);
805 if ($foundUID !== (string)$component->UID) {
806 throw new Sabre_DAV_Exception_BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
811 throw new Sabre_DAV_Exception_BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
816 throw new Sabre_DAV_Exception_BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
821 * This method handles POST requests to the schedule-outbox
823 * @param Sabre_CalDAV_Schedule_IOutbox $outboxNode
826 public function outboxRequest(Sabre_CalDAV_Schedule_IOutbox $outboxNode) {
828 $originator = $this->server->httpRequest->getHeader('Originator');
829 $recipients = $this->server->httpRequest->getHeader('Recipient');
832 throw new Sabre_DAV_Exception_BadRequest('The Originator: header must be specified when making POST requests');
835 throw new Sabre_DAV_Exception_BadRequest('The Recipient: header must be specified when making POST requests');
838 if (!preg_match('/^mailto:(.*)@(.*)$/i', $originator)) {
839 throw new Sabre_DAV_Exception_BadRequest('Originator must start with mailto: and must be valid email address');
841 $originator = substr($originator,7);
843 $recipients = explode(',',$recipients);
844 foreach($recipients as $k=>$recipient) {
846 $recipient = trim($recipient);
847 if (!preg_match('/^mailto:(.*)@(.*)$/i', $recipient)) {
848 throw new Sabre_DAV_Exception_BadRequest('Recipients must start with mailto: and must be valid email address');
850 $recipient = substr($recipient, 7);
851 $recipients[$k] = $recipient;
854 // We need to make sure that 'originator' matches one of the email
855 // addresses of the selected principal.
856 $principal = $outboxNode->getOwner();
857 $props = $this->server->getProperties($principal,array(
858 '{' . self::NS_CALDAV . '}calendar-user-address-set',
861 $addresses = array();
862 if (isset($props['{' . self::NS_CALDAV . '}calendar-user-address-set'])) {
863 $addresses = $props['{' . self::NS_CALDAV . '}calendar-user-address-set']->getHrefs();
866 if (!in_array('mailto:' . $originator, $addresses)) {
867 throw new Sabre_DAV_Exception_Forbidden('The addresses specified in the Originator header did not match any addresses in the owners calendar-user-address-set header');
871 $vObject = Sabre_VObject_Reader::read($this->server->httpRequest->getBody(true));
872 } catch (Sabre_VObject_ParseException $e) {
873 throw new Sabre_DAV_Exception_BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage());
876 // Checking for the object type
877 $componentType = null;
878 foreach($vObject->getComponents() as $component) {
879 if ($component->name !== 'VTIMEZONE') {
880 $componentType = $component->name;
884 if (is_null($componentType)) {
885 throw new Sabre_DAV_Exception_BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
888 // Validating the METHOD
889 $method = strtoupper((string)$vObject->METHOD);
891 throw new Sabre_DAV_Exception_BadRequest('A METHOD property must be specified in iTIP messages');
894 if (in_array($method, array('REQUEST','REPLY','ADD','CANCEL')) && $componentType==='VEVENT') {
895 $result = $this->iMIPMessage($originator, $recipients, $vObject, $principal);
896 $this->server->httpResponse->sendStatus(200);
897 $this->server->httpResponse->setHeader('Content-Type','application/xml');
898 $this->server->httpResponse->sendBody($this->generateScheduleResponse($result));
900 throw new Sabre_DAV_Exception_NotImplemented('This iTIP method is currently not implemented');
906 * Sends an iMIP message by email.
908 * This method must return an array with status codes per recipient.
909 * This should look something like:
912 * 'user1@example.org' => '2.0;Success'
915 * Formatting for this status code can be found at:
916 * https://tools.ietf.org/html/rfc5545#section-3.8.8.3
918 * A list of valid status codes can be found at:
919 * https://tools.ietf.org/html/rfc5546#section-3.6
921 * @param string $originator
922 * @param array $recipients
923 * @param Sabre_VObject_Component $vObject
926 protected function iMIPMessage($originator, array $recipients, Sabre_VObject_Component $vObject, $principal) {
928 if (!$this->imipHandler) {
929 $resultStatus = '5.2;This server does not support this operation';
931 $this->imipHandler->sendMessage($originator, $recipients, $vObject, $principal);
932 $resultStatus = '2.0;Success';
936 foreach($recipients as $recipient) {
937 $result[$recipient] = $resultStatus;
945 * Generates a schedule-response XML body
947 * The recipients array is a key->value list, containing email addresses
948 * and iTip status codes. See the iMIPMessage method for a description of
951 * @param array $recipients
954 public function generateScheduleResponse(array $recipients) {
956 $dom = new DOMDocument('1.0','utf-8');
957 $dom->formatOutput = true;
958 $xscheduleResponse = $dom->createElement('cal:schedule-response');
959 $dom->appendChild($xscheduleResponse);
961 foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
963 $xscheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
967 foreach($recipients as $recipient=>$status) {
968 $xresponse = $dom->createElement('cal:response');
970 $xrecipient = $dom->createElement('cal:recipient');
971 $xrecipient->appendChild($dom->createTextNode($recipient));
972 $xresponse->appendChild($xrecipient);
974 $xrequestStatus = $dom->createElement('cal:request-status');
975 $xrequestStatus->appendChild($dom->createTextNode($status));
976 $xresponse->appendChild($xrequestStatus);
978 $xscheduleResponse->appendChild($xresponse);
982 return $dom->saveXML();
987 * This method is used to generate HTML output for the
988 * Sabre_DAV_Browser_Plugin. This allows us to generate an interface users
989 * can use to create new calendars.
991 * @param Sabre_DAV_INode $node
992 * @param string $output
995 public function htmlActionsPanel(Sabre_DAV_INode $node, &$output) {
997 if (!$node instanceof Sabre_CalDAV_UserCalendars)
1000 $output.= '<tr><td colspan="2"><form method="post" action="">
1001 <h3>Create new calendar</h3>
1002 <input type="hidden" name="sabreAction" value="mkcalendar" />
1003 <label>Name (uri):</label> <input type="text" name="name" /><br />
1004 <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
1005 <input type="submit" value="create" />
1014 * This method allows us to intercept the 'mkcalendar' sabreAction. This
1015 * action enables the user to create new calendars from the browser plugin.
1017 * @param string $uri
1018 * @param string $action
1019 * @param array $postVars
1022 public function browserPostAction($uri, $action, array $postVars) {
1024 if ($action!=='mkcalendar')
1027 $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
1028 $properties = array();
1029 if (isset($postVars['{DAV:}displayname'])) {
1030 $properties['{DAV:}displayname'] = $postVars['{DAV:}displayname'];
1032 $this->server->createCollection($uri . '/' . $postVars['name'],$resourceType,$properties);