1<?php
2
3namespace Sabre\CalDAV;
4
5use DateTimeZone;
6use Sabre\DAV;
7use Sabre\DAV\Exception\BadRequest;
8use Sabre\DAV\MkCol;
9use Sabre\DAV\Xml\Property\Href;
10use Sabre\DAVACL;
11use Sabre\VObject;
12use Sabre\HTTP;
13use Sabre\Uri;
14use Sabre\HTTP\RequestInterface;
15use Sabre\HTTP\ResponseInterface;
16
17/**
18 * CalDAV plugin
19 *
20 * This plugin provides functionality added by CalDAV (RFC 4791)
21 * It implements new reports, and the MKCALENDAR method.
22 *
23 * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
24 * @author Evert Pot (http://evertpot.com/)
25 * @license http://sabre.io/license/ Modified BSD License
26 */
27class Plugin extends DAV\ServerPlugin {
28
29    /**
30     * This is the official CalDAV namespace
31     */
32    const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
33
34    /**
35     * This is the namespace for the proprietary calendarserver extensions
36     */
37    const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
38
39    /**
40     * The hardcoded root for calendar objects. It is unfortunate
41     * that we're stuck with it, but it will have to do for now
42     */
43    const CALENDAR_ROOT = 'calendars';
44
45    /**
46     * Reference to server object
47     *
48     * @var DAV\Server
49     */
50    protected $server;
51
52    /**
53     * The default PDO storage uses a MySQL MEDIUMBLOB for iCalendar data,
54     * which can hold up to 2^24 = 16777216 bytes. This is plenty. We're
55     * capping it to 10M here.
56     */
57    protected $maxResourceSize = 10000000;
58
59    /**
60     * Use this method to tell the server this plugin defines additional
61     * HTTP methods.
62     *
63     * This method is passed a uri. It should only return HTTP methods that are
64     * available for the specified uri.
65     *
66     * @param string $uri
67     * @return array
68     */
69    function getHTTPMethods($uri) {
70
71        // The MKCALENDAR is only available on unmapped uri's, whose
72        // parents extend IExtendedCollection
73        list($parent, $name) = Uri\split($uri);
74
75        $node = $this->server->tree->getNodeForPath($parent);
76
77        if ($node instanceof DAV\IExtendedCollection) {
78            try {
79                $node->getChild($name);
80            } catch (DAV\Exception\NotFound $e) {
81                return ['MKCALENDAR'];
82            }
83        }
84        return [];
85
86    }
87
88    /**
89     * Returns the path to a principal's calendar home.
90     *
91     * The return url must not end with a slash.
92     *
93     * @param string $principalUrl
94     * @return string
95     */
96    function getCalendarHomeForPrincipal($principalUrl) {
97
98        // The default is a bit naive, but it can be overwritten.
99        list(, $nodeName) = Uri\split($principalUrl);
100
101        return self::CALENDAR_ROOT . '/' . $nodeName;
102
103    }
104
105    /**
106     * Returns a list of features for the DAV: HTTP header.
107     *
108     * @return array
109     */
110    function getFeatures() {
111
112        return ['calendar-access', 'calendar-proxy'];
113
114    }
115
116    /**
117     * Returns a plugin name.
118     *
119     * Using this name other plugins will be able to access other plugins
120     * using DAV\Server::getPlugin
121     *
122     * @return string
123     */
124    function getPluginName() {
125
126        return 'caldav';
127
128    }
129
130    /**
131     * Returns a list of reports this plugin supports.
132     *
133     * This will be used in the {DAV:}supported-report-set property.
134     * Note that you still need to subscribe to the 'report' event to actually
135     * implement them
136     *
137     * @param string $uri
138     * @return array
139     */
140    function getSupportedReportSet($uri) {
141
142        $node = $this->server->tree->getNodeForPath($uri);
143
144        $reports = [];
145        if ($node instanceof ICalendarObjectContainer || $node instanceof ICalendarObject) {
146            $reports[] = '{' . self::NS_CALDAV . '}calendar-multiget';
147            $reports[] = '{' . self::NS_CALDAV . '}calendar-query';
148        }
149        if ($node instanceof ICalendar) {
150            $reports[] = '{' . self::NS_CALDAV . '}free-busy-query';
151        }
152        // iCal has a bug where it assumes that sync support is enabled, only
153        // if we say we support it on the calendar-home, even though this is
154        // not actually the case.
155        if ($node instanceof CalendarHome && $this->server->getPlugin('sync')) {
156            $reports[] = '{DAV:}sync-collection';
157        }
158        return $reports;
159
160    }
161
162    /**
163     * Initializes the plugin
164     *
165     * @param DAV\Server $server
166     * @return void
167     */
168    function initialize(DAV\Server $server) {
169
170        $this->server = $server;
171
172        $server->on('method:MKCALENDAR',   [$this, 'httpMkCalendar']);
173        $server->on('report',              [$this, 'report']);
174        $server->on('propFind',            [$this, 'propFind']);
175        $server->on('onHTMLActionsPanel',  [$this, 'htmlActionsPanel']);
176        $server->on('beforeCreateFile',    [$this, 'beforeCreateFile']);
177        $server->on('beforeWriteContent',  [$this, 'beforeWriteContent']);
178        $server->on('afterMethod:GET',     [$this, 'httpAfterGET']);
179
180        $server->xml->namespaceMap[self::NS_CALDAV] = 'cal';
181        $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs';
182
183        $server->xml->elementMap['{' . self::NS_CALDAV . '}calendar-query'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport';
184        $server->xml->elementMap['{' . self::NS_CALDAV . '}calendar-multiget'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport';
185        $server->xml->elementMap['{' . self::NS_CALDAV . '}free-busy-query'] = 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport';
186        $server->xml->elementMap['{' . self::NS_CALDAV . '}mkcalendar'] = 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar';
187        $server->xml->elementMap['{' . self::NS_CALDAV . '}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp';
188        $server->xml->elementMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet';
189
190        $server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar';
191
192        $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read';
193        $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write';
194
195        array_push($server->protectedProperties,
196
197            '{' . self::NS_CALDAV . '}supported-calendar-component-set',
198            '{' . self::NS_CALDAV . '}supported-calendar-data',
199            '{' . self::NS_CALDAV . '}max-resource-size',
200            '{' . self::NS_CALDAV . '}min-date-time',
201            '{' . self::NS_CALDAV . '}max-date-time',
202            '{' . self::NS_CALDAV . '}max-instances',
203            '{' . self::NS_CALDAV . '}max-attendees-per-instance',
204            '{' . self::NS_CALDAV . '}calendar-home-set',
205            '{' . self::NS_CALDAV . '}supported-collation-set',
206            '{' . self::NS_CALDAV . '}calendar-data',
207
208            // CalendarServer extensions
209            '{' . self::NS_CALENDARSERVER . '}getctag',
210            '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for',
211            '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for'
212
213        );
214
215        if ($aclPlugin = $server->getPlugin('acl')) {
216            $aclPlugin->principalSearchPropertySet['{' . self::NS_CALDAV . '}calendar-user-address-set'] = 'Calendar address';
217        }
218    }
219
220    /**
221     * This functions handles REPORT requests specific to CalDAV
222     *
223     * @param string $reportName
224     * @param mixed $report
225     * @return bool
226     */
227    function report($reportName, $report) {
228
229        switch ($reportName) {
230            case '{' . self::NS_CALDAV . '}calendar-multiget' :
231                $this->server->transactionType = 'report-calendar-multiget';
232                $this->calendarMultiGetReport($report);
233                return false;
234            case '{' . self::NS_CALDAV . '}calendar-query' :
235                $this->server->transactionType = 'report-calendar-query';
236                $this->calendarQueryReport($report);
237                return false;
238            case '{' . self::NS_CALDAV . '}free-busy-query' :
239                $this->server->transactionType = 'report-free-busy-query';
240                $this->freeBusyQueryReport($report);
241                return false;
242
243        }
244
245
246    }
247
248    /**
249     * This function handles the MKCALENDAR HTTP method, which creates
250     * a new calendar.
251     *
252     * @param RequestInterface $request
253     * @param ResponseInterface $response
254     * @return bool
255     */
256    function httpMkCalendar(RequestInterface $request, ResponseInterface $response) {
257
258        $body = $request->getBodyAsString();
259        $path = $request->getPath();
260
261        $properties = [];
262
263        if ($body) {
264
265            try {
266                $mkcalendar = $this->server->xml->expect(
267                    '{urn:ietf:params:xml:ns:caldav}mkcalendar',
268                    $body
269                );
270            } catch (\Sabre\Xml\ParseException $e) {
271                throw new BadRequest($e->getMessage(), null, $e);
272            }
273            $properties = $mkcalendar->getProperties();
274
275        }
276
277        // iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored
278        // subscriptions. Before that it used MKCOL which was the correct way
279        // to do this.
280        //
281        // If the body had a {DAV:}resourcetype, it means we stumbled upon this
282        // request, and we simply use it instead of the pre-defined list.
283        if (isset($properties['{DAV:}resourcetype'])) {
284            $resourceType = $properties['{DAV:}resourcetype']->getValue();
285        } else {
286            $resourceType = ['{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar'];
287        }
288
289        $this->server->createCollection($path, new MkCol($resourceType, $properties));
290
291        $this->server->httpResponse->setStatus(201);
292        $this->server->httpResponse->setHeader('Content-Length', 0);
293
294        // This breaks the method chain.
295        return false;
296    }
297
298    /**
299     * PropFind
300     *
301     * This method handler is invoked before any after properties for a
302     * resource are fetched. This allows us to add in any CalDAV specific
303     * properties.
304     *
305     * @param DAV\PropFind $propFind
306     * @param DAV\INode $node
307     * @return void
308     */
309    function propFind(DAV\PropFind $propFind, DAV\INode $node) {
310
311        $ns = '{' . self::NS_CALDAV . '}';
312
313        if ($node instanceof ICalendarObjectContainer) {
314
315            $propFind->handle($ns . 'max-resource-size', $this->maxResourceSize);
316            $propFind->handle($ns . 'supported-calendar-data', function() {
317                return new Xml\Property\SupportedCalendarData();
318            });
319            $propFind->handle($ns . 'supported-collation-set', function() {
320                return new Xml\Property\SupportedCollationSet();
321            });
322
323        }
324
325        if ($node instanceof DAVACL\IPrincipal) {
326
327            $principalUrl = $node->getPrincipalUrl();
328
329            $propFind->handle('{' . self::NS_CALDAV . '}calendar-home-set', function() use ($principalUrl) {
330
331                $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl) . '/';
332                return new Href($calendarHomePath);
333
334            });
335            // The calendar-user-address-set property is basically mapped to
336            // the {DAV:}alternate-URI-set property.
337            $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-address-set', function() use ($node) {
338                $addresses = $node->getAlternateUriSet();
339                $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl() . '/';
340                return new Href($addresses, false);
341            });
342            // For some reason somebody thought it was a good idea to add
343            // another one of these properties. We're supporting it too.
344            $propFind->handle('{' . self::NS_CALENDARSERVER . '}email-address-set', function() use ($node) {
345                $addresses = $node->getAlternateUriSet();
346                $emails = [];
347                foreach ($addresses as $address) {
348                    if (substr($address, 0, 7) === 'mailto:') {
349                        $emails[] = substr($address, 7);
350                    }
351                }
352                return new Xml\Property\EmailAddressSet($emails);
353            });
354
355            // These two properties are shortcuts for ical to easily find
356            // other principals this principal has access to.
357            $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for';
358            $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for';
359
360            if ($propFind->getStatus($propRead) === 404 || $propFind->getStatus($propWrite) === 404) {
361
362                $aclPlugin = $this->server->getPlugin('acl');
363                $membership = $aclPlugin->getPrincipalMembership($propFind->getPath());
364                $readList = [];
365                $writeList = [];
366
367                foreach ($membership as $group) {
368
369                    $groupNode = $this->server->tree->getNodeForPath($group);
370
371                    $listItem = Uri\split($group)[0] . '/';
372
373                    // If the node is either ap proxy-read or proxy-write
374                    // group, we grab the parent principal and add it to the
375                    // list.
376                    if ($groupNode instanceof Principal\IProxyRead) {
377                        $readList[] = $listItem;
378                    }
379                    if ($groupNode instanceof Principal\IProxyWrite) {
380                        $writeList[] = $listItem;
381                    }
382
383                }
384
385                $propFind->set($propRead, new Href($readList));
386                $propFind->set($propWrite, new Href($writeList));
387
388            }
389
390        } // instanceof IPrincipal
391
392        if ($node instanceof ICalendarObject) {
393
394            // The calendar-data property is not supposed to be a 'real'
395            // property, but in large chunks of the spec it does act as such.
396            // Therefore we simply expose it as a property.
397            $propFind->handle('{' . self::NS_CALDAV . '}calendar-data', function() use ($node) {
398                $val = $node->get();
399                if (is_resource($val))
400                    $val = stream_get_contents($val);
401
402                // Taking out \r to not screw up the xml output
403                return str_replace("\r", "", $val);
404
405            });
406
407        }
408
409    }
410
411    /**
412     * This function handles the calendar-multiget REPORT.
413     *
414     * This report is used by the client to fetch the content of a series
415     * of urls. Effectively avoiding a lot of redundant requests.
416     *
417     * @param CalendarMultiGetReport $report
418     * @return void
419     */
420    function calendarMultiGetReport($report) {
421
422        $needsJson = $report->contentType === 'application/calendar+json';
423
424        $timeZones = [];
425        $propertyList = [];
426
427        $paths = array_map(
428            [$this->server, 'calculateUri'],
429            $report->hrefs
430        );
431
432        foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $uri => $objProps) {
433
434            if (($needsJson || $report->expand) && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) {
435                $vObject = VObject\Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']);
436
437                if ($report->expand) {
438                    // We're expanding, and for that we need to figure out the
439                    // calendar's timezone.
440                    list($calendarPath) = Uri\split($uri);
441                    if (!isset($timeZones[$calendarPath])) {
442                        // Checking the calendar-timezone property.
443                        $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone';
444                        $tzResult = $this->server->getProperties($calendarPath, [$tzProp]);
445                        if (isset($tzResult[$tzProp])) {
446                            // This property contains a VCALENDAR with a single
447                            // VTIMEZONE.
448                            $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
449                            $timeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
450                        } else {
451                            // Defaulting to UTC.
452                            $timeZone = new DateTimeZone('UTC');
453                        }
454                        $timeZones[$calendarPath] = $timeZone;
455                    }
456
457                    $vObject->expand($report->expand['start'], $report->expand['end'], $timeZones[$calendarPath]);
458                }
459                if ($needsJson) {
460                    $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize());
461                } else {
462                    $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
463                }
464            }
465
466            $propertyList[] = $objProps;
467
468        }
469
470        $prefer = $this->server->getHTTPPrefer();
471
472        $this->server->httpResponse->setStatus(207);
473        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
474        $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
475        $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, $prefer['return'] === 'minimal'));
476
477    }
478
479    /**
480     * This function handles the calendar-query REPORT
481     *
482     * This report is used by clients to request calendar objects based on
483     * complex conditions.
484     *
485     * @param Xml\Request\CalendarQueryReport $report
486     * @return void
487     */
488    function calendarQueryReport($report) {
489
490        $path = $this->server->getRequestUri();
491
492        $needsJson = $report->contentType === 'application/calendar+json';
493
494        $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
495        $depth = $this->server->getHTTPDepth(0);
496
497        // The default result is an empty array
498        $result = [];
499
500        $calendarTimeZone = null;
501        if ($report->expand) {
502            // We're expanding, and for that we need to figure out the
503            // calendar's timezone.
504            $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone';
505            $tzResult = $this->server->getProperties($path, [$tzProp]);
506            if (isset($tzResult[$tzProp])) {
507                // This property contains a VCALENDAR with a single
508                // VTIMEZONE.
509                $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
510                $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
511                unset($vtimezoneObj);
512            } else {
513                // Defaulting to UTC.
514                $calendarTimeZone = new DateTimeZone('UTC');
515            }
516        }
517
518        // The calendarobject was requested directly. In this case we handle
519        // this locally.
520        if ($depth == 0 && $node instanceof ICalendarObject) {
521
522            $requestedCalendarData = true;
523            $requestedProperties = $report->properties;
524
525            if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
526
527                // We always retrieve calendar-data, as we need it for filtering.
528                $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data';
529
530                // If calendar-data wasn't explicitly requested, we need to remove
531                // it after processing.
532                $requestedCalendarData = false;
533            }
534
535            $properties = $this->server->getPropertiesForPath(
536                $path,
537                $requestedProperties,
538                0
539            );
540
541            // This array should have only 1 element, the first calendar
542            // object.
543            $properties = current($properties);
544
545            // If there wasn't any calendar-data returned somehow, we ignore
546            // this.
547            if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
548
549                $validator = new CalendarQueryValidator();
550
551                $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
552                if ($validator->validate($vObject, $report->filters)) {
553
554                    // If the client didn't require the calendar-data property,
555                    // we won't give it back.
556                    if (!$requestedCalendarData) {
557                        unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
558                    } else {
559
560
561                        if ($report->expand) {
562                            $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone);
563                        }
564                        if ($needsJson) {
565                            $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize());
566                        } elseif ($report->expand) {
567                            $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
568                        }
569                    }
570
571                    $result = [$properties];
572
573                }
574
575            }
576
577        }
578
579        if ($node instanceof ICalendarObjectContainer && $depth === 0) {
580
581            if (strpos($this->server->httpRequest->getHeader('User-Agent'), 'MSFT-WP/') === 0) {
582                // Windows phone incorrectly supplied depth as 0, when it actually
583                // should have set depth to 1. We're implementing a workaround here
584                // to deal with this.
585                $depth = 1;
586            } else {
587                throw new BadRequest('A calendar-query REPORT on a calendar with a Depth: 0 is undefined. Set Depth to 1');
588            }
589
590        }
591
592        // If we're dealing with a calendar, the calendar itself is responsible
593        // for the calendar-query.
594        if ($node instanceof ICalendarObjectContainer && $depth == 1) {
595
596            $nodePaths = $node->calendarQuery($report->filters);
597
598            foreach ($nodePaths as $path) {
599
600                list($properties) =
601                    $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $report->properties);
602
603                if (($needsJson || $report->expand)) {
604                    $vObject = VObject\Reader::read($properties[200]['{' . self::NS_CALDAV . '}calendar-data']);
605
606                    if ($report->expand) {
607                        $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone);
608                    }
609
610                    if ($needsJson) {
611                        $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize());
612                    } else {
613                        $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
614                    }
615                }
616                $result[] = $properties;
617
618            }
619
620        }
621
622        $prefer = $this->server->getHTTPPrefer();
623
624        $this->server->httpResponse->setStatus(207);
625        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
626        $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
627        $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, $prefer['return'] === 'minimal'));
628
629    }
630
631    /**
632     * This method is responsible for parsing the request and generating the
633     * response for the CALDAV:free-busy-query REPORT.
634     *
635     * @param Xml\Request\FreeBusyQueryReport $report
636     * @return void
637     */
638    protected function freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report) {
639
640        $uri = $this->server->getRequestUri();
641
642        $acl = $this->server->getPlugin('acl');
643        if ($acl) {
644            $acl->checkPrivileges($uri, '{' . self::NS_CALDAV . '}read-free-busy');
645        }
646
647        $calendar = $this->server->tree->getNodeForPath($uri);
648        if (!$calendar instanceof ICalendar) {
649            throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars');
650        }
651
652        $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone';
653
654        // Figuring out the default timezone for the calendar, for floating
655        // times.
656        $calendarProps = $this->server->getProperties($uri, [$tzProp]);
657
658        if (isset($calendarProps[$tzProp])) {
659            $vtimezoneObj = VObject\Reader::read($calendarProps[$tzProp]);
660            $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
661        } else {
662            $calendarTimeZone = new DateTimeZone('UTC');
663        }
664
665        // Doing a calendar-query first, to make sure we get the most
666        // performance.
667        $urls = $calendar->calendarQuery([
668            'name'         => 'VCALENDAR',
669            'comp-filters' => [
670                [
671                    'name'           => 'VEVENT',
672                    'comp-filters'   => [],
673                    'prop-filters'   => [],
674                    'is-not-defined' => false,
675                    'time-range'     => [
676                        'start' => $report->start,
677                        'end'   => $report->end,
678                    ],
679                ],
680            ],
681            'prop-filters'   => [],
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($report->start, $report->end);
694        $generator->setTimeZone($calendarTimeZone);
695        $result = $generator->getResult();
696        $result = $result->serialize();
697
698        $this->server->httpResponse->setStatus(200);
699        $this->server->httpResponse->setHeader('Content-Type', 'text/calendar');
700        $this->server->httpResponse->setHeader('Content-Length', strlen($result));
701        $this->server->httpResponse->setBody($result);
702
703    }
704
705    /**
706     * This method is triggered before a file gets updated with new content.
707     *
708     * This plugin uses this method to ensure that CalDAV objects receive
709     * valid calendar data.
710     *
711     * @param string $path
712     * @param DAV\IFile $node
713     * @param resource $data
714     * @param bool $modified Should be set to true, if this event handler
715     *                       changed &$data.
716     * @return void
717     */
718    function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) {
719
720        if (!$node instanceof ICalendarObject)
721            return;
722
723        // We're onyl interested in ICalendarObject nodes that are inside of a
724        // real calendar. This is to avoid triggering validation and scheduling
725        // for non-calendars (such as an inbox).
726        list($parent) = Uri\split($path);
727        $parentNode = $this->server->tree->getNodeForPath($parent);
728
729        if (!$parentNode instanceof ICalendar)
730            return;
731
732        $this->validateICalendar(
733            $data,
734            $path,
735            $modified,
736            $this->server->httpRequest,
737            $this->server->httpResponse,
738            false
739        );
740
741    }
742
743    /**
744     * This method is triggered before a new file is created.
745     *
746     * This plugin uses this method to ensure that newly created calendar
747     * objects contain valid calendar data.
748     *
749     * @param string $path
750     * @param resource $data
751     * @param DAV\ICollection $parentNode
752     * @param bool $modified Should be set to true, if this event handler
753     *                       changed &$data.
754     * @return void
755     */
756    function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) {
757
758        if (!$parentNode instanceof ICalendar)
759            return;
760
761        $this->validateICalendar(
762            $data,
763            $path,
764            $modified,
765            $this->server->httpRequest,
766            $this->server->httpResponse,
767            true
768        );
769
770    }
771
772    /**
773     * Checks if the submitted iCalendar data is in fact, valid.
774     *
775     * An exception is thrown if it's not.
776     *
777     * @param resource|string $data
778     * @param string $path
779     * @param bool $modified Should be set to true, if this event handler
780     *                       changed &$data.
781     * @param RequestInterface $request The http request.
782     * @param ResponseInterface $response The http response.
783     * @param bool $isNew Is the item a new one, or an update.
784     * @return void
785     */
786    protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew) {
787
788        // If it's a stream, we convert it to a string first.
789        if (is_resource($data)) {
790            $data = stream_get_contents($data);
791        }
792
793        $before = md5($data);
794        // Converting the data to unicode, if needed.
795        $data = DAV\StringUtil::ensureUTF8($data);
796
797        if ($before !== md5($data)) $modified = true;
798
799        try {
800
801            // If the data starts with a [, we can reasonably assume we're dealing
802            // with a jCal object.
803            if (substr($data, 0, 1) === '[') {
804                $vobj = VObject\Reader::readJson($data);
805
806                // Converting $data back to iCalendar, as that's what we
807                // technically support everywhere.
808                $data = $vobj->serialize();
809                $modified = true;
810            } else {
811                $vobj = VObject\Reader::read($data);
812            }
813
814        } catch (VObject\ParseException $e) {
815
816            throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
817
818        }
819
820        if ($vobj->name !== 'VCALENDAR') {
821            throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
822        }
823
824        $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
825
826        // Get the Supported Components for the target calendar
827        list($parentPath) = Uri\split($path);
828        $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]);
829
830        if (isset($calendarProperties[$sCCS])) {
831            $supportedComponents = $calendarProperties[$sCCS]->getValue();
832        } else {
833            $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT'];
834        }
835
836        $foundType = null;
837        $foundUID = null;
838        foreach ($vobj->getComponents() as $component) {
839            switch ($component->name) {
840                case 'VTIMEZONE' :
841                    continue 2;
842                case 'VEVENT' :
843                case 'VTODO' :
844                case 'VJOURNAL' :
845                    if (is_null($foundType)) {
846                        $foundType = $component->name;
847                        if (!in_array($foundType, $supportedComponents)) {
848                            throw new Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
849                        }
850                        if (!isset($component->UID)) {
851                            throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID');
852                        }
853                        $foundUID = (string)$component->UID;
854                    } else {
855                        if ($foundType !== $component->name) {
856                            throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType);
857                        }
858                        if ($foundUID !== (string)$component->UID) {
859                            throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
860                        }
861                    }
862                    break;
863                default :
864                    throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
865
866            }
867        }
868        if (!$foundType)
869            throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
870
871        // We use an extra variable to allow event handles to tell us wether
872        // the object was modified or not.
873        //
874        // This helps us determine if we need to re-serialize the object.
875        $subModified = false;
876
877        $this->server->emit(
878            'calendarObjectChange',
879            [
880                $request,
881                $response,
882                $vobj,
883                $parentPath,
884                &$subModified,
885                $isNew
886            ]
887        );
888
889        if ($subModified) {
890            // An event handler told us that it modified the object.
891            $data = $vobj->serialize();
892
893            // Using md5 to figure out if there was an *actual* change.
894            if (!$modified && $before !== md5($data)) {
895                $modified = true;
896            }
897
898        }
899
900    }
901
902
903    /**
904     * This method is used to generate HTML output for the
905     * DAV\Browser\Plugin. This allows us to generate an interface users
906     * can use to create new calendars.
907     *
908     * @param DAV\INode $node
909     * @param string $output
910     * @return bool
911     */
912    function htmlActionsPanel(DAV\INode $node, &$output) {
913
914        if (!$node instanceof CalendarHome)
915            return;
916
917        $output .= '<tr><td colspan="2"><form method="post" action="">
918            <h3>Create new calendar</h3>
919            <input type="hidden" name="sabreAction" value="mkcol" />
920            <input type="hidden" name="resourceType" value="{DAV:}collection,{' . self::NS_CALDAV . '}calendar" />
921            <label>Name (uri):</label> <input type="text" name="name" /><br />
922            <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
923            <input type="submit" value="create" />
924            </form>
925            </td></tr>';
926
927        return false;
928
929    }
930
931    /**
932     * This event is triggered after GET requests.
933     *
934     * This is used to transform data into jCal, if this was requested.
935     *
936     * @param RequestInterface $request
937     * @param ResponseInterface $response
938     * @return void
939     */
940    function httpAfterGet(RequestInterface $request, ResponseInterface $response) {
941
942        if (strpos($response->getHeader('Content-Type'), 'text/calendar') === false) {
943            return;
944        }
945
946        $result = HTTP\Util::negotiate(
947            $request->getHeader('Accept'),
948            ['text/calendar', 'application/calendar+json']
949        );
950
951        if ($result !== 'application/calendar+json') {
952            // Do nothing
953            return;
954        }
955
956        // Transforming.
957        $vobj = VObject\Reader::read($response->getBody());
958
959        $jsonBody = json_encode($vobj->jsonSerialize());
960        $response->setBody($jsonBody);
961
962        $response->setHeader('Content-Type', 'application/calendar+json');
963        $response->setHeader('Content-Length', strlen($jsonBody));
964
965    }
966
967    /**
968     * Returns a bunch of meta-data about the plugin.
969     *
970     * Providing this information is optional, and is mainly displayed by the
971     * Browser plugin.
972     *
973     * The description key in the returned array may contain html and will not
974     * be sanitized.
975     *
976     * @return array
977     */
978    function getPluginInfo() {
979
980        return [
981            'name'        => $this->getPluginName(),
982            'description' => 'Adds support for CalDAV (rfc4791)',
983            'link'        => 'http://sabre.io/dav/caldav/',
984        ];
985
986    }
987
988}
989