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