PageRenderTime 48ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Sabre/DAV/Locks/Plugin.php

https://github.com/KOLANICH/SabreDAV
PHP | 581 lines | 234 code | 136 blank | 211 comment | 34 complexity | b8934269b1e38ac3e52c0b832570939a MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. namespace Sabre\DAV\Locks;
  3. use Sabre\DAV;
  4. /**
  5. * Locking plugin
  6. *
  7. * This plugin provides locking support to a WebDAV server.
  8. * The easiest way to get started, is by hooking it up as such:
  9. *
  10. * $lockBackend = new Sabre\DAV\Locks\Backend\File('./mylockdb');
  11. * $lockPlugin = new Sabre\DAV\Locks\Plugin($lockBackend);
  12. * $server->addPlugin($lockPlugin);
  13. *
  14. * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
  15. * @author Evert Pot (http://evertpot.com/)
  16. * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
  17. */
  18. class Plugin extends DAV\ServerPlugin {
  19. /**
  20. * locksBackend
  21. *
  22. * @var Backend\Backend\Interface
  23. */
  24. protected $locksBackend;
  25. /**
  26. * server
  27. *
  28. * @var Sabre\DAV\Server
  29. */
  30. protected $server;
  31. /**
  32. * __construct
  33. *
  34. * @param Backend\BackendInterface $locksBackend
  35. */
  36. public function __construct(Backend\BackendInterface $locksBackend = null) {
  37. $this->locksBackend = $locksBackend;
  38. }
  39. /**
  40. * Initializes the plugin
  41. *
  42. * This method is automatically called by the Server class after addPlugin.
  43. *
  44. * @param DAV\Server $server
  45. * @return void
  46. */
  47. public function initialize(DAV\Server $server) {
  48. $this->server = $server;
  49. $server->subscribeEvent('unknownMethod',array($this,'unknownMethod'));
  50. $server->subscribeEvent('afterGetProperties',array($this,'afterGetProperties'));
  51. $server->subscribeEvent('validateTokens', array($this, 'validateTokens'));
  52. }
  53. /**
  54. * Returns a plugin name.
  55. *
  56. * Using this name other plugins will be able to access other plugins
  57. * using Sabre\DAV\Server::getPlugin
  58. *
  59. * @return string
  60. */
  61. public function getPluginName() {
  62. return 'locks';
  63. }
  64. /**
  65. * This method is called by the Server if the user used an HTTP method
  66. * the server didn't recognize.
  67. *
  68. * This plugin intercepts the LOCK and UNLOCK methods.
  69. *
  70. * @param string $method
  71. * @param string $uri
  72. * @return bool
  73. */
  74. public function unknownMethod($method, $uri) {
  75. switch($method) {
  76. case 'LOCK' : $this->httpLock($uri); return false;
  77. case 'UNLOCK' : $this->httpUnlock($uri); return false;
  78. }
  79. }
  80. /**
  81. * This method is called after most properties have been found
  82. * it allows us to add in any Lock-related properties
  83. *
  84. * @param string $path
  85. * @param array $newProperties
  86. * @return bool
  87. */
  88. public function afterGetProperties($path, &$newProperties) {
  89. foreach($newProperties[404] as $propName=>$discard) {
  90. switch($propName) {
  91. case '{DAV:}supportedlock' :
  92. $val = false;
  93. if ($this->locksBackend) $val = true;
  94. $newProperties[200][$propName] = new DAV\Property\SupportedLock($val);
  95. unset($newProperties[404][$propName]);
  96. break;
  97. case '{DAV:}lockdiscovery' :
  98. $newProperties[200][$propName] = new DAV\Property\LockDiscovery($this->getLocks($path));
  99. unset($newProperties[404][$propName]);
  100. break;
  101. }
  102. }
  103. return true;
  104. }
  105. /**
  106. * Use this method to tell the server this plugin defines additional
  107. * HTTP methods.
  108. *
  109. * This method is passed a uri. It should only return HTTP methods that are
  110. * available for the specified uri.
  111. *
  112. * @param string $uri
  113. * @return array
  114. */
  115. public function getHTTPMethods($uri) {
  116. if ($this->locksBackend)
  117. return array('LOCK','UNLOCK');
  118. return array();
  119. }
  120. /**
  121. * Returns a list of features for the HTTP OPTIONS Dav: header.
  122. *
  123. * In this case this is only the number 2. The 2 in the Dav: header
  124. * indicates the server supports locks.
  125. *
  126. * @return array
  127. */
  128. public function getFeatures() {
  129. return array(2);
  130. }
  131. /**
  132. * Returns all lock information on a particular uri
  133. *
  134. * This function should return an array with Sabre\DAV\Locks\LockInfo objects. If there are no locks on a file, return an empty array.
  135. *
  136. * Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree
  137. * If the $returnChildLocks argument is set to true, we'll also traverse all the children of the object
  138. * for any possible locks and return those as well.
  139. *
  140. * @param string $uri
  141. * @param bool $returnChildLocks
  142. * @return array
  143. */
  144. public function getLocks($uri, $returnChildLocks = false) {
  145. $lockList = array();
  146. if ($this->locksBackend)
  147. $lockList = array_merge($lockList,$this->locksBackend->getLocks($uri, $returnChildLocks));
  148. return $lockList;
  149. }
  150. /**
  151. * Locks an uri
  152. *
  153. * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock
  154. * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type
  155. * of lock (shared or exclusive) and the owner of the lock
  156. *
  157. * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock
  158. *
  159. * 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
  160. *
  161. * @param string $uri
  162. * @return void
  163. */
  164. protected function httpLock($uri) {
  165. $lastLock = null;
  166. $existingLocks = $this->getLocks($uri);
  167. if ($body = $this->server->httpRequest->getBody(true)) {
  168. // This is a new lock request
  169. $existingLock = null;
  170. // Checking if there's already non-shared locks on the uri.
  171. foreach($existingLocks as $existingLock) {
  172. if ($existingLock->scope === LockInfo::EXCLUSIVE) {
  173. throw new DAV\Exception\ConflictingLock($existingLock);
  174. }
  175. }
  176. $lockInfo = $this->parseLockRequest($body);
  177. $lockInfo->depth = $this->server->getHTTPDepth();
  178. $lockInfo->uri = $uri;
  179. if($existingLock && $lockInfo->scope != LockInfo::SHARED)
  180. throw new DAV\Exception\ConflictingLock($existingLock);
  181. } else {
  182. // Gonna check if this was a lock refresh.
  183. $existingLocks = $this->getLocks($uri);
  184. $conditions = $this->server->getIfConditions();
  185. $found = null;
  186. foreach($existingLocks as $existingLock) {
  187. foreach($conditions as $condition) {
  188. foreach($condition['tokens'] as $token) {
  189. if ($token['token'] === 'opaquelocktoken:' . $existingLock->token) {
  190. $found = $existingLock;
  191. break 3;
  192. }
  193. }
  194. }
  195. }
  196. // If none were found, this request is in error.
  197. if (is_null($found)) {
  198. if ($existingLocks) {
  199. throw new DAV\Exception\Locked(reset($existingLocks));
  200. } else {
  201. throw new DAV\Exception\BadRequest('An xml body is required for lock requests');
  202. }
  203. }
  204. // This must have been a lock refresh
  205. $lockInfo = $found;
  206. // The resource could have been locked through another uri.
  207. if ($uri!=$lockInfo->uri) $uri = $lockInfo->uri;
  208. }
  209. if ($timeout = $this->getTimeoutHeader()) $lockInfo->timeout = $timeout;
  210. $newFile = false;
  211. // 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
  212. try {
  213. $this->server->tree->getNodeForPath($uri);
  214. // We need to call the beforeWriteContent event for RFC3744
  215. // Edit: looks like this is not used, and causing problems now.
  216. //
  217. // See Issue 222
  218. // $this->server->broadcastEvent('beforeWriteContent',array($uri));
  219. } catch (DAV\Exception\NotFound $e) {
  220. // It didn't, lets create it
  221. $this->server->createFile($uri,fopen('php://memory','r'));
  222. $newFile = true;
  223. }
  224. $this->lockNode($uri,$lockInfo);
  225. $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
  226. $this->server->httpResponse->setHeader('Lock-Token','<opaquelocktoken:' . $lockInfo->token . '>');
  227. $this->server->httpResponse->sendStatus($newFile?201:200);
  228. $this->server->httpResponse->sendBody($this->generateLockResponse($lockInfo));
  229. }
  230. /**
  231. * Unlocks a uri
  232. *
  233. * 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
  234. * The server should return 204 (No content) on success
  235. *
  236. * @param string $uri
  237. * @return void
  238. */
  239. protected function httpUnlock($uri) {
  240. $lockToken = $this->server->httpRequest->getHeader('Lock-Token');
  241. // If the locktoken header is not supplied, we need to throw a bad request exception
  242. if (!$lockToken) throw new DAV\Exception\BadRequest('No lock token was supplied');
  243. $locks = $this->getLocks($uri);
  244. // Windows sometimes forgets to include < and > in the Lock-Token
  245. // header
  246. if ($lockToken[0]!=='<') $lockToken = '<' . $lockToken . '>';
  247. foreach($locks as $lock) {
  248. if ('<opaquelocktoken:' . $lock->token . '>' == $lockToken) {
  249. $this->unlockNode($uri,$lock);
  250. $this->server->httpResponse->setHeader('Content-Length','0');
  251. $this->server->httpResponse->sendStatus(204);
  252. return;
  253. }
  254. }
  255. // If we got here, it means the locktoken was invalid
  256. throw new DAV\Exception\LockTokenMatchesRequestUri();
  257. }
  258. /**
  259. * Locks a uri
  260. *
  261. * All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored
  262. * It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client
  263. *
  264. * @param string $uri
  265. * @param LockInfo $lockInfo
  266. * @return bool
  267. */
  268. public function lockNode($uri,LockInfo $lockInfo) {
  269. if (!$this->server->broadcastEvent('beforeLock',array($uri,$lockInfo))) return;
  270. if ($this->locksBackend) return $this->locksBackend->lock($uri,$lockInfo);
  271. throw new 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.');
  272. }
  273. /**
  274. * Unlocks a uri
  275. *
  276. * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified
  277. *
  278. * @param string $uri
  279. * @param LockInfo $lockInfo
  280. * @return bool
  281. */
  282. public function unlockNode($uri, LockInfo $lockInfo) {
  283. if (!$this->server->broadcastEvent('beforeUnlock',array($uri,$lockInfo))) return;
  284. if ($this->locksBackend) return $this->locksBackend->unlock($uri,$lockInfo);
  285. }
  286. /**
  287. * Returns the contents of the HTTP Timeout header.
  288. *
  289. * The method formats the header into an integer.
  290. *
  291. * @return int
  292. */
  293. public function getTimeoutHeader() {
  294. $header = $this->server->httpRequest->getHeader('Timeout');
  295. if ($header) {
  296. if (stripos($header,'second-')===0) $header = (int)(substr($header,7));
  297. else if (strtolower($header)=='infinite') $header = LockInfo::TIMEOUT_INFINITE;
  298. else throw new DAV\Exception\BadRequest('Invalid HTTP timeout header');
  299. } else {
  300. $header = 0;
  301. }
  302. return $header;
  303. }
  304. /**
  305. * Generates the response for successful LOCK requests
  306. *
  307. * @param LockInfo $lockInfo
  308. * @return string
  309. */
  310. protected function generateLockResponse(LockInfo $lockInfo) {
  311. $dom = new \DOMDocument('1.0','utf-8');
  312. $dom->formatOutput = true;
  313. $prop = $dom->createElementNS('DAV:','d:prop');
  314. $dom->appendChild($prop);
  315. $lockDiscovery = $dom->createElementNS('DAV:','d:lockdiscovery');
  316. $prop->appendChild($lockDiscovery);
  317. $lockObj = new DAV\Property\LockDiscovery(array($lockInfo),true);
  318. $lockObj->serialize($this->server,$lockDiscovery);
  319. return $dom->saveXML();
  320. }
  321. /**
  322. * The validateTokens event is triggered before every request.
  323. *
  324. * It's a moment where this plugin can check all the supplied lock tokens
  325. * in the If: header, and check if they are valid.
  326. *
  327. * In addition, it will also ensure that it checks any missing lokens that
  328. * must be present in the request, and reject requests without the proper
  329. * tokens.
  330. *
  331. * @param mixed $conditions
  332. * @return void
  333. */
  334. public function validateTokens( &$conditions ) {
  335. // First we need to gather a list of locks that must be satisfied.
  336. $mustLocks = [];
  337. $method = $this->server->httpRequest->getMethod();
  338. // Methods not in that list are operations that doesn't alter any
  339. // resources, and we don't need to check the lock-states for.
  340. switch($method) {
  341. case 'DELETE' :
  342. $mustLocks = array_merge($mustLocks, $this->getLocks(
  343. $this->server->getRequestUri(),
  344. true
  345. ));
  346. break;
  347. case 'MKCOL' :
  348. case 'MKCALENDAR' :
  349. case 'PROPPATCH' :
  350. case 'PUT' :
  351. case 'PATCH' :
  352. $mustLocks = array_merge($mustLocks, $this->getLocks(
  353. $this->server->getRequestUri(),
  354. false
  355. ));
  356. break;
  357. case 'MOVE' :
  358. $mustLocks = array_merge($mustLocks, $this->getLocks(
  359. $this->server->getRequestUri(),
  360. true
  361. ));
  362. $mustLocks = array_merge($mustLocks, $this->getLocks(
  363. $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')),
  364. false
  365. ));
  366. break;
  367. case 'COPY' :
  368. $mustLocks = array_merge($mustLocks, $this->getLocks(
  369. $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')),
  370. false
  371. ));
  372. break;
  373. }
  374. // It's possible that there's identical locks, because of shared
  375. // parents. We're removing the duplicates here.
  376. $tmp = [];
  377. foreach($mustLocks as $lock) $tmp[$lock->token] = $lock;
  378. $mustLocks = array_values($tmp);
  379. foreach($conditions as $kk=>$condition) {
  380. foreach($condition['tokens'] as $ii=>$token) {
  381. // Lock tokens always start with opaquelocktoken:
  382. if (substr($token['token'], 0, 16) !== 'opaquelocktoken:') {
  383. continue;
  384. }
  385. $checkToken = substr($token['token'],16);
  386. // Looping through our list with locks.
  387. foreach($mustLocks as $jj => $mustLock) {
  388. if ($mustLock->token == $checkToken) {
  389. // We have a match!
  390. // Removing this one from mustlocks
  391. unset($mustLocks[$jj]);
  392. // Marking the condition as valid.
  393. $conditions[$kk]['tokens'][$ii]['validToken'] = true;
  394. // Advancing to the next token
  395. continue 2;
  396. }
  397. // If we got here, it means that there was a
  398. // lock-token, but it was not in 'mustLocks'.
  399. //
  400. // This is an edge-case, as it could mean that token
  401. // was specified with a url that was not 'required' to
  402. // check. So we're doing one extra lookup to make sure
  403. // we really don't know this token.
  404. //
  405. // This also gets triggered when the user specified a
  406. // lock-token that was expired.
  407. $oddLocks = $this->getLocks($condition['uri']);
  408. foreach($oddLocks as $oddLock) {
  409. if ($oddLock->token === $checkToken) {
  410. // We have a hit!
  411. $conditions[$kk]['tokens'][$ii]['validToken'] = true;
  412. continue 2;
  413. }
  414. }
  415. // If we get all the way here, the lock-token was
  416. // really unknown.
  417. }
  418. }
  419. }
  420. // If there's any locks left in the 'mustLocks' array, it means that
  421. // the resource was locked and we must block it.
  422. if ($mustLocks) {
  423. throw new DAV\Exception\Locked(reset($mustLocks));
  424. }
  425. }
  426. /**
  427. * Parses a webdav lock xml body, and returns a new Sabre\DAV\Locks\LockInfo object
  428. *
  429. * @param string $body
  430. * @return LockInfo
  431. */
  432. protected function parseLockRequest($body) {
  433. $xml = simplexml_load_string(
  434. DAV\XMLUtil::convertDAVNamespace($body),
  435. null,
  436. LIBXML_NOWARNING);
  437. $xml->registerXPathNamespace('d','urn:DAV');
  438. $lockInfo = new LockInfo();
  439. $children = $xml->children("urn:DAV");
  440. $lockInfo->owner = (string)$children->owner;
  441. $lockInfo->token = DAV\UUIDUtil::getUUID();
  442. $lockInfo->scope = count($xml->xpath('d:lockscope/d:exclusive'))>0 ? LockInfo::EXCLUSIVE : LockInfo::SHARED;
  443. return $lockInfo;
  444. }
  445. }