xref: /plugin/davcal/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php (revision a1a3b6794e0e143a4a8b51d3185ce2d339be61ab)
1*a1a3b679SAndreas Boehler<?php
2*a1a3b679SAndreas Boehler
3*a1a3b679SAndreas Boehlernamespace Sabre\CalDAV\Schedule;
4*a1a3b679SAndreas Boehler
5*a1a3b679SAndreas Boehleruse DateTimeZone;
6*a1a3b679SAndreas Boehleruse Sabre\DAV\Server;
7*a1a3b679SAndreas Boehleruse Sabre\DAV\ServerPlugin;
8*a1a3b679SAndreas Boehleruse Sabre\DAV\PropFind;
9*a1a3b679SAndreas Boehleruse Sabre\DAV\INode;
10*a1a3b679SAndreas Boehleruse Sabre\DAV\Xml\Property\Href;
11*a1a3b679SAndreas Boehleruse Sabre\HTTP\RequestInterface;
12*a1a3b679SAndreas Boehleruse Sabre\HTTP\ResponseInterface;
13*a1a3b679SAndreas Boehleruse Sabre\VObject;
14*a1a3b679SAndreas Boehleruse Sabre\VObject\Reader;
15*a1a3b679SAndreas Boehleruse Sabre\VObject\Component\VCalendar;
16*a1a3b679SAndreas Boehleruse Sabre\VObject\ITip;
17*a1a3b679SAndreas Boehleruse Sabre\VObject\ITip\Message;
18*a1a3b679SAndreas Boehleruse Sabre\DAVACL;
19*a1a3b679SAndreas Boehleruse Sabre\CalDAV\ICalendar;
20*a1a3b679SAndreas Boehleruse Sabre\CalDAV\ICalendarObject;
21*a1a3b679SAndreas Boehleruse Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
22*a1a3b679SAndreas Boehleruse Sabre\DAV\Exception\NotFound;
23*a1a3b679SAndreas Boehleruse Sabre\DAV\Exception\Forbidden;
24*a1a3b679SAndreas Boehleruse Sabre\DAV\Exception\BadRequest;
25*a1a3b679SAndreas Boehleruse Sabre\DAV\Exception\NotImplemented;
26*a1a3b679SAndreas Boehler
27*a1a3b679SAndreas Boehler/**
28*a1a3b679SAndreas Boehler * CalDAV scheduling plugin.
29*a1a3b679SAndreas Boehler * =========================
30*a1a3b679SAndreas Boehler *
31*a1a3b679SAndreas Boehler * This plugin provides the functionality added by the "Scheduling Extensions
32*a1a3b679SAndreas Boehler * to CalDAV" standard, as defined in RFC6638.
33*a1a3b679SAndreas Boehler *
34*a1a3b679SAndreas Boehler * calendar-auto-schedule largely works by intercepting a users request to
35*a1a3b679SAndreas Boehler * update their local calendar. If a user creates a new event with attendees,
36*a1a3b679SAndreas Boehler * this plugin is supposed to grab the information from that event, and notify
37*a1a3b679SAndreas Boehler * the attendees of this.
38*a1a3b679SAndreas Boehler *
39*a1a3b679SAndreas Boehler * There's 3 possible transports for this:
40*a1a3b679SAndreas Boehler * * local delivery
41*a1a3b679SAndreas Boehler * * delivery through email (iMip)
42*a1a3b679SAndreas Boehler * * server-to-server delivery (iSchedule)
43*a1a3b679SAndreas Boehler *
44*a1a3b679SAndreas Boehler * iMip is simply, because we just need to add the iTip message as an email
45*a1a3b679SAndreas Boehler * attachment. Local delivery is harder, because we both need to add this same
46*a1a3b679SAndreas Boehler * message to a local DAV inbox, as well as live-update the relevant events.
47*a1a3b679SAndreas Boehler *
48*a1a3b679SAndreas Boehler * iSchedule is something for later.
49*a1a3b679SAndreas Boehler *
50*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
51*a1a3b679SAndreas Boehler * @author Evert Pot (http://evertpot.com/)
52*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License
53*a1a3b679SAndreas Boehler */
54*a1a3b679SAndreas Boehlerclass Plugin extends ServerPlugin {
55*a1a3b679SAndreas Boehler
56*a1a3b679SAndreas Boehler    /**
57*a1a3b679SAndreas Boehler     * This is the official CalDAV namespace
58*a1a3b679SAndreas Boehler     */
59*a1a3b679SAndreas Boehler    const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
60*a1a3b679SAndreas Boehler
61*a1a3b679SAndreas Boehler    /**
62*a1a3b679SAndreas Boehler     * Reference to main Server object.
63*a1a3b679SAndreas Boehler     *
64*a1a3b679SAndreas Boehler     * @var Server
65*a1a3b679SAndreas Boehler     */
66*a1a3b679SAndreas Boehler    protected $server;
67*a1a3b679SAndreas Boehler
68*a1a3b679SAndreas Boehler    /**
69*a1a3b679SAndreas Boehler     * Returns a list of features for the DAV: HTTP header.
70*a1a3b679SAndreas Boehler     *
71*a1a3b679SAndreas Boehler     * @return array
72*a1a3b679SAndreas Boehler     */
73*a1a3b679SAndreas Boehler    function getFeatures() {
74*a1a3b679SAndreas Boehler
75*a1a3b679SAndreas Boehler        return ['calendar-auto-schedule'];
76*a1a3b679SAndreas Boehler
77*a1a3b679SAndreas Boehler    }
78*a1a3b679SAndreas Boehler
79*a1a3b679SAndreas Boehler    /**
80*a1a3b679SAndreas Boehler     * Returns the name of the plugin.
81*a1a3b679SAndreas Boehler     *
82*a1a3b679SAndreas Boehler     * Using this name other plugins will be able to access other plugins
83*a1a3b679SAndreas Boehler     * using Server::getPlugin
84*a1a3b679SAndreas Boehler     *
85*a1a3b679SAndreas Boehler     * @return string
86*a1a3b679SAndreas Boehler     */
87*a1a3b679SAndreas Boehler    function getPluginName() {
88*a1a3b679SAndreas Boehler
89*a1a3b679SAndreas Boehler        return 'caldav-schedule';
90*a1a3b679SAndreas Boehler
91*a1a3b679SAndreas Boehler    }
92*a1a3b679SAndreas Boehler
93*a1a3b679SAndreas Boehler    /**
94*a1a3b679SAndreas Boehler     * Initializes the plugin
95*a1a3b679SAndreas Boehler     *
96*a1a3b679SAndreas Boehler     * @param Server $server
97*a1a3b679SAndreas Boehler     * @return void
98*a1a3b679SAndreas Boehler     */
99*a1a3b679SAndreas Boehler    function initialize(Server $server) {
100*a1a3b679SAndreas Boehler
101*a1a3b679SAndreas Boehler        $this->server = $server;
102*a1a3b679SAndreas Boehler        $server->on('method:POST',          [$this, 'httpPost']);
103*a1a3b679SAndreas Boehler        $server->on('propFind',             [$this, 'propFind']);
104*a1a3b679SAndreas Boehler        $server->on('calendarObjectChange', [$this, 'calendarObjectChange']);
105*a1a3b679SAndreas Boehler        $server->on('beforeUnbind',         [$this, 'beforeUnbind']);
106*a1a3b679SAndreas Boehler        $server->on('schedule',             [$this, 'scheduleLocalDelivery']);
107*a1a3b679SAndreas Boehler
108*a1a3b679SAndreas Boehler        $ns = '{' . self::NS_CALDAV . '}';
109*a1a3b679SAndreas Boehler
110*a1a3b679SAndreas Boehler        /**
111*a1a3b679SAndreas Boehler         * This information ensures that the {DAV:}resourcetype property has
112*a1a3b679SAndreas Boehler         * the correct values.
113*a1a3b679SAndreas Boehler         */
114*a1a3b679SAndreas Boehler        $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = $ns . 'schedule-outbox';
115*a1a3b679SAndreas Boehler        $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IInbox'] = $ns . 'schedule-inbox';
116*a1a3b679SAndreas Boehler
117*a1a3b679SAndreas Boehler        /**
118*a1a3b679SAndreas Boehler         * Properties we protect are made read-only by the server.
119*a1a3b679SAndreas Boehler         */
120*a1a3b679SAndreas Boehler        array_push($server->protectedProperties,
121*a1a3b679SAndreas Boehler            $ns . 'schedule-inbox-URL',
122*a1a3b679SAndreas Boehler            $ns . 'schedule-outbox-URL',
123*a1a3b679SAndreas Boehler            $ns . 'calendar-user-address-set',
124*a1a3b679SAndreas Boehler            $ns . 'calendar-user-type',
125*a1a3b679SAndreas Boehler            $ns . 'schedule-default-calendar-URL'
126*a1a3b679SAndreas Boehler        );
127*a1a3b679SAndreas Boehler
128*a1a3b679SAndreas Boehler    }
129*a1a3b679SAndreas Boehler
130*a1a3b679SAndreas Boehler    /**
131*a1a3b679SAndreas Boehler     * Use this method to tell the server this plugin defines additional
132*a1a3b679SAndreas Boehler     * HTTP methods.
133*a1a3b679SAndreas Boehler     *
134*a1a3b679SAndreas Boehler     * This method is passed a uri. It should only return HTTP methods that are
135*a1a3b679SAndreas Boehler     * available for the specified uri.
136*a1a3b679SAndreas Boehler     *
137*a1a3b679SAndreas Boehler     * @param string $uri
138*a1a3b679SAndreas Boehler     * @return array
139*a1a3b679SAndreas Boehler     */
140*a1a3b679SAndreas Boehler    function getHTTPMethods($uri) {
141*a1a3b679SAndreas Boehler
142*a1a3b679SAndreas Boehler        try {
143*a1a3b679SAndreas Boehler            $node = $this->server->tree->getNodeForPath($uri);
144*a1a3b679SAndreas Boehler        } catch (NotFound $e) {
145*a1a3b679SAndreas Boehler            return [];
146*a1a3b679SAndreas Boehler        }
147*a1a3b679SAndreas Boehler
148*a1a3b679SAndreas Boehler        if ($node instanceof IOutbox) {
149*a1a3b679SAndreas Boehler            return ['POST'];
150*a1a3b679SAndreas Boehler        }
151*a1a3b679SAndreas Boehler
152*a1a3b679SAndreas Boehler        return [];
153*a1a3b679SAndreas Boehler
154*a1a3b679SAndreas Boehler    }
155*a1a3b679SAndreas Boehler
156*a1a3b679SAndreas Boehler    /**
157*a1a3b679SAndreas Boehler     * This method handles POST request for the outbox.
158*a1a3b679SAndreas Boehler     *
159*a1a3b679SAndreas Boehler     * @param RequestInterface $request
160*a1a3b679SAndreas Boehler     * @param ResponseInterface $response
161*a1a3b679SAndreas Boehler     * @return bool
162*a1a3b679SAndreas Boehler     */
163*a1a3b679SAndreas Boehler    function httpPost(RequestInterface $request, ResponseInterface $response) {
164*a1a3b679SAndreas Boehler
165*a1a3b679SAndreas Boehler        // Checking if this is a text/calendar content type
166*a1a3b679SAndreas Boehler        $contentType = $request->getHeader('Content-Type');
167*a1a3b679SAndreas Boehler        if (strpos($contentType, 'text/calendar') !== 0) {
168*a1a3b679SAndreas Boehler            return;
169*a1a3b679SAndreas Boehler        }
170*a1a3b679SAndreas Boehler
171*a1a3b679SAndreas Boehler        $path = $request->getPath();
172*a1a3b679SAndreas Boehler
173*a1a3b679SAndreas Boehler        // Checking if we're talking to an outbox
174*a1a3b679SAndreas Boehler        try {
175*a1a3b679SAndreas Boehler            $node = $this->server->tree->getNodeForPath($path);
176*a1a3b679SAndreas Boehler        } catch (NotFound $e) {
177*a1a3b679SAndreas Boehler            return;
178*a1a3b679SAndreas Boehler        }
179*a1a3b679SAndreas Boehler        if (!$node instanceof IOutbox)
180*a1a3b679SAndreas Boehler            return;
181*a1a3b679SAndreas Boehler
182*a1a3b679SAndreas Boehler        $this->server->transactionType = 'post-caldav-outbox';
183*a1a3b679SAndreas Boehler        $this->outboxRequest($node, $request, $response);
184*a1a3b679SAndreas Boehler
185*a1a3b679SAndreas Boehler        // Returning false breaks the event chain and tells the server we've
186*a1a3b679SAndreas Boehler        // handled the request.
187*a1a3b679SAndreas Boehler        return false;
188*a1a3b679SAndreas Boehler
189*a1a3b679SAndreas Boehler    }
190*a1a3b679SAndreas Boehler
191*a1a3b679SAndreas Boehler    /**
192*a1a3b679SAndreas Boehler     * This method handler is invoked during fetching of properties.
193*a1a3b679SAndreas Boehler     *
194*a1a3b679SAndreas Boehler     * We use this event to add calendar-auto-schedule-specific properties.
195*a1a3b679SAndreas Boehler     *
196*a1a3b679SAndreas Boehler     * @param PropFind $propFind
197*a1a3b679SAndreas Boehler     * @param INode $node
198*a1a3b679SAndreas Boehler     * @return void
199*a1a3b679SAndreas Boehler     */
200*a1a3b679SAndreas Boehler    function propFind(PropFind $propFind, INode $node) {
201*a1a3b679SAndreas Boehler
202*a1a3b679SAndreas Boehler        if (!$node instanceof DAVACL\IPrincipal) return;
203*a1a3b679SAndreas Boehler
204*a1a3b679SAndreas Boehler        $caldavPlugin = $this->server->getPlugin('caldav');
205*a1a3b679SAndreas Boehler        $principalUrl = $node->getPrincipalUrl();
206*a1a3b679SAndreas Boehler
207*a1a3b679SAndreas Boehler        // schedule-outbox-URL property
208*a1a3b679SAndreas Boehler        $propFind->handle('{' . self::NS_CALDAV . '}schedule-outbox-URL', function() use ($principalUrl, $caldavPlugin) {
209*a1a3b679SAndreas Boehler
210*a1a3b679SAndreas Boehler            $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
211*a1a3b679SAndreas Boehler            $outboxPath = $calendarHomePath . '/outbox/';
212*a1a3b679SAndreas Boehler
213*a1a3b679SAndreas Boehler            return new Href($outboxPath);
214*a1a3b679SAndreas Boehler
215*a1a3b679SAndreas Boehler        });
216*a1a3b679SAndreas Boehler        // schedule-inbox-URL property
217*a1a3b679SAndreas Boehler        $propFind->handle('{' . self::NS_CALDAV . '}schedule-inbox-URL', function() use ($principalUrl, $caldavPlugin) {
218*a1a3b679SAndreas Boehler
219*a1a3b679SAndreas Boehler            $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
220*a1a3b679SAndreas Boehler            $inboxPath = $calendarHomePath . '/inbox/';
221*a1a3b679SAndreas Boehler
222*a1a3b679SAndreas Boehler            return new Href($inboxPath);
223*a1a3b679SAndreas Boehler
224*a1a3b679SAndreas Boehler        });
225*a1a3b679SAndreas Boehler
226*a1a3b679SAndreas Boehler        $propFind->handle('{' . self::NS_CALDAV . '}schedule-default-calendar-URL', function() use ($principalUrl, $caldavPlugin) {
227*a1a3b679SAndreas Boehler
228*a1a3b679SAndreas Boehler            // We don't support customizing this property yet, so in the
229*a1a3b679SAndreas Boehler            // meantime we just grab the first calendar in the home-set.
230*a1a3b679SAndreas Boehler            $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
231*a1a3b679SAndreas Boehler
232*a1a3b679SAndreas Boehler            $sccs = '{' . self::NS_CALDAV . '}supported-calendar-component-set';
233*a1a3b679SAndreas Boehler
234*a1a3b679SAndreas Boehler            $result = $this->server->getPropertiesForPath($calendarHomePath, [
235*a1a3b679SAndreas Boehler                '{DAV:}resourcetype',
236*a1a3b679SAndreas Boehler                $sccs,
237*a1a3b679SAndreas Boehler            ], 1);
238*a1a3b679SAndreas Boehler
239*a1a3b679SAndreas Boehler            foreach ($result as $child) {
240*a1a3b679SAndreas Boehler                if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{' . self::NS_CALDAV . '}calendar') || $child[200]['{DAV:}resourcetype']->is('{http://calendarserver.org/ns/}shared')) {
241*a1a3b679SAndreas Boehler                    // Node is either not a calendar or a shared instance.
242*a1a3b679SAndreas Boehler                    continue;
243*a1a3b679SAndreas Boehler                }
244*a1a3b679SAndreas Boehler                if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) {
245*a1a3b679SAndreas Boehler                    // Either there is no supported-calendar-component-set
246*a1a3b679SAndreas Boehler                    // (which is fine) or we found one that supports VEVENT.
247*a1a3b679SAndreas Boehler                    return new Href($child['href']);
248*a1a3b679SAndreas Boehler                }
249*a1a3b679SAndreas Boehler            }
250*a1a3b679SAndreas Boehler
251*a1a3b679SAndreas Boehler        });
252*a1a3b679SAndreas Boehler
253*a1a3b679SAndreas Boehler        // The server currently reports every principal to be of type
254*a1a3b679SAndreas Boehler        // 'INDIVIDUAL'
255*a1a3b679SAndreas Boehler        $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-type', function() {
256*a1a3b679SAndreas Boehler
257*a1a3b679SAndreas Boehler            return 'INDIVIDUAL';
258*a1a3b679SAndreas Boehler
259*a1a3b679SAndreas Boehler        });
260*a1a3b679SAndreas Boehler
261*a1a3b679SAndreas Boehler    }
262*a1a3b679SAndreas Boehler
263*a1a3b679SAndreas Boehler    /**
264*a1a3b679SAndreas Boehler     * This method is triggered whenever there was a calendar object gets
265*a1a3b679SAndreas Boehler     * created or updated.
266*a1a3b679SAndreas Boehler     *
267*a1a3b679SAndreas Boehler     * @param RequestInterface $request HTTP request
268*a1a3b679SAndreas Boehler     * @param ResponseInterface $response HTTP Response
269*a1a3b679SAndreas Boehler     * @param VCalendar $vCal Parsed iCalendar object
270*a1a3b679SAndreas Boehler     * @param mixed $calendarPath Path to calendar collection
271*a1a3b679SAndreas Boehler     * @param mixed $modified The iCalendar object has been touched.
272*a1a3b679SAndreas Boehler     * @param mixed $isNew Whether this was a new item or we're updating one
273*a1a3b679SAndreas Boehler     * @return void
274*a1a3b679SAndreas Boehler     */
275*a1a3b679SAndreas Boehler    function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) {
276*a1a3b679SAndreas Boehler
277*a1a3b679SAndreas Boehler        if (!$this->scheduleReply($this->server->httpRequest)) {
278*a1a3b679SAndreas Boehler            return;
279*a1a3b679SAndreas Boehler        }
280*a1a3b679SAndreas Boehler
281*a1a3b679SAndreas Boehler        $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
282*a1a3b679SAndreas Boehler
283*a1a3b679SAndreas Boehler        $addresses = $this->getAddressesForPrincipal(
284*a1a3b679SAndreas Boehler            $calendarNode->getOwner()
285*a1a3b679SAndreas Boehler        );
286*a1a3b679SAndreas Boehler
287*a1a3b679SAndreas Boehler        if (!$isNew) {
288*a1a3b679SAndreas Boehler            $node = $this->server->tree->getNodeForPath($request->getPath());
289*a1a3b679SAndreas Boehler            $oldObj = Reader::read($node->get());
290*a1a3b679SAndreas Boehler        } else {
291*a1a3b679SAndreas Boehler            $oldObj = null;
292*a1a3b679SAndreas Boehler        }
293*a1a3b679SAndreas Boehler
294*a1a3b679SAndreas Boehler        $this->processICalendarChange($oldObj, $vCal, $addresses, [], $modified);
295*a1a3b679SAndreas Boehler
296*a1a3b679SAndreas Boehler    }
297*a1a3b679SAndreas Boehler
298*a1a3b679SAndreas Boehler    /**
299*a1a3b679SAndreas Boehler     * This method is responsible for delivering the ITip message.
300*a1a3b679SAndreas Boehler     *
301*a1a3b679SAndreas Boehler     * @param ITip\Message $itipMessage
302*a1a3b679SAndreas Boehler     * @return void
303*a1a3b679SAndreas Boehler     */
304*a1a3b679SAndreas Boehler    function deliver(ITip\Message $iTipMessage) {
305*a1a3b679SAndreas Boehler
306*a1a3b679SAndreas Boehler        $this->server->emit('schedule', [$iTipMessage]);
307*a1a3b679SAndreas Boehler        if (!$iTipMessage->scheduleStatus) {
308*a1a3b679SAndreas Boehler            $iTipMessage->scheduleStatus = '5.2;There was no system capable of delivering the scheduling message';
309*a1a3b679SAndreas Boehler        }
310*a1a3b679SAndreas Boehler        // In case the change was considered 'insignificant', we are going to
311*a1a3b679SAndreas Boehler        // remove any error statuses, if any. See ticket #525.
312*a1a3b679SAndreas Boehler        list($baseCode) = explode('.', $iTipMessage->scheduleStatus);
313*a1a3b679SAndreas Boehler        if (!$iTipMessage->significantChange && in_array($baseCode, ['3', '5'])) {
314*a1a3b679SAndreas Boehler            $iTipMessage->scheduleStatus = null;
315*a1a3b679SAndreas Boehler        }
316*a1a3b679SAndreas Boehler
317*a1a3b679SAndreas Boehler    }
318*a1a3b679SAndreas Boehler
319*a1a3b679SAndreas Boehler    /**
320*a1a3b679SAndreas Boehler     * This method is triggered before a file gets deleted.
321*a1a3b679SAndreas Boehler     *
322*a1a3b679SAndreas Boehler     * We use this event to make sure that when this happens, attendees get
323*a1a3b679SAndreas Boehler     * cancellations, and organizers get 'DECLINED' statuses.
324*a1a3b679SAndreas Boehler     *
325*a1a3b679SAndreas Boehler     * @param string $path
326*a1a3b679SAndreas Boehler     * @return void
327*a1a3b679SAndreas Boehler     */
328*a1a3b679SAndreas Boehler    function beforeUnbind($path) {
329*a1a3b679SAndreas Boehler
330*a1a3b679SAndreas Boehler        // FIXME: We shouldn't trigger this functionality when we're issuing a
331*a1a3b679SAndreas Boehler        // MOVE. This is a hack.
332*a1a3b679SAndreas Boehler        if ($this->server->httpRequest->getMethod() === 'MOVE') return;
333*a1a3b679SAndreas Boehler
334*a1a3b679SAndreas Boehler        $node = $this->server->tree->getNodeForPath($path);
335*a1a3b679SAndreas Boehler
336*a1a3b679SAndreas Boehler        if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
337*a1a3b679SAndreas Boehler            return;
338*a1a3b679SAndreas Boehler        }
339*a1a3b679SAndreas Boehler
340*a1a3b679SAndreas Boehler        if (!$this->scheduleReply($this->server->httpRequest)) {
341*a1a3b679SAndreas Boehler            return;
342*a1a3b679SAndreas Boehler        }
343*a1a3b679SAndreas Boehler
344*a1a3b679SAndreas Boehler        $addresses = $this->getAddressesForPrincipal(
345*a1a3b679SAndreas Boehler            $node->getOwner()
346*a1a3b679SAndreas Boehler        );
347*a1a3b679SAndreas Boehler
348*a1a3b679SAndreas Boehler        $broker = new ITip\Broker();
349*a1a3b679SAndreas Boehler        $messages = $broker->parseEvent(null, $addresses, $node->get());
350*a1a3b679SAndreas Boehler
351*a1a3b679SAndreas Boehler        foreach ($messages as $message) {
352*a1a3b679SAndreas Boehler            $this->deliver($message);
353*a1a3b679SAndreas Boehler        }
354*a1a3b679SAndreas Boehler
355*a1a3b679SAndreas Boehler    }
356*a1a3b679SAndreas Boehler
357*a1a3b679SAndreas Boehler    /**
358*a1a3b679SAndreas Boehler     * Event handler for the 'schedule' event.
359*a1a3b679SAndreas Boehler     *
360*a1a3b679SAndreas Boehler     * This handler attempts to look at local accounts to deliver the
361*a1a3b679SAndreas Boehler     * scheduling object.
362*a1a3b679SAndreas Boehler     *
363*a1a3b679SAndreas Boehler     * @param ITip\Message $iTipMessage
364*a1a3b679SAndreas Boehler     * @return void
365*a1a3b679SAndreas Boehler     */
366*a1a3b679SAndreas Boehler    function scheduleLocalDelivery(ITip\Message $iTipMessage) {
367*a1a3b679SAndreas Boehler
368*a1a3b679SAndreas Boehler        $aclPlugin = $this->server->getPlugin('acl');
369*a1a3b679SAndreas Boehler
370*a1a3b679SAndreas Boehler        // Local delivery is not available if the ACL plugin is not loaded.
371*a1a3b679SAndreas Boehler        if (!$aclPlugin) {
372*a1a3b679SAndreas Boehler            return;
373*a1a3b679SAndreas Boehler        }
374*a1a3b679SAndreas Boehler
375*a1a3b679SAndreas Boehler        $caldavNS = '{' . self::NS_CALDAV . '}';
376*a1a3b679SAndreas Boehler
377*a1a3b679SAndreas Boehler        $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
378*a1a3b679SAndreas Boehler        if (!$principalUri) {
379*a1a3b679SAndreas Boehler            $iTipMessage->scheduleStatus = '3.7;Could not find principal.';
380*a1a3b679SAndreas Boehler            return;
381*a1a3b679SAndreas Boehler        }
382*a1a3b679SAndreas Boehler
383*a1a3b679SAndreas Boehler        // We found a principal URL, now we need to find its inbox.
384*a1a3b679SAndreas Boehler        // Unfortunately we may not have sufficient privileges to find this, so
385*a1a3b679SAndreas Boehler        // we are temporarily turning off ACL to let this come through.
386*a1a3b679SAndreas Boehler        //
387*a1a3b679SAndreas Boehler        // Once we support PHP 5.5, this should be wrapped in a try..finally
388*a1a3b679SAndreas Boehler        // block so we can ensure that this privilege gets added again after.
389*a1a3b679SAndreas Boehler        $this->server->removeListener('propFind', [$aclPlugin, 'propFind']);
390*a1a3b679SAndreas Boehler
391*a1a3b679SAndreas Boehler        $result = $this->server->getProperties(
392*a1a3b679SAndreas Boehler            $principalUri,
393*a1a3b679SAndreas Boehler            [
394*a1a3b679SAndreas Boehler                '{DAV:}principal-URL',
395*a1a3b679SAndreas Boehler                 $caldavNS . 'calendar-home-set',
396*a1a3b679SAndreas Boehler                 $caldavNS . 'schedule-inbox-URL',
397*a1a3b679SAndreas Boehler                 $caldavNS . 'schedule-default-calendar-URL',
398*a1a3b679SAndreas Boehler                '{http://sabredav.org/ns}email-address',
399*a1a3b679SAndreas Boehler            ]
400*a1a3b679SAndreas Boehler        );
401*a1a3b679SAndreas Boehler
402*a1a3b679SAndreas Boehler        // Re-registering the ACL event
403*a1a3b679SAndreas Boehler        $this->server->on('propFind', [$aclPlugin, 'propFind'], 20);
404*a1a3b679SAndreas Boehler
405*a1a3b679SAndreas Boehler        if (!isset($result[$caldavNS . 'schedule-inbox-URL'])) {
406*a1a3b679SAndreas Boehler            $iTipMessage->scheduleStatus = '5.2;Could not find local inbox';
407*a1a3b679SAndreas Boehler            return;
408*a1a3b679SAndreas Boehler        }
409*a1a3b679SAndreas Boehler        if (!isset($result[$caldavNS . 'calendar-home-set'])) {
410*a1a3b679SAndreas Boehler            $iTipMessage->scheduleStatus = '5.2;Could not locate a calendar-home-set';
411*a1a3b679SAndreas Boehler            return;
412*a1a3b679SAndreas Boehler        }
413*a1a3b679SAndreas Boehler        if (!isset($result[$caldavNS . 'schedule-default-calendar-URL'])) {
414*a1a3b679SAndreas Boehler            $iTipMessage->scheduleStatus = '5.2;Could not find a schedule-default-calendar-URL property';
415*a1a3b679SAndreas Boehler            return;
416*a1a3b679SAndreas Boehler        }
417*a1a3b679SAndreas Boehler
418*a1a3b679SAndreas Boehler        $calendarPath = $result[$caldavNS . 'schedule-default-calendar-URL']->getHref();
419*a1a3b679SAndreas Boehler        $homePath = $result[$caldavNS . 'calendar-home-set']->getHref();
420*a1a3b679SAndreas Boehler        $inboxPath = $result[$caldavNS . 'schedule-inbox-URL']->getHref();
421*a1a3b679SAndreas Boehler
422*a1a3b679SAndreas Boehler        if ($iTipMessage->method === 'REPLY') {
423*a1a3b679SAndreas Boehler            $privilege = 'schedule-deliver-reply';
424*a1a3b679SAndreas Boehler        } else {
425*a1a3b679SAndreas Boehler            $privilege = 'schedule-deliver-invite';
426*a1a3b679SAndreas Boehler        }
427*a1a3b679SAndreas Boehler
428*a1a3b679SAndreas Boehler        if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS . $privilege, DAVACL\Plugin::R_PARENT, false)) {
429*a1a3b679SAndreas Boehler            $iTipMessage->scheduleStatus = '3.8;organizer did not have the ' . $privilege . ' privilege on the attendees inbox';
430*a1a3b679SAndreas Boehler            return;
431*a1a3b679SAndreas Boehler        }
432*a1a3b679SAndreas Boehler
433*a1a3b679SAndreas Boehler        // Next, we're going to find out if the item already exits in one of
434*a1a3b679SAndreas Boehler        // the users' calendars.
435*a1a3b679SAndreas Boehler        $uid = $iTipMessage->uid;
436*a1a3b679SAndreas Boehler
437*a1a3b679SAndreas Boehler        $newFileName = 'sabredav-' . \Sabre\DAV\UUIDUtil::getUUID() . '.ics';
438*a1a3b679SAndreas Boehler
439*a1a3b679SAndreas Boehler        $home = $this->server->tree->getNodeForPath($homePath);
440*a1a3b679SAndreas Boehler        $inbox = $this->server->tree->getNodeForPath($inboxPath);
441*a1a3b679SAndreas Boehler
442*a1a3b679SAndreas Boehler        $currentObject = null;
443*a1a3b679SAndreas Boehler        $objectNode = null;
444*a1a3b679SAndreas Boehler        $isNewNode = false;
445*a1a3b679SAndreas Boehler
446*a1a3b679SAndreas Boehler        $result = $home->getCalendarObjectByUID($uid);
447*a1a3b679SAndreas Boehler        if ($result) {
448*a1a3b679SAndreas Boehler            // There was an existing object, we need to update probably.
449*a1a3b679SAndreas Boehler            $objectPath = $homePath . '/' . $result;
450*a1a3b679SAndreas Boehler            $objectNode = $this->server->tree->getNodeForPath($objectPath);
451*a1a3b679SAndreas Boehler            $oldICalendarData = $objectNode->get();
452*a1a3b679SAndreas Boehler            $currentObject = Reader::read($oldICalendarData);
453*a1a3b679SAndreas Boehler        } else {
454*a1a3b679SAndreas Boehler            $isNewNode = true;
455*a1a3b679SAndreas Boehler        }
456*a1a3b679SAndreas Boehler
457*a1a3b679SAndreas Boehler        $broker = new ITip\Broker();
458*a1a3b679SAndreas Boehler        $newObject = $broker->processMessage($iTipMessage, $currentObject);
459*a1a3b679SAndreas Boehler
460*a1a3b679SAndreas Boehler        $inbox->createFile($newFileName, $iTipMessage->message->serialize());
461*a1a3b679SAndreas Boehler
462*a1a3b679SAndreas Boehler        if (!$newObject) {
463*a1a3b679SAndreas Boehler            // We received an iTip message referring to a UID that we don't
464*a1a3b679SAndreas Boehler            // have in any calendars yet, and processMessage did not give us a
465*a1a3b679SAndreas Boehler            // calendarobject back.
466*a1a3b679SAndreas Boehler            //
467*a1a3b679SAndreas Boehler            // The implication is that processMessage did not understand the
468*a1a3b679SAndreas Boehler            // iTip message.
469*a1a3b679SAndreas Boehler            $iTipMessage->scheduleStatus = '5.0;iTip message was not processed by the server, likely because we didn\'t understand it.';
470*a1a3b679SAndreas Boehler            return;
471*a1a3b679SAndreas Boehler        }
472*a1a3b679SAndreas Boehler
473*a1a3b679SAndreas Boehler        // Note that we are bypassing ACL on purpose by calling this directly.
474*a1a3b679SAndreas Boehler        // We may need to look a bit deeper into this later. Supporting ACL
475*a1a3b679SAndreas Boehler        // here would be nice.
476*a1a3b679SAndreas Boehler        if ($isNewNode) {
477*a1a3b679SAndreas Boehler            $calendar = $this->server->tree->getNodeForPath($calendarPath);
478*a1a3b679SAndreas Boehler            $calendar->createFile($newFileName, $newObject->serialize());
479*a1a3b679SAndreas Boehler        } else {
480*a1a3b679SAndreas Boehler            // If the message was a reply, we may have to inform other
481*a1a3b679SAndreas Boehler            // attendees of this attendees status. Therefore we're shooting off
482*a1a3b679SAndreas Boehler            // another itipMessage.
483*a1a3b679SAndreas Boehler            if ($iTipMessage->method === 'REPLY') {
484*a1a3b679SAndreas Boehler                $this->processICalendarChange(
485*a1a3b679SAndreas Boehler                    $oldICalendarData,
486*a1a3b679SAndreas Boehler                    $newObject,
487*a1a3b679SAndreas Boehler                    [$iTipMessage->recipient],
488*a1a3b679SAndreas Boehler                    [$iTipMessage->sender]
489*a1a3b679SAndreas Boehler                );
490*a1a3b679SAndreas Boehler            }
491*a1a3b679SAndreas Boehler            $objectNode->put($newObject->serialize());
492*a1a3b679SAndreas Boehler        }
493*a1a3b679SAndreas Boehler        $iTipMessage->scheduleStatus = '1.2;Message delivered locally';
494*a1a3b679SAndreas Boehler
495*a1a3b679SAndreas Boehler    }
496*a1a3b679SAndreas Boehler
497*a1a3b679SAndreas Boehler    /**
498*a1a3b679SAndreas Boehler     * This method looks at an old iCalendar object, a new iCalendar object and
499*a1a3b679SAndreas Boehler     * starts sending scheduling messages based on the changes.
500*a1a3b679SAndreas Boehler     *
501*a1a3b679SAndreas Boehler     * A list of addresses needs to be specified, so the system knows who made
502*a1a3b679SAndreas Boehler     * the update, because the behavior may be different based on if it's an
503*a1a3b679SAndreas Boehler     * attendee or an organizer.
504*a1a3b679SAndreas Boehler     *
505*a1a3b679SAndreas Boehler     * This method may update $newObject to add any status changes.
506*a1a3b679SAndreas Boehler     *
507*a1a3b679SAndreas Boehler     * @param VCalendar|string $oldObject
508*a1a3b679SAndreas Boehler     * @param VCalendar $newObject
509*a1a3b679SAndreas Boehler     * @param array $addresses
510*a1a3b679SAndreas Boehler     * @param array $ignore Any addresses to not send messages to.
511*a1a3b679SAndreas Boehler     * @param bool $modified A marker to indicate that the original object
512*a1a3b679SAndreas Boehler     *   modified by this process.
513*a1a3b679SAndreas Boehler     * @return void
514*a1a3b679SAndreas Boehler     */
515*a1a3b679SAndreas Boehler    protected function processICalendarChange($oldObject = null, VCalendar $newObject, array $addresses, array $ignore = [], &$modified = false) {
516*a1a3b679SAndreas Boehler
517*a1a3b679SAndreas Boehler        $broker = new ITip\Broker();
518*a1a3b679SAndreas Boehler        $messages = $broker->parseEvent($newObject, $addresses, $oldObject);
519*a1a3b679SAndreas Boehler
520*a1a3b679SAndreas Boehler        if ($messages) $modified = true;
521*a1a3b679SAndreas Boehler
522*a1a3b679SAndreas Boehler        foreach ($messages as $message) {
523*a1a3b679SAndreas Boehler
524*a1a3b679SAndreas Boehler            if (in_array($message->recipient, $ignore)) {
525*a1a3b679SAndreas Boehler                continue;
526*a1a3b679SAndreas Boehler            }
527*a1a3b679SAndreas Boehler
528*a1a3b679SAndreas Boehler            $this->deliver($message);
529*a1a3b679SAndreas Boehler
530*a1a3b679SAndreas Boehler            if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() === $message->recipient)) {
531*a1a3b679SAndreas Boehler                if ($message->scheduleStatus) {
532*a1a3b679SAndreas Boehler                    $newObject->VEVENT->ORGANIZER['SCHEDULE-STATUS'] = $message->getScheduleStatus();
533*a1a3b679SAndreas Boehler                }
534*a1a3b679SAndreas Boehler                unset($newObject->VEVENT->ORGANIZER['SCHEDULE-FORCE-SEND']);
535*a1a3b679SAndreas Boehler
536*a1a3b679SAndreas Boehler            } else {
537*a1a3b679SAndreas Boehler
538*a1a3b679SAndreas Boehler                if (isset($newObject->VEVENT->ATTENDEE)) foreach ($newObject->VEVENT->ATTENDEE as $attendee) {
539*a1a3b679SAndreas Boehler
540*a1a3b679SAndreas Boehler                    if ($attendee->getNormalizedValue() === $message->recipient) {
541*a1a3b679SAndreas Boehler                        if ($message->scheduleStatus) {
542*a1a3b679SAndreas Boehler                            $attendee['SCHEDULE-STATUS'] = $message->getScheduleStatus();
543*a1a3b679SAndreas Boehler                        }
544*a1a3b679SAndreas Boehler                        unset($attendee['SCHEDULE-FORCE-SEND']);
545*a1a3b679SAndreas Boehler                        break;
546*a1a3b679SAndreas Boehler                    }
547*a1a3b679SAndreas Boehler
548*a1a3b679SAndreas Boehler                }
549*a1a3b679SAndreas Boehler
550*a1a3b679SAndreas Boehler            }
551*a1a3b679SAndreas Boehler
552*a1a3b679SAndreas Boehler        }
553*a1a3b679SAndreas Boehler
554*a1a3b679SAndreas Boehler    }
555*a1a3b679SAndreas Boehler
556*a1a3b679SAndreas Boehler    /**
557*a1a3b679SAndreas Boehler     * Returns a list of addresses that are associated with a principal.
558*a1a3b679SAndreas Boehler     *
559*a1a3b679SAndreas Boehler     * @param string $principal
560*a1a3b679SAndreas Boehler     * @return array
561*a1a3b679SAndreas Boehler     */
562*a1a3b679SAndreas Boehler    protected function getAddressesForPrincipal($principal) {
563*a1a3b679SAndreas Boehler
564*a1a3b679SAndreas Boehler        $CUAS = '{' . self::NS_CALDAV . '}calendar-user-address-set';
565*a1a3b679SAndreas Boehler
566*a1a3b679SAndreas Boehler        $properties = $this->server->getProperties(
567*a1a3b679SAndreas Boehler            $principal,
568*a1a3b679SAndreas Boehler            [$CUAS]
569*a1a3b679SAndreas Boehler        );
570*a1a3b679SAndreas Boehler
571*a1a3b679SAndreas Boehler        // If we can't find this information, we'll stop processing
572*a1a3b679SAndreas Boehler        if (!isset($properties[$CUAS])) {
573*a1a3b679SAndreas Boehler            return;
574*a1a3b679SAndreas Boehler        }
575*a1a3b679SAndreas Boehler
576*a1a3b679SAndreas Boehler        $addresses = $properties[$CUAS]->getHrefs();
577*a1a3b679SAndreas Boehler        return $addresses;
578*a1a3b679SAndreas Boehler
579*a1a3b679SAndreas Boehler    }
580*a1a3b679SAndreas Boehler
581*a1a3b679SAndreas Boehler    /**
582*a1a3b679SAndreas Boehler     * This method handles POST requests to the schedule-outbox.
583*a1a3b679SAndreas Boehler     *
584*a1a3b679SAndreas Boehler     * Currently, two types of requests are support:
585*a1a3b679SAndreas Boehler     *   * FREEBUSY requests from RFC 6638
586*a1a3b679SAndreas Boehler     *   * Simple iTIP messages from draft-desruisseaux-caldav-sched-04
587*a1a3b679SAndreas Boehler     *
588*a1a3b679SAndreas Boehler     * The latter is from an expired early draft of the CalDAV scheduling
589*a1a3b679SAndreas Boehler     * extensions, but iCal depends on a feature from that spec, so we
590*a1a3b679SAndreas Boehler     * implement it.
591*a1a3b679SAndreas Boehler     *
592*a1a3b679SAndreas Boehler     * @param IOutbox $outboxNode
593*a1a3b679SAndreas Boehler     * @param RequestInterface $request
594*a1a3b679SAndreas Boehler     * @param ResponseInterface $response
595*a1a3b679SAndreas Boehler     * @return void
596*a1a3b679SAndreas Boehler     */
597*a1a3b679SAndreas Boehler    function outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response) {
598*a1a3b679SAndreas Boehler
599*a1a3b679SAndreas Boehler        $outboxPath = $request->getPath();
600*a1a3b679SAndreas Boehler
601*a1a3b679SAndreas Boehler        // Parsing the request body
602*a1a3b679SAndreas Boehler        try {
603*a1a3b679SAndreas Boehler            $vObject = VObject\Reader::read($request->getBody());
604*a1a3b679SAndreas Boehler        } catch (VObject\ParseException $e) {
605*a1a3b679SAndreas Boehler            throw new BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage());
606*a1a3b679SAndreas Boehler        }
607*a1a3b679SAndreas Boehler
608*a1a3b679SAndreas Boehler        // The incoming iCalendar object must have a METHOD property, and a
609*a1a3b679SAndreas Boehler        // component. The combination of both determines what type of request
610*a1a3b679SAndreas Boehler        // this is.
611*a1a3b679SAndreas Boehler        $componentType = null;
612*a1a3b679SAndreas Boehler        foreach ($vObject->getComponents() as $component) {
613*a1a3b679SAndreas Boehler            if ($component->name !== 'VTIMEZONE') {
614*a1a3b679SAndreas Boehler                $componentType = $component->name;
615*a1a3b679SAndreas Boehler                break;
616*a1a3b679SAndreas Boehler            }
617*a1a3b679SAndreas Boehler        }
618*a1a3b679SAndreas Boehler        if (is_null($componentType)) {
619*a1a3b679SAndreas Boehler            throw new BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
620*a1a3b679SAndreas Boehler        }
621*a1a3b679SAndreas Boehler
622*a1a3b679SAndreas Boehler        // Validating the METHOD
623*a1a3b679SAndreas Boehler        $method = strtoupper((string)$vObject->METHOD);
624*a1a3b679SAndreas Boehler        if (!$method) {
625*a1a3b679SAndreas Boehler            throw new BadRequest('A METHOD property must be specified in iTIP messages');
626*a1a3b679SAndreas Boehler        }
627*a1a3b679SAndreas Boehler
628*a1a3b679SAndreas Boehler        // So we support one type of request:
629*a1a3b679SAndreas Boehler        //
630*a1a3b679SAndreas Boehler        // REQUEST with a VFREEBUSY component
631*a1a3b679SAndreas Boehler
632*a1a3b679SAndreas Boehler        $acl = $this->server->getPlugin('acl');
633*a1a3b679SAndreas Boehler
634*a1a3b679SAndreas Boehler        if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') {
635*a1a3b679SAndreas Boehler
636*a1a3b679SAndreas Boehler            $acl && $acl->checkPrivileges($outboxPath, '{' . self::NS_CALDAV . '}schedule-query-freebusy');
637*a1a3b679SAndreas Boehler            $this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response);
638*a1a3b679SAndreas Boehler
639*a1a3b679SAndreas Boehler        } else {
640*a1a3b679SAndreas Boehler
641*a1a3b679SAndreas Boehler            throw new NotImplemented('We only support VFREEBUSY (REQUEST) on this endpoint');
642*a1a3b679SAndreas Boehler
643*a1a3b679SAndreas Boehler        }
644*a1a3b679SAndreas Boehler
645*a1a3b679SAndreas Boehler    }
646*a1a3b679SAndreas Boehler
647*a1a3b679SAndreas Boehler    /**
648*a1a3b679SAndreas Boehler     * This method is responsible for parsing a free-busy query request and
649*a1a3b679SAndreas Boehler     * returning it's result.
650*a1a3b679SAndreas Boehler     *
651*a1a3b679SAndreas Boehler     * @param IOutbox $outbox
652*a1a3b679SAndreas Boehler     * @param VObject\Component $vObject
653*a1a3b679SAndreas Boehler     * @param RequestInterface $request
654*a1a3b679SAndreas Boehler     * @param ResponseInterface $response
655*a1a3b679SAndreas Boehler     * @return string
656*a1a3b679SAndreas Boehler     */
657*a1a3b679SAndreas Boehler    protected function handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response) {
658*a1a3b679SAndreas Boehler
659*a1a3b679SAndreas Boehler        $vFreeBusy = $vObject->VFREEBUSY;
660*a1a3b679SAndreas Boehler        $organizer = $vFreeBusy->organizer;
661*a1a3b679SAndreas Boehler
662*a1a3b679SAndreas Boehler        $organizer = (string)$organizer;
663*a1a3b679SAndreas Boehler
664*a1a3b679SAndreas Boehler        // Validating if the organizer matches the owner of the inbox.
665*a1a3b679SAndreas Boehler        $owner = $outbox->getOwner();
666*a1a3b679SAndreas Boehler
667*a1a3b679SAndreas Boehler        $caldavNS = '{' . self::NS_CALDAV . '}';
668*a1a3b679SAndreas Boehler
669*a1a3b679SAndreas Boehler        $uas = $caldavNS . 'calendar-user-address-set';
670*a1a3b679SAndreas Boehler        $props = $this->server->getProperties($owner, [$uas]);
671*a1a3b679SAndreas Boehler
672*a1a3b679SAndreas Boehler        if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
673*a1a3b679SAndreas Boehler            throw new Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
674*a1a3b679SAndreas Boehler        }
675*a1a3b679SAndreas Boehler
676*a1a3b679SAndreas Boehler        if (!isset($vFreeBusy->ATTENDEE)) {
677*a1a3b679SAndreas Boehler            throw new BadRequest('You must at least specify 1 attendee');
678*a1a3b679SAndreas Boehler        }
679*a1a3b679SAndreas Boehler
680*a1a3b679SAndreas Boehler        $attendees = [];
681*a1a3b679SAndreas Boehler        foreach ($vFreeBusy->ATTENDEE as $attendee) {
682*a1a3b679SAndreas Boehler            $attendees[] = (string)$attendee;
683*a1a3b679SAndreas Boehler        }
684*a1a3b679SAndreas Boehler
685*a1a3b679SAndreas Boehler
686*a1a3b679SAndreas Boehler        if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
687*a1a3b679SAndreas Boehler            throw new BadRequest('DTSTART and DTEND must both be specified');
688*a1a3b679SAndreas Boehler        }
689*a1a3b679SAndreas Boehler
690*a1a3b679SAndreas Boehler        $startRange = $vFreeBusy->DTSTART->getDateTime();
691*a1a3b679SAndreas Boehler        $endRange = $vFreeBusy->DTEND->getDateTime();
692*a1a3b679SAndreas Boehler
693*a1a3b679SAndreas Boehler        $results = [];
694*a1a3b679SAndreas Boehler        foreach ($attendees as $attendee) {
695*a1a3b679SAndreas Boehler            $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
696*a1a3b679SAndreas Boehler        }
697*a1a3b679SAndreas Boehler
698*a1a3b679SAndreas Boehler        $dom = new \DOMDocument('1.0', 'utf-8');
699*a1a3b679SAndreas Boehler        $dom->formatOutput = true;
700*a1a3b679SAndreas Boehler        $scheduleResponse = $dom->createElement('cal:schedule-response');
701*a1a3b679SAndreas Boehler        foreach ($this->server->xml->namespaceMap as $namespace => $prefix) {
702*a1a3b679SAndreas Boehler
703*a1a3b679SAndreas Boehler            $scheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
704*a1a3b679SAndreas Boehler
705*a1a3b679SAndreas Boehler        }
706*a1a3b679SAndreas Boehler        $dom->appendChild($scheduleResponse);
707*a1a3b679SAndreas Boehler
708*a1a3b679SAndreas Boehler        foreach ($results as $result) {
709*a1a3b679SAndreas Boehler            $xresponse = $dom->createElement('cal:response');
710*a1a3b679SAndreas Boehler
711*a1a3b679SAndreas Boehler            $recipient = $dom->createElement('cal:recipient');
712*a1a3b679SAndreas Boehler            $recipientHref = $dom->createElement('d:href');
713*a1a3b679SAndreas Boehler
714*a1a3b679SAndreas Boehler            $recipientHref->appendChild($dom->createTextNode($result['href']));
715*a1a3b679SAndreas Boehler            $recipient->appendChild($recipientHref);
716*a1a3b679SAndreas Boehler            $xresponse->appendChild($recipient);
717*a1a3b679SAndreas Boehler
718*a1a3b679SAndreas Boehler            $reqStatus = $dom->createElement('cal:request-status');
719*a1a3b679SAndreas Boehler            $reqStatus->appendChild($dom->createTextNode($result['request-status']));
720*a1a3b679SAndreas Boehler            $xresponse->appendChild($reqStatus);
721*a1a3b679SAndreas Boehler
722*a1a3b679SAndreas Boehler            if (isset($result['calendar-data'])) {
723*a1a3b679SAndreas Boehler
724*a1a3b679SAndreas Boehler                $calendardata = $dom->createElement('cal:calendar-data');
725*a1a3b679SAndreas Boehler                $calendardata->appendChild($dom->createTextNode(str_replace("\r\n", "\n", $result['calendar-data']->serialize())));
726*a1a3b679SAndreas Boehler                $xresponse->appendChild($calendardata);
727*a1a3b679SAndreas Boehler
728*a1a3b679SAndreas Boehler            }
729*a1a3b679SAndreas Boehler            $scheduleResponse->appendChild($xresponse);
730*a1a3b679SAndreas Boehler        }
731*a1a3b679SAndreas Boehler
732*a1a3b679SAndreas Boehler        $response->setStatus(200);
733*a1a3b679SAndreas Boehler        $response->setHeader('Content-Type', 'application/xml');
734*a1a3b679SAndreas Boehler        $response->setBody($dom->saveXML());
735*a1a3b679SAndreas Boehler
736*a1a3b679SAndreas Boehler    }
737*a1a3b679SAndreas Boehler
738*a1a3b679SAndreas Boehler    /**
739*a1a3b679SAndreas Boehler     * Returns free-busy information for a specific address. The returned
740*a1a3b679SAndreas Boehler     * data is an array containing the following properties:
741*a1a3b679SAndreas Boehler     *
742*a1a3b679SAndreas Boehler     * calendar-data : A VFREEBUSY VObject
743*a1a3b679SAndreas Boehler     * request-status : an iTip status code.
744*a1a3b679SAndreas Boehler     * href: The principal's email address, as requested
745*a1a3b679SAndreas Boehler     *
746*a1a3b679SAndreas Boehler     * The following request status codes may be returned:
747*a1a3b679SAndreas Boehler     *   * 2.0;description
748*a1a3b679SAndreas Boehler     *   * 3.7;description
749*a1a3b679SAndreas Boehler     *
750*a1a3b679SAndreas Boehler     * @param string $email address
751*a1a3b679SAndreas Boehler     * @param \DateTime $start
752*a1a3b679SAndreas Boehler     * @param \DateTime $end
753*a1a3b679SAndreas Boehler     * @param VObject\Component $request
754*a1a3b679SAndreas Boehler     * @return array
755*a1a3b679SAndreas Boehler     */
756*a1a3b679SAndreas Boehler    protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request) {
757*a1a3b679SAndreas Boehler
758*a1a3b679SAndreas Boehler        $caldavNS = '{' . self::NS_CALDAV . '}';
759*a1a3b679SAndreas Boehler
760*a1a3b679SAndreas Boehler        $aclPlugin = $this->server->getPlugin('acl');
761*a1a3b679SAndreas Boehler        if (substr($email, 0, 7) === 'mailto:') $email = substr($email, 7);
762*a1a3b679SAndreas Boehler
763*a1a3b679SAndreas Boehler        $result = $aclPlugin->principalSearch(
764*a1a3b679SAndreas Boehler            ['{http://sabredav.org/ns}email-address' => $email],
765*a1a3b679SAndreas Boehler            [
766*a1a3b679SAndreas Boehler                '{DAV:}principal-URL', $caldavNS . 'calendar-home-set',
767*a1a3b679SAndreas Boehler                '{http://sabredav.org/ns}email-address',
768*a1a3b679SAndreas Boehler            ]
769*a1a3b679SAndreas Boehler        );
770*a1a3b679SAndreas Boehler
771*a1a3b679SAndreas Boehler        if (!count($result)) {
772*a1a3b679SAndreas Boehler            return [
773*a1a3b679SAndreas Boehler                'request-status' => '3.7;Could not find principal',
774*a1a3b679SAndreas Boehler                'href'           => 'mailto:' . $email,
775*a1a3b679SAndreas Boehler            ];
776*a1a3b679SAndreas Boehler        }
777*a1a3b679SAndreas Boehler
778*a1a3b679SAndreas Boehler        if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) {
779*a1a3b679SAndreas Boehler            return [
780*a1a3b679SAndreas Boehler                'request-status' => '3.7;No calendar-home-set property found',
781*a1a3b679SAndreas Boehler                'href'           => 'mailto:' . $email,
782*a1a3b679SAndreas Boehler            ];
783*a1a3b679SAndreas Boehler        }
784*a1a3b679SAndreas Boehler        $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref();
785*a1a3b679SAndreas Boehler
786*a1a3b679SAndreas Boehler        // Grabbing the calendar list
787*a1a3b679SAndreas Boehler        $objects = [];
788*a1a3b679SAndreas Boehler        $calendarTimeZone = new DateTimeZone('UTC');
789*a1a3b679SAndreas Boehler
790*a1a3b679SAndreas Boehler        foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
791*a1a3b679SAndreas Boehler            if (!$node instanceof ICalendar) {
792*a1a3b679SAndreas Boehler                continue;
793*a1a3b679SAndreas Boehler            }
794*a1a3b679SAndreas Boehler
795*a1a3b679SAndreas Boehler            $sct = $caldavNS . 'schedule-calendar-transp';
796*a1a3b679SAndreas Boehler            $ctz = $caldavNS . 'calendar-timezone';
797*a1a3b679SAndreas Boehler            $props = $node->getProperties([$sct, $ctz]);
798*a1a3b679SAndreas Boehler
799*a1a3b679SAndreas Boehler            if (isset($props[$sct]) && $props[$sct]->getValue() == ScheduleCalendarTransp::TRANSPARENT) {
800*a1a3b679SAndreas Boehler                // If a calendar is marked as 'transparent', it means we must
801*a1a3b679SAndreas Boehler                // ignore it for free-busy purposes.
802*a1a3b679SAndreas Boehler                continue;
803*a1a3b679SAndreas Boehler            }
804*a1a3b679SAndreas Boehler
805*a1a3b679SAndreas Boehler            $aclPlugin->checkPrivileges($homeSet . $node->getName(), $caldavNS . 'read-free-busy');
806*a1a3b679SAndreas Boehler
807*a1a3b679SAndreas Boehler            if (isset($props[$ctz])) {
808*a1a3b679SAndreas Boehler                $vtimezoneObj = VObject\Reader::read($props[$ctz]);
809*a1a3b679SAndreas Boehler                $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
810*a1a3b679SAndreas Boehler            }
811*a1a3b679SAndreas Boehler
812*a1a3b679SAndreas Boehler            // Getting the list of object uris within the time-range
813*a1a3b679SAndreas Boehler            $urls = $node->calendarQuery([
814*a1a3b679SAndreas Boehler                'name'         => 'VCALENDAR',
815*a1a3b679SAndreas Boehler                'comp-filters' => [
816*a1a3b679SAndreas Boehler                    [
817*a1a3b679SAndreas Boehler                        'name'           => 'VEVENT',
818*a1a3b679SAndreas Boehler                        'comp-filters'   => [],
819*a1a3b679SAndreas Boehler                        'prop-filters'   => [],
820*a1a3b679SAndreas Boehler                        'is-not-defined' => false,
821*a1a3b679SAndreas Boehler                        'time-range'     => [
822*a1a3b679SAndreas Boehler                            'start' => $start,
823*a1a3b679SAndreas Boehler                            'end'   => $end,
824*a1a3b679SAndreas Boehler                        ],
825*a1a3b679SAndreas Boehler                    ],
826*a1a3b679SAndreas Boehler                ],
827*a1a3b679SAndreas Boehler                'prop-filters'   => [],
828*a1a3b679SAndreas Boehler                'is-not-defined' => false,
829*a1a3b679SAndreas Boehler                'time-range'     => null,
830*a1a3b679SAndreas Boehler            ]);
831*a1a3b679SAndreas Boehler
832*a1a3b679SAndreas Boehler            $calObjects = array_map(function($url) use ($node) {
833*a1a3b679SAndreas Boehler                $obj = $node->getChild($url)->get();
834*a1a3b679SAndreas Boehler                return $obj;
835*a1a3b679SAndreas Boehler            }, $urls);
836*a1a3b679SAndreas Boehler
837*a1a3b679SAndreas Boehler            $objects = array_merge($objects, $calObjects);
838*a1a3b679SAndreas Boehler
839*a1a3b679SAndreas Boehler        }
840*a1a3b679SAndreas Boehler
841*a1a3b679SAndreas Boehler        $vcalendar = new VObject\Component\VCalendar();
842*a1a3b679SAndreas Boehler        $vcalendar->METHOD = 'REPLY';
843*a1a3b679SAndreas Boehler
844*a1a3b679SAndreas Boehler        $generator = new VObject\FreeBusyGenerator();
845*a1a3b679SAndreas Boehler        $generator->setObjects($objects);
846*a1a3b679SAndreas Boehler        $generator->setTimeRange($start, $end);
847*a1a3b679SAndreas Boehler        $generator->setBaseObject($vcalendar);
848*a1a3b679SAndreas Boehler        $generator->setTimeZone($calendarTimeZone);
849*a1a3b679SAndreas Boehler
850*a1a3b679SAndreas Boehler        $result = $generator->getResult();
851*a1a3b679SAndreas Boehler
852*a1a3b679SAndreas Boehler        $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email;
853*a1a3b679SAndreas Boehler        $vcalendar->VFREEBUSY->UID = (string)$request->VFREEBUSY->UID;
854*a1a3b679SAndreas Boehler        $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
855*a1a3b679SAndreas Boehler
856*a1a3b679SAndreas Boehler        return [
857*a1a3b679SAndreas Boehler            'calendar-data'  => $result,
858*a1a3b679SAndreas Boehler            'request-status' => '2.0;Success',
859*a1a3b679SAndreas Boehler            'href'           => 'mailto:' . $email,
860*a1a3b679SAndreas Boehler        ];
861*a1a3b679SAndreas Boehler    }
862*a1a3b679SAndreas Boehler
863*a1a3b679SAndreas Boehler    /**
864*a1a3b679SAndreas Boehler     * This method checks the 'Schedule-Reply' header
865*a1a3b679SAndreas Boehler     * and returns false if it's 'F', otherwise true.
866*a1a3b679SAndreas Boehler     *
867*a1a3b679SAndreas Boehler     * @param RequestInterface $request
868*a1a3b679SAndreas Boehler     * @return bool
869*a1a3b679SAndreas Boehler     */
870*a1a3b679SAndreas Boehler    private function scheduleReply(RequestInterface $request) {
871*a1a3b679SAndreas Boehler
872*a1a3b679SAndreas Boehler        $scheduleReply = $request->getHeader('Schedule-Reply');
873*a1a3b679SAndreas Boehler        return $scheduleReply !== 'F';
874*a1a3b679SAndreas Boehler
875*a1a3b679SAndreas Boehler    }
876*a1a3b679SAndreas Boehler
877*a1a3b679SAndreas Boehler    /**
878*a1a3b679SAndreas Boehler     * Returns a bunch of meta-data about the plugin.
879*a1a3b679SAndreas Boehler     *
880*a1a3b679SAndreas Boehler     * Providing this information is optional, and is mainly displayed by the
881*a1a3b679SAndreas Boehler     * Browser plugin.
882*a1a3b679SAndreas Boehler     *
883*a1a3b679SAndreas Boehler     * The description key in the returned array may contain html and will not
884*a1a3b679SAndreas Boehler     * be sanitized.
885*a1a3b679SAndreas Boehler     *
886*a1a3b679SAndreas Boehler     * @return array
887*a1a3b679SAndreas Boehler     */
888*a1a3b679SAndreas Boehler    function getPluginInfo() {
889*a1a3b679SAndreas Boehler
890*a1a3b679SAndreas Boehler        return [
891*a1a3b679SAndreas Boehler            'name'        => $this->getPluginName(),
892*a1a3b679SAndreas Boehler            'description' => 'Adds calendar-auto-schedule, as defined in rf6868',
893*a1a3b679SAndreas Boehler            'link'        => 'http://sabre.io/dav/scheduling/',
894*a1a3b679SAndreas Boehler        ];
895*a1a3b679SAndreas Boehler
896*a1a3b679SAndreas Boehler    }
897*a1a3b679SAndreas Boehler}
898