6 * This plugin 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 plugin
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 plugin name.
66 * Using this name other plugins will be able to access other plugins
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 plugin 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 plugin 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 plugin 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 $this->server->broadcastEvent('beforeWriteContent',array($uri));
298 } catch (Sabre_DAV_Exception_NotFound $e) {
300 // It didn't, lets create it
301 $this->server->createFile($uri,fopen('php://memory','r'));
306 $this->lockNode($uri,$lockInfo);
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));
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
324 protected function httpUnlock($uri) {
326 $lockToken = $this->server->httpRequest->getHeader('Lock-Token');
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');
331 $locks = $this->getLocks($uri);
333 // Windows sometimes forgets to include < and > in the Lock-Token
335 if ($lockToken[0]!=='<') $lockToken = '<' . $lockToken . '>';
337 foreach($locks as $lock) {
339 if ('<opaquelocktoken:' . $lock->token . '>' == $lockToken) {
341 $this->unlockNode($uri,$lock);
342 $this->server->httpResponse->setHeader('Content-Length','0');
343 $this->server->httpResponse->sendStatus(204);
350 // If we got here, it means the locktoken was invalid
351 throw new Sabre_DAV_Exception_LockTokenMatchesRequestUri();
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
362 * @param Sabre_DAV_Locks_LockInfo $lockInfo
365 public function lockNode($uri,Sabre_DAV_Locks_LockInfo $lockInfo) {
367 if (!$this->server->broadcastEvent('beforeLock',array($uri,$lockInfo))) return;
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.');
377 * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified
380 * @param Sabre_DAV_Locks_LockInfo $lockInfo
383 public function unlockNode($uri,Sabre_DAV_Locks_LockInfo $lockInfo) {
385 if (!$this->server->broadcastEvent('beforeUnlock',array($uri,$lockInfo))) return;
386 if ($this->locksBackend) return $this->locksBackend->unlock($uri,$lockInfo);
392 * Returns the contents of the HTTP Timeout header.
394 * The method formats the header into an integer.
398 public function getTimeoutHeader() {
400 $header = $this->server->httpRequest->getHeader('Timeout');
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');
419 * Generates the response for successful LOCK requests
421 * @param Sabre_DAV_Locks_LockInfo $lockInfo
424 protected function generateLockResponse(Sabre_DAV_Locks_LockInfo $lockInfo) {
426 $dom = new DOMDocument('1.0','utf-8');
427 $dom->formatOutput = true;
429 $prop = $dom->createElementNS('DAV:','d:prop');
430 $dom->appendChild($prop);
432 $lockDiscovery = $dom->createElementNS('DAV:','d:lockdiscovery');
433 $prop->appendChild($lockDiscovery);
435 $lockObj = new Sabre_DAV_Property_LockDiscovery(array($lockInfo),true);
436 $lockObj->serialize($this->server,$lockDiscovery);
438 return $dom->saveXML();
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
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.
451 protected function validateLock($urls = null,&$lastLock = null, $checkChildLocks = false) {
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');
461 $conditions = $this->getIfConditions();
463 // We're going to loop through the urls and make sure all lock conditions are satisfied
464 foreach($urls as $url) {
466 $locks = $this->getLocks($url, $checkChildLocks);
468 // If there were no conditions, but there were locks, we fail
469 if (!$conditions && $locks) {
471 $lastLock = current($locks);
475 // If there were no locks or conditions, we go to the next url
476 if (!$locks && !$conditions) continue;
478 foreach($conditions as $condition) {
480 if (!$condition['uri']) {
481 $conditionUri = $this->server->getRequestUri();
483 $conditionUri = $this->server->calculateUri($condition['uri']);
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;
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) {
496 // key 2 can contain an etag
497 if ($conditionToken[2]) {
499 $uri = $conditionUri?$conditionUri:$this->server->getRequestUri();
500 $node = $this->server->tree->getNodeForPath($uri);
501 $etagValid = $node->getETag()==$conditionToken[2];
505 // key 1 can contain a lock token
506 if ($conditionToken[1]) {
509 // Match all the locks
510 foreach($locks as $lockIndex=>$lock) {
512 $lockToken = 'opaquelocktoken:' . $lock->token;
515 if (!$conditionToken[0] && $lockToken != $conditionToken[1]) {
517 // Condition valid, onto the next
521 if ($conditionToken[0] && $lockToken == $conditionToken[1]) {
524 // Condition valid and lock matched
525 unset($locks[$lockIndex]);
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;
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');
543 // Conditions were met, we'll also need to check if all the locks are gone
548 // There's still locks, we fail
549 $lastLock = current($locks);
557 // We got here, this means every condition was satisfied
563 * This method is created to extract information from the WebDAV HTTP 'If:' header
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
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
575 public function getIfConditions() {
577 $header = $this->server->httpRequest->getHeader('If');
578 if (!$header) return array();
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);
585 $conditions = array();
587 foreach($matches as $match) {
590 'uri' => $match['uri'],
592 array($match['not']?0:1,$match['token'],isset($match['etag'])?$match['etag']:'')
596 if (!$condition['uri'] && count($conditions)) $conditions[count($conditions)-1]['tokens'][] = array(
599 isset($match['etag'])?$match['etag']:''
602 $conditions[] = $condition;
612 * Parses a webdav lock xml body, and returns a new Sabre_DAV_Locks_LockInfo object
614 * @param string $body
615 * @return Sabre_DAV_Locks_LockInfo
617 protected function parseLockRequest($body) {
619 $xml = simplexml_load_string($body,null,LIBXML_NOWARNING);
620 $xml->registerXPathNamespace('d','DAV:');
621 $lockInfo = new Sabre_DAV_Locks_LockInfo();
623 $children = $xml->children("DAV:");
624 $lockInfo->owner = (string)$children->owner;
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;