addPlugin($lockPlugin); * * @package Sabre * @subpackage DAV * @copyright Copyright (C) 2007-2010 Rooftop Solutions. All rights reserved. * @author Evert Pot (http://www.rooftopsolutions.nl/) * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License */ class Sabre_DAV_Locks_Plugin extends Sabre_DAV_ServerPlugin { /** * locksBackend * * @var Sabre_DAV_Locks_Backend_Abstract */ private $locksBackend; /** * server * * @var Sabre_DAV_Server */ private $server; /** * __construct * * @param Sabre_DAV_Locks_Backend_Abstract $locksBackend * @return void */ public function __construct(Sabre_DAV_Locks_Backend_Abstract $locksBackend = null) { $this->locksBackend = $locksBackend; } /** * Initializes the plugin * * This method is automatically called by the Server class after addPlugin. * * @param Sabre_DAV_Server $server * @return void */ public function initialize(Sabre_DAV_Server $server) { $this->server = $server; $server->subscribeEvent('unknownMethod',array($this,'unknownMethod')); $server->subscribeEvent('beforeMethod',array($this,'beforeMethod'),50); $server->subscribeEvent('afterGetProperties',array($this,'afterGetProperties')); } /** * This method is called by the Server if the user used an HTTP method * the server didn't recognize. * * This plugin intercepts the LOCK and UNLOCK methods. * * @param string $method * @return bool */ public function unknownMethod($method, $uri) { switch($method) { case 'LOCK' : $this->httpLock($uri); return false; case 'UNLOCK' : $this->httpUnlock($uri); return false; } } /** * This method is called after most properties have been found * it allows us to add in any Lock-related properties * * @param string $path * @param array $properties * @return bool */ public function afterGetProperties($path,&$newProperties) { foreach($newProperties[404] as $propName=>$discard) { $node = null; switch($propName) { case '{DAV:}supportedlock' : $val = false; if ($this->locksBackend) $val = true; else { if (!$node) $node = $this->server->tree->getNodeForPath($path); if ($node instanceof Sabre_DAV_ILockable) $val = true; } $newProperties[200][$propName] = new Sabre_DAV_Property_SupportedLock($val); unset($newProperties[404][$propName]); break; case '{DAV:}lockdiscovery' : $newProperties[200][$propName] = new Sabre_DAV_Property_LockDiscovery($this->getLocks($path)); unset($newProperties[404][$propName]); break; } } return true; } /** * This method is called before the logic for any HTTP method is * handled. * * This plugin uses that feature to intercept access to locked resources. * * @param string $method * @param string $uri * @return bool */ public function beforeMethod($method, $uri) { switch($method) { case 'DELETE' : case 'MKCOL' : case 'PROPPATCH' : case 'PUT' : $lastLock = null; if (!$this->validateLock($uri,$lastLock)) throw new Sabre_DAV_Exception_Locked($lastLock); break; case 'MOVE' : $lastLock = null; if (!$this->validateLock(array( $uri, $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')), ),$lastLock)) throw new Sabre_DAV_Exception_Locked($lastLock); break; case 'COPY' : $lastLock = null; if (!$this->validateLock( $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')), $lastLock)) throw new Sabre_DAV_Exception_Locked($lastLock); break; } return true; } /** * Use this method to tell the server this plugin defines additional * HTTP methods. * * This method is passed a uri. It should only return HTTP methods that are * available for the specified uri. * * @param string $uri * @return array */ public function getHTTPMethods($uri) { if ($this->locksBackend || $this->server->tree->getNodeForPath($uri) instanceof Sabre_DAV_ILocks) { return array('LOCK','UNLOCK'); } return array(); } /** * Returns a list of features for the HTTP OPTIONS Dav: header. * * In this case this is only the number 2. The 2 in the Dav: header * indicates the server supports locks. * * @return array */ public function getFeatures() { return array(2); } /** * Returns all lock information on a particular uri * * This function should return an array with Sabre_DAV_Locks_LockInfo objects. If there are no locks on a file, return an empty array. * * Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree * * @param string $uri * @return array */ public function getLocks($uri) { $lockList = array(); $currentPath = ''; foreach(explode('/',$uri) as $uriPart) { $uriLocks = array(); if ($currentPath) $currentPath.='/'; $currentPath.=$uriPart; try { $node = $this->server->tree->getNodeForPath($currentPath); if ($node instanceof Sabre_DAV_ILockable) $uriLocks = $node->getLocks(); } catch (Sabre_DAV_Exception_FileNotFound $e){ // In case the node didn't exist, this could be a lock-null request } foreach($uriLocks as $uriLock) { // Unless we're on the leaf of the uri-tree we should ingore locks with depth 0 if($uri==$currentPath || $uriLock->depth!=0) { $uriLock->uri = $currentPath; $lockList[] = $uriLock; } } } if ($this->locksBackend) $lockList = array_merge($lockList,$this->locksBackend->getLocks($uri)); return $lockList; } /** * Locks an uri * * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type * of lock (shared or exclusive) and the owner of the lock * * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock * * Additionally, a lock can be requested for a non-existant file. In these case we're obligated to create an empty file as per RFC4918:S7.3 * * @param string $uri * @return void */ protected function httpLock($uri) { $lastLock = null; if (!$this->validateLock($uri,$lastLock)) { // If the existing lock was an exclusive lock, we need to fail if (!$lastLock || $lastLock->scope == Sabre_DAV_Locks_LockInfo::EXCLUSIVE) { //var_dump($lastLock); throw new Sabre_DAV_Exception_ConflictingLock($lastLock); } } if ($body = $this->server->httpRequest->getBody(true)) { // This is a new lock request $lockInfo = $this->parseLockRequest($body); $lockInfo->depth = $this->server->getHTTPDepth(); $lockInfo->uri = $uri; if($lastLock && $lockInfo->scope != Sabre_DAV_Locks_LockInfo::SHARED) throw new Sabre_DAV_Exception_ConflictingLock($lastLock); } elseif ($lastLock) { // This must have been a lock refresh $lockInfo = $lastLock; // The resource could have been locked through another uri. if ($uri!=$lockInfo->uri) $uri = $lockInfo->uri; } else { // There was neither a lock refresh nor a new lock request throw new Sabre_DAV_Exception_BadRequest('An xml body is required for lock requests'); } if ($timeout = $this->getTimeoutHeader()) $lockInfo->timeout = $timeout; $newFile = false; // 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 try { $node = $this->server->tree->getNodeForPath($uri); // We need to call the beforeWriteContent event for RFC3744 $this->server->broadcastEvent('beforeWriteContent',array($uri)); } catch (Sabre_DAV_Exception_FileNotFound $e) { // It didn't, lets create it $this->server->createFile($uri,fopen('php://memory','r')); $newFile = true; } $this->lockNode($uri,$lockInfo); $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8'); $this->server->httpResponse->setHeader('Lock-Token','token . '>'); $this->server->httpResponse->sendStatus($newFile?201:200); $this->server->httpResponse->sendBody($this->generateLockResponse($lockInfo)); } /** * Unlocks a uri * * 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 * The server should return 204 (No content) on success * * @param string $uri * @return void */ protected function httpUnlock($uri) { $lockToken = $this->server->httpRequest->getHeader('Lock-Token'); // If the locktoken header is not supplied, we need to throw a bad request exception if (!$lockToken) throw new Sabre_DAV_Exception_BadRequest('No lock token was supplied'); $locks = $this->getLocks($uri); // We're grabbing the node information, just to rely on the fact it will throw a 404 when the node doesn't exist //$this->server->tree->getNodeForPath($uri); foreach($locks as $lock) { if ('token . '>' == $lockToken) { $this->unlockNode($uri,$lock); $this->server->httpResponse->setHeader('Content-Length','0'); $this->server->httpResponse->sendStatus(204); return; } } // If we got here, it means the locktoken was invalid throw new Sabre_DAV_Exception_LockTokenMatchesRequestUri(); } /** * Locks a uri * * All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored * It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client * * @param string $uri * @param Sabre_DAV_Locks_LockInfo $lockInfo * @return void */ public function lockNode($uri,Sabre_DAV_Locks_LockInfo $lockInfo) { if (!$this->server->broadcastEvent('beforeLock',array($uri,$lockInfo))) return; try { $node = $this->server->tree->getNodeForPath($uri); if ($node instanceof Sabre_DAV_ILockable) return $node->lock($lockInfo); } catch (Sabre_DAV_Exception_FileNotFound $e) { // In case the node didn't exist, this could be a lock-null request } if ($this->locksBackend) return $this->locksBackend->lock($uri,$lockInfo); 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.'); } /** * Unlocks a uri * * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified * * @param string $uri * @param Sabre_DAV_Locks_LockInfo $lockInfo * @return void */ public function unlockNode($uri,Sabre_DAV_Locks_LockInfo $lockInfo) { if (!$this->server->broadcastEvent('beforeUnlock',array($uri,$lockInfo))) return; try { $node = $this->server->tree->getNodeForPath($uri); if ($node instanceof Sabre_DAV_ILockable) return $node->unlock($lockInfo); } catch (Sabre_DAV_Exception_FileNotFound $e) { // In case the node didn't exist, this could be a lock-null request } if ($this->locksBackend) return $this->locksBackend->unlock($uri,$lockInfo); } /** * Returns the contents of the HTTP Timeout header. * * The method formats the header into an integer. * * @return int */ public function getTimeoutHeader() { $header = $this->server->httpRequest->getHeader('Timeout'); if ($header) { if (stripos($header,'second-')===0) $header = (int)(substr($header,7)); else if (strtolower($header)=='infinite') $header=Sabre_DAV_Locks_LockInfo::TIMEOUT_INFINITE; else throw new Sabre_DAV_Exception_BadRequest('Invalid HTTP timeout header'); } else { $header = 0; } return $header; } /** * Generates the response for successfull LOCK requests * * @param Sabre_DAV_Locks_LockInfo $lockInfo * @return string */ protected function generateLockResponse(Sabre_DAV_Locks_LockInfo $lockInfo) { $dom = new DOMDocument('1.0','utf-8'); $dom->formatOutput = true; $prop = $dom->createElementNS('DAV:','d:prop'); $dom->appendChild($prop); $lockDiscovery = $dom->createElementNS('DAV:','d:lockdiscovery'); $prop->appendChild($lockDiscovery); $lockObj = new Sabre_DAV_Property_LockDiscovery(array($lockInfo),true); $lockObj->serialize($this->server,$lockDiscovery); return $dom->saveXML(); } /** * validateLock should be called when a write operation is about to happen * It will check if the requested url is locked, and see if the correct lock tokens are passed * * @param mixed $urls List of relevant urls. Can be an array, a string or nothing at all for the current request uri * @param mixed $lastLock This variable will be populated with the last checked lock object (Sabre_DAV_Locks_LockInfo) * @return bool */ protected function validateLock($urls = null,&$lastLock = null) { if (is_null($urls)) { $urls = array($this->server->getRequestUri()); } elseif (is_string($urls)) { $urls = array($urls); } elseif (!is_array($urls)) { throw new Sabre_DAV_Exception('The urls parameter should either be null, a string or an array'); } $conditions = $this->getIfConditions(); // We're going to loop through the urls and make sure all lock conditions are satisfied foreach($urls as $url) { $locks = $this->getLocks($url); // If there were no conditions, but there were locks, we fail if (!$conditions && $locks) { reset($locks); $lastLock = current($locks); return false; } // If there were no locks or conditions, we go to the next url if (!$locks && !$conditions) continue; foreach($conditions as $condition) { $conditionUri = $condition['uri']?$this->server->calculateUri($condition['uri']):''; // If the condition has a url, and it isn't part of the affected url at all, check the next condition if ($conditionUri && strpos($url,$conditionUri)!==0) continue; // The tokens array contians arrays with 2 elements. 0=true/false for normal/not condition, 1=locktoken // At least 1 condition has to be satisfied foreach($condition['tokens'] as $conditionToken) { $etagValid = true; $lockValid = true; // key 2 can contain an etag if ($conditionToken[2]) { $uri = $conditionUri?$conditionUri:$this->server->getRequestUri(); $node = $this->server->tree->getNodeForPath($uri); $etagValid = $node->getETag()==$conditionToken[2]; } // key 1 can contain a lock token if ($conditionToken[1]) { $lockValid = false; // Match all the locks foreach($locks as $lockIndex=>$lock) { $lockToken = 'opaquelocktoken:' . $lock->token; // Checking NOT if (!$conditionToken[0] && $lockToken != $conditionToken[1]) { // Condition valid, onto the next $lockValid = true; break; } if ($conditionToken[0] && $lockToken == $conditionToken[1]) { $lastLock = $lock; // Condition valid and lock matched unset($locks[$lockIndex]); $lockValid = true; break; } } } // If, after checking both etags and locks they are stil valid, // we can continue with the next condition. if ($etagValid && $lockValid) continue 2; } // No conditions matched, so we fail throw new Sabre_DAV_Exception_PreconditionFailed('The tokens provided in the if header did not match','If'); } // Conditions were met, we'll also need to check if all the locks are gone if (count($locks)) { reset($locks); // There's still locks, we fail $lastLock = current($locks); return false; } } // We got here, this means every condition was satisfied return true; } /** * This method is created to extract information from the WebDAV HTTP 'If:' header * * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information * The function will return an array, containg structs with the following keys * * * uri - the uri the condition applies to. This can be an empty string for 'every relevant url' * * tokens - The lock token. another 2 dimensional array containg 2 elements (0 = true/false.. If this is a negative condition its set to false, 1 = the actual token) * * etag - an etag, if supplied * * @return void */ public function getIfConditions() { $header = $this->server->httpRequest->getHeader('If'); if (!$header) return array(); $matches = array(); $regex = '/(?:\<(?P.*?)\>\s)?\((?PNot\s)?(?:\<(?P[^\>]*)\>)?(?:\s?)(?:\[(?P[^\]]*)\])?\)/im'; preg_match_all($regex,$header,$matches,PREG_SET_ORDER); $conditions = array(); foreach($matches as $match) { $condition = array( 'uri' => $match['uri'], 'tokens' => array( array($match['not']?0:1,$match['token'],isset($match['etag'])?$match['etag']:'') ), ); if (!$condition['uri'] && count($conditions)) $conditions[count($conditions)-1]['tokens'][] = array( $match['not']?0:1, $match['token'], isset($match['etag'])?$match['etag']:'' ); else { $conditions[] = $condition; } } return $conditions; } /** * Parses a webdav lock xml body, and returns a new Sabre_DAV_Locks_LockInfo object * * @param string $body * @return Sabre_DAV_Locks_LockInfo */ protected function parseLockRequest($body) { $xml = simplexml_load_string($body,null,LIBXML_NOWARNING); $xml->registerXPathNamespace('d','DAV:'); $lockInfo = new Sabre_DAV_Locks_LockInfo(); $lockInfo->owner = (string)$xml->owner; $lockToken = '44445502'; $id = md5(microtime() . 'somethingrandom'); $lockToken.='-' . substr($id,0,4) . '-' . substr($id,4,4) . '-' . substr($id,8,4) . '-' . substr($id,12,12); $lockInfo->token = $lockToken; $lockInfo->scope = count($xml->xpath('d:lockscope/d:exclusive'))>0?Sabre_DAV_Locks_LockInfo::EXCLUSIVE:Sabre_DAV_Locks_LockInfo::SHARED; return $lockInfo; } }