ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
Plugin.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Sabre\DAV\Locks;
4 
5 use Sabre\DAV;
8 
23 class Plugin extends DAV\ServerPlugin {
24 
30  protected $locksBackend;
31 
37  protected $server;
38 
44  function __construct(Backend\BackendInterface $locksBackend) {
45 
46  $this->locksBackend = $locksBackend;
47 
48  }
49 
58  function initialize(DAV\Server $server) {
59 
60  $this->server = $server;
61 
62  $this->server->xml->elementMap['{DAV:}lockinfo'] = 'Sabre\\DAV\\Xml\\Request\\Lock';
63 
64  $server->on('method:LOCK', [$this, 'httpLock']);
65  $server->on('method:UNLOCK', [$this, 'httpUnlock']);
66  $server->on('validateTokens', [$this, 'validateTokens']);
67  $server->on('propFind', [$this, 'propFind']);
68  $server->on('afterUnbind', [$this, 'afterUnbind']);
69 
70  }
71 
80  function getPluginName() {
81 
82  return 'locks';
83 
84  }
85 
94  function propFind(DAV\PropFind $propFind, DAV\INode $node) {
95 
96  $propFind->handle('{DAV:}supportedlock', function() {
97  return new DAV\Xml\Property\SupportedLock();
98  });
99  $propFind->handle('{DAV:}lockdiscovery', function() use ($propFind) {
101  $this->getLocks($propFind->getPath())
102  );
103  });
104 
105  }
106 
117  function getHTTPMethods($uri) {
118 
119  return ['LOCK','UNLOCK'];
120 
121  }
122 
131  function getFeatures() {
132 
133  return [2];
134 
135  }
136 
150  function getLocks($uri, $returnChildLocks = false) {
151 
152  return $this->locksBackend->getLocks($uri, $returnChildLocks);
153 
154  }
155 
172 
173  $uri = $request->getPath();
174 
175  $existingLocks = $this->getLocks($uri);
176 
177  if ($body = $request->getBodyAsString()) {
178  // This is a new lock request
179 
180  $existingLock = null;
181  // Checking if there's already non-shared locks on the uri.
182  foreach ($existingLocks as $existingLock) {
183  if ($existingLock->scope === LockInfo::EXCLUSIVE) {
184  throw new DAV\Exception\ConflictingLock($existingLock);
185  }
186  }
187 
188  $lockInfo = $this->parseLockRequest($body);
189  $lockInfo->depth = $this->server->getHTTPDepth();
190  $lockInfo->uri = $uri;
191  if ($existingLock && $lockInfo->scope != LockInfo::SHARED)
192  throw new DAV\Exception\ConflictingLock($existingLock);
193 
194  } else {
195 
196  // Gonna check if this was a lock refresh.
197  $existingLocks = $this->getLocks($uri);
198  $conditions = $this->server->getIfConditions($request);
199  $found = null;
200 
201  foreach ($existingLocks as $existingLock) {
202  foreach ($conditions as $condition) {
203  foreach ($condition['tokens'] as $token) {
204  if ($token['token'] === 'opaquelocktoken:' . $existingLock->token) {
205  $found = $existingLock;
206  break 3;
207  }
208  }
209  }
210  }
211 
212  // If none were found, this request is in error.
213  if (is_null($found)) {
214  if ($existingLocks) {
215  throw new DAV\Exception\Locked(reset($existingLocks));
216  } else {
217  throw new DAV\Exception\BadRequest('An xml body is required for lock requests');
218  }
219 
220  }
221 
222  // This must have been a lock refresh
223  $lockInfo = $found;
224 
225  // The resource could have been locked through another uri.
226  if ($uri != $lockInfo->uri) $uri = $lockInfo->uri;
227 
228  }
229 
230  if ($timeout = $this->getTimeoutHeader()) $lockInfo->timeout = $timeout;
231 
232  $newFile = false;
233 
234  // 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  try {
236  $this->server->tree->getNodeForPath($uri);
237 
238  // We need to call the beforeWriteContent event for RFC3744
239  // Edit: looks like this is not used, and causing problems now.
240  //
241  // See Issue 222
242  // $this->server->emit('beforeWriteContent',array($uri));
243 
244  } catch (DAV\Exception\NotFound $e) {
245 
246  // It didn't, lets create it
247  $this->server->createFile($uri, fopen('php://memory', 'r'));
248  $newFile = true;
249 
250  }
251 
252  $this->lockNode($uri, $lockInfo);
253 
254  $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
255  $response->setHeader('Lock-Token', '<opaquelocktoken:' . $lockInfo->token . '>');
256  $response->setStatus($newFile ? 201 : 200);
257  $response->setBody($this->generateLockResponse($lockInfo));
258 
259  // Returning false will interrupt the event chain and mark this method
260  // as 'handled'.
261  return false;
262 
263  }
264 
276 
277  $lockToken = $request->getHeader('Lock-Token');
278 
279  // If the locktoken header is not supplied, we need to throw a bad request exception
280  if (!$lockToken) throw new DAV\Exception\BadRequest('No lock token was supplied');
281 
282  $path = $request->getPath();
283  $locks = $this->getLocks($path);
284 
285  // Windows sometimes forgets to include < and > in the Lock-Token
286  // header
287  if ($lockToken[0] !== '<') $lockToken = '<' . $lockToken . '>';
288 
289  foreach ($locks as $lock) {
290 
291  if ('<opaquelocktoken:' . $lock->token . '>' == $lockToken) {
292 
293  $this->unlockNode($path, $lock);
294  $response->setHeader('Content-Length', '0');
295  $response->setStatus(204);
296 
297  // Returning false will break the method chain, and mark the
298  // method as 'handled'.
299  return false;
300 
301  }
302 
303  }
304 
305  // If we got here, it means the locktoken was invalid
306  throw new DAV\Exception\LockTokenMatchesRequestUri();
307 
308  }
309 
318  function afterUnbind($path) {
319 
320  $locks = $this->getLocks($path, $includeChildren = true);
321  foreach ($locks as $lock) {
322  $this->unlockNode($path, $lock);
323  }
324 
325  }
326 
337  function lockNode($uri, LockInfo $lockInfo) {
338 
339  if (!$this->server->emit('beforeLock', [$uri, $lockInfo])) return;
340  return $this->locksBackend->lock($uri, $lockInfo);
341 
342  }
343 
353  function unlockNode($uri, LockInfo $lockInfo) {
354 
355  if (!$this->server->emit('beforeUnlock', [$uri, $lockInfo])) return;
356  return $this->locksBackend->unlock($uri, $lockInfo);
357 
358  }
359 
360 
368  function getTimeoutHeader() {
369 
370  $header = $this->server->httpRequest->getHeader('Timeout');
371 
372  if ($header) {
373 
374  if (stripos($header, 'second-') === 0) $header = (int)(substr($header, 7));
375  elseif (stripos($header, 'infinite') === 0) $header = LockInfo::TIMEOUT_INFINITE;
376  else throw new DAV\Exception\BadRequest('Invalid HTTP timeout header');
377 
378  } else {
379 
380  $header = 0;
381 
382  }
383 
384  return $header;
385 
386  }
387 
394  protected function generateLockResponse(LockInfo $lockInfo) {
395 
396  return $this->server->xml->write('{DAV:}prop', [
397  '{DAV:}lockdiscovery' =>
398  new DAV\Xml\Property\LockDiscovery([$lockInfo])
399  ]);
400  }
401 
416  function validateTokens(RequestInterface $request, &$conditions) {
417 
418  // First we need to gather a list of locks that must be satisfied.
419  $mustLocks = [];
420  $method = $request->getMethod();
421 
422  // Methods not in that list are operations that doesn't alter any
423  // resources, and we don't need to check the lock-states for.
424  switch ($method) {
425 
426  case 'DELETE' :
427  $mustLocks = array_merge($mustLocks, $this->getLocks(
428  $request->getPath(),
429  true
430  ));
431  break;
432  case 'MKCOL' :
433  case 'MKCALENDAR' :
434  case 'PROPPATCH' :
435  case 'PUT' :
436  case 'PATCH' :
437  $mustLocks = array_merge($mustLocks, $this->getLocks(
438  $request->getPath(),
439  false
440  ));
441  break;
442  case 'MOVE' :
443  $mustLocks = array_merge($mustLocks, $this->getLocks(
444  $request->getPath(),
445  true
446  ));
447  $mustLocks = array_merge($mustLocks, $this->getLocks(
448  $this->server->calculateUri($request->getHeader('Destination')),
449  false
450  ));
451  break;
452  case 'COPY' :
453  $mustLocks = array_merge($mustLocks, $this->getLocks(
454  $this->server->calculateUri($request->getHeader('Destination')),
455  false
456  ));
457  break;
458  case 'LOCK' :
459  //Temporary measure.. figure out later why this is needed
460  // Here we basically ignore all incoming tokens...
461  foreach ($conditions as $ii => $condition) {
462  foreach ($condition['tokens'] as $jj => $token) {
463  $conditions[$ii]['tokens'][$jj]['validToken'] = true;
464  }
465  }
466  return;
467 
468  }
469 
470  // It's possible that there's identical locks, because of shared
471  // parents. We're removing the duplicates here.
472  $tmp = [];
473  foreach ($mustLocks as $lock) $tmp[$lock->token] = $lock;
474  $mustLocks = array_values($tmp);
475 
476  foreach ($conditions as $kk => $condition) {
477 
478  foreach ($condition['tokens'] as $ii => $token) {
479 
480  // Lock tokens always start with opaquelocktoken:
481  if (substr($token['token'], 0, 16) !== 'opaquelocktoken:') {
482  continue;
483  }
484 
485  $checkToken = substr($token['token'], 16);
486  // Looping through our list with locks.
487  foreach ($mustLocks as $jj => $mustLock) {
488 
489  if ($mustLock->token == $checkToken) {
490 
491  // We have a match!
492  // Removing this one from mustlocks
493  unset($mustLocks[$jj]);
494 
495  // Marking the condition as valid.
496  $conditions[$kk]['tokens'][$ii]['validToken'] = true;
497 
498  // Advancing to the next token
499  continue 2;
500 
501  }
502 
503  }
504 
505  // If we got here, it means that there was a
506  // lock-token, but it was not in 'mustLocks'.
507  //
508  // This is an edge-case, as it could mean that token
509  // was specified with a url that was not 'required' to
510  // check. So we're doing one extra lookup to make sure
511  // we really don't know this token.
512  //
513  // This also gets triggered when the user specified a
514  // lock-token that was expired.
515  $oddLocks = $this->getLocks($condition['uri']);
516  foreach ($oddLocks as $oddLock) {
517 
518  if ($oddLock->token === $checkToken) {
519 
520  // We have a hit!
521  $conditions[$kk]['tokens'][$ii]['validToken'] = true;
522  continue 2;
523 
524  }
525  }
526 
527  // If we get all the way here, the lock-token was
528  // really unknown.
529 
530 
531  }
532 
533  }
534 
535  // If there's any locks left in the 'mustLocks' array, it means that
536  // the resource was locked and we must block it.
537  if ($mustLocks) {
538 
539  throw new DAV\Exception\Locked(reset($mustLocks));
540 
541  }
542 
543  }
544 
551  protected function parseLockRequest($body) {
552 
553  $result = $this->server->xml->expect(
554  '{DAV:}lockinfo',
555  $body
556  );
557 
558  $lockInfo = new LockInfo();
559 
560  $lockInfo->owner = $result->owner;
561  $lockInfo->token = DAV\UUIDUtil::getUUID();
562  $lockInfo->scope = $result->scope;
563 
564  return $lockInfo;
565 
566  }
567 
579  function getPluginInfo() {
580 
581  return [
582  'name' => $this->getPluginName(),
583  'description' => 'The locks plugin turns this server into a class-2 WebDAV server and adds support for LOCK and UNLOCK',
584  'link' => 'http://sabre.io/dav/locks/',
585  ];
586 
587  }
588 
589 }
httpLock(RequestInterface $request, ResponseInterface $response)
Locks an uri.
Definition: Plugin.php:171
This interface represents a HTTP response.
The RequestInterface represents a HTTP request.
getPluginName()
Returns a plugin name.
Definition: Plugin.php:80
$path
Definition: aliased.php:25
This class represents the {DAV:}supportedlock property.
The baseclass for all server plugins.
setBody($body)
Updates the body resource with a new stream.
$result
foreach($paths as $path) $request
Definition: asyncclient.php:32
parseLockRequest($body)
Parses a webdav lock xml body, and returns a new Sabre object.
Definition: Plugin.php:551
getBodyAsString()
Returns the body as a string.
getPluginInfo()
Returns a bunch of meta-data about the plugin.
Definition: Plugin.php:579
afterUnbind($path)
This method is called after a node is deleted.
Definition: Plugin.php:318
const EXCLUSIVE
An exclusive lock.
Definition: LockInfo.php:25
This class holds all the information about a PROPFIND request.
Definition: PropFind.php:11
getFeatures()
Returns a list of features for the HTTP OPTIONS Dav: header.
Definition: Plugin.php:131
getHTTPMethods($uri)
Use this method to tell the server this plugin defines additional HTTP methods.
Definition: Plugin.php:117
unlockNode($uri, LockInfo $lockInfo)
Unlocks a uri.
Definition: Plugin.php:353
Represents {DAV:}lockdiscovery property.
initialize(DAV\Server $server)
Initializes the plugin.
Definition: Plugin.php:58
setStatus($status)
Sets the HTTP status code.
Main DAV server class.
Definition: Server.php:23
getHeader($name)
Returns a specific HTTP header, based on it&#39;s name.
The INode interface is the base interface, and the parent class of both ICollection and IFile...
Definition: INode.php:12
validateTokens(RequestInterface $request, &$conditions)
The validateTokens event is triggered before every request.
Definition: Plugin.php:416
const SHARED
A shared lock.
Definition: LockInfo.php:20
getPath()
Returns the relative path.
getMethod()
Returns the current HTTP method.
getTimeoutHeader()
Returns the contents of the HTTP Timeout header.
Definition: Plugin.php:368
const TIMEOUT_INFINITE
A never expiring timeout.
Definition: LockInfo.php:30
static getUUID()
Returns a pseudo-random v4 UUID.
Definition: UUIDUtil.php:26
LockInfo class.
Definition: LockInfo.php:15
getLocks($uri, $returnChildLocks=false)
Returns all lock information on a particular uri.
Definition: Plugin.php:150
lockNode($uri, LockInfo $lockInfo)
Locks a uri.
Definition: Plugin.php:337
propFind(DAV\PropFind $propFind, DAV\INode $node)
This method is called after most properties have been found it allows us to add in any Lock-related p...
Definition: Plugin.php:94
generateLockResponse(LockInfo $lockInfo)
Generates the response for successful LOCK requests.
Definition: Plugin.php:394
Locking plugin.
Definition: Plugin.php:23
$response
setHeader($name, $value)
Updates a HTTP header.
httpUnlock(RequestInterface $request, ResponseInterface $response)
Unlocks a uri.
Definition: Plugin.php:275
__construct(Backend\BackendInterface $locksBackend)
__construct
Definition: Plugin.php:44