1*a1a3b679SAndreas Boehler<?php 2*a1a3b679SAndreas Boehler 3*a1a3b679SAndreas Boehlernamespace Sabre\DAV\Locks; 4*a1a3b679SAndreas Boehler 5*a1a3b679SAndreas Boehleruse Sabre\DAV; 6*a1a3b679SAndreas Boehleruse Sabre\HTTP\RequestInterface; 7*a1a3b679SAndreas Boehleruse Sabre\HTTP\ResponseInterface; 8*a1a3b679SAndreas Boehler 9*a1a3b679SAndreas Boehler/** 10*a1a3b679SAndreas Boehler * Locking plugin 11*a1a3b679SAndreas Boehler * 12*a1a3b679SAndreas Boehler * This plugin provides locking support to a WebDAV server. 13*a1a3b679SAndreas Boehler * The easiest way to get started, is by hooking it up as such: 14*a1a3b679SAndreas Boehler * 15*a1a3b679SAndreas Boehler * $lockBackend = new Sabre\DAV\Locks\Backend\File('./mylockdb'); 16*a1a3b679SAndreas Boehler * $lockPlugin = new Sabre\DAV\Locks\Plugin($lockBackend); 17*a1a3b679SAndreas Boehler * $server->addPlugin($lockPlugin); 18*a1a3b679SAndreas Boehler * 19*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). 20*a1a3b679SAndreas Boehler * @author Evert Pot (http://evertpot.com/) 21*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License 22*a1a3b679SAndreas Boehler */ 23*a1a3b679SAndreas Boehlerclass Plugin extends DAV\ServerPlugin { 24*a1a3b679SAndreas Boehler 25*a1a3b679SAndreas Boehler /** 26*a1a3b679SAndreas Boehler * locksBackend 27*a1a3b679SAndreas Boehler * 28*a1a3b679SAndreas Boehler * @var Backend\Backend\Interface 29*a1a3b679SAndreas Boehler */ 30*a1a3b679SAndreas Boehler protected $locksBackend; 31*a1a3b679SAndreas Boehler 32*a1a3b679SAndreas Boehler /** 33*a1a3b679SAndreas Boehler * server 34*a1a3b679SAndreas Boehler * 35*a1a3b679SAndreas Boehler * @var Sabre\DAV\Server 36*a1a3b679SAndreas Boehler */ 37*a1a3b679SAndreas Boehler protected $server; 38*a1a3b679SAndreas Boehler 39*a1a3b679SAndreas Boehler /** 40*a1a3b679SAndreas Boehler * __construct 41*a1a3b679SAndreas Boehler * 42*a1a3b679SAndreas Boehler * @param Backend\BackendInterface $locksBackend 43*a1a3b679SAndreas Boehler */ 44*a1a3b679SAndreas Boehler function __construct(Backend\BackendInterface $locksBackend) { 45*a1a3b679SAndreas Boehler 46*a1a3b679SAndreas Boehler $this->locksBackend = $locksBackend; 47*a1a3b679SAndreas Boehler 48*a1a3b679SAndreas Boehler } 49*a1a3b679SAndreas Boehler 50*a1a3b679SAndreas Boehler /** 51*a1a3b679SAndreas Boehler * Initializes the plugin 52*a1a3b679SAndreas Boehler * 53*a1a3b679SAndreas Boehler * This method is automatically called by the Server class after addPlugin. 54*a1a3b679SAndreas Boehler * 55*a1a3b679SAndreas Boehler * @param DAV\Server $server 56*a1a3b679SAndreas Boehler * @return void 57*a1a3b679SAndreas Boehler */ 58*a1a3b679SAndreas Boehler function initialize(DAV\Server $server) { 59*a1a3b679SAndreas Boehler 60*a1a3b679SAndreas Boehler $this->server = $server; 61*a1a3b679SAndreas Boehler 62*a1a3b679SAndreas Boehler $this->server->xml->elementMap['{DAV:}lockinfo'] = 'Sabre\\DAV\\Xml\\Request\\Lock'; 63*a1a3b679SAndreas Boehler 64*a1a3b679SAndreas Boehler $server->on('method:LOCK', [$this, 'httpLock']); 65*a1a3b679SAndreas Boehler $server->on('method:UNLOCK', [$this, 'httpUnlock']); 66*a1a3b679SAndreas Boehler $server->on('validateTokens', [$this, 'validateTokens']); 67*a1a3b679SAndreas Boehler $server->on('propFind', [$this, 'propFind']); 68*a1a3b679SAndreas Boehler $server->on('afterUnbind', [$this, 'afterUnbind']); 69*a1a3b679SAndreas Boehler 70*a1a3b679SAndreas Boehler } 71*a1a3b679SAndreas Boehler 72*a1a3b679SAndreas Boehler /** 73*a1a3b679SAndreas Boehler * Returns a plugin name. 74*a1a3b679SAndreas Boehler * 75*a1a3b679SAndreas Boehler * Using this name other plugins will be able to access other plugins 76*a1a3b679SAndreas Boehler * using Sabre\DAV\Server::getPlugin 77*a1a3b679SAndreas Boehler * 78*a1a3b679SAndreas Boehler * @return string 79*a1a3b679SAndreas Boehler */ 80*a1a3b679SAndreas Boehler function getPluginName() { 81*a1a3b679SAndreas Boehler 82*a1a3b679SAndreas Boehler return 'locks'; 83*a1a3b679SAndreas Boehler 84*a1a3b679SAndreas Boehler } 85*a1a3b679SAndreas Boehler 86*a1a3b679SAndreas Boehler /** 87*a1a3b679SAndreas Boehler * This method is called after most properties have been found 88*a1a3b679SAndreas Boehler * it allows us to add in any Lock-related properties 89*a1a3b679SAndreas Boehler * 90*a1a3b679SAndreas Boehler * @param DAV\PropFind $propFind 91*a1a3b679SAndreas Boehler * @param DAV\INode $node 92*a1a3b679SAndreas Boehler * @return void 93*a1a3b679SAndreas Boehler */ 94*a1a3b679SAndreas Boehler function propFind(DAV\PropFind $propFind, DAV\INode $node) { 95*a1a3b679SAndreas Boehler 96*a1a3b679SAndreas Boehler $propFind->handle('{DAV:}supportedlock', function() { 97*a1a3b679SAndreas Boehler return new DAV\Xml\Property\SupportedLock(); 98*a1a3b679SAndreas Boehler }); 99*a1a3b679SAndreas Boehler $propFind->handle('{DAV:}lockdiscovery', function() use ($propFind) { 100*a1a3b679SAndreas Boehler return new DAV\Xml\Property\LockDiscovery( 101*a1a3b679SAndreas Boehler $this->getLocks($propFind->getPath()) 102*a1a3b679SAndreas Boehler ); 103*a1a3b679SAndreas Boehler }); 104*a1a3b679SAndreas Boehler 105*a1a3b679SAndreas Boehler } 106*a1a3b679SAndreas Boehler 107*a1a3b679SAndreas Boehler /** 108*a1a3b679SAndreas Boehler * Use this method to tell the server this plugin defines additional 109*a1a3b679SAndreas Boehler * HTTP methods. 110*a1a3b679SAndreas Boehler * 111*a1a3b679SAndreas Boehler * This method is passed a uri. It should only return HTTP methods that are 112*a1a3b679SAndreas Boehler * available for the specified uri. 113*a1a3b679SAndreas Boehler * 114*a1a3b679SAndreas Boehler * @param string $uri 115*a1a3b679SAndreas Boehler * @return array 116*a1a3b679SAndreas Boehler */ 117*a1a3b679SAndreas Boehler function getHTTPMethods($uri) { 118*a1a3b679SAndreas Boehler 119*a1a3b679SAndreas Boehler return ['LOCK','UNLOCK']; 120*a1a3b679SAndreas Boehler 121*a1a3b679SAndreas Boehler } 122*a1a3b679SAndreas Boehler 123*a1a3b679SAndreas Boehler /** 124*a1a3b679SAndreas Boehler * Returns a list of features for the HTTP OPTIONS Dav: header. 125*a1a3b679SAndreas Boehler * 126*a1a3b679SAndreas Boehler * In this case this is only the number 2. The 2 in the Dav: header 127*a1a3b679SAndreas Boehler * indicates the server supports locks. 128*a1a3b679SAndreas Boehler * 129*a1a3b679SAndreas Boehler * @return array 130*a1a3b679SAndreas Boehler */ 131*a1a3b679SAndreas Boehler function getFeatures() { 132*a1a3b679SAndreas Boehler 133*a1a3b679SAndreas Boehler return [2]; 134*a1a3b679SAndreas Boehler 135*a1a3b679SAndreas Boehler } 136*a1a3b679SAndreas Boehler 137*a1a3b679SAndreas Boehler /** 138*a1a3b679SAndreas Boehler * Returns all lock information on a particular uri 139*a1a3b679SAndreas Boehler * 140*a1a3b679SAndreas Boehler * This function should return an array with Sabre\DAV\Locks\LockInfo objects. If there are no locks on a file, return an empty array. 141*a1a3b679SAndreas Boehler * 142*a1a3b679SAndreas Boehler * Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree 143*a1a3b679SAndreas Boehler * If the $returnChildLocks argument is set to true, we'll also traverse all the children of the object 144*a1a3b679SAndreas Boehler * for any possible locks and return those as well. 145*a1a3b679SAndreas Boehler * 146*a1a3b679SAndreas Boehler * @param string $uri 147*a1a3b679SAndreas Boehler * @param bool $returnChildLocks 148*a1a3b679SAndreas Boehler * @return array 149*a1a3b679SAndreas Boehler */ 150*a1a3b679SAndreas Boehler function getLocks($uri, $returnChildLocks = false) { 151*a1a3b679SAndreas Boehler 152*a1a3b679SAndreas Boehler return $this->locksBackend->getLocks($uri, $returnChildLocks); 153*a1a3b679SAndreas Boehler 154*a1a3b679SAndreas Boehler } 155*a1a3b679SAndreas Boehler 156*a1a3b679SAndreas Boehler /** 157*a1a3b679SAndreas Boehler * Locks an uri 158*a1a3b679SAndreas Boehler * 159*a1a3b679SAndreas Boehler * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock 160*a1a3b679SAndreas Boehler * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type 161*a1a3b679SAndreas Boehler * of lock (shared or exclusive) and the owner of the lock 162*a1a3b679SAndreas Boehler * 163*a1a3b679SAndreas Boehler * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock 164*a1a3b679SAndreas Boehler * 165*a1a3b679SAndreas Boehler * 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 166*a1a3b679SAndreas Boehler * 167*a1a3b679SAndreas Boehler * @param RequestInterface $request 168*a1a3b679SAndreas Boehler * @param ResponseInterface $response 169*a1a3b679SAndreas Boehler * @return bool 170*a1a3b679SAndreas Boehler */ 171*a1a3b679SAndreas Boehler function httpLock(RequestInterface $request, ResponseInterface $response) { 172*a1a3b679SAndreas Boehler 173*a1a3b679SAndreas Boehler $uri = $request->getPath(); 174*a1a3b679SAndreas Boehler 175*a1a3b679SAndreas Boehler $existingLocks = $this->getLocks($uri); 176*a1a3b679SAndreas Boehler 177*a1a3b679SAndreas Boehler if ($body = $request->getBodyAsString()) { 178*a1a3b679SAndreas Boehler // This is a new lock request 179*a1a3b679SAndreas Boehler 180*a1a3b679SAndreas Boehler $existingLock = null; 181*a1a3b679SAndreas Boehler // Checking if there's already non-shared locks on the uri. 182*a1a3b679SAndreas Boehler foreach ($existingLocks as $existingLock) { 183*a1a3b679SAndreas Boehler if ($existingLock->scope === LockInfo::EXCLUSIVE) { 184*a1a3b679SAndreas Boehler throw new DAV\Exception\ConflictingLock($existingLock); 185*a1a3b679SAndreas Boehler } 186*a1a3b679SAndreas Boehler } 187*a1a3b679SAndreas Boehler 188*a1a3b679SAndreas Boehler $lockInfo = $this->parseLockRequest($body); 189*a1a3b679SAndreas Boehler $lockInfo->depth = $this->server->getHTTPDepth(); 190*a1a3b679SAndreas Boehler $lockInfo->uri = $uri; 191*a1a3b679SAndreas Boehler if ($existingLock && $lockInfo->scope != LockInfo::SHARED) 192*a1a3b679SAndreas Boehler throw new DAV\Exception\ConflictingLock($existingLock); 193*a1a3b679SAndreas Boehler 194*a1a3b679SAndreas Boehler } else { 195*a1a3b679SAndreas Boehler 196*a1a3b679SAndreas Boehler // Gonna check if this was a lock refresh. 197*a1a3b679SAndreas Boehler $existingLocks = $this->getLocks($uri); 198*a1a3b679SAndreas Boehler $conditions = $this->server->getIfConditions($request); 199*a1a3b679SAndreas Boehler $found = null; 200*a1a3b679SAndreas Boehler 201*a1a3b679SAndreas Boehler foreach ($existingLocks as $existingLock) { 202*a1a3b679SAndreas Boehler foreach ($conditions as $condition) { 203*a1a3b679SAndreas Boehler foreach ($condition['tokens'] as $token) { 204*a1a3b679SAndreas Boehler if ($token['token'] === 'opaquelocktoken:' . $existingLock->token) { 205*a1a3b679SAndreas Boehler $found = $existingLock; 206*a1a3b679SAndreas Boehler break 3; 207*a1a3b679SAndreas Boehler } 208*a1a3b679SAndreas Boehler } 209*a1a3b679SAndreas Boehler } 210*a1a3b679SAndreas Boehler } 211*a1a3b679SAndreas Boehler 212*a1a3b679SAndreas Boehler // If none were found, this request is in error. 213*a1a3b679SAndreas Boehler if (is_null($found)) { 214*a1a3b679SAndreas Boehler if ($existingLocks) { 215*a1a3b679SAndreas Boehler throw new DAV\Exception\Locked(reset($existingLocks)); 216*a1a3b679SAndreas Boehler } else { 217*a1a3b679SAndreas Boehler throw new DAV\Exception\BadRequest('An xml body is required for lock requests'); 218*a1a3b679SAndreas Boehler } 219*a1a3b679SAndreas Boehler 220*a1a3b679SAndreas Boehler } 221*a1a3b679SAndreas Boehler 222*a1a3b679SAndreas Boehler // This must have been a lock refresh 223*a1a3b679SAndreas Boehler $lockInfo = $found; 224*a1a3b679SAndreas Boehler 225*a1a3b679SAndreas Boehler // The resource could have been locked through another uri. 226*a1a3b679SAndreas Boehler if ($uri != $lockInfo->uri) $uri = $lockInfo->uri; 227*a1a3b679SAndreas Boehler 228*a1a3b679SAndreas Boehler } 229*a1a3b679SAndreas Boehler 230*a1a3b679SAndreas Boehler if ($timeout = $this->getTimeoutHeader()) $lockInfo->timeout = $timeout; 231*a1a3b679SAndreas Boehler 232*a1a3b679SAndreas Boehler $newFile = false; 233*a1a3b679SAndreas Boehler 234*a1a3b679SAndreas Boehler // 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 235*a1a3b679SAndreas Boehler try { 236*a1a3b679SAndreas Boehler $this->server->tree->getNodeForPath($uri); 237*a1a3b679SAndreas Boehler 238*a1a3b679SAndreas Boehler // We need to call the beforeWriteContent event for RFC3744 239*a1a3b679SAndreas Boehler // Edit: looks like this is not used, and causing problems now. 240*a1a3b679SAndreas Boehler // 241*a1a3b679SAndreas Boehler // See Issue 222 242*a1a3b679SAndreas Boehler // $this->server->emit('beforeWriteContent',array($uri)); 243*a1a3b679SAndreas Boehler 244*a1a3b679SAndreas Boehler } catch (DAV\Exception\NotFound $e) { 245*a1a3b679SAndreas Boehler 246*a1a3b679SAndreas Boehler // It didn't, lets create it 247*a1a3b679SAndreas Boehler $this->server->createFile($uri, fopen('php://memory', 'r')); 248*a1a3b679SAndreas Boehler $newFile = true; 249*a1a3b679SAndreas Boehler 250*a1a3b679SAndreas Boehler } 251*a1a3b679SAndreas Boehler 252*a1a3b679SAndreas Boehler $this->lockNode($uri, $lockInfo); 253*a1a3b679SAndreas Boehler 254*a1a3b679SAndreas Boehler $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); 255*a1a3b679SAndreas Boehler $response->setHeader('Lock-Token', '<opaquelocktoken:' . $lockInfo->token . '>'); 256*a1a3b679SAndreas Boehler $response->setStatus($newFile ? 201 : 200); 257*a1a3b679SAndreas Boehler $response->setBody($this->generateLockResponse($lockInfo)); 258*a1a3b679SAndreas Boehler 259*a1a3b679SAndreas Boehler // Returning false will interupt the event chain and mark this method 260*a1a3b679SAndreas Boehler // as 'handled'. 261*a1a3b679SAndreas Boehler return false; 262*a1a3b679SAndreas Boehler 263*a1a3b679SAndreas Boehler } 264*a1a3b679SAndreas Boehler 265*a1a3b679SAndreas Boehler /** 266*a1a3b679SAndreas Boehler * Unlocks a uri 267*a1a3b679SAndreas Boehler * 268*a1a3b679SAndreas Boehler * 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 269*a1a3b679SAndreas Boehler * The server should return 204 (No content) on success 270*a1a3b679SAndreas Boehler * 271*a1a3b679SAndreas Boehler * @param RequestInterface $request 272*a1a3b679SAndreas Boehler * @param ResponseInterface $response 273*a1a3b679SAndreas Boehler * @return void 274*a1a3b679SAndreas Boehler */ 275*a1a3b679SAndreas Boehler function httpUnlock(RequestInterface $request, ResponseInterface $response) { 276*a1a3b679SAndreas Boehler 277*a1a3b679SAndreas Boehler $lockToken = $request->getHeader('Lock-Token'); 278*a1a3b679SAndreas Boehler 279*a1a3b679SAndreas Boehler // If the locktoken header is not supplied, we need to throw a bad request exception 280*a1a3b679SAndreas Boehler if (!$lockToken) throw new DAV\Exception\BadRequest('No lock token was supplied'); 281*a1a3b679SAndreas Boehler 282*a1a3b679SAndreas Boehler $path = $request->getPath(); 283*a1a3b679SAndreas Boehler $locks = $this->getLocks($path); 284*a1a3b679SAndreas Boehler 285*a1a3b679SAndreas Boehler // Windows sometimes forgets to include < and > in the Lock-Token 286*a1a3b679SAndreas Boehler // header 287*a1a3b679SAndreas Boehler if ($lockToken[0] !== '<') $lockToken = '<' . $lockToken . '>'; 288*a1a3b679SAndreas Boehler 289*a1a3b679SAndreas Boehler foreach ($locks as $lock) { 290*a1a3b679SAndreas Boehler 291*a1a3b679SAndreas Boehler if ('<opaquelocktoken:' . $lock->token . '>' == $lockToken) { 292*a1a3b679SAndreas Boehler 293*a1a3b679SAndreas Boehler $this->unlockNode($path, $lock); 294*a1a3b679SAndreas Boehler $response->setHeader('Content-Length', '0'); 295*a1a3b679SAndreas Boehler $response->setStatus(204); 296*a1a3b679SAndreas Boehler 297*a1a3b679SAndreas Boehler // Returning false will break the method chain, and mark the 298*a1a3b679SAndreas Boehler // method as 'handled'. 299*a1a3b679SAndreas Boehler return false; 300*a1a3b679SAndreas Boehler 301*a1a3b679SAndreas Boehler } 302*a1a3b679SAndreas Boehler 303*a1a3b679SAndreas Boehler } 304*a1a3b679SAndreas Boehler 305*a1a3b679SAndreas Boehler // If we got here, it means the locktoken was invalid 306*a1a3b679SAndreas Boehler throw new DAV\Exception\LockTokenMatchesRequestUri(); 307*a1a3b679SAndreas Boehler 308*a1a3b679SAndreas Boehler } 309*a1a3b679SAndreas Boehler 310*a1a3b679SAndreas Boehler /** 311*a1a3b679SAndreas Boehler * This method is called after a node is deleted. 312*a1a3b679SAndreas Boehler * 313*a1a3b679SAndreas Boehler * We use this event to clean up any locks that still exist on the node. 314*a1a3b679SAndreas Boehler * 315*a1a3b679SAndreas Boehler * @param string $path 316*a1a3b679SAndreas Boehler * @return void 317*a1a3b679SAndreas Boehler */ 318*a1a3b679SAndreas Boehler function afterUnbind($path) { 319*a1a3b679SAndreas Boehler 320*a1a3b679SAndreas Boehler $locks = $this->getLocks($path, $includeChildren = true); 321*a1a3b679SAndreas Boehler foreach ($locks as $lock) { 322*a1a3b679SAndreas Boehler $this->unlockNode($path, $lock); 323*a1a3b679SAndreas Boehler } 324*a1a3b679SAndreas Boehler 325*a1a3b679SAndreas Boehler } 326*a1a3b679SAndreas Boehler 327*a1a3b679SAndreas Boehler /** 328*a1a3b679SAndreas Boehler * Locks a uri 329*a1a3b679SAndreas Boehler * 330*a1a3b679SAndreas Boehler * All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored 331*a1a3b679SAndreas Boehler * It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client 332*a1a3b679SAndreas Boehler * 333*a1a3b679SAndreas Boehler * @param string $uri 334*a1a3b679SAndreas Boehler * @param LockInfo $lockInfo 335*a1a3b679SAndreas Boehler * @return bool 336*a1a3b679SAndreas Boehler */ 337*a1a3b679SAndreas Boehler function lockNode($uri, LockInfo $lockInfo) { 338*a1a3b679SAndreas Boehler 339*a1a3b679SAndreas Boehler if (!$this->server->emit('beforeLock', [$uri, $lockInfo])) return; 340*a1a3b679SAndreas Boehler return $this->locksBackend->lock($uri, $lockInfo); 341*a1a3b679SAndreas Boehler 342*a1a3b679SAndreas Boehler } 343*a1a3b679SAndreas Boehler 344*a1a3b679SAndreas Boehler /** 345*a1a3b679SAndreas Boehler * Unlocks a uri 346*a1a3b679SAndreas Boehler * 347*a1a3b679SAndreas Boehler * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified 348*a1a3b679SAndreas Boehler * 349*a1a3b679SAndreas Boehler * @param string $uri 350*a1a3b679SAndreas Boehler * @param LockInfo $lockInfo 351*a1a3b679SAndreas Boehler * @return bool 352*a1a3b679SAndreas Boehler */ 353*a1a3b679SAndreas Boehler function unlockNode($uri, LockInfo $lockInfo) { 354*a1a3b679SAndreas Boehler 355*a1a3b679SAndreas Boehler if (!$this->server->emit('beforeUnlock', [$uri, $lockInfo])) return; 356*a1a3b679SAndreas Boehler return $this->locksBackend->unlock($uri, $lockInfo); 357*a1a3b679SAndreas Boehler 358*a1a3b679SAndreas Boehler } 359*a1a3b679SAndreas Boehler 360*a1a3b679SAndreas Boehler 361*a1a3b679SAndreas Boehler /** 362*a1a3b679SAndreas Boehler * Returns the contents of the HTTP Timeout header. 363*a1a3b679SAndreas Boehler * 364*a1a3b679SAndreas Boehler * The method formats the header into an integer. 365*a1a3b679SAndreas Boehler * 366*a1a3b679SAndreas Boehler * @return int 367*a1a3b679SAndreas Boehler */ 368*a1a3b679SAndreas Boehler function getTimeoutHeader() { 369*a1a3b679SAndreas Boehler 370*a1a3b679SAndreas Boehler $header = $this->server->httpRequest->getHeader('Timeout'); 371*a1a3b679SAndreas Boehler 372*a1a3b679SAndreas Boehler if ($header) { 373*a1a3b679SAndreas Boehler 374*a1a3b679SAndreas Boehler if (stripos($header, 'second-') === 0) $header = (int)(substr($header, 7)); 375*a1a3b679SAndreas Boehler elseif (stripos($header, 'infinite') === 0) $header = LockInfo::TIMEOUT_INFINITE; 376*a1a3b679SAndreas Boehler else throw new DAV\Exception\BadRequest('Invalid HTTP timeout header'); 377*a1a3b679SAndreas Boehler 378*a1a3b679SAndreas Boehler } else { 379*a1a3b679SAndreas Boehler 380*a1a3b679SAndreas Boehler $header = 0; 381*a1a3b679SAndreas Boehler 382*a1a3b679SAndreas Boehler } 383*a1a3b679SAndreas Boehler 384*a1a3b679SAndreas Boehler return $header; 385*a1a3b679SAndreas Boehler 386*a1a3b679SAndreas Boehler } 387*a1a3b679SAndreas Boehler 388*a1a3b679SAndreas Boehler /** 389*a1a3b679SAndreas Boehler * Generates the response for successful LOCK requests 390*a1a3b679SAndreas Boehler * 391*a1a3b679SAndreas Boehler * @param LockInfo $lockInfo 392*a1a3b679SAndreas Boehler * @return string 393*a1a3b679SAndreas Boehler */ 394*a1a3b679SAndreas Boehler protected function generateLockResponse(LockInfo $lockInfo) { 395*a1a3b679SAndreas Boehler 396*a1a3b679SAndreas Boehler return $this->server->xml->write('{DAV:}prop', [ 397*a1a3b679SAndreas Boehler '{DAV:}lockdiscovery' => 398*a1a3b679SAndreas Boehler new DAV\Xml\Property\LockDiscovery([$lockInfo]) 399*a1a3b679SAndreas Boehler ]); 400*a1a3b679SAndreas Boehler } 401*a1a3b679SAndreas Boehler 402*a1a3b679SAndreas Boehler /** 403*a1a3b679SAndreas Boehler * The validateTokens event is triggered before every request. 404*a1a3b679SAndreas Boehler * 405*a1a3b679SAndreas Boehler * It's a moment where this plugin can check all the supplied lock tokens 406*a1a3b679SAndreas Boehler * in the If: header, and check if they are valid. 407*a1a3b679SAndreas Boehler * 408*a1a3b679SAndreas Boehler * In addition, it will also ensure that it checks any missing lokens that 409*a1a3b679SAndreas Boehler * must be present in the request, and reject requests without the proper 410*a1a3b679SAndreas Boehler * tokens. 411*a1a3b679SAndreas Boehler * 412*a1a3b679SAndreas Boehler * @param RequestInterface $request 413*a1a3b679SAndreas Boehler * @param mixed $conditions 414*a1a3b679SAndreas Boehler * @return void 415*a1a3b679SAndreas Boehler */ 416*a1a3b679SAndreas Boehler function validateTokens(RequestInterface $request, &$conditions) { 417*a1a3b679SAndreas Boehler 418*a1a3b679SAndreas Boehler // First we need to gather a list of locks that must be satisfied. 419*a1a3b679SAndreas Boehler $mustLocks = []; 420*a1a3b679SAndreas Boehler $method = $request->getMethod(); 421*a1a3b679SAndreas Boehler 422*a1a3b679SAndreas Boehler // Methods not in that list are operations that doesn't alter any 423*a1a3b679SAndreas Boehler // resources, and we don't need to check the lock-states for. 424*a1a3b679SAndreas Boehler switch ($method) { 425*a1a3b679SAndreas Boehler 426*a1a3b679SAndreas Boehler case 'DELETE' : 427*a1a3b679SAndreas Boehler $mustLocks = array_merge($mustLocks, $this->getLocks( 428*a1a3b679SAndreas Boehler $request->getPath(), 429*a1a3b679SAndreas Boehler true 430*a1a3b679SAndreas Boehler )); 431*a1a3b679SAndreas Boehler break; 432*a1a3b679SAndreas Boehler case 'MKCOL' : 433*a1a3b679SAndreas Boehler case 'MKCALENDAR' : 434*a1a3b679SAndreas Boehler case 'PROPPATCH' : 435*a1a3b679SAndreas Boehler case 'PUT' : 436*a1a3b679SAndreas Boehler case 'PATCH' : 437*a1a3b679SAndreas Boehler $mustLocks = array_merge($mustLocks, $this->getLocks( 438*a1a3b679SAndreas Boehler $request->getPath(), 439*a1a3b679SAndreas Boehler false 440*a1a3b679SAndreas Boehler )); 441*a1a3b679SAndreas Boehler break; 442*a1a3b679SAndreas Boehler case 'MOVE' : 443*a1a3b679SAndreas Boehler $mustLocks = array_merge($mustLocks, $this->getLocks( 444*a1a3b679SAndreas Boehler $request->getPath(), 445*a1a3b679SAndreas Boehler true 446*a1a3b679SAndreas Boehler )); 447*a1a3b679SAndreas Boehler $mustLocks = array_merge($mustLocks, $this->getLocks( 448*a1a3b679SAndreas Boehler $this->server->calculateUri($request->getHeader('Destination')), 449*a1a3b679SAndreas Boehler false 450*a1a3b679SAndreas Boehler )); 451*a1a3b679SAndreas Boehler break; 452*a1a3b679SAndreas Boehler case 'COPY' : 453*a1a3b679SAndreas Boehler $mustLocks = array_merge($mustLocks, $this->getLocks( 454*a1a3b679SAndreas Boehler $this->server->calculateUri($request->getHeader('Destination')), 455*a1a3b679SAndreas Boehler false 456*a1a3b679SAndreas Boehler )); 457*a1a3b679SAndreas Boehler break; 458*a1a3b679SAndreas Boehler case 'LOCK' : 459*a1a3b679SAndreas Boehler //Temporary measure.. figure out later why this is needed 460*a1a3b679SAndreas Boehler // Here we basically ignore all incoming tokens... 461*a1a3b679SAndreas Boehler foreach ($conditions as $ii => $condition) { 462*a1a3b679SAndreas Boehler foreach ($condition['tokens'] as $jj => $token) { 463*a1a3b679SAndreas Boehler $conditions[$ii]['tokens'][$jj]['validToken'] = true; 464*a1a3b679SAndreas Boehler } 465*a1a3b679SAndreas Boehler } 466*a1a3b679SAndreas Boehler return; 467*a1a3b679SAndreas Boehler 468*a1a3b679SAndreas Boehler } 469*a1a3b679SAndreas Boehler 470*a1a3b679SAndreas Boehler // It's possible that there's identical locks, because of shared 471*a1a3b679SAndreas Boehler // parents. We're removing the duplicates here. 472*a1a3b679SAndreas Boehler $tmp = []; 473*a1a3b679SAndreas Boehler foreach ($mustLocks as $lock) $tmp[$lock->token] = $lock; 474*a1a3b679SAndreas Boehler $mustLocks = array_values($tmp); 475*a1a3b679SAndreas Boehler 476*a1a3b679SAndreas Boehler foreach ($conditions as $kk => $condition) { 477*a1a3b679SAndreas Boehler 478*a1a3b679SAndreas Boehler foreach ($condition['tokens'] as $ii => $token) { 479*a1a3b679SAndreas Boehler 480*a1a3b679SAndreas Boehler // Lock tokens always start with opaquelocktoken: 481*a1a3b679SAndreas Boehler if (substr($token['token'], 0, 16) !== 'opaquelocktoken:') { 482*a1a3b679SAndreas Boehler continue; 483*a1a3b679SAndreas Boehler } 484*a1a3b679SAndreas Boehler 485*a1a3b679SAndreas Boehler $checkToken = substr($token['token'], 16); 486*a1a3b679SAndreas Boehler // Looping through our list with locks. 487*a1a3b679SAndreas Boehler foreach ($mustLocks as $jj => $mustLock) { 488*a1a3b679SAndreas Boehler 489*a1a3b679SAndreas Boehler if ($mustLock->token == $checkToken) { 490*a1a3b679SAndreas Boehler 491*a1a3b679SAndreas Boehler // We have a match! 492*a1a3b679SAndreas Boehler // Removing this one from mustlocks 493*a1a3b679SAndreas Boehler unset($mustLocks[$jj]); 494*a1a3b679SAndreas Boehler 495*a1a3b679SAndreas Boehler // Marking the condition as valid. 496*a1a3b679SAndreas Boehler $conditions[$kk]['tokens'][$ii]['validToken'] = true; 497*a1a3b679SAndreas Boehler 498*a1a3b679SAndreas Boehler // Advancing to the next token 499*a1a3b679SAndreas Boehler continue 2; 500*a1a3b679SAndreas Boehler 501*a1a3b679SAndreas Boehler } 502*a1a3b679SAndreas Boehler 503*a1a3b679SAndreas Boehler } 504*a1a3b679SAndreas Boehler 505*a1a3b679SAndreas Boehler // If we got here, it means that there was a 506*a1a3b679SAndreas Boehler // lock-token, but it was not in 'mustLocks'. 507*a1a3b679SAndreas Boehler // 508*a1a3b679SAndreas Boehler // This is an edge-case, as it could mean that token 509*a1a3b679SAndreas Boehler // was specified with a url that was not 'required' to 510*a1a3b679SAndreas Boehler // check. So we're doing one extra lookup to make sure 511*a1a3b679SAndreas Boehler // we really don't know this token. 512*a1a3b679SAndreas Boehler // 513*a1a3b679SAndreas Boehler // This also gets triggered when the user specified a 514*a1a3b679SAndreas Boehler // lock-token that was expired. 515*a1a3b679SAndreas Boehler $oddLocks = $this->getLocks($condition['uri']); 516*a1a3b679SAndreas Boehler foreach ($oddLocks as $oddLock) { 517*a1a3b679SAndreas Boehler 518*a1a3b679SAndreas Boehler if ($oddLock->token === $checkToken) { 519*a1a3b679SAndreas Boehler 520*a1a3b679SAndreas Boehler // We have a hit! 521*a1a3b679SAndreas Boehler $conditions[$kk]['tokens'][$ii]['validToken'] = true; 522*a1a3b679SAndreas Boehler continue 2; 523*a1a3b679SAndreas Boehler 524*a1a3b679SAndreas Boehler } 525*a1a3b679SAndreas Boehler } 526*a1a3b679SAndreas Boehler 527*a1a3b679SAndreas Boehler // If we get all the way here, the lock-token was 528*a1a3b679SAndreas Boehler // really unknown. 529*a1a3b679SAndreas Boehler 530*a1a3b679SAndreas Boehler 531*a1a3b679SAndreas Boehler } 532*a1a3b679SAndreas Boehler 533*a1a3b679SAndreas Boehler } 534*a1a3b679SAndreas Boehler 535*a1a3b679SAndreas Boehler // If there's any locks left in the 'mustLocks' array, it means that 536*a1a3b679SAndreas Boehler // the resource was locked and we must block it. 537*a1a3b679SAndreas Boehler if ($mustLocks) { 538*a1a3b679SAndreas Boehler 539*a1a3b679SAndreas Boehler throw new DAV\Exception\Locked(reset($mustLocks)); 540*a1a3b679SAndreas Boehler 541*a1a3b679SAndreas Boehler } 542*a1a3b679SAndreas Boehler 543*a1a3b679SAndreas Boehler } 544*a1a3b679SAndreas Boehler 545*a1a3b679SAndreas Boehler /** 546*a1a3b679SAndreas Boehler * Parses a webdav lock xml body, and returns a new Sabre\DAV\Locks\LockInfo object 547*a1a3b679SAndreas Boehler * 548*a1a3b679SAndreas Boehler * @param string $body 549*a1a3b679SAndreas Boehler * @return LockInfo 550*a1a3b679SAndreas Boehler */ 551*a1a3b679SAndreas Boehler protected function parseLockRequest($body) { 552*a1a3b679SAndreas Boehler 553*a1a3b679SAndreas Boehler $result = $this->server->xml->expect( 554*a1a3b679SAndreas Boehler '{DAV:}lockinfo', 555*a1a3b679SAndreas Boehler $body 556*a1a3b679SAndreas Boehler ); 557*a1a3b679SAndreas Boehler 558*a1a3b679SAndreas Boehler $lockInfo = new LockInfo(); 559*a1a3b679SAndreas Boehler 560*a1a3b679SAndreas Boehler $lockInfo->owner = $result->owner; 561*a1a3b679SAndreas Boehler $lockInfo->token = DAV\UUIDUtil::getUUID(); 562*a1a3b679SAndreas Boehler $lockInfo->scope = $result->scope; 563*a1a3b679SAndreas Boehler 564*a1a3b679SAndreas Boehler return $lockInfo; 565*a1a3b679SAndreas Boehler 566*a1a3b679SAndreas Boehler } 567*a1a3b679SAndreas Boehler 568*a1a3b679SAndreas Boehler /** 569*a1a3b679SAndreas Boehler * Returns a bunch of meta-data about the plugin. 570*a1a3b679SAndreas Boehler * 571*a1a3b679SAndreas Boehler * Providing this information is optional, and is mainly displayed by the 572*a1a3b679SAndreas Boehler * Browser plugin. 573*a1a3b679SAndreas Boehler * 574*a1a3b679SAndreas Boehler * The description key in the returned array may contain html and will not 575*a1a3b679SAndreas Boehler * be sanitized. 576*a1a3b679SAndreas Boehler * 577*a1a3b679SAndreas Boehler * @return array 578*a1a3b679SAndreas Boehler */ 579*a1a3b679SAndreas Boehler function getPluginInfo() { 580*a1a3b679SAndreas Boehler 581*a1a3b679SAndreas Boehler return [ 582*a1a3b679SAndreas Boehler 'name' => $this->getPluginName(), 583*a1a3b679SAndreas Boehler 'description' => 'The locks plugin turns this server into a class-2 WebDAV server and adds support for LOCK and UNLOCK', 584*a1a3b679SAndreas Boehler 'link' => 'http://sabre.io/dav/locks/', 585*a1a3b679SAndreas Boehler ]; 586*a1a3b679SAndreas Boehler 587*a1a3b679SAndreas Boehler } 588*a1a3b679SAndreas Boehler 589*a1a3b679SAndreas Boehler} 590