1<?php
2
3namespace Sabre\DAV\Sharing;
4
5use Sabre\DAV\Exception\BadRequest;
6use Sabre\DAV\Exception\Forbidden;
7use Sabre\DAV\INode;
8use Sabre\DAV\PropFind;
9use Sabre\DAV\Server;
10use Sabre\DAV\ServerPlugin;
11use Sabre\DAV\Xml\Element\Sharee;
12use Sabre\DAV\Xml\Property;
13use Sabre\HTTP\RequestInterface;
14use Sabre\HTTP\ResponseInterface;
15
16/**
17 * This plugin implements HTTP requests and properties related to:
18 *
19 * draft-pot-webdav-resource-sharing
20 *
21 * This specification allows people to share webdav resources with others.
22 *
23 * @copyright Copyright (C) 2007-2015 fruux GmbH. (https://fruux.com/)
24 * @author Evert Pot (http://evertpot.com/)
25 * @license http://sabre.io/license/ Modified BSD License
26 */
27class Plugin extends ServerPlugin {
28
29    const ACCESS_NOTSHARED = 0;
30    const ACCESS_SHAREDOWNER = 1;
31    const ACCESS_READ = 2;
32    const ACCESS_READWRITE = 3;
33    const ACCESS_NOACCESS = 4;
34
35    const INVITE_NORESPONSE = 1;
36    const INVITE_ACCEPTED = 2;
37    const INVITE_DECLINED = 3;
38    const INVITE_INVALID = 4;
39
40    /**
41     * Reference to SabreDAV server object.
42     *
43     * @var Server
44     */
45    protected $server;
46
47    /**
48     * This method should return a list of server-features.
49     *
50     * This is for example 'versioning' and is added to the DAV: header
51     * in an OPTIONS response.
52     *
53     * @return array
54     */
55    function getFeatures() {
56
57        return ['resource-sharing'];
58
59    }
60
61    /**
62     * Returns a plugin name.
63     *
64     * Using this name other plugins will be able to access other plugins
65     * using \Sabre\DAV\Server::getPlugin
66     *
67     * @return string
68     */
69    function getPluginName() {
70
71        return 'sharing';
72
73    }
74
75    /**
76     * This initializes the plugin.
77     *
78     * This function is called by Sabre\DAV\Server, after
79     * addPlugin is called.
80     *
81     * This method should set up the required event subscriptions.
82     *
83     * @param Server $server
84     * @return void
85     */
86    function initialize(Server $server) {
87
88        $this->server = $server;
89
90        $server->xml->elementMap['{DAV:}share-resource'] = 'Sabre\\DAV\\Xml\\Request\\ShareResource';
91
92        array_push(
93            $server->protectedProperties,
94            '{DAV:}share-mode'
95        );
96
97        $server->on('method:POST',              [$this, 'httpPost']);
98        $server->on('propFind',                 [$this, 'propFind']);
99        $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']);
100        $server->on('onHTMLActionsPanel',       [$this, 'htmlActionsPanel']);
101        $server->on('onBrowserPostAction',      [$this, 'browserPostAction']);
102
103    }
104
105    /**
106     * Updates the list of sharees on a shared resource.
107     *
108     * The sharees  array is a list of people that are to be added modified
109     * or removed in the list of shares.
110     *
111     * @param string $path
112     * @param Sharee[] $sharees
113     * @return void
114     */
115    function shareResource($path, array $sharees) {
116
117        $node = $this->server->tree->getNodeForPath($path);
118
119        if (!$node instanceof ISharedNode) {
120
121            throw new Forbidden('Sharing is not allowed on this node');
122
123        }
124
125        // Getting ACL info
126        $acl = $this->server->getPlugin('acl');
127
128        // If there's no ACL support, we allow everything
129        if ($acl) {
130            $acl->checkPrivileges($path, '{DAV:}share');
131        }
132
133        foreach ($sharees as $sharee) {
134            // We're going to attempt to get a local principal uri for a share
135            // href by emitting the getPrincipalByUri event.
136            $principal = null;
137            $this->server->emit('getPrincipalByUri', [$sharee->href, &$principal]);
138            $sharee->principal = $principal;
139        }
140        $node->updateInvites($sharees);
141
142    }
143
144    /**
145     * This event is triggered when properties are requested for nodes.
146     *
147     * This allows us to inject any sharings-specific properties.
148     *
149     * @param PropFind $propFind
150     * @param INode $node
151     * @return void
152     */
153    function propFind(PropFind $propFind, INode $node) {
154
155        if ($node instanceof ISharedNode) {
156
157            $propFind->handle('{DAV:}share-access', function() use ($node) {
158
159                return new Property\ShareAccess($node->getShareAccess());
160
161            });
162            $propFind->handle('{DAV:}invite', function() use ($node) {
163
164                return new Property\Invite($node->getInvites());
165
166            });
167            $propFind->handle('{DAV:}share-resource-uri', function() use ($node) {
168
169                return new Property\Href($node->getShareResourceUri());
170
171            });
172
173        }
174
175    }
176
177    /**
178     * We intercept this to handle POST requests on shared resources
179     *
180     * @param RequestInterface $request
181     * @param ResponseInterface $response
182     * @return null|bool
183     */
184    function httpPost(RequestInterface $request, ResponseInterface $response) {
185
186        $path = $request->getPath();
187        $contentType = $request->getHeader('Content-Type');
188
189        // We're only interested in the davsharing content type.
190        if (strpos($contentType, 'application/davsharing+xml') === false) {
191            return;
192        }
193
194        $message = $this->server->xml->parse(
195            $request->getBody(),
196            $request->getUrl(),
197            $documentType
198        );
199
200        switch ($documentType) {
201
202            case '{DAV:}share-resource':
203
204                $this->shareResource($path, $message->sharees);
205                $response->setStatus(200);
206                // Adding this because sending a response body may cause issues,
207                // and I wanted some type of indicator the response was handled.
208                $response->setHeader('X-Sabre-Status', 'everything-went-well');
209
210                // Breaking the event chain
211                return false;
212
213            default :
214                throw new BadRequest('Unexpected document type: ' . $documentType . ' for this Content-Type');
215
216        }
217
218    }
219
220    /**
221     * This method is triggered whenever a subsystem reqeuests the privileges
222     * hat are supported on a particular node.
223     *
224     * We need to add a number of privileges for scheduling purposes.
225     *
226     * @param INode $node
227     * @param array $supportedPrivilegeSet
228     */
229    function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) {
230
231        if ($node instanceof ISharedNode) {
232            $supportedPrivilegeSet['{DAV:}share'] = [
233                'abstract'   => false,
234                'aggregates' => [],
235            ];
236        }
237    }
238
239    /**
240     * Returns a bunch of meta-data about the plugin.
241     *
242     * Providing this information is optional, and is mainly displayed by the
243     * Browser plugin.
244     *
245     * The description key in the returned array may contain html and will not
246     * be sanitized.
247     *
248     * @return array
249     */
250    function getPluginInfo() {
251
252        return [
253            'name'        => $this->getPluginName(),
254            'description' => 'This plugin implements WebDAV resource sharing',
255            'link'        => 'https://github.com/evert/webdav-sharing'
256        ];
257
258    }
259
260    /**
261     * This method is used to generate HTML output for the
262     * DAV\Browser\Plugin.
263     *
264     * @param INode $node
265     * @param string $output
266     * @param string $path
267     * @return bool|null
268     */
269    function htmlActionsPanel(INode $node, &$output, $path) {
270
271        if (!$node instanceof ISharedNode) {
272            return;
273        }
274
275        $aclPlugin = $this->server->getPlugin('acl');
276        if ($aclPlugin) {
277            if (!$aclPlugin->checkPrivileges($path, '{DAV:}share', \Sabre\DAVACL\Plugin::R_PARENT, false)) {
278                // Sharing is not permitted, we will not draw this interface.
279                return;
280            }
281        }
282
283        $output .= '<tr><td colspan="2"><form method="post" action="">
284            <h3>Share this resource</h3>
285            <input type="hidden" name="sabreAction" value="share" />
286            <label>Share with (uri):</label> <input type="text" name="href" placeholder="mailto:user@example.org"/><br />
287            <label>Access</label>
288                <select name="access">
289                    <option value="readwrite">Read-write</option>
290                    <option value="read">Read-only</option>
291                    <option value="no-access">Revoke access</option>
292                </select><br />
293             <input type="submit" value="share" />
294            </form>
295            </td></tr>';
296
297    }
298
299    /**
300     * This method is triggered for POST actions generated by the browser
301     * plugin.
302     *
303     * @param string $path
304     * @param string $action
305     * @param array $postVars
306     */
307    function browserPostAction($path, $action, $postVars) {
308
309        if ($action !== 'share') {
310            return;
311        }
312
313        if (empty($postVars['href'])) {
314            throw new BadRequest('The "href" POST parameter is required');
315        }
316        if (empty($postVars['access'])) {
317            throw new BadRequest('The "access" POST parameter is required');
318        }
319
320        $accessMap = [
321            'readwrite' => self::ACCESS_READWRITE,
322            'read'      => self::ACCESS_READ,
323            'no-access' => self::ACCESS_NOACCESS,
324        ];
325
326        if (!isset($accessMap[$postVars['access']])) {
327            throw new BadRequest('The "access" POST must be readwrite, read or no-access');
328        }
329        $sharee = new Sharee([
330            'href'   => $postVars['href'],
331            'access' => $accessMap[$postVars['access']],
332        ]);
333
334        $this->shareResource(
335            $path,
336            [$sharee]
337        );
338        return false;
339
340    }
341
342}
343