1<?php
2
3namespace Sabre\CalDAV;
4
5use Sabre\DAV;
6use Sabre\DAV\Xml\Property\Href;
7use Sabre\HTTP\RequestInterface;
8use Sabre\HTTP\ResponseInterface;
9
10/**
11 * This plugin implements support for caldav sharing.
12 *
13 * This spec is defined at:
14 * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
15 *
16 * See:
17 * Sabre\CalDAV\Backend\SharingSupport for all the documentation.
18 *
19 * Note: This feature is experimental, and may change in between different
20 * SabreDAV versions.
21 *
22 * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
23 * @author Evert Pot (http://evertpot.com/)
24 * @license http://sabre.io/license/ Modified BSD License
25 */
26class SharingPlugin extends DAV\ServerPlugin {
27
28    /**
29     * These are the various status constants used by sharing-messages.
30     */
31    const STATUS_ACCEPTED = 1;
32    const STATUS_DECLINED = 2;
33    const STATUS_DELETED = 3;
34    const STATUS_NORESPONSE = 4;
35    const STATUS_INVALID = 5;
36
37    /**
38     * Reference to SabreDAV server object.
39     *
40     * @var Sabre\DAV\Server
41     */
42    protected $server;
43
44    /**
45     * This method should return a list of server-features.
46     *
47     * This is for example 'versioning' and is added to the DAV: header
48     * in an OPTIONS response.
49     *
50     * @return array
51     */
52    function getFeatures() {
53
54        return ['calendarserver-sharing'];
55
56    }
57
58    /**
59     * Returns a plugin name.
60     *
61     * Using this name other plugins will be able to access other plugins
62     * using Sabre\DAV\Server::getPlugin
63     *
64     * @return string
65     */
66    function getPluginName() {
67
68        return 'caldav-sharing';
69
70    }
71
72    /**
73     * This initializes the plugin.
74     *
75     * This function is called by Sabre\DAV\Server, after
76     * addPlugin is called.
77     *
78     * This method should set up the required event subscriptions.
79     *
80     * @param DAV\Server $server
81     * @return void
82     */
83    function initialize(DAV\Server $server) {
84
85        $this->server = $server;
86        $server->resourceTypeMapping['Sabre\\CalDAV\\ISharedCalendar'] = '{' . Plugin::NS_CALENDARSERVER . '}shared';
87
88        array_push(
89            $this->server->protectedProperties,
90            '{' . Plugin::NS_CALENDARSERVER . '}invite',
91            '{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes',
92            '{' . Plugin::NS_CALENDARSERVER . '}shared-url'
93        );
94
95        $this->server->xml->elementMap['{' . Plugin::NS_CALENDARSERVER . '}share'] = 'Sabre\\CalDAV\\Xml\\Request\\Share';
96        $this->server->xml->elementMap['{' . Plugin::NS_CALENDARSERVER . '}invite-reply'] = 'Sabre\\CalDAV\\Xml\\Request\\InviteReply';
97
98        $this->server->on('propFind',     [$this, 'propFindEarly']);
99        $this->server->on('propFind',     [$this, 'propFindLate'], 150);
100        $this->server->on('propPatch',    [$this, 'propPatch'], 40);
101        $this->server->on('method:POST',  [$this, 'httpPost']);
102
103    }
104
105    /**
106     * This event is triggered when properties are requested for a certain
107     * node.
108     *
109     * This allows us to inject any properties early.
110     *
111     * @param DAV\PropFind $propFind
112     * @param DAV\INode $node
113     * @return void
114     */
115    function propFindEarly(DAV\PropFind $propFind, DAV\INode $node) {
116
117        if ($node instanceof IShareableCalendar) {
118
119            $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}invite', function() use ($node) {
120                return new Xml\Property\Invite(
121                    $node->getShares()
122                );
123            });
124
125        }
126
127        if ($node instanceof ISharedCalendar) {
128
129            $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}shared-url', function() use ($node) {
130                return new Href(
131                    $node->getSharedUrl()
132                );
133            });
134
135            $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}invite', function() use ($node) {
136
137                // Fetching owner information
138                $props = $this->server->getPropertiesForPath($node->getOwner(), [
139                    '{http://sabredav.org/ns}email-address',
140                    '{DAV:}displayname',
141                ], 0);
142
143                $ownerInfo = [
144                    'href' => $node->getOwner(),
145                ];
146
147                if (isset($props[0][200])) {
148
149                    // We're mapping the internal webdav properties to the
150                    // elements caldav-sharing expects.
151                    if (isset($props[0][200]['{http://sabredav.org/ns}email-address'])) {
152                        $ownerInfo['href'] = 'mailto:' . $props[0][200]['{http://sabredav.org/ns}email-address'];
153                    }
154                    if (isset($props[0][200]['{DAV:}displayname'])) {
155                        $ownerInfo['commonName'] = $props[0][200]['{DAV:}displayname'];
156                    }
157
158                }
159
160                return new Xml\Property\Invite(
161                    $node->getShares(),
162                    $ownerInfo
163                );
164
165            });
166
167        }
168
169    }
170
171    /**
172     * This method is triggered *after* all properties have been retrieved.
173     * This allows us to inject the correct resourcetype for calendars that
174     * have been shared.
175     *
176     * @param DAV\PropFind $propFind
177     * @param DAV\INode $node
178     * @return void
179     */
180    function propFindLate(DAV\PropFind $propFind, DAV\INode $node) {
181
182        if ($node instanceof IShareableCalendar) {
183            if ($rt = $propFind->get('{DAV:}resourcetype')) {
184                if (count($node->getShares()) > 0) {
185                    $rt->add('{' . Plugin::NS_CALENDARSERVER . '}shared-owner');
186                }
187            }
188            $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes', function() {
189                return new Xml\Property\AllowedSharingModes(true, false);
190            });
191
192        }
193
194    }
195
196    /**
197     * This method is trigged when a user attempts to update a node's
198     * properties.
199     *
200     * A previous draft of the sharing spec stated that it was possible to use
201     * PROPPATCH to remove 'shared-owner' from the resourcetype, thus unsharing
202     * the calendar.
203     *
204     * Even though this is no longer in the current spec, we keep this around
205     * because OS X 10.7 may still make use of this feature.
206     *
207     * @param string $path
208     * @param DAV\PropPatch $propPatch
209     * @return void
210     */
211    function propPatch($path, DAV\PropPatch $propPatch) {
212
213        $node = $this->server->tree->getNodeForPath($path);
214        if (!$node instanceof IShareableCalendar)
215            return;
216
217        $propPatch->handle('{DAV:}resourcetype', function($value) use ($node) {
218            if ($value->is('{' . Plugin::NS_CALENDARSERVER . '}shared-owner')) return false;
219            $shares = $node->getShares();
220            $remove = [];
221            foreach ($shares as $share) {
222                $remove[] = $share['href'];
223            }
224            $node->updateShares([], $remove);
225
226            return true;
227
228        });
229
230    }
231
232    /**
233     * We intercept this to handle POST requests on calendars.
234     *
235     * @param RequestInterface $request
236     * @param ResponseInterface $response
237     * @return null|bool
238     */
239    function httpPost(RequestInterface $request, ResponseInterface $response) {
240
241        $path = $request->getPath();
242
243        // Only handling xml
244        $contentType = $request->getHeader('Content-Type');
245        if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false)
246            return;
247
248        // Making sure the node exists
249        try {
250            $node = $this->server->tree->getNodeForPath($path);
251        } catch (DAV\Exception\NotFound $e) {
252            return;
253        }
254
255        $requestBody = $request->getBodyAsString();
256
257        // If this request handler could not deal with this POST request, it
258        // will return 'null' and other plugins get a chance to handle the
259        // request.
260        //
261        // However, we already requested the full body. This is a problem,
262        // because a body can only be read once. This is why we preemptively
263        // re-populated the request body with the existing data.
264        $request->setBody($requestBody);
265
266        $message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType);
267
268        switch ($documentType) {
269
270            // Dealing with the 'share' document, which modified invitees on a
271            // calendar.
272            case '{' . Plugin::NS_CALENDARSERVER . '}share' :
273
274                // We can only deal with IShareableCalendar objects
275                if (!$node instanceof IShareableCalendar) {
276                    return;
277                }
278
279                $this->server->transactionType = 'post-calendar-share';
280
281                // Getting ACL info
282                $acl = $this->server->getPlugin('acl');
283
284                // If there's no ACL support, we allow everything
285                if ($acl) {
286                    $acl->checkPrivileges($path, '{DAV:}write');
287                }
288
289                $node->updateShares($message->set, $message->remove);
290
291                $response->setStatus(200);
292                // Adding this because sending a response body may cause issues,
293                // and I wanted some type of indicator the response was handled.
294                $response->setHeader('X-Sabre-Status', 'everything-went-well');
295
296                // Breaking the event chain
297                return false;
298
299            // The invite-reply document is sent when the user replies to an
300            // invitation of a calendar share.
301            case '{' . Plugin::NS_CALENDARSERVER . '}invite-reply' :
302
303                // This only works on the calendar-home-root node.
304                if (!$node instanceof CalendarHome) {
305                    return;
306                }
307                $this->server->transactionType = 'post-invite-reply';
308
309                // Getting ACL info
310                $acl = $this->server->getPlugin('acl');
311
312                // If there's no ACL support, we allow everything
313                if ($acl) {
314                    $acl->checkPrivileges($path, '{DAV:}write');
315                }
316
317                $url = $node->shareReply(
318                    $message->href,
319                    $message->status,
320                    $message->calendarUri,
321                    $message->inReplyTo,
322                    $message->summary
323                );
324
325                $response->setStatus(200);
326                // Adding this because sending a response body may cause issues,
327                // and I wanted some type of indicator the response was handled.
328                $response->setHeader('X-Sabre-Status', 'everything-went-well');
329
330                if ($url) {
331                    $writer = $this->server->xml->getWriter($this->server->getBaseUri());
332                    $writer->openMemory();
333                    $writer->startDocument();
334                    $writer->startElement('{' . Plugin::NS_CALENDARSERVER . '}shared-as');
335                    $writer->write(new Href($url));
336                    $writer->endElement();
337                    $response->setHeader('Content-Type', 'application/xml');
338                    $response->setBody($writer->outputMemory());
339
340                }
341
342                // Breaking the event chain
343                return false;
344
345            case '{' . Plugin::NS_CALENDARSERVER . '}publish-calendar' :
346
347                // We can only deal with IShareableCalendar objects
348                if (!$node instanceof IShareableCalendar) {
349                    return;
350                }
351                $this->server->transactionType = 'post-publish-calendar';
352
353                // Getting ACL info
354                $acl = $this->server->getPlugin('acl');
355
356                // If there's no ACL support, we allow everything
357                if ($acl) {
358                    $acl->checkPrivileges($path, '{DAV:}write');
359                }
360
361                $node->setPublishStatus(true);
362
363                // iCloud sends back the 202, so we will too.
364                $response->setStatus(202);
365
366                // Adding this because sending a response body may cause issues,
367                // and I wanted some type of indicator the response was handled.
368                $response->setHeader('X-Sabre-Status', 'everything-went-well');
369
370                // Breaking the event chain
371                return false;
372
373            case '{' . Plugin::NS_CALENDARSERVER . '}unpublish-calendar' :
374
375                // We can only deal with IShareableCalendar objects
376                if (!$node instanceof IShareableCalendar) {
377                    return;
378                }
379                $this->server->transactionType = 'post-unpublish-calendar';
380
381                // Getting ACL info
382                $acl = $this->server->getPlugin('acl');
383
384                // If there's no ACL support, we allow everything
385                if ($acl) {
386                    $acl->checkPrivileges($path, '{DAV:}write');
387                }
388
389                $node->setPublishStatus(false);
390
391                $response->setStatus(200);
392
393                // Adding this because sending a response body may cause issues,
394                // and I wanted some type of indicator the response was handled.
395                $response->setHeader('X-Sabre-Status', 'everything-went-well');
396
397                // Breaking the event chain
398                return false;
399
400        }
401
402
403
404    }
405
406    /**
407     * Returns a bunch of meta-data about the plugin.
408     *
409     * Providing this information is optional, and is mainly displayed by the
410     * Browser plugin.
411     *
412     * The description key in the returned array may contain html and will not
413     * be sanitized.
414     *
415     * @return array
416     */
417    function getPluginInfo() {
418
419        return [
420            'name'        => $this->getPluginName(),
421            'description' => 'Adds support for caldav-sharing.',
422            'link'        => 'http://sabre.io/dav/caldav-sharing/',
423        ];
424
425    }
426}
427