]> git.mxchange.org Git - friendica-addons.git/blob - dav/SabreDAV/lib/Sabre/DAV/Locks/Plugin.php
Merge remote branch 'friendica/master'
[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             $this->server->broadcastEvent('beforeWriteContent',array($uri));
297
298         } catch (Sabre_DAV_Exception_NotFound $e) {
299
300             // It didn't, lets create it
301             $this->server->createFile($uri,fopen('php://memory','r'));
302             $newFile = true;
303
304         }
305
306         $this->lockNode($uri,$lockInfo);
307
308         $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
309         $this->server->httpResponse->setHeader('Lock-Token','<opaquelocktoken:' . $lockInfo->token . '>');
310         $this->server->httpResponse->sendStatus($newFile?201:200);
311         $this->server->httpResponse->sendBody($this->generateLockResponse($lockInfo));
312
313     }
314
315     /**
316      * Unlocks a uri
317      *
318      * 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
319      * The server should return 204 (No content) on success
320      *
321      * @param string $uri
322      * @return void
323      */
324     protected function httpUnlock($uri) {
325
326         $lockToken = $this->server->httpRequest->getHeader('Lock-Token');
327
328         // If the locktoken header is not supplied, we need to throw a bad request exception
329         if (!$lockToken) throw new Sabre_DAV_Exception_BadRequest('No lock token was supplied');
330
331         $locks = $this->getLocks($uri);
332
333         // Windows sometimes forgets to include < and > in the Lock-Token
334         // header
335         if ($lockToken[0]!=='<') $lockToken = '<' . $lockToken . '>';
336
337         foreach($locks as $lock) {
338
339             if ('<opaquelocktoken:' . $lock->token . '>' == $lockToken) {
340
341                 $this->unlockNode($uri,$lock);
342                 $this->server->httpResponse->setHeader('Content-Length','0');
343                 $this->server->httpResponse->sendStatus(204);
344                 return;
345
346             }
347
348         }
349
350         // If we got here, it means the locktoken was invalid
351         throw new Sabre_DAV_Exception_LockTokenMatchesRequestUri();
352
353     }
354
355     /**
356      * Locks a uri
357      *
358      * All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored
359      * It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client
360      *
361      * @param string $uri
362      * @param Sabre_DAV_Locks_LockInfo $lockInfo
363      * @return bool
364      */
365     public function lockNode($uri,Sabre_DAV_Locks_LockInfo $lockInfo) {
366
367         if (!$this->server->broadcastEvent('beforeLock',array($uri,$lockInfo))) return;
368
369         if ($this->locksBackend) return $this->locksBackend->lock($uri,$lockInfo);
370         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.');
371
372     }
373
374     /**
375      * Unlocks a uri
376      *
377      * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified
378      *
379      * @param string $uri
380      * @param Sabre_DAV_Locks_LockInfo $lockInfo
381      * @return bool
382      */
383     public function unlockNode($uri,Sabre_DAV_Locks_LockInfo $lockInfo) {
384
385         if (!$this->server->broadcastEvent('beforeUnlock',array($uri,$lockInfo))) return;
386         if ($this->locksBackend) return $this->locksBackend->unlock($uri,$lockInfo);
387
388     }
389
390
391     /**
392      * Returns the contents of the HTTP Timeout header.
393      *
394      * The method formats the header into an integer.
395      *
396      * @return int
397      */
398     public function getTimeoutHeader() {
399
400         $header = $this->server->httpRequest->getHeader('Timeout');
401
402         if ($header) {
403
404             if (stripos($header,'second-')===0) $header = (int)(substr($header,7));
405             else if (strtolower($header)=='infinite') $header=Sabre_DAV_Locks_LockInfo::TIMEOUT_INFINITE;
406             else throw new Sabre_DAV_Exception_BadRequest('Invalid HTTP timeout header');
407
408         } else {
409
410             $header = 0;
411
412         }
413
414         return $header;
415
416     }
417
418     /**
419      * Generates the response for successful LOCK requests
420      *
421      * @param Sabre_DAV_Locks_LockInfo $lockInfo
422      * @return string
423      */
424     protected function generateLockResponse(Sabre_DAV_Locks_LockInfo $lockInfo) {
425
426         $dom = new DOMDocument('1.0','utf-8');
427         $dom->formatOutput = true;
428
429         $prop = $dom->createElementNS('DAV:','d:prop');
430         $dom->appendChild($prop);
431
432         $lockDiscovery = $dom->createElementNS('DAV:','d:lockdiscovery');
433         $prop->appendChild($lockDiscovery);
434
435         $lockObj = new Sabre_DAV_Property_LockDiscovery(array($lockInfo),true);
436         $lockObj->serialize($this->server,$lockDiscovery);
437
438         return $dom->saveXML();
439
440     }
441
442     /**
443      * validateLock should be called when a write operation is about to happen
444      * It will check if the requested url is locked, and see if the correct lock tokens are passed
445      *
446      * @param mixed $urls List of relevant urls. Can be an array, a string or nothing at all for the current request uri
447      * @param mixed $lastLock This variable will be populated with the last checked lock object (Sabre_DAV_Locks_LockInfo)
448      * @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.
449      * @return bool
450      */
451     protected function validateLock($urls = null,&$lastLock = null, $checkChildLocks = false) {
452
453         if (is_null($urls)) {
454             $urls = array($this->server->getRequestUri());
455         } elseif (is_string($urls)) {
456             $urls = array($urls);
457         } elseif (!is_array($urls)) {
458             throw new Sabre_DAV_Exception('The urls parameter should either be null, a string or an array');
459         }
460
461         $conditions = $this->getIfConditions();
462
463         // We're going to loop through the urls and make sure all lock conditions are satisfied
464         foreach($urls as $url) {
465
466             $locks = $this->getLocks($url, $checkChildLocks);
467
468             // If there were no conditions, but there were locks, we fail
469             if (!$conditions && $locks) {
470                 reset($locks);
471                 $lastLock = current($locks);
472                 return false;
473             }
474
475             // If there were no locks or conditions, we go to the next url
476             if (!$locks && !$conditions) continue;
477
478             foreach($conditions as $condition) {
479
480                 if (!$condition['uri']) {
481                     $conditionUri = $this->server->getRequestUri();
482                 } else {
483                     $conditionUri = $this->server->calculateUri($condition['uri']);
484                 }
485
486                 // If the condition has a url, and it isn't part of the affected url at all, check the next condition
487                 if ($conditionUri && strpos($url,$conditionUri)!==0) continue;
488
489                 // The tokens array contians arrays with 2 elements. 0=true/false for normal/not condition, 1=locktoken
490                 // At least 1 condition has to be satisfied
491                 foreach($condition['tokens'] as $conditionToken) {
492
493                     $etagValid = true;
494                     $lockValid  = true;
495
496                     // key 2 can contain an etag
497                     if ($conditionToken[2]) {
498
499                         $uri = $conditionUri?$conditionUri:$this->server->getRequestUri();
500                         $node = $this->server->tree->getNodeForPath($uri);
501                         $etagValid = $node->getETag()==$conditionToken[2];
502
503                     }
504
505                     // key 1 can contain a lock token
506                     if ($conditionToken[1]) {
507
508                         $lockValid = false;
509                         // Match all the locks
510                         foreach($locks as $lockIndex=>$lock) {
511
512                             $lockToken = 'opaquelocktoken:' . $lock->token;
513
514                             // Checking NOT
515                             if (!$conditionToken[0] && $lockToken != $conditionToken[1]) {
516
517                                 // Condition valid, onto the next
518                                 $lockValid = true;
519                                 break;
520                             }
521                             if ($conditionToken[0] && $lockToken == $conditionToken[1]) {
522
523                                 $lastLock = $lock;
524                                 // Condition valid and lock matched
525                                 unset($locks[$lockIndex]);
526                                 $lockValid = true;
527                                 break;
528
529                             }
530
531                         }
532
533                     }
534
535                     // If, after checking both etags and locks they are stil valid,
536                     // we can continue with the next condition.
537                     if ($etagValid && $lockValid) continue 2;
538                }
539                // No conditions matched, so we fail
540                throw new Sabre_DAV_Exception_PreconditionFailed('The tokens provided in the if header did not match','If');
541             }
542
543             // Conditions were met, we'll also need to check if all the locks are gone
544             if (count($locks)) {
545
546                 reset($locks);
547
548                 // There's still locks, we fail
549                 $lastLock = current($locks);
550                 return false;
551
552             }
553
554
555         }
556
557         // We got here, this means every condition was satisfied
558         return true;
559
560     }
561
562     /**
563      * This method is created to extract information from the WebDAV HTTP 'If:' header
564      *
565      * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information
566      * The function will return an array, containing structs with the following keys
567      *
568      *   * uri   - the uri the condition applies to. If this is returned as an
569      *     empty string, this implies it's referring to the request url.
570      *   * 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)
571      *   * etag - an etag, if supplied
572      *
573      * @return array
574      */
575     public function getIfConditions() {
576
577         $header = $this->server->httpRequest->getHeader('If');
578         if (!$header) return array();
579
580         $matches = array();
581
582         $regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im';
583         preg_match_all($regex,$header,$matches,PREG_SET_ORDER);
584
585         $conditions = array();
586
587         foreach($matches as $match) {
588
589             $condition = array(
590                 'uri'   => $match['uri'],
591                 'tokens' => array(
592                     array($match['not']?0:1,$match['token'],isset($match['etag'])?$match['etag']:'')
593                 ),
594             );
595
596             if (!$condition['uri'] && count($conditions)) $conditions[count($conditions)-1]['tokens'][] = array(
597                 $match['not']?0:1,
598                 $match['token'],
599                 isset($match['etag'])?$match['etag']:''
600             );
601             else {
602                 $conditions[] = $condition;
603             }
604
605         }
606
607         return $conditions;
608
609     }
610
611     /**
612      * Parses a webdav lock xml body, and returns a new Sabre_DAV_Locks_LockInfo object
613      *
614      * @param string $body
615      * @return Sabre_DAV_Locks_LockInfo
616      */
617     protected function parseLockRequest($body) {
618
619         $xml = simplexml_load_string($body,null,LIBXML_NOWARNING);
620         $xml->registerXPathNamespace('d','DAV:');
621         $lockInfo = new Sabre_DAV_Locks_LockInfo();
622
623         $children = $xml->children("DAV:");
624         $lockInfo->owner = (string)$children->owner;
625
626         $lockInfo->token = Sabre_DAV_UUIDUtil::getUUID();
627         $lockInfo->scope = count($xml->xpath('d:lockscope/d:exclusive'))>0?Sabre_DAV_Locks_LockInfo::EXCLUSIVE:Sabre_DAV_Locks_LockInfo::SHARED;
628
629         return $lockInfo;
630
631     }
632
633
634 }