]> git.mxchange.org Git - friendica-addons.git/blob - dav/SabreDAV/lib/Sabre/CalDAV/Plugin.php
Move friendica-specific parts into an own subdirectory
[friendica-addons.git] / dav / SabreDAV / lib / Sabre / CalDAV / Plugin.php
1 <?php
2
3 /**
4  * CalDAV plugin
5  *
6  * This plugin provides functionality added by CalDAV (RFC 4791)
7  * It implements new reports, and the MKCALENDAR method.
8  *
9  * @package Sabre
10  * @subpackage CalDAV
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
14  */
15 class Sabre_CalDAV_Plugin extends Sabre_DAV_ServerPlugin {
16
17     /**
18      * This is the official CalDAV namespace
19      */
20     const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
21
22     /**
23      * This is the namespace for the proprietary calendarserver extensions
24      */
25     const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
26
27     /**
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
30      */
31     const CALENDAR_ROOT = 'calendars';
32
33     /**
34      * Reference to server object
35      *
36      * @var Sabre_DAV_Server
37      */
38     private $server;
39
40     /**
41      * The email handler for invites and other scheduling messages.
42      *
43      * @var Sabre_CalDAV_Schedule_IMip
44      */
45     protected $imipHandler;
46
47     /**
48      * Sets the iMIP handler.
49      *
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.
53      *
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
56      * events.
57      *
58      * @param Sabre_CalDAV_Schedule_IMip $imipHandler
59      * @return void
60      */
61     public function setIMipHandler(Sabre_CalDAV_Schedule_IMip $imipHandler) {
62
63         $this->imipHandler = $imipHandler;
64
65     }
66
67     /**
68      * Use this method to tell the server this plugin defines additional
69      * HTTP methods.
70      *
71      * This method is passed a uri. It should only return HTTP methods that are
72      * available for the specified uri.
73      *
74      * @param string $uri
75      * @return array
76      */
77     public function getHTTPMethods($uri) {
78
79         // The MKCALENDAR is only available on unmapped uri's, whose
80         // parents extend IExtendedCollection
81         list($parent, $name) = Sabre_DAV_URLUtil::splitPath($uri);
82
83         $node = $this->server->tree->getNodeForPath($parent);
84
85         if ($node instanceof Sabre_DAV_IExtendedCollection) {
86             try {
87                 $node->getChild($name);
88             } catch (Sabre_DAV_Exception_NotFound $e) {
89                 return array('MKCALENDAR');
90             }
91         }
92         return array();
93
94     }
95
96     /**
97      * Returns a list of features for the DAV: HTTP header.
98      *
99      * @return array
100      */
101     public function getFeatures() {
102
103         return array('calendar-access', 'calendar-proxy');
104
105     }
106
107     /**
108      * Returns a plugin name.
109      *
110      * Using this name other plugins will be able to access other plugins
111      * using Sabre_DAV_Server::getPlugin
112      *
113      * @return string
114      */
115     public function getPluginName() {
116
117         return 'caldav';
118
119     }
120
121     /**
122      * Returns a list of reports this plugin supports.
123      *
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
126      * implement them
127      *
128      * @param string $uri
129      * @return array
130      */
131     public function getSupportedReportSet($uri) {
132
133         $node = $this->server->tree->getNodeForPath($uri);
134
135         $reports = array();
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';
139         }
140         if ($node instanceof Sabre_CalDAV_ICalendar) {
141             $reports[] = '{' . self::NS_CALDAV . '}free-busy-query';
142         }
143         return $reports;
144
145     }
146
147     /**
148      * Initializes the plugin
149      *
150      * @param Sabre_DAV_Server $server
151      * @return void
152      */
153     public function initialize(Sabre_DAV_Server $server) {
154
155         $this->server = $server;
156
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'));
166
167         $server->xmlNamespaces[self::NS_CALDAV] = 'cal';
168         $server->xmlNamespaces[self::NS_CALENDARSERVER] = 'cs';
169
170         $server->propertyMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre_CalDAV_Property_SupportedCalendarComponentSet';
171
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';
178
179         array_push($server->protectedProperties,
180
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',
191
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',
197
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'
204
205         );
206     }
207
208     /**
209      * This function handles support for the MKCALENDAR method
210      *
211      * @param string $method
212      * @param string $uri
213      * @return bool
214      */
215     public function unknownMethod($method, $uri) {
216
217         switch ($method) {
218             case 'MKCALENDAR' :
219                 $this->httpMkCalendar($uri);
220                 // false is returned to stop the propagation of the
221                 // unknownMethod event.
222                 return false;
223             case 'POST' :
224                 // Checking if we're talking to an outbox
225                 try {
226                     $node = $this->server->tree->getNodeForPath($uri);
227                 } catch (Sabre_DAV_Exception_NotFound $e) {
228                     return;
229                 }
230                 if (!$node instanceof Sabre_CalDAV_Schedule_IOutbox)
231                     return;
232
233                 $this->outboxRequest($node);
234                 return false;
235
236         }
237
238     }
239
240     /**
241      * This functions handles REPORT requests specific to CalDAV
242      *
243      * @param string $reportName
244      * @param DOMNode $dom
245      * @return bool
246      */
247     public function report($reportName,$dom) {
248
249         switch($reportName) {
250             case '{'.self::NS_CALDAV.'}calendar-multiget' :
251                 $this->calendarMultiGetReport($dom);
252                 return false;
253             case '{'.self::NS_CALDAV.'}calendar-query' :
254                 $this->calendarQueryReport($dom);
255                 return false;
256             case '{'.self::NS_CALDAV.'}free-busy-query' :
257                 $this->freeBusyQueryReport($dom);
258                 return false;
259
260         }
261
262
263     }
264
265     /**
266      * This function handles the MKCALENDAR HTTP method, which creates
267      * a new calendar.
268      *
269      * @param string $uri
270      * @return void
271      */
272     public function httpMkCalendar($uri) {
273
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.');
279         //}
280
281         $body = $this->server->httpRequest->getBody(true);
282         $properties = array();
283
284         if ($body) {
285
286             $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
287
288             foreach($dom->firstChild->childNodes as $child) {
289
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;
293                 }
294
295             }
296         }
297
298         $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
299
300         $this->server->createCollection($uri,$resourceType,$properties);
301
302         $this->server->httpResponse->sendStatus(201);
303         $this->server->httpResponse->setHeader('Content-Length',0);
304     }
305
306     /**
307      * beforeGetProperties
308      *
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
311      * properties.
312      *
313      * @param string $path
314      * @param Sabre_DAV_INode $node
315      * @param array $requestedProperties
316      * @param array $returnedProperties
317      * @return void
318      */
319     public function beforeGetProperties($path, Sabre_DAV_INode $node, &$requestedProperties, &$returnedProperties) {
320
321         if ($node instanceof Sabre_DAVACL_IPrincipal) {
322
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);
330             }
331
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);
339             }
340
341             // calendar-user-address-set property
342             $calProp = '{' . self::NS_CALDAV . '}calendar-user-address-set';
343             if (in_array($calProp,$requestedProperties)) {
344
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);
349
350             }
351
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)) {
357
358                 $membership = $node->getGroupMembership();
359                 $readList = array();
360                 $writeList = array();
361
362                 foreach($membership as $group) {
363
364                     $groupNode = $this->server->tree->getNodeForPath($group);
365
366                     // If the node is either ap proxy-read or proxy-write
367                     // group, we grab the parent principal and add it to the
368                     // list.
369                     if ($groupNode instanceof Sabre_CalDAV_Principal_ProxyRead) {
370                         list($readList[]) = Sabre_DAV_URLUtil::splitPath($group);
371                     }
372                     if ($groupNode instanceof Sabre_CalDAV_Principal_ProxyWrite) {
373                         list($writeList[]) = Sabre_DAV_URLUtil::splitPath($group);
374                     }
375
376                 }
377                 if (in_array($propRead,$requestedProperties)) {
378                     unset($requestedProperties[$propRead]);
379                     $returnedProperties[200][$propRead] = new Sabre_DAV_Property_HrefList($readList);
380                 }
381                 if (in_array($propWrite,$requestedProperties)) {
382                     unset($requestedProperties[$propWrite]);
383                     $returnedProperties[200][$propWrite] = new Sabre_DAV_Property_HrefList($writeList);
384                 }
385
386             }
387
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);
395             }
396
397         } // instanceof IPrincipal
398
399         if ($node instanceof Sabre_CalDAV_Notifications_INode) {
400
401             $propertyName = '{' . self::NS_CALENDARSERVER . '}notificationtype';
402             if (($index = array_search($propertyName, $requestedProperties)) !== false) {
403
404                 $returnedProperties[200][$propertyName] =
405                     $node->getNotificationType();
406
407                 unset($requestedProperties[$index]);
408
409             }
410
411         } // instanceof Notifications_INode
412
413
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]);
421                 $val = $node->get();
422                 if (is_resource($val))
423                     $val = stream_get_contents($val);
424
425                 // Taking out \r to not screw up the xml output
426                 $returnedProperties[200][$calDataProp] = str_replace("\r","", $val);
427
428             }
429         }
430
431     }
432
433     /**
434      * This function handles the calendar-multiget REPORT.
435      *
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.
438      *
439      * @param DOMNode $dom
440      * @return void
441      */
442     public function calendarMultiGetReport($dom) {
443
444         $properties = array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild));
445         $hrefElems = $dom->getElementsByTagNameNS('DAV:','href');
446
447         $xpath = new DOMXPath($dom);
448         $xpath->registerNameSpace('cal',Sabre_CalDAV_Plugin::NS_CALDAV);
449         $xpath->registerNameSpace('dav','DAV:');
450
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');
458             }
459             $start = Sabre_VObject_DateTimeParser::parseDateTime($start);
460             $end = Sabre_VObject_DateTimeParser::parseDateTime($end);
461
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.');
464             }
465
466             $expand = true;
467
468         } else {
469
470             $expand = false;
471
472         }
473
474         foreach($hrefElems as $elem) {
475             $uri = $this->server->calculateUri($elem->nodeValue);
476             list($objProps) = $this->server->getPropertiesForPath($uri,$properties);
477
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();
482             }
483
484             $propertyList[]=$objProps;
485
486         }
487
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));
491
492     }
493
494     /**
495      * This function handles the calendar-query REPORT
496      *
497      * This report is used by clients to request calendar objects based on
498      * complex conditions.
499      *
500      * @param DOMNode $dom
501      * @return void
502      */
503     public function calendarQueryReport($dom) {
504
505         $parser = new Sabre_CalDAV_CalendarQueryParser($dom);
506         $parser->parse();
507
508         $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
509         $depth = $this->server->getHTTPDepth(0);
510
511         // The default result is an empty array
512         $result = array();
513
514         // The calendarobject was requested directly. In this case we handle
515         // this locally.
516         if ($depth == 0 && $node instanceof Sabre_CalDAV_ICalendarObject) {
517
518             $requestedCalendarData = true;
519             $requestedProperties = $parser->requestedProperties;
520
521             if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
522
523                 // We always retrieve calendar-data, as we need it for filtering.
524                 $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data';
525
526                 // If calendar-data wasn't explicitly requested, we need to remove
527                 // it after processing.
528                 $requestedCalendarData = false;
529             }
530
531             $properties = $this->server->getPropertiesForPath(
532                 $this->server->getRequestUri(),
533                 $requestedProperties,
534                 0
535             );
536
537             // This array should have only 1 element, the first calendar
538             // object.
539             $properties = current($properties);
540
541             // If there wasn't any calendar-data returned somehow, we ignore
542             // this.
543             if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
544
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)) {
548
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']);
553                     } else {
554                         if ($parser->expand) {
555                             $vObject->expand($parser->expand['start'], $parser->expand['end']);
556                             $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
557                         }
558                     }
559
560                     $result = array($properties);
561
562                 }
563
564             }
565
566         }
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) {
570
571             $nodePaths = $node->calendarQuery($parser->filters);
572
573             foreach($nodePaths as $path) {
574
575                 list($properties) =
576                     $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $parser->requestedProperties);
577
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();
583                 }
584
585                 $result[] = $properties;
586
587             }
588
589         }
590
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));
594
595     }
596
597     /**
598      * This method is responsible for parsing the request and generating the
599      * response for the CALDAV:free-busy-query REPORT.
600      *
601      * @param DOMNode $dom
602      * @return void
603      */
604     protected function freeBusyQueryReport(DOMNode $dom) {
605
606         $start = null;
607         $end = null;
608
609         foreach($dom->firstChild->childNodes as $childNode) {
610
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');
615                 break;
616             }
617
618         }
619         if ($start) {
620             $start = Sabre_VObject_DateTimeParser::parseDateTime($start);
621         }
622         if ($end) {
623             $end = Sabre_VObject_DateTimeParser::parseDateTime($end);
624         }
625
626         if (!$start && !$end) {
627             throw new Sabre_DAV_Exception_BadRequest('The freebusy report must have a time-range filter');
628         }
629         $acl = $this->server->getPlugin('acl');
630
631         if (!$acl) {
632             throw new Sabre_DAV_Exception('The ACL plugin must be loaded for free-busy queries to work');
633         }
634         $uri = $this->server->getRequestUri();
635         $acl->checkPrivileges($uri,'{' . self::NS_CALDAV . '}read-free-busy');
636
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');
640         }
641
642         $objects = array_map(function($child) {
643             $obj = $child->get();
644             if (is_resource($obj)) {
645                 $obj = stream_get_contents($obj);
646             }
647             return $obj;
648         }, $calendar->getChildren());
649
650         $generator = new Sabre_VObject_FreeBusyGenerator();
651         $generator->setObjects($objects);
652         $generator->setTimeRange($start, $end);
653         $result = $generator->getResult();
654         $result = $result->serialize();
655
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);
660
661     }
662
663     /**
664      * This method is triggered before a file gets updated with new content.
665      *
666      * This plugin uses this method to ensure that CalDAV objects receive
667      * valid calendar data.
668      *
669      * @param string $path
670      * @param Sabre_DAV_IFile $node
671      * @param resource $data
672      * @return void
673      */
674     public function beforeWriteContent($path, Sabre_DAV_IFile $node, &$data) {
675
676         if (!$node instanceof Sabre_CalDAV_ICalendarObject)
677             return;
678
679         $this->validateICalendar($data, $path);
680
681     }
682
683     /**
684      * This method is triggered before a new file is created.
685      *
686      * This plugin uses this method to ensure that newly created calendar
687      * objects contain valid calendar data.
688      *
689      * @param string $path
690      * @param resource $data
691      * @param Sabre_DAV_ICollection $parentNode
692      * @return void
693      */
694     public function beforeCreateFile($path, &$data, Sabre_DAV_ICollection $parentNode) {
695
696         if (!$parentNode instanceof Sabre_CalDAV_Calendar)
697             return;
698
699         $this->validateICalendar($data, $path);
700
701     }
702
703     /**
704      * This event is triggered before any HTTP request is handled.
705      *
706      * We use this to intercept GET calls to notification nodes, and return the
707      * proper response.
708      * 
709      * @param string $method 
710      * @param string $path 
711      * @return void 
712      */
713     public function beforeMethod($method, $path) {
714
715         if ($method!=='GET') return;
716
717         try {
718             $node = $this->server->tree->getNodeForPath($path);
719         } catch (Sabre_DAV_Exception_NotFound $e) {
720             return;
721         }
722
723         if (!$node instanceof Sabre_CalDAV_Notifications_INode)
724             return;
725
726         $dom = new DOMDocument('1.0', 'UTF-8');
727         $dom->formatOutput = true;
728
729         $root = $dom->createElement('cs:notification');
730         foreach($this->server->xmlNamespaces as $namespace => $prefix) {
731             $root->setAttribute('xmlns:' . $prefix, $namespace);
732         }
733
734         $dom->appendChild($root);
735         $node->getNotificationType()->serializeBody($this->server, $root);
736
737         $this->server->httpResponse->setHeader('Content-Type','application/xml');
738         $this->server->httpResponse->sendStatus(200);
739         $this->server->httpResponse->sendBody($dom->saveXML());
740
741         return false;
742
743     }
744
745     /**
746      * Checks if the submitted iCalendar data is in fact, valid.
747      *
748      * An exception is thrown if it's not.
749      *
750      * @param resource|string $data
751      * @param string $path
752      * @return void
753      */
754     protected function validateICalendar(&$data, $path) {
755
756         // If it's a stream, we convert it to a string first.
757         if (is_resource($data)) {
758             $data = stream_get_contents($data);
759         }
760
761         // Converting the data to unicode, if needed.
762         $data = Sabre_DAV_StringUtil::ensureUTF8($data);
763
764         try {
765
766             $vobj = Sabre_VObject_Reader::read($data);
767
768         } catch (Sabre_VObject_ParseException $e) {
769
770             throw new Sabre_DAV_Exception_UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
771
772         }
773
774         if ($vobj->name !== 'VCALENDAR') {
775             throw new Sabre_DAV_Exception_UnsupportedMediaType('This collection can only support iCalendar objects.');
776         }
777
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();
782
783         $foundType = null;
784         $foundUID = null;
785         foreach($vobj->getComponents() as $component) {
786             switch($component->name) {
787                 case 'VTIMEZONE' :
788                     continue 2;
789                 case 'VEVENT' :
790                 case 'VTODO' :
791                 case 'VJOURNAL' :
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);
796                         }
797                         if (!isset($component->UID)) {
798                             throw new Sabre_DAV_Exception_BadRequest('Every ' . $component->name . ' component must have an UID');
799                         }
800                         $foundUID = (string)$component->UID;
801                     } else {
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);
804                         }
805                         if ($foundUID !== (string)$component->UID) {
806                             throw new Sabre_DAV_Exception_BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
807                         }
808                     }
809                     break;
810                 default :
811                     throw new Sabre_DAV_Exception_BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
812
813             }
814         }
815         if (!$foundType)
816             throw new Sabre_DAV_Exception_BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
817
818     }
819
820     /**
821      * This method handles POST requests to the schedule-outbox
822      *
823      * @param Sabre_CalDAV_Schedule_IOutbox $outboxNode
824      * @return void
825      */
826     public function outboxRequest(Sabre_CalDAV_Schedule_IOutbox $outboxNode) {
827
828         $originator = $this->server->httpRequest->getHeader('Originator');
829         $recipients = $this->server->httpRequest->getHeader('Recipient');
830
831         if (!$originator) {
832             throw new Sabre_DAV_Exception_BadRequest('The Originator: header must be specified when making POST requests');
833         }
834         if (!$recipients) {
835             throw new Sabre_DAV_Exception_BadRequest('The Recipient: header must be specified when making POST requests');
836         }
837
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');
840         }
841         $originator = substr($originator,7);
842
843         $recipients = explode(',',$recipients);
844         foreach($recipients as $k=>$recipient) {
845
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');
849             }
850             $recipient = substr($recipient, 7);
851             $recipients[$k] = $recipient;
852         }
853
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',
859         ));
860
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();
864         }
865
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');
868         }
869
870         try {
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());
874         }
875
876         // Checking for the object type
877         $componentType = null;
878         foreach($vObject->getComponents() as $component) {
879             if ($component->name !== 'VTIMEZONE') {
880                 $componentType = $component->name;
881                 break;
882             }
883         }
884         if (is_null($componentType)) {
885             throw new Sabre_DAV_Exception_BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
886         }
887
888         // Validating the METHOD
889         $method = strtoupper((string)$vObject->METHOD);
890         if (!$method) {
891             throw new Sabre_DAV_Exception_BadRequest('A METHOD property must be specified in iTIP messages');
892         }
893
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));
899         } else {
900             throw new Sabre_DAV_Exception_NotImplemented('This iTIP method is currently not implemented');
901         }
902
903     }
904
905     /**
906      * Sends an iMIP message by email.
907      *
908      * This method must return an array with status codes per recipient.
909      * This should look something like:
910      *
911      * array(
912      *    'user1@example.org' => '2.0;Success'
913      * )
914      *
915      * Formatting for this status code can be found at:
916      * https://tools.ietf.org/html/rfc5545#section-3.8.8.3
917      *
918      * A list of valid status codes can be found at:
919      * https://tools.ietf.org/html/rfc5546#section-3.6
920      *
921      * @param string $originator
922      * @param array $recipients
923      * @param Sabre_VObject_Component $vObject
924      * @return array
925      */
926     protected function iMIPMessage($originator, array $recipients, Sabre_VObject_Component $vObject, $principal) {
927
928         if (!$this->imipHandler) {
929             $resultStatus = '5.2;This server does not support this operation';
930         } else {
931             $this->imipHandler->sendMessage($originator, $recipients, $vObject, $principal);
932             $resultStatus = '2.0;Success';
933         }
934
935         $result = array();
936         foreach($recipients as $recipient) {
937             $result[$recipient] = $resultStatus;
938         }
939
940         return $result;
941
942     }
943
944     /**
945      * Generates a schedule-response XML body
946      *
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
949      * the value.
950      *
951      * @param array $recipients
952      * @return string
953      */
954     public function generateScheduleResponse(array $recipients) {
955
956         $dom = new DOMDocument('1.0','utf-8');
957         $dom->formatOutput = true;
958         $xscheduleResponse = $dom->createElement('cal:schedule-response');
959         $dom->appendChild($xscheduleResponse);
960
961         foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
962
963             $xscheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
964
965         }
966
967         foreach($recipients as $recipient=>$status) {
968             $xresponse = $dom->createElement('cal:response');
969
970             $xrecipient = $dom->createElement('cal:recipient');
971             $xrecipient->appendChild($dom->createTextNode($recipient));
972             $xresponse->appendChild($xrecipient);
973
974             $xrequestStatus = $dom->createElement('cal:request-status');
975             $xrequestStatus->appendChild($dom->createTextNode($status));
976             $xresponse->appendChild($xrequestStatus);
977
978             $xscheduleResponse->appendChild($xresponse);
979
980         }
981
982         return $dom->saveXML();
983
984     }
985
986     /**
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.
990      *
991      * @param Sabre_DAV_INode $node
992      * @param string $output
993      * @return bool
994      */
995     public function htmlActionsPanel(Sabre_DAV_INode $node, &$output) {
996
997         if (!$node instanceof Sabre_CalDAV_UserCalendars)
998             return;
999
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" />
1006             </form>
1007             </td></tr>';
1008
1009         return false;
1010
1011     }
1012
1013     /**
1014      * This method allows us to intercept the 'mkcalendar' sabreAction. This
1015      * action enables the user to create new calendars from the browser plugin.
1016      *
1017      * @param string $uri
1018      * @param string $action
1019      * @param array $postVars
1020      * @return bool
1021      */
1022     public function browserPostAction($uri, $action, array $postVars) {
1023
1024         if ($action!=='mkcalendar')
1025             return;
1026
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'];
1031         }
1032         $this->server->createCollection($uri . '/' . $postVars['name'],$resourceType,$properties);
1033         return false;
1034
1035     }
1036
1037 }