8 * This addon provides functionality added by CalDAV (RFC 4791)
9 * It implements new reports, and the MKCALENDAR method.
13 * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved.
14 * @author Evert Pot (http://www.rooftopsolutions.nl/)
15 * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
17 class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin {
20 * This is the official CalDAV namespace
22 const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
25 * This is the namespace for the proprietary calendarserver extensions
27 const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
30 * The hardcoded root for calendar objects. It is unfortunate
31 * that we're stuck with it, but it will have to do for now
33 const CALENDAR_ROOT = 'calendars';
36 * Reference to server object
38 * @var Sabre_DAV_Server
43 * The email handler for invites and other scheduling messages.
45 * @var Sabre_CalDAV_Schedule_IMip
47 protected $imipHandler;
50 * Sets the iMIP handler.
52 * iMIP = The email transport of iCalendar scheduling messages. Setting
53 * this is optional, but if you want the server to allow invites to be sent
54 * out, you must set a handler.
56 * Specifically iCal will plain assume that the server supports this. If
57 * the server doesn't, iCal will display errors when inviting people to
60 * @param Sabre_CalDAV_Schedule_IMip $imipHandler
63 public function setIMipHandler(Sabre_CalDAV_Schedule_IMip $imipHandler) {
65 $this->imipHandler = $imipHandler;
70 * Use this method to tell the server this addon defines additional
73 * This method is passed a uri. It should only return HTTP methods that are
74 * available for the specified uri.
79 public function getHTTPMethods($uri) {
81 // The MKCALENDAR is only available on unmapped uri's, whose
82 // parents extend IExtendedCollection
83 list($parent, $name) = Sabre_DAV_URLUtil::splitPath($uri);
85 $node = $this->server->tree->getNodeForPath($parent);
87 if ($node instanceof Sabre_DAV_IExtendedCollection) {
89 $node->getChild($name);
90 } catch (Sabre_DAV_Exception_NotFound $e) {
91 return array('MKCALENDAR');
99 * Returns a list of features for the DAV: HTTP header.
103 public function getFeatures() {
105 return array('calendar-access', 'calendar-proxy');
110 * Returns a addon name.
112 * Using this name other addons will be able to access other addons
113 * using Sabre_DAV_Server::getPlugin
117 public function getPluginName() {
124 * Returns a list of reports this addon supports.
126 * This will be used in the {DAV:}supported-report-set property.
127 * Note that you still need to subscribe to the 'report' event to actually
133 public function getSupportedReportSet($uri) {
135 $node = $this->server->tree->getNodeForPath($uri);
138 if ($node instanceof Sabre_CalDAV_ICalendar || $node instanceof Sabre_CalDAV_ICalendarObject) {
139 $reports[] = '{' . self::NS_CALDAV . '}calendar-multiget';
140 $reports[] = '{' . self::NS_CALDAV . '}calendar-query';
142 if ($node instanceof Sabre_CalDAV_ICalendar) {
143 $reports[] = '{' . self::NS_CALDAV . '}free-busy-query';
150 * Initializes the addon
152 * @param Sabre_DAV_Server $server
155 public function initialize(Sabre_DAV_Server $server) {
157 $this->server = $server;
159 $server->subscribeEvent('unknownMethod',array($this,'unknownMethod'));
160 //$server->subscribeEvent('unknownMethod',array($this,'unknownMethod2'),1000);
161 $server->subscribeEvent('report',array($this,'report'));
162 $server->subscribeEvent('beforeGetProperties',array($this,'beforeGetProperties'));
163 $server->subscribeEvent('onHTMLActionsPanel', array($this,'htmlActionsPanel'));
164 $server->subscribeEvent('onBrowserPostAction', array($this,'browserPostAction'));
165 $server->subscribeEvent('beforeWriteContent', array($this, 'beforeWriteContent'));
166 $server->subscribeEvent('beforeCreateFile', array($this, 'beforeCreateFile'));
167 $server->subscribeEvent('beforeMethod', array($this,'beforeMethod'));
169 $server->xmlNamespaces[self::NS_CALDAV] = 'cal';
170 $server->xmlNamespaces[self::NS_CALENDARSERVER] = 'cs';
172 $server->propertyMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre_CalDAV_Property_SupportedCalendarComponentSet';
174 $server->resourceTypeMapping['Sabre_CalDAV_ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar';
175 $server->resourceTypeMapping['Sabre_CalDAV_Schedule_IOutbox'] = '{urn:ietf:params:xml:ns:caldav}schedule-outbox';
176 $server->resourceTypeMapping['Sabre_CalDAV_Principal_ProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read';
177 $server->resourceTypeMapping['Sabre_CalDAV_Principal_ProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write';
178 $server->resourceTypeMapping['Sabre_CalDAV_Notifications_ICollection'] = '{' . self::NS_CALENDARSERVER . '}notifications';
179 $server->resourceTypeMapping['Sabre_CalDAV_Notifications_INode'] = '{' . self::NS_CALENDARSERVER . '}notification';
181 array_push($server->protectedProperties,
183 '{' . self::NS_CALDAV . '}supported-calendar-component-set',
184 '{' . self::NS_CALDAV . '}supported-calendar-data',
185 '{' . self::NS_CALDAV . '}max-resource-size',
186 '{' . self::NS_CALDAV . '}min-date-time',
187 '{' . self::NS_CALDAV . '}max-date-time',
188 '{' . self::NS_CALDAV . '}max-instances',
189 '{' . self::NS_CALDAV . '}max-attendees-per-instance',
190 '{' . self::NS_CALDAV . '}calendar-home-set',
191 '{' . self::NS_CALDAV . '}supported-collation-set',
192 '{' . self::NS_CALDAV . '}calendar-data',
194 // scheduling extension
195 '{' . self::NS_CALDAV . '}schedule-inbox-URL',
196 '{' . self::NS_CALDAV . '}schedule-outbox-URL',
197 '{' . self::NS_CALDAV . '}calendar-user-address-set',
198 '{' . self::NS_CALDAV . '}calendar-user-type',
200 // CalendarServer extensions
201 '{' . self::NS_CALENDARSERVER . '}getctag',
202 '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for',
203 '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for',
204 '{' . self::NS_CALENDARSERVER . '}notification-URL',
205 '{' . self::NS_CALENDARSERVER . '}notificationtype'
211 * This function handles support for the MKCALENDAR method
213 * @param string $method
217 public function unknownMethod($method, $uri) {
221 $this->httpMkCalendar($uri);
222 // false is returned to stop the propagation of the
223 // unknownMethod event.
226 // Checking if we're talking to an outbox
228 $node = $this->server->tree->getNodeForPath($uri);
229 } catch (Sabre_DAV_Exception_NotFound $e) {
232 if (!$node instanceof Sabre_CalDAV_Schedule_IOutbox)
235 $this->outboxRequest($node);
243 * This functions handles REPORT requests specific to CalDAV
245 * @param string $reportName
246 * @param DOMNode $dom
249 public function report($reportName,$dom) {
251 switch($reportName) {
252 case '{'.self::NS_CALDAV.'}calendar-multiget' :
253 $this->calendarMultiGetReport($dom);
255 case '{'.self::NS_CALDAV.'}calendar-query' :
256 $this->calendarQueryReport($dom);
258 case '{'.self::NS_CALDAV.'}free-busy-query' :
259 $this->freeBusyQueryReport($dom);
268 * This function handles the MKCALENDAR HTTP method, which creates
274 public function httpMkCalendar($uri) {
276 // Due to unforgivable bugs in iCal, we're completely disabling MKCALENDAR support
277 // for clients matching iCal in the user agent
278 //$ua = $this->server->httpRequest->getHeader('User-Agent');
279 //if (strpos($ua,'iCal/')!==false) {
280 // 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.');
283 $body = $this->server->httpRequest->getBody(true);
284 $properties = array();
288 $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
290 foreach($dom->firstChild->childNodes as $child) {
292 if (Sabre_DAV_XMLUtil::toClarkNotation($child)!=='{DAV:}set') continue;
293 foreach(Sabre_DAV_XMLUtil::parseProperties($child,$this->server->propertyMap) as $k=>$prop) {
294 $properties[$k] = $prop;
300 $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
302 $this->server->createCollection($uri,$resourceType,$properties);
304 $this->server->httpResponse->sendStatus(201);
305 $this->server->httpResponse->setHeader('Content-Length',0);
309 * beforeGetProperties
311 * This method handler is invoked before any after properties for a
312 * resource are fetched. This allows us to add in any CalDAV specific
315 * @param string $path
316 * @param Sabre_DAV_INode $node
317 * @param array $requestedProperties
318 * @param array $returnedProperties
321 public function beforeGetProperties($path, Sabre_DAV_INode $node, &$requestedProperties, &$returnedProperties) {
323 if ($node instanceof Sabre_DAVACL_IPrincipal) {
325 // calendar-home-set property
326 $calHome = '{' . self::NS_CALDAV . '}calendar-home-set';
327 if (in_array($calHome,$requestedProperties)) {
328 $principalId = $node->getName();
329 $calendarHomePath = self::CALENDAR_ROOT . '/' . $principalId . '/';
330 unset($requestedProperties[$calHome]);
331 $returnedProperties[200][$calHome] = new Sabre_DAV_Property_Href($calendarHomePath);
334 // schedule-outbox-URL property
335 $scheduleProp = '{' . self::NS_CALDAV . '}schedule-outbox-URL';
336 if (in_array($scheduleProp,$requestedProperties)) {
337 $principalId = $node->getName();
338 $outboxPath = self::CALENDAR_ROOT . '/' . $principalId . '/outbox';
339 unset($requestedProperties[$scheduleProp]);
340 $returnedProperties[200][$scheduleProp] = new Sabre_DAV_Property_Href($outboxPath);
343 // calendar-user-address-set property
344 $calProp = '{' . self::NS_CALDAV . '}calendar-user-address-set';
345 if (in_array($calProp,$requestedProperties)) {
347 $addresses = $node->getAlternateUriSet();
348 $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl();
349 unset($requestedProperties[$calProp]);
350 $returnedProperties[200][$calProp] = new Sabre_DAV_Property_HrefList($addresses, false);
354 // These two properties are shortcuts for ical to easily find
355 // other principals this principal has access to.
356 $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for';
357 $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for';
358 if (in_array($propRead,$requestedProperties) || in_array($propWrite,$requestedProperties)) {
360 $membership = $node->getGroupMembership();
362 $writeList = array();
364 foreach($membership as $group) {
366 $groupNode = $this->server->tree->getNodeForPath($group);
368 // If the node is either ap proxy-read or proxy-write
369 // group, we grab the parent principal and add it to the
371 if ($groupNode instanceof Sabre_CalDAV_Principal_ProxyRead) {
372 list($readList[]) = Sabre_DAV_URLUtil::splitPath($group);
374 if ($groupNode instanceof Sabre_CalDAV_Principal_ProxyWrite) {
375 list($writeList[]) = Sabre_DAV_URLUtil::splitPath($group);
379 if (in_array($propRead,$requestedProperties)) {
380 unset($requestedProperties[$propRead]);
381 $returnedProperties[200][$propRead] = new Sabre_DAV_Property_HrefList($readList);
383 if (in_array($propWrite,$requestedProperties)) {
384 unset($requestedProperties[$propWrite]);
385 $returnedProperties[200][$propWrite] = new Sabre_DAV_Property_HrefList($writeList);
390 // notification-URL property
391 $notificationUrl = '{' . self::NS_CALENDARSERVER . '}notification-URL';
392 if (($index = array_search($notificationUrl, $requestedProperties)) !== false) {
393 $principalId = $node->getName();
394 $calendarHomePath = 'calendars/' . $principalId . '/notifications/';
395 unset($requestedProperties[$index]);
396 $returnedProperties[200][$notificationUrl] = new Sabre_DAV_Property_Href($calendarHomePath);
399 } // instanceof IPrincipal
401 if ($node instanceof Sabre_CalDAV_Notifications_INode) {
403 $propertyName = '{' . self::NS_CALENDARSERVER . '}notificationtype';
404 if (($index = array_search($propertyName, $requestedProperties)) !== false) {
406 $returnedProperties[200][$propertyName] =
407 $node->getNotificationType();
409 unset($requestedProperties[$index]);
413 } // instanceof Notifications_INode
416 if ($node instanceof Sabre_CalDAV_ICalendarObject) {
417 // The calendar-data property is not supposed to be a 'real'
418 // property, but in large chunks of the spec it does act as such.
419 // Therefore we simply expose it as a property.
420 $calDataProp = '{' . Sabre_CalDAV_Plugin::NS_CALDAV . '}calendar-data';
421 if (in_array($calDataProp, $requestedProperties)) {
422 unset($requestedProperties[$calDataProp]);
424 if (is_resource($val))
425 $val = stream_get_contents($val);
427 // Taking out \r to not screw up the xml output
428 $returnedProperties[200][$calDataProp] = str_replace("\r","", $val);
436 * This function handles the calendar-multiget REPORT.
438 * This report is used by the client to fetch the content of a series
439 * of urls. Effectively avoiding a lot of redundant requests.
441 * @param DOMNode $dom
444 public function calendarMultiGetReport($dom) {
446 $properties = array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild));
447 $hrefElems = $dom->getElementsByTagNameNS('DAV:','href');
449 $xpath = new DOMXPath($dom);
450 $xpath->registerNameSpace('cal',Sabre_CalDAV_Plugin::NS_CALDAV);
451 $xpath->registerNameSpace('dav','DAV:');
453 $expand = $xpath->query('/cal:calendar-multiget/dav:prop/cal:calendar-data/cal:expand');
454 if ($expand->length>0) {
455 $expandElem = $expand->item(0);
456 $start = $expandElem->getAttribute('start');
457 $end = $expandElem->getAttribute('end');
458 if(!$start || !$end) {
459 throw new Sabre_DAV_Exception_BadRequest('The "start" and "end" attributes are required for the CALDAV:expand element');
461 $start = VObject\DateTimeParser::parseDateTime($start);
462 $end = VObject\DateTimeParser::parseDateTime($end);
464 if ($end <= $start) {
465 throw new Sabre_DAV_Exception_BadRequest('The end-date must be larger than the start-date in the expand element.');
476 foreach($hrefElems as $elem) {
477 $uri = $this->server->calculateUri($elem->nodeValue);
478 list($objProps) = $this->server->getPropertiesForPath($uri,$properties);
480 if ($expand && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) {
481 $vObject = VObject\Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']);
482 $vObject->expand($start, $end);
483 $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
486 $propertyList[]=$objProps;
490 $this->server->httpResponse->sendStatus(207);
491 $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
492 $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList));
497 * This function handles the calendar-query REPORT
499 * This report is used by clients to request calendar objects based on
500 * complex conditions.
502 * @param DOMNode $dom
505 public function calendarQueryReport($dom) {
507 $parser = new Sabre_CalDAV_CalendarQueryParser($dom);
510 $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
511 $depth = $this->server->getHTTPDepth(0);
513 // The default result is an empty array
516 // The calendarobject was requested directly. In this case we handle
518 if ($depth == 0 && $node instanceof Sabre_CalDAV_ICalendarObject) {
520 $requestedCalendarData = true;
521 $requestedProperties = $parser->requestedProperties;
523 if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
525 // We always retrieve calendar-data, as we need it for filtering.
526 $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data';
528 // If calendar-data wasn't explicitly requested, we need to remove
529 // it after processing.
530 $requestedCalendarData = false;
533 $properties = $this->server->getPropertiesForPath(
534 $this->server->getRequestUri(),
535 $requestedProperties,
539 // This array should have only 1 element, the first calendar
541 $properties = current($properties);
543 // If there wasn't any calendar-data returned somehow, we ignore
545 if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
547 $validator = new Sabre_CalDAV_CalendarQueryValidator();
548 $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
549 if ($validator->validate($vObject,$parser->filters)) {
551 // If the client didn't require the calendar-data property,
552 // we won't give it back.
553 if (!$requestedCalendarData) {
554 unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
556 if ($parser->expand) {
557 $vObject->expand($parser->expand['start'], $parser->expand['end']);
558 $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
562 $result = array($properties);
569 // If we're dealing with a calendar, the calendar itself is responsible
570 // for the calendar-query.
571 if ($node instanceof Sabre_CalDAV_ICalendar && $depth = 1) {
573 $nodePaths = $node->calendarQuery($parser->filters);
575 foreach($nodePaths as $path) {
578 $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $parser->requestedProperties);
580 if ($parser->expand) {
581 // We need to do some post-processing
582 $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
583 $vObject->expand($parser->expand['start'], $parser->expand['end']);
584 $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
587 $result[] = $properties;
593 $this->server->httpResponse->sendStatus(207);
594 $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
595 $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result));
600 * This method is responsible for parsing the request and generating the
601 * response for the CALDAV:free-busy-query REPORT.
603 * @param DOMNode $dom
606 protected function freeBusyQueryReport(DOMNode $dom) {
611 foreach($dom->firstChild->childNodes as $childNode) {
613 $clark = Sabre_DAV_XMLUtil::toClarkNotation($childNode);
614 if ($clark == '{' . self::NS_CALDAV . '}time-range') {
615 $start = $childNode->getAttribute('start');
616 $end = $childNode->getAttribute('end');
622 $start = VObject\DateTimeParser::parseDateTime($start);
625 $end = VObject\DateTimeParser::parseDateTime($end);
628 if (!$start && !$end) {
629 throw new Sabre_DAV_Exception_BadRequest('The freebusy report must have a time-range filter');
631 $acl = $this->server->getPlugin('acl');
634 throw new Sabre_DAV_Exception('The ACL addon must be loaded for free-busy queries to work');
636 $uri = $this->server->getRequestUri();
637 $acl->checkPrivileges($uri,'{' . self::NS_CALDAV . '}read-free-busy');
639 $calendar = $this->server->tree->getNodeForPath($uri);
640 if (!$calendar instanceof Sabre_CalDAV_ICalendar) {
641 throw new Sabre_DAV_Exception_NotImplemented('The free-busy-query REPORT is only implemented on calendars');
644 $objects = array_map(function($child) {
645 $obj = $child->get();
646 if (is_resource($obj)) {
647 $obj = stream_get_contents($obj);
650 }, $calendar->getChildren());
652 $generator = new VObject\FreeBusyGenerator();
653 $generator->setObjects($objects);
654 $generator->setTimeRange($start, $end);
655 $result = $generator->getResult();
656 $result = $result->serialize();
658 $this->server->httpResponse->sendStatus(200);
659 $this->server->httpResponse->setHeader('Content-Type', 'text/calendar');
660 $this->server->httpResponse->setHeader('Content-Length', strlen($result));
661 $this->server->httpResponse->sendBody($result);
666 * This method is triggered before a file gets updated with new content.
668 * This addon uses this method to ensure that CalDAV objects receive
669 * valid calendar data.
671 * @param string $path
672 * @param Sabre_DAV_IFile $node
673 * @param resource $data
676 public function beforeWriteContent($path, Sabre_DAV_IFile $node, &$data) {
678 if (!$node instanceof Sabre_CalDAV_ICalendarObject)
681 $this->validateICalendar($data, $path);
686 * This method is triggered before a new file is created.
688 * This addon uses this method to ensure that newly created calendar
689 * objects contain valid calendar data.
691 * @param string $path
692 * @param resource $data
693 * @param Sabre_DAV_ICollection $parentNode
696 public function beforeCreateFile($path, &$data, Sabre_DAV_ICollection $parentNode) {
698 if (!$parentNode instanceof Sabre_CalDAV_Calendar)
701 $this->validateICalendar($data, $path);
706 * This event is triggered before any HTTP request is handled.
708 * We use this to intercept GET calls to notification nodes, and return the
711 * @param string $method
712 * @param string $path
715 public function beforeMethod($method, $path) {
717 if ($method!=='GET') return;
720 $node = $this->server->tree->getNodeForPath($path);
721 } catch (Sabre_DAV_Exception_NotFound $e) {
725 if (!$node instanceof Sabre_CalDAV_Notifications_INode)
728 $dom = new DOMDocument('1.0', 'UTF-8');
729 $dom->formatOutput = true;
731 $root = $dom->createElement('cs:notification');
732 foreach($this->server->xmlNamespaces as $namespace => $prefix) {
733 $root->setAttribute('xmlns:' . $prefix, $namespace);
736 $dom->appendChild($root);
737 $node->getNotificationType()->serializeBody($this->server, $root);
739 $this->server->httpResponse->setHeader('Content-Type','application/xml');
740 $this->server->httpResponse->sendStatus(200);
741 $this->server->httpResponse->sendBody($dom->saveXML());
748 * Checks if the submitted iCalendar data is in fact, valid.
750 * An exception is thrown if it's not.
752 * @param resource|string $data
753 * @param string $path
756 protected function validateICalendar(&$data, $path) {
758 // If it's a stream, we convert it to a string first.
759 if (is_resource($data)) {
760 $data = stream_get_contents($data);
763 // Converting the data to unicode, if needed.
764 $data = Sabre_DAV_StringUtil::ensureUTF8($data);
768 $vobj = VObject\Reader::read($data);
770 } catch (VObject\ParseException $e) {
772 throw new Sabre_DAV_Exception_UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
776 if ($vobj->name !== 'VCALENDAR') {
777 throw new Sabre_DAV_Exception_UnsupportedMediaType('This collection can only support iCalendar objects.');
780 // Get the Supported Components for the target calendar
781 list($parentPath,$object) = Sabre_Dav_URLUtil::splitPath($path);
782 $calendarProperties = $this->server->getProperties($parentPath,array('{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'));
783 $supportedComponents = $calendarProperties['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']->getValue();
787 foreach($vobj->getComponents() as $component) {
788 switch($component->name) {
794 if (is_null($foundType)) {
795 $foundType = $component->name;
796 if (!in_array($foundType, $supportedComponents)) {
797 throw new Sabre_CalDAV_Exception_InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
799 if (!isset($component->UID)) {
800 throw new Sabre_DAV_Exception_BadRequest('Every ' . $component->name . ' component must have an UID');
802 $foundUID = (string)$component->UID;
804 if ($foundType !== $component->name) {
805 throw new Sabre_DAV_Exception_BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType);
807 if ($foundUID !== (string)$component->UID) {
808 throw new Sabre_DAV_Exception_BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
813 throw new Sabre_DAV_Exception_BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
818 throw new Sabre_DAV_Exception_BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
823 * This method handles POST requests to the schedule-outbox
825 * @param Sabre_CalDAV_Schedule_IOutbox $outboxNode
828 public function outboxRequest(Sabre_CalDAV_Schedule_IOutbox $outboxNode) {
830 $originator = $this->server->httpRequest->getHeader('Originator');
831 $recipients = $this->server->httpRequest->getHeader('Recipient');
834 throw new Sabre_DAV_Exception_BadRequest('The Originator: header must be specified when making POST requests');
837 throw new Sabre_DAV_Exception_BadRequest('The Recipient: header must be specified when making POST requests');
840 if (!preg_match('/^mailto:(.*)@(.*)$/i', $originator)) {
841 throw new Sabre_DAV_Exception_BadRequest('Originator must start with mailto: and must be valid email address');
843 $originator = substr($originator,7);
845 $recipients = explode(',',$recipients);
846 foreach($recipients as $k=>$recipient) {
848 $recipient = trim($recipient);
849 if (!preg_match('/^mailto:(.*)@(.*)$/i', $recipient)) {
850 throw new Sabre_DAV_Exception_BadRequest('Recipients must start with mailto: and must be valid email address');
852 $recipient = substr($recipient, 7);
853 $recipients[$k] = $recipient;
856 // We need to make sure that 'originator' matches one of the email
857 // addresses of the selected principal.
858 $principal = $outboxNode->getOwner();
859 $props = $this->server->getProperties($principal,array(
860 '{' . self::NS_CALDAV . '}calendar-user-address-set',
863 $addresses = array();
864 if (isset($props['{' . self::NS_CALDAV . '}calendar-user-address-set'])) {
865 $addresses = $props['{' . self::NS_CALDAV . '}calendar-user-address-set']->getHrefs();
868 if (!in_array('mailto:' . $originator, $addresses)) {
869 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');
873 $vObject = VObject\Reader::read($this->server->httpRequest->getBody(true));
874 } catch (VObject\ParseException $e) {
875 throw new Sabre_DAV_Exception_BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage());
878 // Checking for the object type
879 $componentType = null;
880 foreach($vObject->getComponents() as $component) {
881 if ($component->name !== 'VTIMEZONE') {
882 $componentType = $component->name;
886 if (is_null($componentType)) {
887 throw new Sabre_DAV_Exception_BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
890 // Validating the METHOD
891 $method = strtoupper((string)$vObject->METHOD);
893 throw new Sabre_DAV_Exception_BadRequest('A METHOD property must be specified in iTIP messages');
896 if (in_array($method, array('REQUEST','REPLY','ADD','CANCEL')) && $componentType==='VEVENT') {
897 $result = $this->iMIPMessage($originator, $recipients, $vObject, $principal);
898 $this->server->httpResponse->sendStatus(200);
899 $this->server->httpResponse->setHeader('Content-Type','application/xml');
900 $this->server->httpResponse->sendBody($this->generateScheduleResponse($result));
902 throw new Sabre_DAV_Exception_NotImplemented('This iTIP method is currently not implemented');
908 * Sends an iMIP message by email.
910 * This method must return an array with status codes per recipient.
911 * This should look something like:
914 * 'user1@example.org' => '2.0;Success'
917 * Formatting for this status code can be found at:
918 * https://tools.ietf.org/html/rfc5545#section-3.8.8.3
920 * A list of valid status codes can be found at:
921 * https://tools.ietf.org/html/rfc5546#section-3.6
923 * @param string $originator
924 * @param array $recipients
925 * @param Sabre\VObject\Component $vObject
928 protected function iMIPMessage($originator, array $recipients, VObject\Component $vObject, $principal) {
930 if (!$this->imipHandler) {
931 $resultStatus = '5.2;This server does not support this operation';
933 $this->imipHandler->sendMessage($originator, $recipients, $vObject, $principal);
934 $resultStatus = '2.0;Success';
938 foreach($recipients as $recipient) {
939 $result[$recipient] = $resultStatus;
947 * Generates a schedule-response XML body
949 * The recipients array is a key->value list, containing email addresses
950 * and iTip status codes. See the iMIPMessage method for a description of
953 * @param array $recipients
956 public function generateScheduleResponse(array $recipients) {
958 $dom = new DOMDocument('1.0','utf-8');
959 $dom->formatOutput = true;
960 $xscheduleResponse = $dom->createElement('cal:schedule-response');
961 $dom->appendChild($xscheduleResponse);
963 foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
965 $xscheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
969 foreach($recipients as $recipient=>$status) {
970 $xresponse = $dom->createElement('cal:response');
972 $xrecipient = $dom->createElement('cal:recipient');
973 $xrecipient->appendChild($dom->createTextNode($recipient));
974 $xresponse->appendChild($xrecipient);
976 $xrequestStatus = $dom->createElement('cal:request-status');
977 $xrequestStatus->appendChild($dom->createTextNode($status));
978 $xresponse->appendChild($xrequestStatus);
980 $xscheduleResponse->appendChild($xresponse);
984 return $dom->saveXML();
989 * This method is used to generate HTML output for the
990 * Sabre_DAV_Browser_Plugin. This allows us to generate an interface users
991 * can use to create new calendars.
993 * @param Sabre_DAV_INode $node
994 * @param string $output
997 public function htmlActionsPanel(Sabre_DAV_INode $node, &$output) {
999 if (!$node instanceof Sabre_CalDAV_UserCalendars)
1002 $output.= '<tr><td colspan="2"><form method="post" action="">
1003 <h3>Create new calendar</h3>
1004 <input type="hidden" name="sabreAction" value="mkcalendar" />
1005 <label>Name (uri):</label> <input type="text" name="name" /><br />
1006 <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
1007 <input type="submit" value="create" />
1016 * This method allows us to intercept the 'mkcalendar' sabreAction. This
1017 * action enables the user to create new calendars from the browser addon.
1019 * @param string $uri
1020 * @param string $action
1021 * @param array $postVars
1024 public function browserPostAction($uri, $action, array $postVars) {
1026 if ($action!=='mkcalendar')
1029 $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
1030 $properties = array();
1031 if (isset($postVars['{DAV:}displayname'])) {
1032 $properties['{DAV:}displayname'] = $postVars['{DAV:}displayname'];
1034 $this->server->createCollection($uri . '/' . $postVars['name'],$resourceType,$properties);