1<?php
2
3namespace SabreForRainLoop\CalDAV;
4
5use SabreForRainLoop\DAV;
6use SabreForRainLoop\DAVACL;
7use SabreForRainLoop\VObject;
8
9/**
10 * CalDAV plugin
11 *
12 * This plugin provides functionality added by CalDAV (RFC 4791)
13 * It implements new reports, and the MKCALENDAR method.
14 *
15 * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
16 * @author Evert Pot (http://evertpot.com/)
17 * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
18 */
19class Plugin extends DAV\ServerPlugin {
20
21    /**
22     * This is the official CalDAV namespace
23     */
24    const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
25
26    /**
27     * This is the namespace for the proprietary calendarserver extensions
28     */
29    const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
30
31    /**
32     * The hardcoded root for calendar objects. It is unfortunate
33     * that we're stuck with it, but it will have to do for now
34     */
35    const CALENDAR_ROOT = 'calendars';
36
37    /**
38     * Reference to server object
39     *
40     * @var DAV\Server
41     */
42    protected $server;
43
44    /**
45     * The email handler for invites and other scheduling messages.
46     *
47     * @var Schedule\IMip
48     */
49    protected $imipHandler;
50
51    /**
52     * Sets the iMIP handler.
53     *
54     * iMIP = The email transport of iCalendar scheduling messages. Setting
55     * this is optional, but if you want the server to allow invites to be sent
56     * out, you must set a handler.
57     *
58     * Specifically iCal will plain assume that the server supports this. If
59     * the server doesn't, iCal will display errors when inviting people to
60     * events.
61     *
62     * @param Schedule\IMip $imipHandler
63     * @return void
64     */
65    public function setIMipHandler(Schedule\IMip $imipHandler) {
66
67        $this->imipHandler = $imipHandler;
68
69    }
70
71    /**
72     * Use this method to tell the server this plugin defines additional
73     * HTTP methods.
74     *
75     * This method is passed a uri. It should only return HTTP methods that are
76     * available for the specified uri.
77     *
78     * @param string $uri
79     * @return array
80     */
81    public function getHTTPMethods($uri) {
82
83        // The MKCALENDAR is only available on unmapped uri's, whose
84        // parents extend IExtendedCollection
85        list($parent, $name) = DAV\URLUtil::splitPath($uri);
86
87        $node = $this->server->tree->getNodeForPath($parent);
88
89        if ($node instanceof DAV\IExtendedCollection) {
90            try {
91                $node->getChild($name);
92            } catch (DAV\Exception\NotFound $e) {
93                return array('MKCALENDAR');
94            }
95        }
96        return array();
97
98    }
99
100    /**
101     * Returns a list of features for the DAV: HTTP header.
102     *
103     * @return array
104     */
105    public function getFeatures() {
106
107        return array('calendar-access', 'calendar-proxy');
108
109    }
110
111    /**
112     * Returns a plugin name.
113     *
114     * Using this name other plugins will be able to access other plugins
115     * using DAV\Server::getPlugin
116     *
117     * @return string
118     */
119    public function getPluginName() {
120
121        return 'caldav';
122
123    }
124
125    /**
126     * Returns a list of reports this plugin supports.
127     *
128     * This will be used in the {DAV:}supported-report-set property.
129     * Note that you still need to subscribe to the 'report' event to actually
130     * implement them
131     *
132     * @param string $uri
133     * @return array
134     */
135    public function getSupportedReportSet($uri) {
136
137        $node = $this->server->tree->getNodeForPath($uri);
138
139        $reports = array();
140        if ($node instanceof ICalendar || $node instanceof ICalendarObject) {
141            $reports[] = '{' . self::NS_CALDAV . '}calendar-multiget';
142            $reports[] = '{' . self::NS_CALDAV . '}calendar-query';
143        }
144        if ($node instanceof ICalendar) {
145            $reports[] = '{' . self::NS_CALDAV . '}free-busy-query';
146        }
147        return $reports;
148
149    }
150
151    /**
152     * Initializes the plugin
153     *
154     * @param DAV\Server $server
155     * @return void
156     */
157    public function initialize(DAV\Server $server) {
158
159        $this->server = $server;
160
161        $server->subscribeEvent('unknownMethod',array($this,'unknownMethod'));
162        //$server->subscribeEvent('unknownMethod',array($this,'unknownMethod2'),1000);
163        $server->subscribeEvent('report',array($this,'report'));
164        $server->subscribeEvent('beforeGetProperties',array($this,'beforeGetProperties'));
165        $server->subscribeEvent('onHTMLActionsPanel', array($this,'htmlActionsPanel'));
166        $server->subscribeEvent('onBrowserPostAction', array($this,'browserPostAction'));
167        $server->subscribeEvent('beforeWriteContent', array($this, 'beforeWriteContent'));
168        $server->subscribeEvent('beforeCreateFile', array($this, 'beforeCreateFile'));
169        $server->subscribeEvent('beforeMethod', array($this,'beforeMethod'));
170
171        $server->xmlNamespaces[self::NS_CALDAV] = 'cal';
172        $server->xmlNamespaces[self::NS_CALENDARSERVER] = 'cs';
173
174        $server->propertyMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'SabreForRainLoop\\CalDAV\\Property\\SupportedCalendarComponentSet';
175        $server->propertyMap['{' . self::NS_CALDAV . '}schedule-calendar-transp'] = 'SabreForRainLoop\\CalDAV\\Property\\ScheduleCalendarTransp';
176
177        $server->resourceTypeMapping['\\SabreForRainLoop\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar';
178        $server->resourceTypeMapping['\\SabreForRainLoop\\CalDAV\\Schedule\\IOutbox'] = '{urn:ietf:params:xml:ns:caldav}schedule-outbox';
179        $server->resourceTypeMapping['\\SabreForRainLoop\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read';
180        $server->resourceTypeMapping['\\SabreForRainLoop\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write';
181        $server->resourceTypeMapping['\\SabreForRainLoop\\CalDAV\\Notifications\\ICollection'] = '{' . self::NS_CALENDARSERVER . '}notification';
182
183        array_push($server->protectedProperties,
184
185            '{' . self::NS_CALDAV . '}supported-calendar-component-set',
186            '{' . self::NS_CALDAV . '}supported-calendar-data',
187            '{' . self::NS_CALDAV . '}max-resource-size',
188            '{' . self::NS_CALDAV . '}min-date-time',
189            '{' . self::NS_CALDAV . '}max-date-time',
190            '{' . self::NS_CALDAV . '}max-instances',
191            '{' . self::NS_CALDAV . '}max-attendees-per-instance',
192            '{' . self::NS_CALDAV . '}calendar-home-set',
193            '{' . self::NS_CALDAV . '}supported-collation-set',
194            '{' . self::NS_CALDAV . '}calendar-data',
195
196            // scheduling extension
197            '{' . self::NS_CALDAV . '}schedule-inbox-URL',
198            '{' . self::NS_CALDAV . '}schedule-outbox-URL',
199            '{' . self::NS_CALDAV . '}calendar-user-address-set',
200            '{' . self::NS_CALDAV . '}calendar-user-type',
201
202            // CalendarServer extensions
203            '{' . self::NS_CALENDARSERVER . '}getctag',
204            '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for',
205            '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for',
206            '{' . self::NS_CALENDARSERVER . '}notification-URL',
207            '{' . self::NS_CALENDARSERVER . '}notificationtype'
208
209        );
210    }
211
212    /**
213     * This function handles support for the MKCALENDAR method
214     *
215     * @param string $method
216     * @param string $uri
217     * @return bool
218     */
219    public function unknownMethod($method, $uri) {
220
221        switch ($method) {
222            case 'MKCALENDAR' :
223                $this->httpMkCalendar($uri);
224                // false is returned to stop the propagation of the
225                // unknownMethod event.
226                return false;
227            case 'POST' :
228
229                // Checking if this is a text/calendar content type
230                $contentType = $this->server->httpRequest->getHeader('Content-Type');
231                if (strpos($contentType, 'text/calendar')!==0) {
232                    return;
233                }
234
235                // Checking if we're talking to an outbox
236                try {
237                    $node = $this->server->tree->getNodeForPath($uri);
238                } catch (DAV\Exception\NotFound $e) {
239                    return;
240                }
241                if (!$node instanceof Schedule\IOutbox)
242                    return;
243
244                $this->outboxRequest($node, $uri);
245                return false;
246
247        }
248
249    }
250
251    /**
252     * This functions handles REPORT requests specific to CalDAV
253     *
254     * @param string $reportName
255     * @param \DOMNode $dom
256     * @return bool
257     */
258    public function report($reportName,$dom) {
259
260        switch($reportName) {
261            case '{'.self::NS_CALDAV.'}calendar-multiget' :
262                $this->calendarMultiGetReport($dom);
263                return false;
264            case '{'.self::NS_CALDAV.'}calendar-query' :
265                $this->calendarQueryReport($dom);
266                return false;
267            case '{'.self::NS_CALDAV.'}free-busy-query' :
268                $this->freeBusyQueryReport($dom);
269                return false;
270
271        }
272
273
274    }
275
276    /**
277     * This function handles the MKCALENDAR HTTP method, which creates
278     * a new calendar.
279     *
280     * @param string $uri
281     * @return void
282     */
283    public function httpMkCalendar($uri) {
284
285        // Due to unforgivable bugs in iCal, we're completely disabling MKCALENDAR support
286        // for clients matching iCal in the user agent
287        //$ua = $this->server->httpRequest->getHeader('User-Agent');
288        //if (strpos($ua,'iCal/')!==false) {
289        //    throw new \SabreForRainLoop\DAV\Exception\Forbidden('iCal has major bugs in it\'s RFC3744 support. Therefore we are left with no other choice but disabling this feature.');
290        //}
291
292        $body = $this->server->httpRequest->getBody(true);
293        $properties = array();
294
295        if ($body) {
296
297            $dom = DAV\XMLUtil::loadDOMDocument($body);
298
299            foreach($dom->firstChild->childNodes as $child) {
300
301                if (DAV\XMLUtil::toClarkNotation($child)!=='{DAV:}set') continue;
302                foreach(DAV\XMLUtil::parseProperties($child,$this->server->propertyMap) as $k=>$prop) {
303                    $properties[$k] = $prop;
304                }
305
306            }
307        }
308
309        $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
310
311        $this->server->createCollection($uri,$resourceType,$properties);
312
313        $this->server->httpResponse->sendStatus(201);
314        $this->server->httpResponse->setHeader('Content-Length',0);
315    }
316
317    /**
318     * beforeGetProperties
319     *
320     * This method handler is invoked before any after properties for a
321     * resource are fetched. This allows us to add in any CalDAV specific
322     * properties.
323     *
324     * @param string $path
325     * @param DAV\INode $node
326     * @param array $requestedProperties
327     * @param array $returnedProperties
328     * @return void
329     */
330    public function beforeGetProperties($path, DAV\INode $node, &$requestedProperties, &$returnedProperties) {
331
332        if ($node instanceof DAVACL\IPrincipal) {
333
334            // calendar-home-set property
335            $calHome = '{' . self::NS_CALDAV . '}calendar-home-set';
336            if (in_array($calHome,$requestedProperties)) {
337                $principalId = $node->getName();
338                $calendarHomePath = self::CALENDAR_ROOT . '/' . $principalId . '/';
339
340                unset($requestedProperties[array_search($calHome, $requestedProperties)]);
341                $returnedProperties[200][$calHome] = new DAV\Property\Href($calendarHomePath);
342
343            }
344
345            // schedule-outbox-URL property
346            $scheduleProp = '{' . self::NS_CALDAV . '}schedule-outbox-URL';
347            if (in_array($scheduleProp,$requestedProperties)) {
348                $principalId = $node->getName();
349                $outboxPath = self::CALENDAR_ROOT . '/' . $principalId . '/outbox';
350
351                unset($requestedProperties[array_search($scheduleProp, $requestedProperties)]);
352                $returnedProperties[200][$scheduleProp] = new DAV\Property\Href($outboxPath);
353
354            }
355
356            // calendar-user-address-set property
357            $calProp = '{' . self::NS_CALDAV . '}calendar-user-address-set';
358            if (in_array($calProp,$requestedProperties)) {
359
360                $addresses = $node->getAlternateUriSet();
361                $addresses[] = $this->server->getBaseUri() . DAV\URLUtil::encodePath($node->getPrincipalUrl() . '/');
362                unset($requestedProperties[array_search($calProp, $requestedProperties)]);
363                $returnedProperties[200][$calProp] = new DAV\Property\HrefList($addresses, false);
364
365            }
366
367            // These two properties are shortcuts for ical to easily find
368            // other principals this principal has access to.
369            $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for';
370            $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for';
371            if (in_array($propRead,$requestedProperties) || in_array($propWrite,$requestedProperties)) {
372
373                $aclPlugin = $this->server->getPlugin('acl');
374                $membership = $aclPlugin->getPrincipalMembership($path);
375                $readList = array();
376                $writeList = array();
377
378                foreach($membership as $group) {
379
380                    $groupNode = $this->server->tree->getNodeForPath($group);
381
382                    // If the node is either ap proxy-read or proxy-write
383                    // group, we grab the parent principal and add it to the
384                    // list.
385                    if ($groupNode instanceof Principal\IProxyRead) {
386                        list($readList[]) = DAV\URLUtil::splitPath($group);
387                    }
388                    if ($groupNode instanceof Principal\IProxyWrite) {
389                        list($writeList[]) = DAV\URLUtil::splitPath($group);
390                    }
391
392                }
393                if (in_array($propRead,$requestedProperties)) {
394                    unset($requestedProperties[$propRead]);
395                    $returnedProperties[200][$propRead] = new DAV\Property\HrefList($readList);
396                }
397                if (in_array($propWrite,$requestedProperties)) {
398                    unset($requestedProperties[$propWrite]);
399                    $returnedProperties[200][$propWrite] = new DAV\Property\HrefList($writeList);
400                }
401
402            }
403
404            // notification-URL property
405            $notificationUrl = '{' . self::NS_CALENDARSERVER . '}notification-URL';
406            if (($index = array_search($notificationUrl, $requestedProperties)) !== false) {
407                $principalId = $node->getName();
408                $calendarHomePath = 'calendars/' . $principalId . '/notifications/';
409                unset($requestedProperties[$index]);
410                $returnedProperties[200][$notificationUrl] = new DAV\Property\Href($calendarHomePath);
411            }
412
413        } // instanceof IPrincipal
414
415        if ($node instanceof Notifications\INode) {
416
417            $propertyName = '{' . self::NS_CALENDARSERVER . '}notificationtype';
418            if (($index = array_search($propertyName, $requestedProperties)) !== false) {
419
420                $returnedProperties[200][$propertyName] =
421                    $node->getNotificationType();
422
423                unset($requestedProperties[$index]);
424
425            }
426
427        } // instanceof Notifications_INode
428
429
430        if ($node instanceof ICalendarObject) {
431            // The calendar-data property is not supposed to be a 'real'
432            // property, but in large chunks of the spec it does act as such.
433            // Therefore we simply expose it as a property.
434            $calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data';
435            if (in_array($calDataProp, $requestedProperties)) {
436                unset($requestedProperties[$calDataProp]);
437                $val = $node->get();
438                if (is_resource($val))
439                    $val = stream_get_contents($val);
440
441                // Taking out \r to not screw up the xml output
442                $returnedProperties[200][$calDataProp] = str_replace("\r","", $val);
443
444            }
445        }
446
447    }
448
449    /**
450     * This function handles the calendar-multiget REPORT.
451     *
452     * This report is used by the client to fetch the content of a series
453     * of urls. Effectively avoiding a lot of redundant requests.
454     *
455     * @param \DOMNode $dom
456     * @return void
457     */
458    public function calendarMultiGetReport($dom) {
459
460        $properties = array_keys(DAV\XMLUtil::parseProperties($dom->firstChild));
461        $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href');
462
463        $xpath = new \DOMXPath($dom);
464        $xpath->registerNameSpace('cal',Plugin::NS_CALDAV);
465        $xpath->registerNameSpace('dav','urn:DAV');
466
467        $expand = $xpath->query('/cal:calendar-multiget/dav:prop/cal:calendar-data/cal:expand');
468        if ($expand->length>0) {
469            $expandElem = $expand->item(0);
470            $start = $expandElem->getAttribute('start');
471            $end = $expandElem->getAttribute('end');
472            if(!$start || !$end) {
473                throw new DAV\Exception\BadRequest('The "start" and "end" attributes are required for the CALDAV:expand element');
474            }
475            $start = VObject\DateTimeParser::parseDateTime($start);
476            $end = VObject\DateTimeParser::parseDateTime($end);
477
478            if ($end <= $start) {
479                throw new DAV\Exception\BadRequest('The end-date must be larger than the start-date in the expand element.');
480            }
481
482            $expand = true;
483
484        } else {
485
486            $expand = false;
487
488        }
489
490        foreach($hrefElems as $elem) {
491            $uri = $this->server->calculateUri($elem->nodeValue);
492            list($objProps) = $this->server->getPropertiesForPath($uri,$properties);
493
494            if ($expand && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) {
495                $vObject = VObject\Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']);
496                $vObject->expand($start, $end);
497                $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
498            }
499
500            $propertyList[]=$objProps;
501
502        }
503
504        $prefer = $this->server->getHTTPPRefer();
505
506        $this->server->httpResponse->sendStatus(207);
507        $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
508        $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
509        $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList, $prefer['return-minimal']));
510
511    }
512
513    /**
514     * This function handles the calendar-query REPORT
515     *
516     * This report is used by clients to request calendar objects based on
517     * complex conditions.
518     *
519     * @param \DOMNode $dom
520     * @return void
521     */
522    public function calendarQueryReport($dom) {
523
524        $parser = new CalendarQueryParser($dom);
525        $parser->parse();
526
527        $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
528        $depth = $this->server->getHTTPDepth(0);
529
530        // The default result is an empty array
531        $result = array();
532
533        // The calendarobject was requested directly. In this case we handle
534        // this locally.
535        if ($depth == 0 && $node instanceof ICalendarObject) {
536
537            $requestedCalendarData = true;
538            $requestedProperties = $parser->requestedProperties;
539
540            if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
541
542                // We always retrieve calendar-data, as we need it for filtering.
543                $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data';
544
545                // If calendar-data wasn't explicitly requested, we need to remove
546                // it after processing.
547                $requestedCalendarData = false;
548            }
549
550            $properties = $this->server->getPropertiesForPath(
551                $this->server->getRequestUri(),
552                $requestedProperties,
553                0
554            );
555
556            // This array should have only 1 element, the first calendar
557            // object.
558            $properties = current($properties);
559
560            // If there wasn't any calendar-data returned somehow, we ignore
561            // this.
562            if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
563
564                $validator = new CalendarQueryValidator();
565
566                $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
567                if ($validator->validate($vObject,$parser->filters)) {
568
569                    // If the client didn't require the calendar-data property,
570                    // we won't give it back.
571                    if (!$requestedCalendarData) {
572                        unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
573                    } else {
574                        if ($parser->expand) {
575                            $vObject->expand($parser->expand['start'], $parser->expand['end']);
576                            $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
577                        }
578                    }
579
580                    $result = array($properties);
581
582                }
583
584            }
585
586        }
587        // If we're dealing with a calendar, the calendar itself is responsible
588        // for the calendar-query.
589        if ($node instanceof ICalendar && $depth = 1) {
590
591            $nodePaths = $node->calendarQuery($parser->filters);
592
593            foreach($nodePaths as $path) {
594
595                list($properties) =
596                    $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $parser->requestedProperties);
597
598                if ($parser->expand) {
599                    // We need to do some post-processing
600                    $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
601                    $vObject->expand($parser->expand['start'], $parser->expand['end']);
602                    $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
603                }
604
605                $result[] = $properties;
606
607            }
608
609        }
610
611        $prefer = $this->server->getHTTPPRefer();
612
613        $this->server->httpResponse->sendStatus(207);
614        $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
615        $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
616        $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result, $prefer['return-minimal']));
617
618    }
619
620    /**
621     * This method is responsible for parsing the request and generating the
622     * response for the CALDAV:free-busy-query REPORT.
623     *
624     * @param \DOMNode $dom
625     * @return void
626     */
627    protected function freeBusyQueryReport(\DOMNode $dom) {
628
629        $start = null;
630        $end = null;
631
632        foreach($dom->firstChild->childNodes as $childNode) {
633
634            $clark = DAV\XMLUtil::toClarkNotation($childNode);
635            if ($clark == '{' . self::NS_CALDAV . '}time-range') {
636                $start = $childNode->getAttribute('start');
637                $end = $childNode->getAttribute('end');
638                break;
639            }
640
641        }
642        if ($start) {
643            $start = VObject\DateTimeParser::parseDateTime($start);
644        }
645        if ($end) {
646            $end = VObject\DateTimeParser::parseDateTime($end);
647        }
648
649        if (!$start && !$end) {
650            throw new DAV\Exception\BadRequest('The freebusy report must have a time-range filter');
651        }
652        $acl = $this->server->getPlugin('acl');
653
654        if (!$acl) {
655            throw new DAV\Exception('The ACL plugin must be loaded for free-busy queries to work');
656        }
657        $uri = $this->server->getRequestUri();
658        $acl->checkPrivileges($uri,'{' . self::NS_CALDAV . '}read-free-busy');
659
660        $calendar = $this->server->tree->getNodeForPath($uri);
661        if (!$calendar instanceof ICalendar) {
662            throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars');
663        }
664
665        // Doing a calendar-query first, to make sure we get the most
666        // performance.
667        $urls = $calendar->calendarQuery(array(
668            'name' => 'VCALENDAR',
669            'comp-filters' => array(
670                array(
671                    'name' => 'VEVENT',
672                    'comp-filters' => array(),
673                    'prop-filters' => array(),
674                    'is-not-defined' => false,
675                    'time-range' => array(
676                        'start' => $start,
677                        'end' => $end,
678                    ),
679                ),
680            ),
681            'prop-filters' => array(),
682            'is-not-defined' => false,
683            'time-range' => null,
684        ));
685
686        $objects = array_map(function($url) use ($calendar) {
687            $obj = $calendar->getChild($url)->get();
688            return $obj;
689        }, $urls);
690
691        $generator = new VObject\FreeBusyGenerator();
692        $generator->setObjects($objects);
693        $generator->setTimeRange($start, $end);
694        $result = $generator->getResult();
695        $result = $result->serialize();
696
697        $this->server->httpResponse->sendStatus(200);
698        $this->server->httpResponse->setHeader('Content-Type', 'text/calendar');
699        $this->server->httpResponse->setHeader('Content-Length', strlen($result));
700        $this->server->httpResponse->sendBody($result);
701
702    }
703
704    /**
705     * This method is triggered before a file gets updated with new content.
706     *
707     * This plugin uses this method to ensure that CalDAV objects receive
708     * valid calendar data.
709     *
710     * @param string $path
711     * @param DAV\IFile $node
712     * @param resource $data
713     * @return void
714     */
715    public function beforeWriteContent($path, DAV\IFile $node, &$data) {
716
717        if (!$node instanceof ICalendarObject)
718            return;
719
720        $this->validateICalendar($data, $path);
721
722    }
723
724    /**
725     * This method is triggered before a new file is created.
726     *
727     * This plugin uses this method to ensure that newly created calendar
728     * objects contain valid calendar data.
729     *
730     * @param string $path
731     * @param resource $data
732     * @param DAV\ICollection $parentNode
733     * @return void
734     */
735    public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode) {
736
737        if (!$parentNode instanceof Calendar)
738            return;
739
740        $this->validateICalendar($data, $path);
741
742    }
743
744    /**
745     * This event is triggered before any HTTP request is handled.
746     *
747     * We use this to intercept GET calls to notification nodes, and return the
748     * proper response.
749     *
750     * @param string $method
751     * @param string $path
752     * @return void
753     */
754    public function beforeMethod($method, $path) {
755
756        if ($method!=='GET') return;
757
758        try {
759            $node = $this->server->tree->getNodeForPath($path);
760        } catch (DAV\Exception\NotFound $e) {
761            return;
762        }
763
764        if (!$node instanceof Notifications\INode)
765            return;
766
767        if (!$this->server->checkPreconditions(true)) return false;
768        $dom = new \DOMDocument('1.0', 'UTF-8');
769
770        $dom->formatOutput = true;
771
772        $root = $dom->createElement('cs:notification');
773        foreach($this->server->xmlNamespaces as $namespace => $prefix) {
774            $root->setAttribute('xmlns:' . $prefix, $namespace);
775        }
776
777        $dom->appendChild($root);
778        $node->getNotificationType()->serializeBody($this->server, $root);
779
780        $this->server->httpResponse->setHeader('Content-Type','application/xml');
781        $this->server->httpResponse->setHeader('ETag',$node->getETag());
782        $this->server->httpResponse->sendStatus(200);
783        $this->server->httpResponse->sendBody($dom->saveXML());
784
785        return false;
786
787    }
788
789    /**
790     * Checks if the submitted iCalendar data is in fact, valid.
791     *
792     * An exception is thrown if it's not.
793     *
794     * @param resource|string $data
795     * @param string $path
796     * @return void
797     */
798    protected function validateICalendar(&$data, $path) {
799
800        // If it's a stream, we convert it to a string first.
801        if (is_resource($data)) {
802            $data = stream_get_contents($data);
803        }
804
805        // Converting the data to unicode, if needed.
806        $data = DAV\StringUtil::ensureUTF8($data);
807
808        try {
809
810            $vobj = VObject\Reader::read($data);
811
812        } catch (VObject\ParseException $e) {
813
814            throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
815
816        }
817
818        if ($vobj->name !== 'VCALENDAR') {
819            throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
820        }
821
822        // Get the Supported Components for the target calendar
823        list($parentPath,$object) = DAV\URLUtil::splitPath($path);
824        $calendarProperties = $this->server->getProperties($parentPath,array('{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'));
825        $supportedComponents = $calendarProperties['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']->getValue();
826
827        $foundType = null;
828        $foundUID = null;
829        foreach($vobj->getComponents() as $component) {
830            switch($component->name) {
831                case 'VTIMEZONE' :
832                    continue 2;
833                case 'VEVENT' :
834                case 'VTODO' :
835                case 'VJOURNAL' :
836                    if (is_null($foundType)) {
837                        $foundType = $component->name;
838                        if (!in_array($foundType, $supportedComponents)) {
839                            throw new Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
840                        }
841                        if (!isset($component->UID)) {
842                            throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID');
843                        }
844                        $foundUID = (string)$component->UID;
845                    } else {
846                        if ($foundType !== $component->name) {
847                            throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType);
848                        }
849                        if ($foundUID !== (string)$component->UID) {
850                            throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
851                        }
852                    }
853                    break;
854                default :
855                    throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
856
857            }
858        }
859        if (!$foundType)
860            throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
861
862    }
863
864    /**
865     * This method handles POST requests to the schedule-outbox.
866     *
867     * Currently, two types of requests are support:
868     *   * FREEBUSY requests from RFC 6638
869     *   * Simple iTIP messages from draft-desruisseaux-caldav-sched-04
870     *
871     * The latter is from an expired early draft of the CalDAV scheduling
872     * extensions, but iCal depends on a feature from that spec, so we
873     * implement it.
874     *
875     * @param Schedule\IOutbox $outboxNode
876     * @param string $outboxUri
877     * @return void
878     */
879    public function outboxRequest(Schedule\IOutbox $outboxNode, $outboxUri) {
880
881        // Parsing the request body
882        try {
883            $vObject = VObject\Reader::read($this->server->httpRequest->getBody(true));
884        } catch (VObject\ParseException $e) {
885            throw new DAV\Exception\BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage());
886        }
887
888        // The incoming iCalendar object must have a METHOD property, and a
889        // component. The combination of both determines what type of request
890        // this is.
891        $componentType = null;
892        foreach($vObject->getComponents() as $component) {
893            if ($component->name !== 'VTIMEZONE') {
894                $componentType = $component->name;
895                break;
896            }
897        }
898        if (is_null($componentType)) {
899            throw new DAV\Exception\BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
900        }
901
902        // Validating the METHOD
903        $method = strtoupper((string)$vObject->METHOD);
904        if (!$method) {
905            throw new DAV\Exception\BadRequest('A METHOD property must be specified in iTIP messages');
906        }
907
908        // So we support two types of requests:
909        //
910        // REQUEST with a VFREEBUSY component
911        // REQUEST, REPLY, ADD, CANCEL on VEVENT components
912
913        $acl = $this->server->getPlugin('acl');
914
915        if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') {
916
917            $acl && $acl->checkPrivileges($outboxUri,'{' . Plugin::NS_CALDAV . '}schedule-query-freebusy');
918            $this->handleFreeBusyRequest($outboxNode, $vObject);
919
920        } elseif ($componentType === 'VEVENT' && in_array($method, array('REQUEST','REPLY','ADD','CANCEL'))) {
921
922            $acl && $acl->checkPrivileges($outboxUri,'{' . Plugin::NS_CALDAV . '}schedule-post-vevent');
923            $this->handleEventNotification($outboxNode, $vObject);
924
925        } else {
926
927            throw new DAV\Exception\NotImplemented('SabreDAV supports only VFREEBUSY (REQUEST) and VEVENT (REQUEST, REPLY, ADD, CANCEL)');
928
929        }
930
931    }
932
933    /**
934     * This method handles the REQUEST, REPLY, ADD and CANCEL methods for
935     * VEVENT iTip messages.
936     *
937     * @return void
938     */
939    protected function handleEventNotification(Schedule\IOutbox $outboxNode, VObject\Component $vObject) {
940
941        $originator = $this->server->httpRequest->getHeader('Originator');
942        $recipients = $this->server->httpRequest->getHeader('Recipient');
943
944        if (!$originator) {
945            throw new DAV\Exception\BadRequest('The Originator: header must be specified when making POST requests');
946        }
947        if (!$recipients) {
948            throw new DAV\Exception\BadRequest('The Recipient: header must be specified when making POST requests');
949        }
950
951        $recipients = explode(',',$recipients);
952        foreach($recipients as $k=>$recipient) {
953
954            $recipient = trim($recipient);
955            if (!preg_match('/^mailto:(.*)@(.*)$/i', $recipient)) {
956                throw new DAV\Exception\BadRequest('Recipients must start with mailto: and must be valid email address');
957            }
958            $recipient = substr($recipient, 7);
959            $recipients[$k] = $recipient;
960        }
961
962        // We need to make sure that 'originator' matches one of the email
963        // addresses of the selected principal.
964        $principal = $outboxNode->getOwner();
965        $props = $this->server->getProperties($principal,array(
966            '{' . self::NS_CALDAV . '}calendar-user-address-set',
967        ));
968
969        $addresses = array();
970        if (isset($props['{' . self::NS_CALDAV . '}calendar-user-address-set'])) {
971            $addresses = $props['{' . self::NS_CALDAV . '}calendar-user-address-set']->getHrefs();
972        }
973
974        $found = false;
975        foreach($addresses as $address) {
976
977            // Trimming the / on both sides, just in case..
978            if (rtrim(strtolower($originator),'/') === rtrim(strtolower($address),'/')) {
979                $found = true;
980                break;
981            }
982
983        }
984
985        if (!$found) {
986            throw new DAV\Exception\Forbidden('The addresses specified in the Originator header did not match any addresses in the owners calendar-user-address-set header');
987        }
988
989        // If the Originator header was a url, and not a mailto: address..
990        // we're going to try to pull the mailto: from the vobject body.
991        if (strtolower(substr($originator,0,7)) !== 'mailto:') {
992            $originator = (string)$vObject->VEVENT->ORGANIZER;
993
994        }
995        if (strtolower(substr($originator,0,7)) !== 'mailto:') {
996            throw new DAV\Exception\Forbidden('Could not find mailto: address in both the Orignator header, and the ORGANIZER property in the VEVENT');
997        }
998        $originator = substr($originator,7);
999
1000        $result = $this->iMIPMessage($originator, $recipients, $vObject, $principal);
1001        $this->server->httpResponse->sendStatus(200);
1002        $this->server->httpResponse->setHeader('Content-Type','application/xml');
1003        $this->server->httpResponse->sendBody($this->generateScheduleResponse($result));
1004
1005    }
1006
1007    /**
1008     * Sends an iMIP message by email.
1009     *
1010     * This method must return an array with status codes per recipient.
1011     * This should look something like:
1012     *
1013     * array(
1014     *    'user1@example.org' => '2.0;Success'
1015     * )
1016     *
1017     * Formatting for this status code can be found at:
1018     * https://tools.ietf.org/html/rfc5545#section-3.8.8.3
1019     *
1020     * A list of valid status codes can be found at:
1021     * https://tools.ietf.org/html/rfc5546#section-3.6
1022     *
1023     * @param string $originator
1024     * @param array $recipients
1025     * @param VObject\Component $vObject
1026     * @param string $principal Principal url
1027     * @return array
1028     */
1029    protected function iMIPMessage($originator, array $recipients, VObject\Component $vObject, $principal) {
1030
1031        if (!$this->imipHandler) {
1032            $resultStatus = '5.2;This server does not support this operation';
1033        } else {
1034            $this->imipHandler->sendMessage($originator, $recipients, $vObject, $principal);
1035            $resultStatus = '2.0;Success';
1036        }
1037
1038        $result = array();
1039        foreach($recipients as $recipient) {
1040            $result[$recipient] = $resultStatus;
1041        }
1042
1043        return $result;
1044
1045    }
1046
1047    /**
1048     * Generates a schedule-response XML body
1049     *
1050     * The recipients array is a key->value list, containing email addresses
1051     * and iTip status codes. See the iMIPMessage method for a description of
1052     * the value.
1053     *
1054     * @param array $recipients
1055     * @return string
1056     */
1057    public function generateScheduleResponse(array $recipients) {
1058
1059        $dom = new \DOMDocument('1.0','utf-8');
1060        $dom->formatOutput = true;
1061        $xscheduleResponse = $dom->createElement('cal:schedule-response');
1062        $dom->appendChild($xscheduleResponse);
1063
1064        foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
1065
1066            $xscheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
1067
1068        }
1069
1070        foreach($recipients as $recipient=>$status) {
1071            $xresponse = $dom->createElement('cal:response');
1072
1073            $xrecipient = $dom->createElement('cal:recipient');
1074            $xrecipient->appendChild($dom->createTextNode($recipient));
1075            $xresponse->appendChild($xrecipient);
1076
1077            $xrequestStatus = $dom->createElement('cal:request-status');
1078            $xrequestStatus->appendChild($dom->createTextNode($status));
1079            $xresponse->appendChild($xrequestStatus);
1080
1081            $xscheduleResponse->appendChild($xresponse);
1082
1083        }
1084
1085        return $dom->saveXML();
1086
1087    }
1088
1089    /**
1090     * This method is responsible for parsing a free-busy query request and
1091     * returning it's result.
1092     *
1093     * @param Schedule\IOutbox $outbox
1094     * @param string $request
1095     * @return string
1096     */
1097    protected function handleFreeBusyRequest(Schedule\IOutbox $outbox, VObject\Component $vObject) {
1098
1099        $vFreeBusy = $vObject->VFREEBUSY;
1100        $organizer = $vFreeBusy->organizer;
1101
1102        $organizer = (string)$organizer;
1103
1104        // Validating if the organizer matches the owner of the inbox.
1105        $owner = $outbox->getOwner();
1106
1107        $caldavNS = '{' . Plugin::NS_CALDAV . '}';
1108
1109        $uas = $caldavNS . 'calendar-user-address-set';
1110        $props = $this->server->getProperties($owner,array($uas));
1111
1112        if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
1113            throw new DAV\Exception\Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
1114        }
1115
1116        if (!isset($vFreeBusy->ATTENDEE)) {
1117            throw new DAV\Exception\BadRequest('You must at least specify 1 attendee');
1118        }
1119
1120        $attendees = array();
1121        foreach($vFreeBusy->ATTENDEE as $attendee) {
1122            $attendees[]= (string)$attendee;
1123        }
1124
1125
1126        if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
1127            throw new DAV\Exception\BadRequest('DTSTART and DTEND must both be specified');
1128        }
1129
1130        $startRange = $vFreeBusy->DTSTART->getDateTime();
1131        $endRange = $vFreeBusy->DTEND->getDateTime();
1132
1133        $results = array();
1134        foreach($attendees as $attendee) {
1135            $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
1136        }
1137
1138        $dom = new \DOMDocument('1.0','utf-8');
1139        $dom->formatOutput = true;
1140        $scheduleResponse = $dom->createElement('cal:schedule-response');
1141        foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
1142
1143            $scheduleResponse->setAttribute('xmlns:' . $prefix,$namespace);
1144
1145        }
1146        $dom->appendChild($scheduleResponse);
1147
1148        foreach($results as $result) {
1149            $response = $dom->createElement('cal:response');
1150
1151            $recipient = $dom->createElement('cal:recipient');
1152            $recipientHref = $dom->createElement('d:href');
1153
1154            $recipientHref->appendChild($dom->createTextNode($result['href']));
1155            $recipient->appendChild($recipientHref);
1156            $response->appendChild($recipient);
1157
1158            $reqStatus = $dom->createElement('cal:request-status');
1159            $reqStatus->appendChild($dom->createTextNode($result['request-status']));
1160            $response->appendChild($reqStatus);
1161
1162            if (isset($result['calendar-data'])) {
1163
1164                $calendardata = $dom->createElement('cal:calendar-data');
1165                $calendardata->appendChild($dom->createTextNode(str_replace("\r\n","\n",$result['calendar-data']->serialize())));
1166                $response->appendChild($calendardata);
1167
1168            }
1169            $scheduleResponse->appendChild($response);
1170        }
1171
1172        $this->server->httpResponse->sendStatus(200);
1173        $this->server->httpResponse->setHeader('Content-Type','application/xml');
1174        $this->server->httpResponse->sendBody($dom->saveXML());
1175
1176    }
1177
1178    /**
1179     * Returns free-busy information for a specific address. The returned
1180     * data is an array containing the following properties:
1181     *
1182     * calendar-data : A VFREEBUSY VObject
1183     * request-status : an iTip status code.
1184     * href: The principal's email address, as requested
1185     *
1186     * The following request status codes may be returned:
1187     *   * 2.0;description
1188     *   * 3.7;description
1189     *
1190     * @param string $email address
1191     * @param \DateTime $start
1192     * @param \DateTime $end
1193     * @param VObject\Component $request
1194     * @return array
1195     */
1196    protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request) {
1197
1198        $caldavNS = '{' . Plugin::NS_CALDAV . '}';
1199
1200        $aclPlugin = $this->server->getPlugin('acl');
1201        if (substr($email,0,7)==='mailto:') $email = substr($email,7);
1202
1203        $result = $aclPlugin->principalSearch(
1204            array('{http://sabredav.org/ns}email-address' => $email),
1205            array(
1206                '{DAV:}principal-URL', $caldavNS . 'calendar-home-set',
1207                '{http://sabredav.org/ns}email-address',
1208            )
1209        );
1210
1211        if (!count($result)) {
1212            return array(
1213                'request-status' => '3.7;Could not find principal',
1214                'href' => 'mailto:' . $email,
1215            );
1216        }
1217
1218        if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) {
1219            return array(
1220                'request-status' => '3.7;No calendar-home-set property found',
1221                'href' => 'mailto:' . $email,
1222            );
1223        }
1224        $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref();
1225
1226        // Grabbing the calendar list
1227        $objects = array();
1228        foreach($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
1229            if (!$node instanceof ICalendar) {
1230                continue;
1231            }
1232            $aclPlugin->checkPrivileges($homeSet . $node->getName() ,$caldavNS . 'read-free-busy');
1233
1234            // Getting the list of object uris within the time-range
1235            $urls = $node->calendarQuery(array(
1236                'name' => 'VCALENDAR',
1237                'comp-filters' => array(
1238                    array(
1239                        'name' => 'VEVENT',
1240                        'comp-filters' => array(),
1241                        'prop-filters' => array(),
1242                        'is-not-defined' => false,
1243                        'time-range' => array(
1244                            'start' => $start,
1245                            'end' => $end,
1246                        ),
1247                    ),
1248                ),
1249                'prop-filters' => array(),
1250                'is-not-defined' => false,
1251                'time-range' => null,
1252            ));
1253
1254            $calObjects = array_map(function($url) use ($node) {
1255                $obj = $node->getChild($url)->get();
1256                return $obj;
1257            }, $urls);
1258
1259            $objects = array_merge($objects,$calObjects);
1260
1261        }
1262
1263        $vcalendar = new VObject\Component\VCalendar();
1264        $vcalendar->VERSION = '2.0';
1265        $vcalendar->METHOD = 'REPLY';
1266        $vcalendar->CALSCALE = 'GREGORIAN';
1267        $vcalendar->PRODID = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN';
1268
1269        $generator = new VObject\FreeBusyGenerator();
1270        $generator->setObjects($objects);
1271        $generator->setTimeRange($start, $end);
1272        $generator->setBaseObject($vcalendar);
1273
1274        $result = $generator->getResult();
1275
1276        $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email;
1277        $vcalendar->VFREEBUSY->UID = (string)$request->VFREEBUSY->UID;
1278        $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
1279
1280        return array(
1281            'calendar-data' => $result,
1282            'request-status' => '2.0;Success',
1283            'href' => 'mailto:' . $email,
1284        );
1285    }
1286
1287    /**
1288     * This method is used to generate HTML output for the
1289     * DAV\Browser\Plugin. This allows us to generate an interface users
1290     * can use to create new calendars.
1291     *
1292     * @param DAV\INode $node
1293     * @param string $output
1294     * @return bool
1295     */
1296    public function htmlActionsPanel(DAV\INode $node, &$output) {
1297
1298        if (!$node instanceof UserCalendars)
1299            return;
1300
1301        $output.= '<tr><td colspan="2"><form method="post" action="">
1302            <h3>Create new calendar</h3>
1303            <input type="hidden" name="sabreAction" value="mkcalendar" />
1304            <label>Name (uri):</label> <input type="text" name="name" /><br />
1305            <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
1306            <input type="submit" value="create" />
1307            </form>
1308            </td></tr>';
1309
1310        return false;
1311
1312    }
1313
1314    /**
1315     * This method allows us to intercept the 'mkcalendar' sabreAction. This
1316     * action enables the user to create new calendars from the browser plugin.
1317     *
1318     * @param string $uri
1319     * @param string $action
1320     * @param array $postVars
1321     * @return bool
1322     */
1323    public function browserPostAction($uri, $action, array $postVars) {
1324
1325        if ($action!=='mkcalendar')
1326            return;
1327
1328        $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
1329        $properties = array();
1330        if (isset($postVars['{DAV:}displayname'])) {
1331            $properties['{DAV:}displayname'] = $postVars['{DAV:}displayname'];
1332        }
1333        $this->server->createCollection($uri . '/' . $postVars['name'],$resourceType,$properties);
1334        return false;
1335
1336    }
1337
1338}
1339