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