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