xref: /plugin/davcal/vendor/sabre/dav/lib/DAV/Locks/Plugin.php (revision a1a3b6794e0e143a4a8b51d3185ce2d339be61ab)
1*a1a3b679SAndreas Boehler<?php
2*a1a3b679SAndreas Boehler
3*a1a3b679SAndreas Boehlernamespace Sabre\DAV\Locks;
4*a1a3b679SAndreas Boehler
5*a1a3b679SAndreas Boehleruse Sabre\DAV;
6*a1a3b679SAndreas Boehleruse Sabre\HTTP\RequestInterface;
7*a1a3b679SAndreas Boehleruse Sabre\HTTP\ResponseInterface;
8*a1a3b679SAndreas Boehler
9*a1a3b679SAndreas Boehler/**
10*a1a3b679SAndreas Boehler * Locking plugin
11*a1a3b679SAndreas Boehler *
12*a1a3b679SAndreas Boehler * This plugin provides locking support to a WebDAV server.
13*a1a3b679SAndreas Boehler * The easiest way to get started, is by hooking it up as such:
14*a1a3b679SAndreas Boehler *
15*a1a3b679SAndreas Boehler * $lockBackend = new Sabre\DAV\Locks\Backend\File('./mylockdb');
16*a1a3b679SAndreas Boehler * $lockPlugin = new Sabre\DAV\Locks\Plugin($lockBackend);
17*a1a3b679SAndreas Boehler * $server->addPlugin($lockPlugin);
18*a1a3b679SAndreas Boehler *
19*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
20*a1a3b679SAndreas Boehler * @author Evert Pot (http://evertpot.com/)
21*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License
22*a1a3b679SAndreas Boehler */
23*a1a3b679SAndreas Boehlerclass Plugin extends DAV\ServerPlugin {
24*a1a3b679SAndreas Boehler
25*a1a3b679SAndreas Boehler    /**
26*a1a3b679SAndreas Boehler     * locksBackend
27*a1a3b679SAndreas Boehler     *
28*a1a3b679SAndreas Boehler     * @var Backend\Backend\Interface
29*a1a3b679SAndreas Boehler     */
30*a1a3b679SAndreas Boehler    protected $locksBackend;
31*a1a3b679SAndreas Boehler
32*a1a3b679SAndreas Boehler    /**
33*a1a3b679SAndreas Boehler     * server
34*a1a3b679SAndreas Boehler     *
35*a1a3b679SAndreas Boehler     * @var Sabre\DAV\Server
36*a1a3b679SAndreas Boehler     */
37*a1a3b679SAndreas Boehler    protected $server;
38*a1a3b679SAndreas Boehler
39*a1a3b679SAndreas Boehler    /**
40*a1a3b679SAndreas Boehler     * __construct
41*a1a3b679SAndreas Boehler     *
42*a1a3b679SAndreas Boehler     * @param Backend\BackendInterface $locksBackend
43*a1a3b679SAndreas Boehler     */
44*a1a3b679SAndreas Boehler    function __construct(Backend\BackendInterface $locksBackend) {
45*a1a3b679SAndreas Boehler
46*a1a3b679SAndreas Boehler        $this->locksBackend = $locksBackend;
47*a1a3b679SAndreas Boehler
48*a1a3b679SAndreas Boehler    }
49*a1a3b679SAndreas Boehler
50*a1a3b679SAndreas Boehler    /**
51*a1a3b679SAndreas Boehler     * Initializes the plugin
52*a1a3b679SAndreas Boehler     *
53*a1a3b679SAndreas Boehler     * This method is automatically called by the Server class after addPlugin.
54*a1a3b679SAndreas Boehler     *
55*a1a3b679SAndreas Boehler     * @param DAV\Server $server
56*a1a3b679SAndreas Boehler     * @return void
57*a1a3b679SAndreas Boehler     */
58*a1a3b679SAndreas Boehler    function initialize(DAV\Server $server) {
59*a1a3b679SAndreas Boehler
60*a1a3b679SAndreas Boehler        $this->server = $server;
61*a1a3b679SAndreas Boehler
62*a1a3b679SAndreas Boehler        $this->server->xml->elementMap['{DAV:}lockinfo'] = 'Sabre\\DAV\\Xml\\Request\\Lock';
63*a1a3b679SAndreas Boehler
64*a1a3b679SAndreas Boehler        $server->on('method:LOCK',    [$this, 'httpLock']);
65*a1a3b679SAndreas Boehler        $server->on('method:UNLOCK',  [$this, 'httpUnlock']);
66*a1a3b679SAndreas Boehler        $server->on('validateTokens', [$this, 'validateTokens']);
67*a1a3b679SAndreas Boehler        $server->on('propFind',       [$this, 'propFind']);
68*a1a3b679SAndreas Boehler        $server->on('afterUnbind',    [$this, 'afterUnbind']);
69*a1a3b679SAndreas Boehler
70*a1a3b679SAndreas Boehler    }
71*a1a3b679SAndreas Boehler
72*a1a3b679SAndreas Boehler    /**
73*a1a3b679SAndreas Boehler     * Returns a plugin name.
74*a1a3b679SAndreas Boehler     *
75*a1a3b679SAndreas Boehler     * Using this name other plugins will be able to access other plugins
76*a1a3b679SAndreas Boehler     * using Sabre\DAV\Server::getPlugin
77*a1a3b679SAndreas Boehler     *
78*a1a3b679SAndreas Boehler     * @return string
79*a1a3b679SAndreas Boehler     */
80*a1a3b679SAndreas Boehler    function getPluginName() {
81*a1a3b679SAndreas Boehler
82*a1a3b679SAndreas Boehler        return 'locks';
83*a1a3b679SAndreas Boehler
84*a1a3b679SAndreas Boehler    }
85*a1a3b679SAndreas Boehler
86*a1a3b679SAndreas Boehler    /**
87*a1a3b679SAndreas Boehler     * This method is called after most properties have been found
88*a1a3b679SAndreas Boehler     * it allows us to add in any Lock-related properties
89*a1a3b679SAndreas Boehler     *
90*a1a3b679SAndreas Boehler     * @param DAV\PropFind $propFind
91*a1a3b679SAndreas Boehler     * @param DAV\INode $node
92*a1a3b679SAndreas Boehler     * @return void
93*a1a3b679SAndreas Boehler     */
94*a1a3b679SAndreas Boehler    function propFind(DAV\PropFind $propFind, DAV\INode $node) {
95*a1a3b679SAndreas Boehler
96*a1a3b679SAndreas Boehler        $propFind->handle('{DAV:}supportedlock', function() {
97*a1a3b679SAndreas Boehler            return new DAV\Xml\Property\SupportedLock();
98*a1a3b679SAndreas Boehler        });
99*a1a3b679SAndreas Boehler        $propFind->handle('{DAV:}lockdiscovery', function() use ($propFind) {
100*a1a3b679SAndreas Boehler            return new DAV\Xml\Property\LockDiscovery(
101*a1a3b679SAndreas Boehler                $this->getLocks($propFind->getPath())
102*a1a3b679SAndreas Boehler            );
103*a1a3b679SAndreas Boehler        });
104*a1a3b679SAndreas Boehler
105*a1a3b679SAndreas Boehler    }
106*a1a3b679SAndreas Boehler
107*a1a3b679SAndreas Boehler    /**
108*a1a3b679SAndreas Boehler     * Use this method to tell the server this plugin defines additional
109*a1a3b679SAndreas Boehler     * HTTP methods.
110*a1a3b679SAndreas Boehler     *
111*a1a3b679SAndreas Boehler     * This method is passed a uri. It should only return HTTP methods that are
112*a1a3b679SAndreas Boehler     * available for the specified uri.
113*a1a3b679SAndreas Boehler     *
114*a1a3b679SAndreas Boehler     * @param string $uri
115*a1a3b679SAndreas Boehler     * @return array
116*a1a3b679SAndreas Boehler     */
117*a1a3b679SAndreas Boehler    function getHTTPMethods($uri) {
118*a1a3b679SAndreas Boehler
119*a1a3b679SAndreas Boehler        return ['LOCK','UNLOCK'];
120*a1a3b679SAndreas Boehler
121*a1a3b679SAndreas Boehler    }
122*a1a3b679SAndreas Boehler
123*a1a3b679SAndreas Boehler    /**
124*a1a3b679SAndreas Boehler     * Returns a list of features for the HTTP OPTIONS Dav: header.
125*a1a3b679SAndreas Boehler     *
126*a1a3b679SAndreas Boehler     * In this case this is only the number 2. The 2 in the Dav: header
127*a1a3b679SAndreas Boehler     * indicates the server supports locks.
128*a1a3b679SAndreas Boehler     *
129*a1a3b679SAndreas Boehler     * @return array
130*a1a3b679SAndreas Boehler     */
131*a1a3b679SAndreas Boehler    function getFeatures() {
132*a1a3b679SAndreas Boehler
133*a1a3b679SAndreas Boehler        return [2];
134*a1a3b679SAndreas Boehler
135*a1a3b679SAndreas Boehler    }
136*a1a3b679SAndreas Boehler
137*a1a3b679SAndreas Boehler    /**
138*a1a3b679SAndreas Boehler     * Returns all lock information on a particular uri
139*a1a3b679SAndreas Boehler     *
140*a1a3b679SAndreas Boehler     * This function should return an array with Sabre\DAV\Locks\LockInfo objects. If there are no locks on a file, return an empty array.
141*a1a3b679SAndreas Boehler     *
142*a1a3b679SAndreas Boehler     * Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree
143*a1a3b679SAndreas Boehler     * If the $returnChildLocks argument is set to true, we'll also traverse all the children of the object
144*a1a3b679SAndreas Boehler     * for any possible locks and return those as well.
145*a1a3b679SAndreas Boehler     *
146*a1a3b679SAndreas Boehler     * @param string $uri
147*a1a3b679SAndreas Boehler     * @param bool $returnChildLocks
148*a1a3b679SAndreas Boehler     * @return array
149*a1a3b679SAndreas Boehler     */
150*a1a3b679SAndreas Boehler    function getLocks($uri, $returnChildLocks = false) {
151*a1a3b679SAndreas Boehler
152*a1a3b679SAndreas Boehler        return $this->locksBackend->getLocks($uri, $returnChildLocks);
153*a1a3b679SAndreas Boehler
154*a1a3b679SAndreas Boehler    }
155*a1a3b679SAndreas Boehler
156*a1a3b679SAndreas Boehler    /**
157*a1a3b679SAndreas Boehler     * Locks an uri
158*a1a3b679SAndreas Boehler     *
159*a1a3b679SAndreas Boehler     * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock
160*a1a3b679SAndreas Boehler     * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type
161*a1a3b679SAndreas Boehler     * of lock (shared or exclusive) and the owner of the lock
162*a1a3b679SAndreas Boehler     *
163*a1a3b679SAndreas Boehler     * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock
164*a1a3b679SAndreas Boehler     *
165*a1a3b679SAndreas Boehler     * Additionally, a lock can be requested for a non-existent file. In these case we're obligated to create an empty file as per RFC4918:S7.3
166*a1a3b679SAndreas Boehler     *
167*a1a3b679SAndreas Boehler     * @param RequestInterface $request
168*a1a3b679SAndreas Boehler     * @param ResponseInterface $response
169*a1a3b679SAndreas Boehler     * @return bool
170*a1a3b679SAndreas Boehler     */
171*a1a3b679SAndreas Boehler    function httpLock(RequestInterface $request, ResponseInterface $response) {
172*a1a3b679SAndreas Boehler
173*a1a3b679SAndreas Boehler        $uri = $request->getPath();
174*a1a3b679SAndreas Boehler
175*a1a3b679SAndreas Boehler        $existingLocks = $this->getLocks($uri);
176*a1a3b679SAndreas Boehler
177*a1a3b679SAndreas Boehler        if ($body = $request->getBodyAsString()) {
178*a1a3b679SAndreas Boehler            // This is a new lock request
179*a1a3b679SAndreas Boehler
180*a1a3b679SAndreas Boehler            $existingLock = null;
181*a1a3b679SAndreas Boehler            // Checking if there's already non-shared locks on the uri.
182*a1a3b679SAndreas Boehler            foreach ($existingLocks as $existingLock) {
183*a1a3b679SAndreas Boehler                if ($existingLock->scope === LockInfo::EXCLUSIVE) {
184*a1a3b679SAndreas Boehler                    throw new DAV\Exception\ConflictingLock($existingLock);
185*a1a3b679SAndreas Boehler                }
186*a1a3b679SAndreas Boehler            }
187*a1a3b679SAndreas Boehler
188*a1a3b679SAndreas Boehler            $lockInfo = $this->parseLockRequest($body);
189*a1a3b679SAndreas Boehler            $lockInfo->depth = $this->server->getHTTPDepth();
190*a1a3b679SAndreas Boehler            $lockInfo->uri = $uri;
191*a1a3b679SAndreas Boehler            if ($existingLock && $lockInfo->scope != LockInfo::SHARED)
192*a1a3b679SAndreas Boehler                throw new DAV\Exception\ConflictingLock($existingLock);
193*a1a3b679SAndreas Boehler
194*a1a3b679SAndreas Boehler        } else {
195*a1a3b679SAndreas Boehler
196*a1a3b679SAndreas Boehler            // Gonna check if this was a lock refresh.
197*a1a3b679SAndreas Boehler            $existingLocks = $this->getLocks($uri);
198*a1a3b679SAndreas Boehler            $conditions = $this->server->getIfConditions($request);
199*a1a3b679SAndreas Boehler            $found = null;
200*a1a3b679SAndreas Boehler
201*a1a3b679SAndreas Boehler            foreach ($existingLocks as $existingLock) {
202*a1a3b679SAndreas Boehler                foreach ($conditions as $condition) {
203*a1a3b679SAndreas Boehler                    foreach ($condition['tokens'] as $token) {
204*a1a3b679SAndreas Boehler                        if ($token['token'] === 'opaquelocktoken:' . $existingLock->token) {
205*a1a3b679SAndreas Boehler                            $found = $existingLock;
206*a1a3b679SAndreas Boehler                            break 3;
207*a1a3b679SAndreas Boehler                        }
208*a1a3b679SAndreas Boehler                    }
209*a1a3b679SAndreas Boehler                }
210*a1a3b679SAndreas Boehler            }
211*a1a3b679SAndreas Boehler
212*a1a3b679SAndreas Boehler            // If none were found, this request is in error.
213*a1a3b679SAndreas Boehler            if (is_null($found)) {
214*a1a3b679SAndreas Boehler                if ($existingLocks) {
215*a1a3b679SAndreas Boehler                    throw new DAV\Exception\Locked(reset($existingLocks));
216*a1a3b679SAndreas Boehler                } else {
217*a1a3b679SAndreas Boehler                    throw new DAV\Exception\BadRequest('An xml body is required for lock requests');
218*a1a3b679SAndreas Boehler                }
219*a1a3b679SAndreas Boehler
220*a1a3b679SAndreas Boehler            }
221*a1a3b679SAndreas Boehler
222*a1a3b679SAndreas Boehler            // This must have been a lock refresh
223*a1a3b679SAndreas Boehler            $lockInfo = $found;
224*a1a3b679SAndreas Boehler
225*a1a3b679SAndreas Boehler            // The resource could have been locked through another uri.
226*a1a3b679SAndreas Boehler            if ($uri != $lockInfo->uri) $uri = $lockInfo->uri;
227*a1a3b679SAndreas Boehler
228*a1a3b679SAndreas Boehler        }
229*a1a3b679SAndreas Boehler
230*a1a3b679SAndreas Boehler        if ($timeout = $this->getTimeoutHeader()) $lockInfo->timeout = $timeout;
231*a1a3b679SAndreas Boehler
232*a1a3b679SAndreas Boehler        $newFile = false;
233*a1a3b679SAndreas Boehler
234*a1a3b679SAndreas Boehler        // If we got this far.. we should go check if this node actually exists. If this is not the case, we need to create it first
235*a1a3b679SAndreas Boehler        try {
236*a1a3b679SAndreas Boehler            $this->server->tree->getNodeForPath($uri);
237*a1a3b679SAndreas Boehler
238*a1a3b679SAndreas Boehler            // We need to call the beforeWriteContent event for RFC3744
239*a1a3b679SAndreas Boehler            // Edit: looks like this is not used, and causing problems now.
240*a1a3b679SAndreas Boehler            //
241*a1a3b679SAndreas Boehler            // See Issue 222
242*a1a3b679SAndreas Boehler            // $this->server->emit('beforeWriteContent',array($uri));
243*a1a3b679SAndreas Boehler
244*a1a3b679SAndreas Boehler        } catch (DAV\Exception\NotFound $e) {
245*a1a3b679SAndreas Boehler
246*a1a3b679SAndreas Boehler            // It didn't, lets create it
247*a1a3b679SAndreas Boehler            $this->server->createFile($uri, fopen('php://memory', 'r'));
248*a1a3b679SAndreas Boehler            $newFile = true;
249*a1a3b679SAndreas Boehler
250*a1a3b679SAndreas Boehler        }
251*a1a3b679SAndreas Boehler
252*a1a3b679SAndreas Boehler        $this->lockNode($uri, $lockInfo);
253*a1a3b679SAndreas Boehler
254*a1a3b679SAndreas Boehler        $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
255*a1a3b679SAndreas Boehler        $response->setHeader('Lock-Token', '<opaquelocktoken:' . $lockInfo->token . '>');
256*a1a3b679SAndreas Boehler        $response->setStatus($newFile ? 201 : 200);
257*a1a3b679SAndreas Boehler        $response->setBody($this->generateLockResponse($lockInfo));
258*a1a3b679SAndreas Boehler
259*a1a3b679SAndreas Boehler        // Returning false will interupt the event chain and mark this method
260*a1a3b679SAndreas Boehler        // as 'handled'.
261*a1a3b679SAndreas Boehler        return false;
262*a1a3b679SAndreas Boehler
263*a1a3b679SAndreas Boehler    }
264*a1a3b679SAndreas Boehler
265*a1a3b679SAndreas Boehler    /**
266*a1a3b679SAndreas Boehler     * Unlocks a uri
267*a1a3b679SAndreas Boehler     *
268*a1a3b679SAndreas Boehler     * This WebDAV method allows you to remove a lock from a node. The client should provide a valid locktoken through the Lock-token http header
269*a1a3b679SAndreas Boehler     * The server should return 204 (No content) on success
270*a1a3b679SAndreas Boehler     *
271*a1a3b679SAndreas Boehler     * @param RequestInterface $request
272*a1a3b679SAndreas Boehler     * @param ResponseInterface $response
273*a1a3b679SAndreas Boehler     * @return void
274*a1a3b679SAndreas Boehler     */
275*a1a3b679SAndreas Boehler    function httpUnlock(RequestInterface $request, ResponseInterface $response) {
276*a1a3b679SAndreas Boehler
277*a1a3b679SAndreas Boehler        $lockToken = $request->getHeader('Lock-Token');
278*a1a3b679SAndreas Boehler
279*a1a3b679SAndreas Boehler        // If the locktoken header is not supplied, we need to throw a bad request exception
280*a1a3b679SAndreas Boehler        if (!$lockToken) throw new DAV\Exception\BadRequest('No lock token was supplied');
281*a1a3b679SAndreas Boehler
282*a1a3b679SAndreas Boehler        $path = $request->getPath();
283*a1a3b679SAndreas Boehler        $locks = $this->getLocks($path);
284*a1a3b679SAndreas Boehler
285*a1a3b679SAndreas Boehler        // Windows sometimes forgets to include < and > in the Lock-Token
286*a1a3b679SAndreas Boehler        // header
287*a1a3b679SAndreas Boehler        if ($lockToken[0] !== '<') $lockToken = '<' . $lockToken . '>';
288*a1a3b679SAndreas Boehler
289*a1a3b679SAndreas Boehler        foreach ($locks as $lock) {
290*a1a3b679SAndreas Boehler
291*a1a3b679SAndreas Boehler            if ('<opaquelocktoken:' . $lock->token . '>' == $lockToken) {
292*a1a3b679SAndreas Boehler
293*a1a3b679SAndreas Boehler                $this->unlockNode($path, $lock);
294*a1a3b679SAndreas Boehler                $response->setHeader('Content-Length', '0');
295*a1a3b679SAndreas Boehler                $response->setStatus(204);
296*a1a3b679SAndreas Boehler
297*a1a3b679SAndreas Boehler                // Returning false will break the method chain, and mark the
298*a1a3b679SAndreas Boehler                // method as 'handled'.
299*a1a3b679SAndreas Boehler                return false;
300*a1a3b679SAndreas Boehler
301*a1a3b679SAndreas Boehler            }
302*a1a3b679SAndreas Boehler
303*a1a3b679SAndreas Boehler        }
304*a1a3b679SAndreas Boehler
305*a1a3b679SAndreas Boehler        // If we got here, it means the locktoken was invalid
306*a1a3b679SAndreas Boehler        throw new DAV\Exception\LockTokenMatchesRequestUri();
307*a1a3b679SAndreas Boehler
308*a1a3b679SAndreas Boehler    }
309*a1a3b679SAndreas Boehler
310*a1a3b679SAndreas Boehler    /**
311*a1a3b679SAndreas Boehler     * This method is called after a node is deleted.
312*a1a3b679SAndreas Boehler     *
313*a1a3b679SAndreas Boehler     * We use this event to clean up any locks that still exist on the node.
314*a1a3b679SAndreas Boehler     *
315*a1a3b679SAndreas Boehler     * @param string $path
316*a1a3b679SAndreas Boehler     * @return void
317*a1a3b679SAndreas Boehler     */
318*a1a3b679SAndreas Boehler    function afterUnbind($path) {
319*a1a3b679SAndreas Boehler
320*a1a3b679SAndreas Boehler        $locks = $this->getLocks($path, $includeChildren = true);
321*a1a3b679SAndreas Boehler        foreach ($locks as $lock) {
322*a1a3b679SAndreas Boehler            $this->unlockNode($path, $lock);
323*a1a3b679SAndreas Boehler        }
324*a1a3b679SAndreas Boehler
325*a1a3b679SAndreas Boehler    }
326*a1a3b679SAndreas Boehler
327*a1a3b679SAndreas Boehler    /**
328*a1a3b679SAndreas Boehler     * Locks a uri
329*a1a3b679SAndreas Boehler     *
330*a1a3b679SAndreas Boehler     * All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored
331*a1a3b679SAndreas Boehler     * It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client
332*a1a3b679SAndreas Boehler     *
333*a1a3b679SAndreas Boehler     * @param string $uri
334*a1a3b679SAndreas Boehler     * @param LockInfo $lockInfo
335*a1a3b679SAndreas Boehler     * @return bool
336*a1a3b679SAndreas Boehler     */
337*a1a3b679SAndreas Boehler    function lockNode($uri, LockInfo $lockInfo) {
338*a1a3b679SAndreas Boehler
339*a1a3b679SAndreas Boehler        if (!$this->server->emit('beforeLock', [$uri, $lockInfo])) return;
340*a1a3b679SAndreas Boehler        return $this->locksBackend->lock($uri, $lockInfo);
341*a1a3b679SAndreas Boehler
342*a1a3b679SAndreas Boehler    }
343*a1a3b679SAndreas Boehler
344*a1a3b679SAndreas Boehler    /**
345*a1a3b679SAndreas Boehler     * Unlocks a uri
346*a1a3b679SAndreas Boehler     *
347*a1a3b679SAndreas Boehler     * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified
348*a1a3b679SAndreas Boehler     *
349*a1a3b679SAndreas Boehler     * @param string $uri
350*a1a3b679SAndreas Boehler     * @param LockInfo $lockInfo
351*a1a3b679SAndreas Boehler     * @return bool
352*a1a3b679SAndreas Boehler     */
353*a1a3b679SAndreas Boehler    function unlockNode($uri, LockInfo $lockInfo) {
354*a1a3b679SAndreas Boehler
355*a1a3b679SAndreas Boehler        if (!$this->server->emit('beforeUnlock', [$uri, $lockInfo])) return;
356*a1a3b679SAndreas Boehler        return $this->locksBackend->unlock($uri, $lockInfo);
357*a1a3b679SAndreas Boehler
358*a1a3b679SAndreas Boehler    }
359*a1a3b679SAndreas Boehler
360*a1a3b679SAndreas Boehler
361*a1a3b679SAndreas Boehler    /**
362*a1a3b679SAndreas Boehler     * Returns the contents of the HTTP Timeout header.
363*a1a3b679SAndreas Boehler     *
364*a1a3b679SAndreas Boehler     * The method formats the header into an integer.
365*a1a3b679SAndreas Boehler     *
366*a1a3b679SAndreas Boehler     * @return int
367*a1a3b679SAndreas Boehler     */
368*a1a3b679SAndreas Boehler    function getTimeoutHeader() {
369*a1a3b679SAndreas Boehler
370*a1a3b679SAndreas Boehler        $header = $this->server->httpRequest->getHeader('Timeout');
371*a1a3b679SAndreas Boehler
372*a1a3b679SAndreas Boehler        if ($header) {
373*a1a3b679SAndreas Boehler
374*a1a3b679SAndreas Boehler            if (stripos($header, 'second-') === 0) $header = (int)(substr($header, 7));
375*a1a3b679SAndreas Boehler            elseif (stripos($header, 'infinite') === 0) $header = LockInfo::TIMEOUT_INFINITE;
376*a1a3b679SAndreas Boehler            else throw new DAV\Exception\BadRequest('Invalid HTTP timeout header');
377*a1a3b679SAndreas Boehler
378*a1a3b679SAndreas Boehler        } else {
379*a1a3b679SAndreas Boehler
380*a1a3b679SAndreas Boehler            $header = 0;
381*a1a3b679SAndreas Boehler
382*a1a3b679SAndreas Boehler        }
383*a1a3b679SAndreas Boehler
384*a1a3b679SAndreas Boehler        return $header;
385*a1a3b679SAndreas Boehler
386*a1a3b679SAndreas Boehler    }
387*a1a3b679SAndreas Boehler
388*a1a3b679SAndreas Boehler    /**
389*a1a3b679SAndreas Boehler     * Generates the response for successful LOCK requests
390*a1a3b679SAndreas Boehler     *
391*a1a3b679SAndreas Boehler     * @param LockInfo $lockInfo
392*a1a3b679SAndreas Boehler     * @return string
393*a1a3b679SAndreas Boehler     */
394*a1a3b679SAndreas Boehler    protected function generateLockResponse(LockInfo $lockInfo) {
395*a1a3b679SAndreas Boehler
396*a1a3b679SAndreas Boehler        return $this->server->xml->write('{DAV:}prop', [
397*a1a3b679SAndreas Boehler            '{DAV:}lockdiscovery' =>
398*a1a3b679SAndreas Boehler                new DAV\Xml\Property\LockDiscovery([$lockInfo])
399*a1a3b679SAndreas Boehler        ]);
400*a1a3b679SAndreas Boehler    }
401*a1a3b679SAndreas Boehler
402*a1a3b679SAndreas Boehler    /**
403*a1a3b679SAndreas Boehler     * The validateTokens event is triggered before every request.
404*a1a3b679SAndreas Boehler     *
405*a1a3b679SAndreas Boehler     * It's a moment where this plugin can check all the supplied lock tokens
406*a1a3b679SAndreas Boehler     * in the If: header, and check if they are valid.
407*a1a3b679SAndreas Boehler     *
408*a1a3b679SAndreas Boehler     * In addition, it will also ensure that it checks any missing lokens that
409*a1a3b679SAndreas Boehler     * must be present in the request, and reject requests without the proper
410*a1a3b679SAndreas Boehler     * tokens.
411*a1a3b679SAndreas Boehler     *
412*a1a3b679SAndreas Boehler     * @param RequestInterface $request
413*a1a3b679SAndreas Boehler     * @param mixed $conditions
414*a1a3b679SAndreas Boehler     * @return void
415*a1a3b679SAndreas Boehler     */
416*a1a3b679SAndreas Boehler    function validateTokens(RequestInterface $request, &$conditions) {
417*a1a3b679SAndreas Boehler
418*a1a3b679SAndreas Boehler        // First we need to gather a list of locks that must be satisfied.
419*a1a3b679SAndreas Boehler        $mustLocks = [];
420*a1a3b679SAndreas Boehler        $method = $request->getMethod();
421*a1a3b679SAndreas Boehler
422*a1a3b679SAndreas Boehler        // Methods not in that list are operations that doesn't alter any
423*a1a3b679SAndreas Boehler        // resources, and we don't need to check the lock-states for.
424*a1a3b679SAndreas Boehler        switch ($method) {
425*a1a3b679SAndreas Boehler
426*a1a3b679SAndreas Boehler            case 'DELETE' :
427*a1a3b679SAndreas Boehler                $mustLocks = array_merge($mustLocks, $this->getLocks(
428*a1a3b679SAndreas Boehler                    $request->getPath(),
429*a1a3b679SAndreas Boehler                    true
430*a1a3b679SAndreas Boehler                ));
431*a1a3b679SAndreas Boehler                break;
432*a1a3b679SAndreas Boehler            case 'MKCOL' :
433*a1a3b679SAndreas Boehler            case 'MKCALENDAR' :
434*a1a3b679SAndreas Boehler            case 'PROPPATCH' :
435*a1a3b679SAndreas Boehler            case 'PUT' :
436*a1a3b679SAndreas Boehler            case 'PATCH' :
437*a1a3b679SAndreas Boehler                $mustLocks = array_merge($mustLocks, $this->getLocks(
438*a1a3b679SAndreas Boehler                    $request->getPath(),
439*a1a3b679SAndreas Boehler                    false
440*a1a3b679SAndreas Boehler                ));
441*a1a3b679SAndreas Boehler                break;
442*a1a3b679SAndreas Boehler            case 'MOVE' :
443*a1a3b679SAndreas Boehler                $mustLocks = array_merge($mustLocks, $this->getLocks(
444*a1a3b679SAndreas Boehler                    $request->getPath(),
445*a1a3b679SAndreas Boehler                    true
446*a1a3b679SAndreas Boehler                ));
447*a1a3b679SAndreas Boehler                $mustLocks = array_merge($mustLocks, $this->getLocks(
448*a1a3b679SAndreas Boehler                    $this->server->calculateUri($request->getHeader('Destination')),
449*a1a3b679SAndreas Boehler                    false
450*a1a3b679SAndreas Boehler                ));
451*a1a3b679SAndreas Boehler                break;
452*a1a3b679SAndreas Boehler            case 'COPY' :
453*a1a3b679SAndreas Boehler                $mustLocks = array_merge($mustLocks, $this->getLocks(
454*a1a3b679SAndreas Boehler                    $this->server->calculateUri($request->getHeader('Destination')),
455*a1a3b679SAndreas Boehler                    false
456*a1a3b679SAndreas Boehler                ));
457*a1a3b679SAndreas Boehler                break;
458*a1a3b679SAndreas Boehler            case 'LOCK' :
459*a1a3b679SAndreas Boehler                //Temporary measure.. figure out later why this is needed
460*a1a3b679SAndreas Boehler                // Here we basically ignore all incoming tokens...
461*a1a3b679SAndreas Boehler                foreach ($conditions as $ii => $condition) {
462*a1a3b679SAndreas Boehler                    foreach ($condition['tokens'] as $jj => $token) {
463*a1a3b679SAndreas Boehler                        $conditions[$ii]['tokens'][$jj]['validToken'] = true;
464*a1a3b679SAndreas Boehler                    }
465*a1a3b679SAndreas Boehler                }
466*a1a3b679SAndreas Boehler                return;
467*a1a3b679SAndreas Boehler
468*a1a3b679SAndreas Boehler        }
469*a1a3b679SAndreas Boehler
470*a1a3b679SAndreas Boehler        // It's possible that there's identical locks, because of shared
471*a1a3b679SAndreas Boehler        // parents. We're removing the duplicates here.
472*a1a3b679SAndreas Boehler        $tmp = [];
473*a1a3b679SAndreas Boehler        foreach ($mustLocks as $lock) $tmp[$lock->token] = $lock;
474*a1a3b679SAndreas Boehler        $mustLocks = array_values($tmp);
475*a1a3b679SAndreas Boehler
476*a1a3b679SAndreas Boehler        foreach ($conditions as $kk => $condition) {
477*a1a3b679SAndreas Boehler
478*a1a3b679SAndreas Boehler            foreach ($condition['tokens'] as $ii => $token) {
479*a1a3b679SAndreas Boehler
480*a1a3b679SAndreas Boehler                // Lock tokens always start with opaquelocktoken:
481*a1a3b679SAndreas Boehler                if (substr($token['token'], 0, 16) !== 'opaquelocktoken:') {
482*a1a3b679SAndreas Boehler                    continue;
483*a1a3b679SAndreas Boehler                }
484*a1a3b679SAndreas Boehler
485*a1a3b679SAndreas Boehler                $checkToken = substr($token['token'], 16);
486*a1a3b679SAndreas Boehler                // Looping through our list with locks.
487*a1a3b679SAndreas Boehler                foreach ($mustLocks as $jj => $mustLock) {
488*a1a3b679SAndreas Boehler
489*a1a3b679SAndreas Boehler                    if ($mustLock->token == $checkToken) {
490*a1a3b679SAndreas Boehler
491*a1a3b679SAndreas Boehler                        // We have a match!
492*a1a3b679SAndreas Boehler                        // Removing this one from mustlocks
493*a1a3b679SAndreas Boehler                        unset($mustLocks[$jj]);
494*a1a3b679SAndreas Boehler
495*a1a3b679SAndreas Boehler                        // Marking the condition as valid.
496*a1a3b679SAndreas Boehler                        $conditions[$kk]['tokens'][$ii]['validToken'] = true;
497*a1a3b679SAndreas Boehler
498*a1a3b679SAndreas Boehler                        // Advancing to the next token
499*a1a3b679SAndreas Boehler                        continue 2;
500*a1a3b679SAndreas Boehler
501*a1a3b679SAndreas Boehler                    }
502*a1a3b679SAndreas Boehler
503*a1a3b679SAndreas Boehler                }
504*a1a3b679SAndreas Boehler
505*a1a3b679SAndreas Boehler                // If we got here, it means that there was a
506*a1a3b679SAndreas Boehler                // lock-token, but it was not in 'mustLocks'.
507*a1a3b679SAndreas Boehler                //
508*a1a3b679SAndreas Boehler                // This is an edge-case, as it could mean that token
509*a1a3b679SAndreas Boehler                // was specified with a url that was not 'required' to
510*a1a3b679SAndreas Boehler                // check. So we're doing one extra lookup to make sure
511*a1a3b679SAndreas Boehler                // we really don't know this token.
512*a1a3b679SAndreas Boehler                //
513*a1a3b679SAndreas Boehler                // This also gets triggered when the user specified a
514*a1a3b679SAndreas Boehler                // lock-token that was expired.
515*a1a3b679SAndreas Boehler                $oddLocks = $this->getLocks($condition['uri']);
516*a1a3b679SAndreas Boehler                foreach ($oddLocks as $oddLock) {
517*a1a3b679SAndreas Boehler
518*a1a3b679SAndreas Boehler                    if ($oddLock->token === $checkToken) {
519*a1a3b679SAndreas Boehler
520*a1a3b679SAndreas Boehler                        // We have a hit!
521*a1a3b679SAndreas Boehler                        $conditions[$kk]['tokens'][$ii]['validToken'] = true;
522*a1a3b679SAndreas Boehler                        continue 2;
523*a1a3b679SAndreas Boehler
524*a1a3b679SAndreas Boehler                    }
525*a1a3b679SAndreas Boehler                }
526*a1a3b679SAndreas Boehler
527*a1a3b679SAndreas Boehler                // If we get all the way here, the lock-token was
528*a1a3b679SAndreas Boehler                // really unknown.
529*a1a3b679SAndreas Boehler
530*a1a3b679SAndreas Boehler
531*a1a3b679SAndreas Boehler            }
532*a1a3b679SAndreas Boehler
533*a1a3b679SAndreas Boehler        }
534*a1a3b679SAndreas Boehler
535*a1a3b679SAndreas Boehler        // If there's any locks left in the 'mustLocks' array, it means that
536*a1a3b679SAndreas Boehler        // the resource was locked and we must block it.
537*a1a3b679SAndreas Boehler        if ($mustLocks) {
538*a1a3b679SAndreas Boehler
539*a1a3b679SAndreas Boehler            throw new DAV\Exception\Locked(reset($mustLocks));
540*a1a3b679SAndreas Boehler
541*a1a3b679SAndreas Boehler        }
542*a1a3b679SAndreas Boehler
543*a1a3b679SAndreas Boehler    }
544*a1a3b679SAndreas Boehler
545*a1a3b679SAndreas Boehler    /**
546*a1a3b679SAndreas Boehler     * Parses a webdav lock xml body, and returns a new Sabre\DAV\Locks\LockInfo object
547*a1a3b679SAndreas Boehler     *
548*a1a3b679SAndreas Boehler     * @param string $body
549*a1a3b679SAndreas Boehler     * @return LockInfo
550*a1a3b679SAndreas Boehler     */
551*a1a3b679SAndreas Boehler    protected function parseLockRequest($body) {
552*a1a3b679SAndreas Boehler
553*a1a3b679SAndreas Boehler        $result = $this->server->xml->expect(
554*a1a3b679SAndreas Boehler            '{DAV:}lockinfo',
555*a1a3b679SAndreas Boehler            $body
556*a1a3b679SAndreas Boehler        );
557*a1a3b679SAndreas Boehler
558*a1a3b679SAndreas Boehler        $lockInfo = new LockInfo();
559*a1a3b679SAndreas Boehler
560*a1a3b679SAndreas Boehler        $lockInfo->owner = $result->owner;
561*a1a3b679SAndreas Boehler        $lockInfo->token = DAV\UUIDUtil::getUUID();
562*a1a3b679SAndreas Boehler        $lockInfo->scope = $result->scope;
563*a1a3b679SAndreas Boehler
564*a1a3b679SAndreas Boehler        return $lockInfo;
565*a1a3b679SAndreas Boehler
566*a1a3b679SAndreas Boehler    }
567*a1a3b679SAndreas Boehler
568*a1a3b679SAndreas Boehler    /**
569*a1a3b679SAndreas Boehler     * Returns a bunch of meta-data about the plugin.
570*a1a3b679SAndreas Boehler     *
571*a1a3b679SAndreas Boehler     * Providing this information is optional, and is mainly displayed by the
572*a1a3b679SAndreas Boehler     * Browser plugin.
573*a1a3b679SAndreas Boehler     *
574*a1a3b679SAndreas Boehler     * The description key in the returned array may contain html and will not
575*a1a3b679SAndreas Boehler     * be sanitized.
576*a1a3b679SAndreas Boehler     *
577*a1a3b679SAndreas Boehler     * @return array
578*a1a3b679SAndreas Boehler     */
579*a1a3b679SAndreas Boehler    function getPluginInfo() {
580*a1a3b679SAndreas Boehler
581*a1a3b679SAndreas Boehler        return [
582*a1a3b679SAndreas Boehler            'name'        => $this->getPluginName(),
583*a1a3b679SAndreas Boehler            'description' => 'The locks plugin turns this server into a class-2 WebDAV server and adds support for LOCK and UNLOCK',
584*a1a3b679SAndreas Boehler            'link'        => 'http://sabre.io/dav/locks/',
585*a1a3b679SAndreas Boehler        ];
586*a1a3b679SAndreas Boehler
587*a1a3b679SAndreas Boehler    }
588*a1a3b679SAndreas Boehler
589*a1a3b679SAndreas Boehler}
590