6 * This addon provides locking support to a WebDAV server.
7 * The easiest way to get started, is by hooking it up as such:
9 * $lockBackend = new Sabre_DAV_Locks_Backend_File('./mylockdb');
10 * $lockPlugin = new Sabre_DAV_Locks_Plugin($lockBackend);
11 * $server->addPlugin($lockPlugin);
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
19 class Sabre_DAV_Locks_Plugin extends Sabre_DAV_ServerPlugin {
24 * @var Sabre_DAV_Locks_Backend_Abstract
26 private $locksBackend;
31 * @var Sabre_DAV_Server
38 * @param Sabre_DAV_Locks_Backend_Abstract $locksBackend
40 public function __construct(Sabre_DAV_Locks_Backend_Abstract $locksBackend = null) {
42 $this->locksBackend = $locksBackend;
47 * Initializes the addon
49 * This method is automatically called by the Server class after addPlugin.
51 * @param Sabre_DAV_Server $server
54 public function initialize(Sabre_DAV_Server $server) {
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'));
64 * Returns a addon name.
66 * Using this name other addons will be able to access other addons
67 * using Sabre_DAV_Server::getPlugin
71 public function getPluginName() {
78 * This method is called by the Server if the user used an HTTP method
79 * the server didn't recognize.
81 * This addon intercepts the LOCK and UNLOCK methods.
83 * @param string $method
87 public function unknownMethod($method, $uri) {
91 case 'LOCK' : $this->httpLock($uri); return false;
92 case 'UNLOCK' : $this->httpUnlock($uri); return false;
99 * This method is called after most properties have been found
100 * it allows us to add in any Lock-related properties
102 * @param string $path
103 * @param array $newProperties
106 public function afterGetProperties($path, &$newProperties) {
108 foreach($newProperties[404] as $propName=>$discard) {
112 case '{DAV:}supportedlock' :
114 if ($this->locksBackend) $val = true;
115 $newProperties[200][$propName] = new Sabre_DAV_Property_SupportedLock($val);
116 unset($newProperties[404][$propName]);
119 case '{DAV:}lockdiscovery' :
120 $newProperties[200][$propName] = new Sabre_DAV_Property_LockDiscovery($this->getLocks($path));
121 unset($newProperties[404][$propName]);
134 * This method is called before the logic for any HTTP method is
137 * This addon uses that feature to intercept access to locked resources.
139 * @param string $method
143 public function beforeMethod($method, $uri) {
149 if (!$this->validateLock($uri,$lastLock, true))
150 throw new Sabre_DAV_Exception_Locked($lastLock);
157 if (!$this->validateLock($uri,$lastLock))
158 throw new Sabre_DAV_Exception_Locked($lastLock);
162 if (!$this->validateLock(array(
164 $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')),
166 throw new Sabre_DAV_Exception_Locked($lastLock);
170 if (!$this->validateLock(
171 $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')),
173 throw new Sabre_DAV_Exception_Locked($lastLock);
182 * Use this method to tell the server this addon defines additional
185 * This method is passed a uri. It should only return HTTP methods that are
186 * available for the specified uri.
191 public function getHTTPMethods($uri) {
193 if ($this->locksBackend)
194 return array('LOCK','UNLOCK');
201 * Returns a list of features for the HTTP OPTIONS Dav: header.
203 * In this case this is only the number 2. The 2 in the Dav: header
204 * indicates the server supports locks.
208 public function getFeatures() {
215 * Returns all lock information on a particular uri
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.
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.
224 * @param bool $returnChildLocks
227 public function getLocks($uri, $returnChildLocks = false) {
231 if ($this->locksBackend)
232 $lockList = array_merge($lockList,$this->locksBackend->getLocks($uri, $returnChildLocks));
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
245 * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock
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
252 protected function httpLock($uri) {
255 if (!$this->validateLock($uri,$lastLock)) {
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);
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);
272 } elseif ($lastLock) {
274 // This must have been a lock refresh
275 $lockInfo = $lastLock;
277 // The resource could have been locked through another uri.
278 if ($uri!=$lockInfo->uri) $uri = $lockInfo->uri;
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');
287 if ($timeout = $this->getTimeoutHeader()) $lockInfo->timeout = $timeout;
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
293 $this->server->tree->getNodeForPath($uri);
295 // We need to call the beforeWriteContent event for RFC3744
296 // Edit: looks like this is not used, and causing problems now.
299 // $this->server->broadcastEvent('beforeWriteContent',array($uri));
301 } catch (Sabre_DAV_Exception_NotFound $e) {
303 // It didn't, lets create it
304 $this->server->createFile($uri,fopen('php://memory','r'));
309 $this->lockNode($uri,$lockInfo);
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));
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
327 protected function httpUnlock($uri) {
329 $lockToken = $this->server->httpRequest->getHeader('Lock-Token');
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');
334 $locks = $this->getLocks($uri);
336 // Windows sometimes forgets to include < and > in the Lock-Token
338 if ($lockToken[0]!=='<') $lockToken = '<' . $lockToken . '>';
340 foreach($locks as $lock) {
342 if ('<opaquelocktoken:' . $lock->token . '>' == $lockToken) {
344 $this->unlockNode($uri,$lock);
345 $this->server->httpResponse->setHeader('Content-Length','0');
346 $this->server->httpResponse->sendStatus(204);
353 // If we got here, it means the locktoken was invalid
354 throw new Sabre_DAV_Exception_LockTokenMatchesRequestUri();
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
365 * @param Sabre_DAV_Locks_LockInfo $lockInfo
368 public function lockNode($uri,Sabre_DAV_Locks_LockInfo $lockInfo) {
370 if (!$this->server->broadcastEvent('beforeLock',array($uri,$lockInfo))) return;
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.');
380 * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified
383 * @param Sabre_DAV_Locks_LockInfo $lockInfo
386 public function unlockNode($uri,Sabre_DAV_Locks_LockInfo $lockInfo) {
388 if (!$this->server->broadcastEvent('beforeUnlock',array($uri,$lockInfo))) return;
389 if ($this->locksBackend) return $this->locksBackend->unlock($uri,$lockInfo);
395 * Returns the contents of the HTTP Timeout header.
397 * The method formats the header into an integer.
401 public function getTimeoutHeader() {
403 $header = $this->server->httpRequest->getHeader('Timeout');
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');
422 * Generates the response for successful LOCK requests
424 * @param Sabre_DAV_Locks_LockInfo $lockInfo
427 protected function generateLockResponse(Sabre_DAV_Locks_LockInfo $lockInfo) {
429 $dom = new DOMDocument('1.0','utf-8');
430 $dom->formatOutput = true;
432 $prop = $dom->createElementNS('DAV:','d:prop');
433 $dom->appendChild($prop);
435 $lockDiscovery = $dom->createElementNS('DAV:','d:lockdiscovery');
436 $prop->appendChild($lockDiscovery);
438 $lockObj = new Sabre_DAV_Property_LockDiscovery(array($lockInfo),true);
439 $lockObj->serialize($this->server,$lockDiscovery);
441 return $dom->saveXML();
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
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.
454 protected function validateLock($urls = null,&$lastLock = null, $checkChildLocks = false) {
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');
464 $conditions = $this->getIfConditions();
466 // We're going to loop through the urls and make sure all lock conditions are satisfied
467 foreach($urls as $url) {
469 $locks = $this->getLocks($url, $checkChildLocks);
471 // If there were no conditions, but there were locks, we fail
472 if (!$conditions && $locks) {
474 $lastLock = current($locks);
478 // If there were no locks or conditions, we go to the next url
479 if (!$locks && !$conditions) continue;
481 foreach($conditions as $condition) {
483 if (!$condition['uri']) {
484 $conditionUri = $this->server->getRequestUri();
486 $conditionUri = $this->server->calculateUri($condition['uri']);
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;
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) {
499 // key 2 can contain an etag
500 if ($conditionToken[2]) {
502 $uri = $conditionUri?$conditionUri:$this->server->getRequestUri();
503 $node = $this->server->tree->getNodeForPath($uri);
504 $etagValid = $node->getETag()==$conditionToken[2];
508 // key 1 can contain a lock token
509 if ($conditionToken[1]) {
512 // Match all the locks
513 foreach($locks as $lockIndex=>$lock) {
515 $lockToken = 'opaquelocktoken:' . $lock->token;
518 if (!$conditionToken[0] && $lockToken != $conditionToken[1]) {
520 // Condition valid, onto the next
524 if ($conditionToken[0] && $lockToken == $conditionToken[1]) {
527 // Condition valid and lock matched
528 unset($locks[$lockIndex]);
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;
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');
546 // Conditions were met, we'll also need to check if all the locks are gone
551 // There's still locks, we fail
552 $lastLock = current($locks);
560 // We got here, this means every condition was satisfied
566 * This method is created to extract information from the WebDAV HTTP 'If:' header
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
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
578 public function getIfConditions() {
580 $header = $this->server->httpRequest->getHeader('If');
581 if (!$header) return array();
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);
588 $conditions = array();
590 foreach($matches as $match) {
593 'uri' => $match['uri'],
595 array($match['not']?0:1,$match['token'],isset($match['etag'])?$match['etag']:'')
599 if (!$condition['uri'] && count($conditions)) $conditions[count($conditions)-1]['tokens'][] = array(
602 isset($match['etag'])?$match['etag']:''
605 $conditions[] = $condition;
615 * Parses a webdav lock xml body, and returns a new Sabre_DAV_Locks_LockInfo object
617 * @param string $body
618 * @return Sabre_DAV_Locks_LockInfo
620 protected function parseLockRequest($body) {
622 $xml = simplexml_load_string($body,null,LIBXML_NOWARNING);
623 $xml->registerXPathNamespace('d','DAV:');
624 $lockInfo = new Sabre_DAV_Locks_LockInfo();
626 $children = $xml->children("DAV:");
627 $lockInfo->owner = (string)$children->owner;
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;