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