]> git.mxchange.org Git - friendica-addons.git/blob - dav/SabreDAV/lib/Sabre/DAV/Locks/Plugin.php
957ac506a9c1bb9baf96e2689fae69ea17ca50b1
[friendica-addons.git] / dav / SabreDAV / lib / Sabre / DAV / Locks / Plugin.php
1 <?php
2
3 /**
4  * Locking plugin
5  *
6  * This plugin provides locking support to a WebDAV server.
7  * The easiest way to get started, is by hooking it up as such:
8  *
9  * $lockBackend = new Sabre_DAV_Locks_Backend_File('./mylockdb');
10  * $lockPlugin = new Sabre_DAV_Locks_Plugin($lockBackend);
11  * $server->addPlugin($lockPlugin);
12  *
13  * @package Sabre
14  * @subpackage DAV
15  * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved.
16  * @author Evert Pot (http://www.rooftopsolutions.nl/)
17  * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
18  */
19 class Sabre_DAV_Locks_Plugin extends Sabre_DAV_ServerPlugin {
20
21     /**
22      * locksBackend
23      *
24      * @var Sabre_DAV_Locks_Backend_Abstract
25      */
26     private $locksBackend;
27
28     /**
29      * server
30      *
31      * @var Sabre_DAV_Server
32      */
33     private $server;
34
35     /**
36      * __construct
37      *
38      * @param Sabre_DAV_Locks_Backend_Abstract $locksBackend
39      */
40     public function __construct(Sabre_DAV_Locks_Backend_Abstract $locksBackend = null) {
41
42         $this->locksBackend = $locksBackend;
43
44     }
45
46     /**
47      * Initializes the plugin
48      *
49      * This method is automatically called by the Server class after addPlugin.
50      *
51      * @param Sabre_DAV_Server $server
52      * @return void
53      */
54     public function initialize(Sabre_DAV_Server $server) {
55
56         $this->server = $server;
57         $server->subscribeEvent('unknownMethod',array($this,'unknownMethod'));
58         $server->subscribeEvent('beforeMethod',array($this,'beforeMethod'),50);
59         $server->subscribeEvent('afterGetProperties',array($this,'afterGetProperties'));
60
61     }
62
63     /**
64      * Returns a plugin name.
65      *
66      * Using this name other plugins will be able to access other plugins
67      * using Sabre_DAV_Server::getPlugin
68      *
69      * @return string
70      */
71     public function getPluginName() {
72
73         return 'locks';
74
75     }
76
77     /**
78      * This method is called by the Server if the user used an HTTP method
79      * the server didn't recognize.
80      *
81      * This plugin intercepts the LOCK and UNLOCK methods.
82      *
83      * @param string $method
84      * @param string $uri
85      * @return bool
86      */
87     public function unknownMethod($method, $uri) {
88
89         switch($method) {
90
91             case 'LOCK'   : $this->httpLock($uri); return false;
92             case 'UNLOCK' : $this->httpUnlock($uri); return false;
93
94         }
95
96     }
97
98     /**
99      * This method is called after most properties have been found
100      * it allows us to add in any Lock-related properties
101      *
102      * @param string $path
103      * @param array $newProperties
104      * @return bool
105      */
106     public function afterGetProperties($path, &$newProperties) {
107
108         foreach($newProperties[404] as $propName=>$discard) {
109
110             switch($propName) {
111
112                 case '{DAV:}supportedlock' :
113                     $val = false;
114                     if ($this->locksBackend) $val = true;
115                     $newProperties[200][$propName] = new Sabre_DAV_Property_SupportedLock($val);
116                     unset($newProperties[404][$propName]);
117                     break;
118
119                 case '{DAV:}lockdiscovery' :
120                     $newProperties[200][$propName] = new Sabre_DAV_Property_LockDiscovery($this->getLocks($path));
121                     unset($newProperties[404][$propName]);
122                     break;
123
124             }
125
126
127         }
128         return true;
129
130     }
131
132
133     /**
134      * This method is called before the logic for any HTTP method is
135      * handled.
136      *
137      * This plugin uses that feature to intercept access to locked resources.
138      *
139      * @param string $method
140      * @param string $uri
141      * @return bool
142      */
143     public function beforeMethod($method, $uri) {
144
145         switch($method) {
146
147             case 'DELETE' :
148                 $lastLock = null;
149                 if (!$this->validateLock($uri,$lastLock, true))
150                     throw new Sabre_DAV_Exception_Locked($lastLock);
151                 break;
152             case 'MKCOL' :
153             case 'PROPPATCH' :
154             case 'PUT' :
155             case 'PATCH' :
156                 $lastLock = null;
157                 if (!$this->validateLock($uri,$lastLock))
158                     throw new Sabre_DAV_Exception_Locked($lastLock);
159                 break;
160             case 'MOVE' :
161                 $lastLock = null;
162                 if (!$this->validateLock(array(
163                       $uri,
164                       $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')),
165                     ),$lastLock, true))
166                         throw new Sabre_DAV_Exception_Locked($lastLock);
167                 break;
168             case 'COPY' :
169                 $lastLock = null;
170                 if (!$this->validateLock(
171                       $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')),
172                       $lastLock, true))
173                         throw new Sabre_DAV_Exception_Locked($lastLock);
174                 break;
175         }
176
177         return true;
178
179     }
180
181     /**
182      * Use this method to tell the server this plugin defines additional
183      * HTTP methods.
184      *
185      * This method is passed a uri. It should only return HTTP methods that are
186      * available for the specified uri.
187      *
188      * @param string $uri
189      * @return array
190      */
191     public function getHTTPMethods($uri) {
192
193         if ($this->locksBackend)
194             return array('LOCK','UNLOCK');
195
196         return array();
197
198     }
199
200     /**
201      * Returns a list of features for the HTTP OPTIONS Dav: header.
202      *
203      * In this case this is only the number 2. The 2 in the Dav: header
204      * indicates the server supports locks.
205      *
206      * @return array
207      */
208     public function getFeatures() {
209
210         return array(2);
211
212     }
213
214     /**
215      * Returns all lock information on a particular uri
216      *
217      * This function should return an array with Sabre_DAV_Locks_LockInfo objects. If there are no locks on a file, return an empty array.
218      *
219      * Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree
220      * If the $returnChildLocks argument is set to true, we'll also traverse all the children of the object
221      * for any possible locks and return those as well.
222      *
223      * @param string $uri
224      * @param bool $returnChildLocks
225      * @return array
226      */
227     public function getLocks($uri, $returnChildLocks = false) {
228
229         $lockList = array();
230
231         if ($this->locksBackend)
232             $lockList = array_merge($lockList,$this->locksBackend->getLocks($uri, $returnChildLocks));
233
234         return $lockList;
235
236     }
237
238     /**
239      * Locks an uri
240      *
241      * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock
242      * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type
243      * of lock (shared or exclusive) and the owner of the lock
244      *
245      * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock
246      *
247      * 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
248      *
249      * @param string $uri
250      * @return void
251      */
252     protected function httpLock($uri) {
253
254         $lastLock = null;
255         if (!$this->validateLock($uri,$lastLock)) {
256
257             // If the existing lock was an exclusive lock, we need to fail
258             if (!$lastLock || $lastLock->scope == Sabre_DAV_Locks_LockInfo::EXCLUSIVE) {
259                 //var_dump($lastLock);
260                 throw new Sabre_DAV_Exception_ConflictingLock($lastLock);
261             }
262
263         }
264
265         if ($body = $this->server->httpRequest->getBody(true)) {
266             // This is a new lock request
267             $lockInfo = $this->parseLockRequest($body);
268             $lockInfo->depth = $this->server->getHTTPDepth();
269             $lockInfo->uri = $uri;
270             if($lastLock && $lockInfo->scope != Sabre_DAV_Locks_LockInfo::SHARED) throw new Sabre_DAV_Exception_ConflictingLock($lastLock);
271
272         } elseif ($lastLock) {
273
274             // This must have been a lock refresh
275             $lockInfo = $lastLock;
276
277             // The resource could have been locked through another uri.
278             if ($uri!=$lockInfo->uri) $uri = $lockInfo->uri;
279
280         } else {
281
282             // There was neither a lock refresh nor a new lock request
283             throw new Sabre_DAV_Exception_BadRequest('An xml body is required for lock requests');
284
285         }
286
287         if ($timeout = $this->getTimeoutHeader()) $lockInfo->timeout = $timeout;
288
289         $newFile = false;
290
291         // 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
292         try {
293             $this->server->tree->getNodeForPath($uri);
294
295             // We need to call the beforeWriteContent event for RFC3744
296             // Edit: looks like this is not used, and causing problems now.
297             //
298             // See Issue 222
299             // $this->server->broadcastEvent('beforeWriteContent',array($uri));
300
301         } catch (Sabre_DAV_Exception_NotFound $e) {
302
303             // It didn't, lets create it
304             $this->server->createFile($uri,fopen('php://memory','r'));
305             $newFile = true;
306
307         }
308
309         $this->lockNode($uri,$lockInfo);
310
311         $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
312         $this->server->httpResponse->setHeader('Lock-Token','<opaquelocktoken:' . $lockInfo->token . '>');
313         $this->server->httpResponse->sendStatus($newFile?201:200);
314         $this->server->httpResponse->sendBody($this->generateLockResponse($lockInfo));
315
316     }
317
318     /**
319      * Unlocks a uri
320      *
321      * 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
322      * The server should return 204 (No content) on success
323      *
324      * @param string $uri
325      * @return void
326      */
327     protected function httpUnlock($uri) {
328
329         $lockToken = $this->server->httpRequest->getHeader('Lock-Token');
330
331         // If the locktoken header is not supplied, we need to throw a bad request exception
332         if (!$lockToken) throw new Sabre_DAV_Exception_BadRequest('No lock token was supplied');
333
334         $locks = $this->getLocks($uri);
335
336         // Windows sometimes forgets to include < and > in the Lock-Token
337         // header
338         if ($lockToken[0]!=='<') $lockToken = '<' . $lockToken . '>';
339
340         foreach($locks as $lock) {
341
342             if ('<opaquelocktoken:' . $lock->token . '>' == $lockToken) {
343
344                 $this->unlockNode($uri,$lock);
345                 $this->server->httpResponse->setHeader('Content-Length','0');
346                 $this->server->httpResponse->sendStatus(204);
347                 return;
348
349             }
350
351         }
352
353         // If we got here, it means the locktoken was invalid
354         throw new Sabre_DAV_Exception_LockTokenMatchesRequestUri();
355
356     }
357
358     /**
359      * Locks a uri
360      *
361      * All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored
362      * It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client
363      *
364      * @param string $uri
365      * @param Sabre_DAV_Locks_LockInfo $lockInfo
366      * @return bool
367      */
368     public function lockNode($uri,Sabre_DAV_Locks_LockInfo $lockInfo) {
369
370         if (!$this->server->broadcastEvent('beforeLock',array($uri,$lockInfo))) return;
371
372         if ($this->locksBackend) return $this->locksBackend->lock($uri,$lockInfo);
373         throw new Sabre_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.');
374
375     }
376
377     /**
378      * Unlocks a uri
379      *
380      * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified
381      *
382      * @param string $uri
383      * @param Sabre_DAV_Locks_LockInfo $lockInfo
384      * @return bool
385      */
386     public function unlockNode($uri,Sabre_DAV_Locks_LockInfo $lockInfo) {
387
388         if (!$this->server->broadcastEvent('beforeUnlock',array($uri,$lockInfo))) return;
389         if ($this->locksBackend) return $this->locksBackend->unlock($uri,$lockInfo);
390
391     }
392
393
394     /**
395      * Returns the contents of the HTTP Timeout header.
396      *
397      * The method formats the header into an integer.
398      *
399      * @return int
400      */
401     public function getTimeoutHeader() {
402
403         $header = $this->server->httpRequest->getHeader('Timeout');
404
405         if ($header) {
406
407             if (stripos($header,'second-')===0) $header = (int)(substr($header,7));
408             else if (strtolower($header)=='infinite') $header=Sabre_DAV_Locks_LockInfo::TIMEOUT_INFINITE;
409             else throw new Sabre_DAV_Exception_BadRequest('Invalid HTTP timeout header');
410
411         } else {
412
413             $header = 0;
414
415         }
416
417         return $header;
418
419     }
420
421     /**
422      * Generates the response for successful LOCK requests
423      *
424      * @param Sabre_DAV_Locks_LockInfo $lockInfo
425      * @return string
426      */
427     protected function generateLockResponse(Sabre_DAV_Locks_LockInfo $lockInfo) {
428
429         $dom = new DOMDocument('1.0','utf-8');
430         $dom->formatOutput = true;
431
432         $prop = $dom->createElementNS('DAV:','d:prop');
433         $dom->appendChild($prop);
434
435         $lockDiscovery = $dom->createElementNS('DAV:','d:lockdiscovery');
436         $prop->appendChild($lockDiscovery);
437
438         $lockObj = new Sabre_DAV_Property_LockDiscovery(array($lockInfo),true);
439         $lockObj->serialize($this->server,$lockDiscovery);
440
441         return $dom->saveXML();
442
443     }
444
445     /**
446      * validateLock should be called when a write operation is about to happen
447      * It will check if the requested url is locked, and see if the correct lock tokens are passed
448      *
449      * @param mixed $urls List of relevant urls. Can be an array, a string or nothing at all for the current request uri
450      * @param mixed $lastLock This variable will be populated with the last checked lock object (Sabre_DAV_Locks_LockInfo)
451      * @param bool $checkChildLocks If set to true, this function will also look for any locks set on child resources of the supplied urls. This is needed for for example deletion of entire trees.
452      * @return bool
453      */
454     protected function validateLock($urls = null,&$lastLock = null, $checkChildLocks = false) {
455
456         if (is_null($urls)) {
457             $urls = array($this->server->getRequestUri());
458         } elseif (is_string($urls)) {
459             $urls = array($urls);
460         } elseif (!is_array($urls)) {
461             throw new Sabre_DAV_Exception('The urls parameter should either be null, a string or an array');
462         }
463
464         $conditions = $this->getIfConditions();
465
466         // We're going to loop through the urls and make sure all lock conditions are satisfied
467         foreach($urls as $url) {
468
469             $locks = $this->getLocks($url, $checkChildLocks);
470
471             // If there were no conditions, but there were locks, we fail
472             if (!$conditions && $locks) {
473                 reset($locks);
474                 $lastLock = current($locks);
475                 return false;
476             }
477
478             // If there were no locks or conditions, we go to the next url
479             if (!$locks && !$conditions) continue;
480
481             foreach($conditions as $condition) {
482
483                 if (!$condition['uri']) {
484                     $conditionUri = $this->server->getRequestUri();
485                 } else {
486                     $conditionUri = $this->server->calculateUri($condition['uri']);
487                 }
488
489                 // If the condition has a url, and it isn't part of the affected url at all, check the next condition
490                 if ($conditionUri && strpos($url,$conditionUri)!==0) continue;
491
492                 // The tokens array contians arrays with 2 elements. 0=true/false for normal/not condition, 1=locktoken
493                 // At least 1 condition has to be satisfied
494                 foreach($condition['tokens'] as $conditionToken) {
495
496                     $etagValid = true;
497                     $lockValid  = true;
498
499                     // key 2 can contain an etag
500                     if ($conditionToken[2]) {
501
502                         $uri = $conditionUri?$conditionUri:$this->server->getRequestUri();
503                         $node = $this->server->tree->getNodeForPath($uri);
504                         $etagValid = $node->getETag()==$conditionToken[2];
505
506                     }
507
508                     // key 1 can contain a lock token
509                     if ($conditionToken[1]) {
510
511                         $lockValid = false;
512                         // Match all the locks
513                         foreach($locks as $lockIndex=>$lock) {
514
515                             $lockToken = 'opaquelocktoken:' . $lock->token;
516
517                             // Checking NOT
518                             if (!$conditionToken[0] && $lockToken != $conditionToken[1]) {
519
520                                 // Condition valid, onto the next
521                                 $lockValid = true;
522                                 break;
523                             }
524                             if ($conditionToken[0] && $lockToken == $conditionToken[1]) {
525
526                                 $lastLock = $lock;
527                                 // Condition valid and lock matched
528                                 unset($locks[$lockIndex]);
529                                 $lockValid = true;
530                                 break;
531
532                             }
533
534                         }
535
536                     }
537
538                     // If, after checking both etags and locks they are stil valid,
539                     // we can continue with the next condition.
540                     if ($etagValid && $lockValid) continue 2;
541                }
542                // No conditions matched, so we fail
543                throw new Sabre_DAV_Exception_PreconditionFailed('The tokens provided in the if header did not match','If');
544             }
545
546             // Conditions were met, we'll also need to check if all the locks are gone
547             if (count($locks)) {
548
549                 reset($locks);
550
551                 // There's still locks, we fail
552                 $lastLock = current($locks);
553                 return false;
554
555             }
556
557
558         }
559
560         // We got here, this means every condition was satisfied
561         return true;
562
563     }
564
565     /**
566      * This method is created to extract information from the WebDAV HTTP 'If:' header
567      *
568      * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information
569      * The function will return an array, containing structs with the following keys
570      *
571      *   * uri   - the uri the condition applies to. If this is returned as an
572      *     empty string, this implies it's referring to the request url.
573      *   * tokens - The lock token. another 2 dimensional array containing 2 elements (0 = true/false.. If this is a negative condition its set to false, 1 = the actual token)
574      *   * etag - an etag, if supplied
575      *
576      * @return array
577      */
578     public function getIfConditions() {
579
580         $header = $this->server->httpRequest->getHeader('If');
581         if (!$header) return array();
582
583         $matches = array();
584
585         $regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im';
586         preg_match_all($regex,$header,$matches,PREG_SET_ORDER);
587
588         $conditions = array();
589
590         foreach($matches as $match) {
591
592             $condition = array(
593                 'uri'   => $match['uri'],
594                 'tokens' => array(
595                     array($match['not']?0:1,$match['token'],isset($match['etag'])?$match['etag']:'')
596                 ),
597             );
598
599             if (!$condition['uri'] && count($conditions)) $conditions[count($conditions)-1]['tokens'][] = array(
600                 $match['not']?0:1,
601                 $match['token'],
602                 isset($match['etag'])?$match['etag']:''
603             );
604             else {
605                 $conditions[] = $condition;
606             }
607
608         }
609
610         return $conditions;
611
612     }
613
614     /**
615      * Parses a webdav lock xml body, and returns a new Sabre_DAV_Locks_LockInfo object
616      *
617      * @param string $body
618      * @return Sabre_DAV_Locks_LockInfo
619      */
620     protected function parseLockRequest($body) {
621
622         $xml = simplexml_load_string($body,null,LIBXML_NOWARNING);
623         $xml->registerXPathNamespace('d','DAV:');
624         $lockInfo = new Sabre_DAV_Locks_LockInfo();
625
626         $children = $xml->children("DAV:");
627         $lockInfo->owner = (string)$children->owner;
628
629         $lockInfo->token = Sabre_DAV_UUIDUtil::getUUID();
630         $lockInfo->scope = count($xml->xpath('d:lockscope/d:exclusive'))>0?Sabre_DAV_Locks_LockInfo::EXCLUSIVE:Sabre_DAV_Locks_LockInfo::SHARED;
631
632         return $lockInfo;
633
634     }
635
636
637 }