1<?php
2
3declare(strict_types=1);
4
5namespace Sabre\CalDAV;
6
7use Sabre\DAV;
8use Sabre\DAV\Xml\Property\LocalHref;
9use Sabre\HTTP\RequestInterface;
10use Sabre\HTTP\ResponseInterface;
11
12/**
13 * This plugin implements support for caldav sharing.
14 *
15 * This spec is defined at:
16 * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
17 *
18 * See:
19 * Sabre\CalDAV\Backend\SharingSupport for all the documentation.
20 *
21 * Note: This feature is experimental, and may change in between different
22 * SabreDAV versions.
23 *
24 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
25 * @author Evert Pot (http://evertpot.com/)
26 * @license http://sabre.io/license/ Modified BSD License
27 */
28class SharingPlugin extends DAV\ServerPlugin
29{
30    /**
31     * Reference to SabreDAV server object.
32     *
33     * @var DAV\Server
34     */
35    protected $server;
36
37    /**
38     * This method should return a list of server-features.
39     *
40     * This is for example 'versioning' and is added to the DAV: header
41     * in an OPTIONS response.
42     *
43     * @return array
44     */
45    public function getFeatures()
46    {
47        return ['calendarserver-sharing'];
48    }
49
50    /**
51     * Returns a plugin name.
52     *
53     * Using this name other plugins will be able to access other plugins
54     * using Sabre\DAV\Server::getPlugin
55     *
56     * @return string
57     */
58    public function getPluginName()
59    {
60        return 'caldav-sharing';
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    public function initialize(DAV\Server $server)
72    {
73        $this->server = $server;
74
75        if (is_null($this->server->getPlugin('sharing'))) {
76            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.');
77        }
78
79        array_push(
80            $this->server->protectedProperties,
81            '{'.Plugin::NS_CALENDARSERVER.'}invite',
82            '{'.Plugin::NS_CALENDARSERVER.'}allowed-sharing-modes',
83            '{'.Plugin::NS_CALENDARSERVER.'}shared-url'
84        );
85
86        $this->server->xml->elementMap['{'.Plugin::NS_CALENDARSERVER.'}share'] = 'Sabre\\CalDAV\\Xml\\Request\\Share';
87        $this->server->xml->elementMap['{'.Plugin::NS_CALENDARSERVER.'}invite-reply'] = 'Sabre\\CalDAV\\Xml\\Request\\InviteReply';
88
89        $this->server->on('propFind', [$this, 'propFindEarly']);
90        $this->server->on('propFind', [$this, 'propFindLate'], 150);
91        $this->server->on('propPatch', [$this, 'propPatch'], 40);
92        $this->server->on('method:POST', [$this, 'httpPost']);
93    }
94
95    /**
96     * This event is triggered when properties are requested for a certain
97     * node.
98     *
99     * This allows us to inject any properties early.
100     */
101    public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node)
102    {
103        if ($node instanceof ISharedCalendar) {
104            $propFind->handle('{'.Plugin::NS_CALENDARSERVER.'}invite', function () use ($node) {
105                return new Xml\Property\Invite(
106                    $node->getInvites()
107                );
108            });
109        }
110    }
111
112    /**
113     * This method is triggered *after* all properties have been retrieved.
114     * This allows us to inject the correct resourcetype for calendars that
115     * have been shared.
116     */
117    public function propFindLate(DAV\PropFind $propFind, DAV\INode $node)
118    {
119        if ($node instanceof ISharedCalendar) {
120            $shareAccess = $node->getShareAccess();
121            if ($rt = $propFind->get('{DAV:}resourcetype')) {
122                switch ($shareAccess) {
123                    case \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER:
124                        $rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared-owner');
125                        break;
126                    case \Sabre\DAV\Sharing\Plugin::ACCESS_READ:
127                    case \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE:
128                        $rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared');
129                        break;
130                }
131            }
132            $propFind->handle('{'.Plugin::NS_CALENDARSERVER.'}allowed-sharing-modes', function () {
133                return new Xml\Property\AllowedSharingModes(true, false);
134            });
135        }
136    }
137
138    /**
139     * This method is trigged when a user attempts to update a node's
140     * properties.
141     *
142     * A previous draft of the sharing spec stated that it was possible to use
143     * PROPPATCH to remove 'shared-owner' from the resourcetype, thus unsharing
144     * the calendar.
145     *
146     * Even though this is no longer in the current spec, we keep this around
147     * because OS X 10.7 may still make use of this feature.
148     *
149     * @param string $path
150     */
151    public function propPatch($path, DAV\PropPatch $propPatch)
152    {
153        $node = $this->server->tree->getNodeForPath($path);
154        if (!$node instanceof ISharedCalendar) {
155            return;
156        }
157
158        if (\Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $node->getShareAccess() || \Sabre\DAV\Sharing\Plugin::ACCESS_NOTSHARED === $node->getShareAccess()) {
159            $propPatch->handle('{DAV:}resourcetype', function ($value) use ($node) {
160                if ($value->is('{'.Plugin::NS_CALENDARSERVER.'}shared-owner')) {
161                    return false;
162                }
163                $shares = $node->getInvites();
164                foreach ($shares as $share) {
165                    $share->access = DAV\Sharing\Plugin::ACCESS_NOACCESS;
166                }
167                $node->updateInvites($shares);
168
169                return true;
170            });
171        }
172    }
173
174    /**
175     * We intercept this to handle POST requests on calendars.
176     *
177     * @return bool|null
178     */
179    public function httpPost(RequestInterface $request, ResponseInterface $response)
180    {
181        $path = $request->getPath();
182
183        // Only handling xml
184        $contentType = $request->getHeader('Content-Type');
185        if (null === $contentType) {
186            return;
187        }
188        if (false === strpos($contentType, 'application/xml') && false === strpos($contentType, 'text/xml')) {
189            return;
190        }
191
192        // Making sure the node exists
193        try {
194            $node = $this->server->tree->getNodeForPath($path);
195        } catch (DAV\Exception\NotFound $e) {
196            return;
197        }
198
199        $requestBody = $request->getBodyAsString();
200
201        // If this request handler could not deal with this POST request, it
202        // will return 'null' and other plugins get a chance to handle the
203        // request.
204        //
205        // However, we already requested the full body. This is a problem,
206        // because a body can only be read once. This is why we preemptively
207        // re-populated the request body with the existing data.
208        $request->setBody($requestBody);
209
210        $message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType);
211
212        switch ($documentType) {
213            // Both the DAV:share-resource and CALENDARSERVER:share requests
214            // behave identically.
215            case '{'.Plugin::NS_CALENDARSERVER.'}share':
216                $sharingPlugin = $this->server->getPlugin('sharing');
217                $sharingPlugin->shareResource($path, $message->sharees);
218
219                $response->setStatus(200);
220                // Adding this because sending a response body may cause issues,
221                // and I wanted some type of indicator the response was handled.
222                $response->setHeader('X-Sabre-Status', 'everything-went-well');
223
224                // Breaking the event chain
225                return false;
226
227            // The invite-reply document is sent when the user replies to an
228            // invitation of a calendar share.
229            case '{'.Plugin::NS_CALENDARSERVER.'}invite-reply':
230                // This only works on the calendar-home-root node.
231                if (!$node instanceof CalendarHome) {
232                    return;
233                }
234                $this->server->transactionType = 'post-invite-reply';
235
236                // Getting ACL info
237                $acl = $this->server->getPlugin('acl');
238
239                // If there's no ACL support, we allow everything
240                if ($acl) {
241                    $acl->checkPrivileges($path, '{DAV:}write');
242                }
243
244                $url = $node->shareReply(
245                    $message->href,
246                    $message->status,
247                    $message->calendarUri,
248                    $message->inReplyTo,
249                    $message->summary
250                );
251
252                $response->setStatus(200);
253                // Adding this because sending a response body may cause issues,
254                // and I wanted some type of indicator the response was handled.
255                $response->setHeader('X-Sabre-Status', 'everything-went-well');
256
257                if ($url) {
258                    $writer = $this->server->xml->getWriter();
259                    $writer->contextUri = $request->getUrl();
260                    $writer->openMemory();
261                    $writer->startDocument();
262                    $writer->startElement('{'.Plugin::NS_CALENDARSERVER.'}shared-as');
263                    $writer->write(new LocalHref($url));
264                    $writer->endElement();
265                    $response->setHeader('Content-Type', 'application/xml');
266                    $response->setBody($writer->outputMemory());
267                }
268
269                // Breaking the event chain
270                return false;
271
272            case '{'.Plugin::NS_CALENDARSERVER.'}publish-calendar':
273                // We can only deal with IShareableCalendar objects
274                if (!$node instanceof ISharedCalendar) {
275                    return;
276                }
277                $this->server->transactionType = 'post-publish-calendar';
278
279                // Getting ACL info
280                $acl = $this->server->getPlugin('acl');
281
282                // If there's no ACL support, we allow everything
283                if ($acl) {
284                    $acl->checkPrivileges($path, '{DAV:}share');
285                }
286
287                $node->setPublishStatus(true);
288
289                // iCloud sends back the 202, so we will too.
290                $response->setStatus(202);
291
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            case '{'.Plugin::NS_CALENDARSERVER.'}unpublish-calendar':
300                // We can only deal with IShareableCalendar objects
301                if (!$node instanceof ISharedCalendar) {
302                    return;
303                }
304                $this->server->transactionType = 'post-unpublish-calendar';
305
306                // Getting ACL info
307                $acl = $this->server->getPlugin('acl');
308
309                // If there's no ACL support, we allow everything
310                if ($acl) {
311                    $acl->checkPrivileges($path, '{DAV:}share');
312                }
313
314                $node->setPublishStatus(false);
315
316                $response->setStatus(200);
317
318                // Adding this because sending a response body may cause issues,
319                // and I wanted some type of indicator the response was handled.
320                $response->setHeader('X-Sabre-Status', 'everything-went-well');
321
322                // Breaking the event chain
323                return false;
324        }
325    }
326
327    /**
328     * Returns a bunch of meta-data about the plugin.
329     *
330     * Providing this information is optional, and is mainly displayed by the
331     * Browser plugin.
332     *
333     * The description key in the returned array may contain html and will not
334     * be sanitized.
335     *
336     * @return array
337     */
338    public function getPluginInfo()
339    {
340        return [
341            'name' => $this->getPluginName(),
342            'description' => 'Adds support for caldav-sharing.',
343            'link' => 'http://sabre.io/dav/caldav-sharing/',
344        ];
345    }
346}
347