PageRenderTime 146ms CodeModel.GetById 70ms app.highlight 17ms RepoModel.GetById 33ms app.codeStats 1ms

/lib/Sabre/DAV/Locks/Plugin.php

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