PageRenderTime 37ms CodeModel.GetById 0ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Sabre/DAV/Server.php

https://github.com/agilastic/sabre-dav
PHP | 1824 lines | 783 code | 378 blank | 663 comment | 151 complexity | 5d6719f7271199b7f0eeca78f9a0f60d MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. namespace Sabre\DAV;
  3. use
  4. Sabre\Event\EventEmitter,
  5. Sabre\HTTP,
  6. Sabre\HTTP\RequestInterface,
  7. Sabre\HTTP\ResponseInterface,
  8. Sabre\HTTP\URLUtil;
  9. /**
  10. * Main DAV server class
  11. *
  12. * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
  13. * @author Evert Pot (http://evertpot.com/)
  14. * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
  15. */
  16. class Server extends EventEmitter {
  17. /**
  18. * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree
  19. */
  20. const DEPTH_INFINITY = -1;
  21. /**
  22. * Nodes that are files, should have this as the type property
  23. */
  24. const NODE_FILE = 1;
  25. /**
  26. * Nodes that are directories, should use this value as the type property
  27. */
  28. const NODE_DIRECTORY = 2;
  29. /**
  30. * XML namespace for all SabreDAV related elements
  31. */
  32. const NS_SABREDAV = 'http://sabredav.org/ns';
  33. /**
  34. * The tree object
  35. *
  36. * @var Sabre\DAV\Tree
  37. */
  38. public $tree;
  39. /**
  40. * The base uri
  41. *
  42. * @var string
  43. */
  44. protected $baseUri = null;
  45. /**
  46. * httpResponse
  47. *
  48. * @var Sabre\HTTP\Response
  49. */
  50. public $httpResponse;
  51. /**
  52. * httpRequest
  53. *
  54. * @var Sabre\HTTP\Request
  55. */
  56. public $httpRequest;
  57. /**
  58. * The list of plugins
  59. *
  60. * @var array
  61. */
  62. protected $plugins = [];
  63. /**
  64. * This array contains a list of callbacks we should call when certain events are triggered
  65. *
  66. * @var array
  67. */
  68. protected $eventSubscriptions = [];
  69. /**
  70. * This property will be filled with a unique string that describes the
  71. * transaction. This is useful for performance measuring and logging
  72. * purposes.
  73. *
  74. * By default it will just fill it with a lowercased HTTP method name, but
  75. * plugins override this. For example, the WebDAV-Sync sync-collection
  76. * report will set this to 'report-sync-collection'.
  77. *
  78. * @var string
  79. */
  80. public $transactionType;
  81. /**
  82. * This is a default list of namespaces.
  83. *
  84. * If you are defining your own custom namespace, add it here to reduce
  85. * bandwidth and improve legibility of xml bodies.
  86. *
  87. * @var array
  88. */
  89. public $xmlNamespaces = [
  90. 'DAV:' => 'd',
  91. 'http://sabredav.org/ns' => 's',
  92. ];
  93. /**
  94. * The propertymap can be used to map properties from
  95. * requests to property classes.
  96. *
  97. * @var array
  98. */
  99. public $propertyMap = [
  100. '{DAV:}resourcetype' => 'Sabre\\DAV\\Property\\ResourceType',
  101. ];
  102. public $protectedProperties = [
  103. // RFC4918
  104. '{DAV:}getcontentlength',
  105. '{DAV:}getetag',
  106. '{DAV:}getlastmodified',
  107. '{DAV:}lockdiscovery',
  108. '{DAV:}supportedlock',
  109. // RFC4331
  110. '{DAV:}quota-available-bytes',
  111. '{DAV:}quota-used-bytes',
  112. // RFC3744
  113. '{DAV:}supported-privilege-set',
  114. '{DAV:}current-user-privilege-set',
  115. '{DAV:}acl',
  116. '{DAV:}acl-restrictions',
  117. '{DAV:}inherited-acl-set',
  118. ];
  119. /**
  120. * This is a flag that allow or not showing file, line and code
  121. * of the exception in the returned XML
  122. *
  123. * @var bool
  124. */
  125. public $debugExceptions = false;
  126. /**
  127. * This property allows you to automatically add the 'resourcetype' value
  128. * based on a node's classname or interface.
  129. *
  130. * The preset ensures that {DAV:}collection is automaticlly added for nodes
  131. * implementing Sabre\DAV\ICollection.
  132. *
  133. * @var array
  134. */
  135. public $resourceTypeMapping = [
  136. 'Sabre\\DAV\\ICollection' => '{DAV:}collection',
  137. ];
  138. /**
  139. * If this setting is turned off, SabreDAV's version number will be hidden
  140. * from various places.
  141. *
  142. * Some people feel this is a good security measure.
  143. *
  144. * @var bool
  145. */
  146. static public $exposeVersion = true;
  147. /**
  148. * Sets up the server
  149. *
  150. * If a Sabre\DAV\Tree object is passed as an argument, it will
  151. * use it as the directory tree. If a Sabre\DAV\INode is passed, it
  152. * will create a Sabre\DAV\ObjectTree and use the node as the root.
  153. *
  154. * If nothing is passed, a Sabre\DAV\SimpleCollection is created in
  155. * a Sabre\DAV\ObjectTree.
  156. *
  157. * If an array is passed, we automatically create a root node, and use
  158. * the nodes in the array as top-level children.
  159. *
  160. * @param Tree|INode|array|null $treeOrNode The tree object
  161. */
  162. public function __construct($treeOrNode = null) {
  163. if ($treeOrNode instanceof Tree) {
  164. $this->tree = $treeOrNode;
  165. } elseif ($treeOrNode instanceof INode) {
  166. $this->tree = new ObjectTree($treeOrNode);
  167. } elseif (is_array($treeOrNode)) {
  168. // If it's an array, a list of nodes was passed, and we need to
  169. // create the root node.
  170. foreach($treeOrNode as $node) {
  171. if (!($node instanceof INode)) {
  172. throw new Exception('Invalid argument passed to constructor. If you\'re passing an array, all the values must implement Sabre\\DAV\\INode');
  173. }
  174. }
  175. $root = new SimpleCollection('root', $treeOrNode);
  176. $this->tree = new ObjectTree($root);
  177. } elseif (is_null($treeOrNode)) {
  178. $root = new SimpleCollection('root');
  179. $this->tree = new ObjectTree($root);
  180. } else {
  181. throw new Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre\\DAV\\Tree, Sabre\\DAV\\INode, an array or null');
  182. }
  183. $this->httpResponse = new HTTP\Response();
  184. $this->httpRequest = HTTP\Request::createFromPHPRequest();
  185. $this->addPlugin(new CorePlugin());
  186. }
  187. /**
  188. * Starts the DAV Server
  189. *
  190. * @return void
  191. */
  192. public function exec() {
  193. try {
  194. // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an
  195. // origin, we must make sure we send back HTTP/1.0 if this was
  196. // requested.
  197. // This is mainly because nginx doesn't support Chunked Transfer
  198. // Encoding, and this forces the webserver SabreDAV is running on,
  199. // to buffer entire responses to calculate Content-Length.
  200. $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion());
  201. // Setting the base url
  202. $this->httpRequest->setBaseUrl($this->getBaseUri());
  203. $this->invokeMethod($this->httpRequest, $this->httpResponse);
  204. } catch (Exception $e) {
  205. try {
  206. $this->emit('exception', [$e]);
  207. } catch (Exception $ignore) {
  208. }
  209. $DOM = new \DOMDocument('1.0','utf-8');
  210. $DOM->formatOutput = true;
  211. $error = $DOM->createElementNS('DAV:','d:error');
  212. $error->setAttribute('xmlns:s',self::NS_SABREDAV);
  213. $DOM->appendChild($error);
  214. $h = function($v) {
  215. return htmlspecialchars($v, ENT_NOQUOTES, 'UTF-8');
  216. };
  217. if (self::$exposeVersion) {
  218. $error->appendChild($DOM->createElement('s:sabredav-version',$h(Version::VERSION)));
  219. }
  220. $error->appendChild($DOM->createElement('s:exception',$h(get_class($e))));
  221. $error->appendChild($DOM->createElement('s:message',$h($e->getMessage())));
  222. if ($this->debugExceptions) {
  223. $error->appendChild($DOM->createElement('s:file',$h($e->getFile())));
  224. $error->appendChild($DOM->createElement('s:line',$h($e->getLine())));
  225. $error->appendChild($DOM->createElement('s:code',$h($e->getCode())));
  226. $error->appendChild($DOM->createElement('s:stacktrace',$h($e->getTraceAsString())));
  227. }
  228. if ($this->debugExceptions) {
  229. $previous = $e;
  230. while ($previous = $previous->getPrevious()) {
  231. $xPrevious = $DOM->createElement('s:previous-exception');
  232. $xPrevious->appendChild($DOM->createElement('s:exception',$h(get_class($previous))));
  233. $xPrevious->appendChild($DOM->createElement('s:message',$h($previous->getMessage())));
  234. $xPrevious->appendChild($DOM->createElement('s:file',$h($previous->getFile())));
  235. $xPrevious->appendChild($DOM->createElement('s:line',$h($previous->getLine())));
  236. $xPrevious->appendChild($DOM->createElement('s:code',$h($previous->getCode())));
  237. $xPrevious->appendChild($DOM->createElement('s:stacktrace',$h($previous->getTraceAsString())));
  238. $error->appendChild($xPrevious);
  239. }
  240. }
  241. if($e instanceof Exception) {
  242. $httpCode = $e->getHTTPCode();
  243. $e->serialize($this,$error);
  244. $headers = $e->getHTTPHeaders($this);
  245. } else {
  246. $httpCode = 500;
  247. $headers = [];
  248. }
  249. $headers['Content-Type'] = 'application/xml; charset=utf-8';
  250. $this->httpResponse->setStatus($httpCode);
  251. $this->httpResponse->addHeaders($headers);
  252. $this->httpResponse->setBody($DOM->saveXML());
  253. $this->httpResponse->send();
  254. }
  255. }
  256. /**
  257. * Sets the base server uri
  258. *
  259. * @param string $uri
  260. * @return void
  261. */
  262. public function setBaseUri($uri) {
  263. // If the baseUri does not end with a slash, we must add it
  264. if ($uri[strlen($uri)-1]!=='/')
  265. $uri.='/';
  266. $this->baseUri = $uri;
  267. }
  268. /**
  269. * Returns the base responding uri
  270. *
  271. * @return string
  272. */
  273. public function getBaseUri() {
  274. if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri();
  275. return $this->baseUri;
  276. }
  277. /**
  278. * This method attempts to detect the base uri.
  279. * Only the PATH_INFO variable is considered.
  280. *
  281. * If this variable is not set, the root (/) is assumed.
  282. *
  283. * @return string
  284. */
  285. public function guessBaseUri() {
  286. $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO');
  287. $uri = $this->httpRequest->getRawServerValue('REQUEST_URI');
  288. // If PATH_INFO is found, we can assume it's accurate.
  289. if (!empty($pathInfo)) {
  290. // We need to make sure we ignore the QUERY_STRING part
  291. if ($pos = strpos($uri,'?'))
  292. $uri = substr($uri,0,$pos);
  293. // PATH_INFO is only set for urls, such as: /example.php/path
  294. // in that case PATH_INFO contains '/path'.
  295. // Note that REQUEST_URI is percent encoded, while PATH_INFO is
  296. // not, Therefore they are only comparable if we first decode
  297. // REQUEST_INFO as well.
  298. $decodedUri = URLUtil::decodePath($uri);
  299. // A simple sanity check:
  300. if(substr($decodedUri,strlen($decodedUri)-strlen($pathInfo))===$pathInfo) {
  301. $baseUri = substr($decodedUri,0,strlen($decodedUri)-strlen($pathInfo));
  302. return rtrim($baseUri,'/') . '/';
  303. }
  304. throw new Exception('The REQUEST_URI ('. $uri . ') did not end with the contents of PATH_INFO (' . $pathInfo . '). This server might be misconfigured.');
  305. }
  306. // The last fallback is that we're just going to assume the server root.
  307. return '/';
  308. }
  309. /**
  310. * Adds a plugin to the server
  311. *
  312. * For more information, console the documentation of Sabre\DAV\ServerPlugin
  313. *
  314. * @param ServerPlugin $plugin
  315. * @return void
  316. */
  317. public function addPlugin(ServerPlugin $plugin) {
  318. $this->plugins[$plugin->getPluginName()] = $plugin;
  319. $plugin->initialize($this);
  320. }
  321. /**
  322. * Returns an initialized plugin by it's name.
  323. *
  324. * This function returns null if the plugin was not found.
  325. *
  326. * @param string $name
  327. * @return ServerPlugin
  328. */
  329. public function getPlugin($name) {
  330. if (isset($this->plugins[$name]))
  331. return $this->plugins[$name];
  332. // This is a fallback and deprecated.
  333. foreach($this->plugins as $plugin) {
  334. if (get_class($plugin)===$name) return $plugin;
  335. }
  336. return null;
  337. }
  338. /**
  339. * Returns all plugins
  340. *
  341. * @return array
  342. */
  343. public function getPlugins() {
  344. return $this->plugins;
  345. }
  346. /**
  347. * Handles a http request, and execute a method based on its name
  348. *
  349. * @param RequestInterface $request
  350. * @param ResponseInterface $response
  351. * @return void
  352. */
  353. public function invokeMethod(RequestInterface $request, ResponseInterface $response) {
  354. $method = $request->getMethod();
  355. if (!$this->emit('beforeMethod:' . $method,[$request, $response])) return;
  356. if (!$this->emit('beforeMethod',[$request, $response])) return;
  357. $this->transactionType = strtolower($method);
  358. if ($this->emit('method:' . $method, [$request, $response])) {
  359. if ($this->emit('method',[$request, $response])) {
  360. // Unsupported method
  361. throw new Exception\NotImplemented('There was no handler found for this "' . $method . '" method');
  362. }
  363. }
  364. if (!$this->emit('afterMethod:' . $method,[$request, $response])) return;
  365. if (!$this->emit('afterMethod', [$request, $response])) return;
  366. $response->send();
  367. }
  368. // {{{ HTTP/WebDAV protocol helpers
  369. /**
  370. * Returns an array with all the supported HTTP methods for a specific uri.
  371. *
  372. * @param string $uri
  373. * @return array
  374. */
  375. public function getAllowedMethods($uri) {
  376. $methods = [
  377. 'OPTIONS',
  378. 'GET',
  379. 'HEAD',
  380. 'DELETE',
  381. 'PROPFIND',
  382. 'PUT',
  383. 'PROPPATCH',
  384. 'COPY',
  385. 'MOVE',
  386. 'REPORT'
  387. ];
  388. // The MKCOL is only allowed on an unmapped uri
  389. try {
  390. $this->tree->getNodeForPath($uri);
  391. } catch (Exception\NotFound $e) {
  392. $methods[] = 'MKCOL';
  393. }
  394. // We're also checking if any of the plugins register any new methods
  395. foreach($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods($uri));
  396. array_unique($methods);
  397. return $methods;
  398. }
  399. /**
  400. * Gets the uri for the request, keeping the base uri into consideration
  401. *
  402. * @return string
  403. */
  404. public function getRequestUri() {
  405. return $this->calculateUri($this->httpRequest->getUrl());
  406. }
  407. /**
  408. * Calculates the uri for a request, making sure that the base uri is stripped out
  409. *
  410. * @param string $uri
  411. * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri
  412. * @return string
  413. */
  414. public function calculateUri($uri) {
  415. if ($uri[0]!='/' && strpos($uri,'://')) {
  416. $uri = parse_url($uri,PHP_URL_PATH);
  417. }
  418. $uri = str_replace('//','/',$uri);
  419. if (strpos($uri,$this->getBaseUri())===0) {
  420. return trim(URLUtil::decodePath(substr($uri,strlen($this->getBaseUri()))),'/');
  421. // A special case, if the baseUri was accessed without a trailing
  422. // slash, we'll accept it as well.
  423. } elseif ($uri.'/' === $this->getBaseUri()) {
  424. return '';
  425. } else {
  426. throw new Exception\Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')');
  427. }
  428. }
  429. /**
  430. * Returns the HTTP depth header
  431. *
  432. * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre\DAV\Server::DEPTH_INFINITY object
  433. * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent
  434. *
  435. * @param mixed $default
  436. * @return int
  437. */
  438. public function getHTTPDepth($default = self::DEPTH_INFINITY) {
  439. // If its not set, we'll grab the default
  440. $depth = $this->httpRequest->getHeader('Depth');
  441. if (is_null($depth)) return $default;
  442. if ($depth == 'infinity') return self::DEPTH_INFINITY;
  443. // If its an unknown value. we'll grab the default
  444. if (!ctype_digit($depth)) return $default;
  445. return (int)$depth;
  446. }
  447. /**
  448. * Returns the HTTP range header
  449. *
  450. * This method returns null if there is no well-formed HTTP range request
  451. * header or array($start, $end).
  452. *
  453. * The first number is the offset of the first byte in the range.
  454. * The second number is the offset of the last byte in the range.
  455. *
  456. * If the second offset is null, it should be treated as the offset of the last byte of the entity
  457. * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity
  458. *
  459. * @return array|null
  460. */
  461. public function getHTTPRange() {
  462. $range = $this->httpRequest->getHeader('range');
  463. if (is_null($range)) return null;
  464. // Matching "Range: bytes=1234-5678: both numbers are optional
  465. if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i',$range,$matches)) return null;
  466. if ($matches[1]==='' && $matches[2]==='') return null;
  467. return [
  468. $matches[1]!==''?$matches[1]:null,
  469. $matches[2]!==''?$matches[2]:null,
  470. ];
  471. }
  472. /**
  473. * Returns the HTTP Prefer header information.
  474. *
  475. * The prefer header is defined in:
  476. * http://tools.ietf.org/html/draft-snell-http-prefer-14
  477. *
  478. * This method will return an array with options.
  479. *
  480. * Currently, the following options may be returned:
  481. * [
  482. * 'return-asynch' => true,
  483. * 'return-minimal' => true,
  484. * 'return-representation' => true,
  485. * 'wait' => 30,
  486. * 'strict' => true,
  487. * 'lenient' => true,
  488. * ]
  489. *
  490. * This method also supports the Brief header, and will also return
  491. * 'return-minimal' if the brief header was set to 't'.
  492. *
  493. * For the boolean options, false will be returned if the headers are not
  494. * specified. For the integer options it will be 'null'.
  495. *
  496. * @return array
  497. */
  498. public function getHTTPPrefer() {
  499. $result = [
  500. 'return-asynch' => false,
  501. 'return-minimal' => false,
  502. 'return-representation' => false,
  503. 'wait' => null,
  504. 'strict' => false,
  505. 'lenient' => false,
  506. ];
  507. if ($prefer = $this->httpRequest->getHeader('Prefer')) {
  508. $parameters = array_map('trim',
  509. explode(',', $prefer)
  510. );
  511. foreach($parameters as $parameter) {
  512. // Right now our regex only supports the tokens actually
  513. // specified in the draft. We may need to expand this if new
  514. // tokens get registered.
  515. if(!preg_match('/^(?P<token>[a-z0-9-]+)(?:=(?P<value>[0-9]+))?$/', $parameter, $matches)) {
  516. continue;
  517. }
  518. switch($matches['token']) {
  519. case 'return-asynch' :
  520. case 'return-minimal' :
  521. case 'return-representation' :
  522. case 'strict' :
  523. case 'lenient' :
  524. $result[$matches['token']] = true;
  525. break;
  526. case 'wait' :
  527. $result[$matches['token']] = $matches['value'];
  528. break;
  529. }
  530. }
  531. }
  532. if ($this->httpRequest->getHeader('Brief')=='t') {
  533. $result['return-minimal'] = true;
  534. }
  535. return $result;
  536. }
  537. /**
  538. * Returns information about Copy and Move requests
  539. *
  540. * This function is created to help getting information about the source and the destination for the
  541. * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions
  542. *
  543. * The returned value is an array with the following keys:
  544. * * destination - Destination path
  545. * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten)
  546. *
  547. * @param RequestInterface $request
  548. * @throws Exception\BadRequest upon missing or broken request headers
  549. * @throws Exception\UnsupportedMediaType when trying to copy into a
  550. * non-collection.
  551. * @throws Exception\PreconditionFailed If overwrite is set to false, but
  552. * the destination exists.
  553. * @throws Exception\Forbidden when source and destination paths are
  554. * identical.
  555. * @throws Exception\Conflict When trying to copy a node into its own
  556. * subtree.
  557. * @return array
  558. */
  559. public function getCopyAndMoveInfo(RequestInterface $request) {
  560. // Collecting the relevant HTTP headers
  561. if (!$request->getHeader('Destination')) throw new Exception\BadRequest('The destination header was not supplied');
  562. $destination = $this->calculateUri($request->getHeader('Destination'));
  563. $overwrite = $request->getHeader('Overwrite');
  564. if (!$overwrite) $overwrite = 'T';
  565. if (strtoupper($overwrite)=='T') $overwrite = true;
  566. elseif (strtoupper($overwrite)=='F') $overwrite = false;
  567. // We need to throw a bad request exception, if the header was invalid
  568. else throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F');
  569. list($destinationDir) = URLUtil::splitPath($destination);
  570. try {
  571. $destinationParent = $this->tree->getNodeForPath($destinationDir);
  572. if (!($destinationParent instanceof ICollection)) throw new Exception\UnsupportedMediaType('The destination node is not a collection');
  573. } catch (Exception\NotFound $e) {
  574. // If the destination parent node is not found, we throw a 409
  575. throw new Exception\Conflict('The destination node is not found');
  576. }
  577. try {
  578. $destinationNode = $this->tree->getNodeForPath($destination);
  579. // If this succeeded, it means the destination already exists
  580. // we'll need to throw precondition failed in case overwrite is false
  581. if (!$overwrite) throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false','Overwrite');
  582. } catch (Exception\NotFound $e) {
  583. // Destination didn't exist, we're all good
  584. $destinationNode = false;
  585. }
  586. $requestPath = $request->getPath();
  587. if ($destination===$requestPath) {
  588. throw new Exception\Forbidden('Source and destination uri are identical.');
  589. }
  590. if (substr($destination, 0, strlen($requestPath)+1) === $requestPath . '/') {
  591. throw new Exception\Conflict('The destination may not be part of the same subtree as the source path.');
  592. }
  593. // These are the three relevant properties we need to return
  594. return [
  595. 'destination' => $destination,
  596. 'destinationExists' => $destinationNode==true,
  597. 'destinationNode' => $destinationNode,
  598. ];
  599. }
  600. /**
  601. * Returns a list of properties for a path
  602. *
  603. * This is a simplified version getPropertiesForPath.
  604. * if you aren't interested in status codes, but you just
  605. * want to have a flat list of properties. Use this method.
  606. *
  607. * @param string $path
  608. * @param array $propertyNames
  609. */
  610. public function getProperties($path, $propertyNames) {
  611. $result = $this->getPropertiesForPath($path,$propertyNames,0);
  612. return $result[0][200];
  613. }
  614. /**
  615. * A kid-friendly way to fetch properties for a node's children.
  616. *
  617. * The returned array will be indexed by the path of the of child node.
  618. * Only properties that are actually found will be returned.
  619. *
  620. * The parent node will not be returned.
  621. *
  622. * @param string $path
  623. * @param array $propertyNames
  624. * @return array
  625. */
  626. public function getPropertiesForChildren($path, $propertyNames) {
  627. $result = [];
  628. foreach($this->getPropertiesForPath($path,$propertyNames,1) as $k=>$row) {
  629. // Skipping the parent path
  630. if ($k === 0) continue;
  631. $result[$row['href']] = $row[200];
  632. }
  633. return $result;
  634. }
  635. /**
  636. * Returns a list of HTTP headers for a particular resource
  637. *
  638. * The generated http headers are based on properties provided by the
  639. * resource. The method basically provides a simple mapping between
  640. * DAV property and HTTP header.
  641. *
  642. * The headers are intended to be used for HEAD and GET requests.
  643. *
  644. * @param string $path
  645. * @return array
  646. */
  647. public function getHTTPHeaders($path) {
  648. $propertyMap = [
  649. '{DAV:}getcontenttype' => 'Content-Type',
  650. '{DAV:}getcontentlength' => 'Content-Length',
  651. '{DAV:}getlastmodified' => 'Last-Modified',
  652. '{DAV:}getetag' => 'ETag',
  653. ];
  654. $properties = $this->getProperties($path,array_keys($propertyMap));
  655. $headers = [];
  656. foreach($propertyMap as $property=>$header) {
  657. if (!isset($properties[$property])) continue;
  658. if (is_scalar($properties[$property])) {
  659. $headers[$header] = $properties[$property];
  660. // GetLastModified gets special cased
  661. } elseif ($properties[$property] instanceof Property\GetLastModified) {
  662. $headers[$header] = HTTP\Util::toHTTPDate($properties[$property]->getTime());
  663. }
  664. }
  665. return $headers;
  666. }
  667. /**
  668. * Returns a list of properties for a given path
  669. *
  670. * The path that should be supplied should have the baseUrl stripped out
  671. * The list of properties should be supplied in Clark notation. If the list is empty
  672. * 'allprops' is assumed.
  673. *
  674. * If a depth of 1 is requested child elements will also be returned.
  675. *
  676. * @param string $path
  677. * @param array $propertyNames
  678. * @param int $depth
  679. * @return array
  680. */
  681. public function getPropertiesForPath($path, $propertyNames = [], $depth = 0) {
  682. if ($depth!=0) $depth = 1;
  683. $path = rtrim($path,'/');
  684. // This event allows people to intercept these requests early on in the
  685. // process.
  686. //
  687. // We're not doing anything with the result, but this can be helpful to
  688. // pre-fetch certain expensive live properties.
  689. $this->emit('beforeGetPropertiesForPath', [$path, $propertyNames, $depth]);
  690. $returnPropertyList = [];
  691. $parentNode = $this->tree->getNodeForPath($path);
  692. $nodes = [
  693. $path => $parentNode
  694. ];
  695. if ($depth==1 && $parentNode instanceof ICollection) {
  696. foreach($this->tree->getChildren($path) as $childNode)
  697. $nodes[$path . '/' . $childNode->getName()] = $childNode;
  698. }
  699. foreach($nodes as $myPath=>$node) {
  700. $r = $this->getPropertiesByNode($myPath, $node, $propertyNames);
  701. if ($r) {
  702. $returnPropertyList[] = $r;
  703. }
  704. }
  705. return $returnPropertyList;
  706. }
  707. /**
  708. * Returns a list of properties for a list of paths.
  709. *
  710. * The path that should be supplied should have the baseUrl stripped out
  711. * The list of properties should be supplied in Clark notation. If the list is empty
  712. * 'allprops' is assumed.
  713. *
  714. * The result is returned as an array, with paths for it's keys.
  715. * The result may be returned out of order.
  716. *
  717. * @param array $paths
  718. * @param array $propertyNames
  719. * @return array
  720. */
  721. public function getPropertiesForMultiplePaths(array $paths, array $propertyNames = []) {
  722. $result = [
  723. ];
  724. $nodes = $this->tree->getMultipleNodes($paths);
  725. foreach($nodes as $path=>$node) {
  726. $result[$path] = $this->getPropertiesByNode($path, $node, $propertyNames);
  727. }
  728. return $result;
  729. }
  730. /**
  731. * Determines all properties for a node.
  732. *
  733. * This method tries to grab all properties for a node. This method is used
  734. * internally getPropertiesForPath and a few others.
  735. *
  736. * It could be useful to call this, if you already have an instance of your
  737. * target node and simply want to run through the system to get a correct
  738. * list of properties.
  739. *
  740. * @param string $path The path we're properties for fetching.
  741. * @param INode $node
  742. * @param array $propertyNames list of properties to fetch.
  743. * @return array
  744. */
  745. public function getPropertiesByNode($path, INode $node, array $propertyNames) {
  746. $newProperties = [
  747. '200' => [],
  748. '404' => [],
  749. ];
  750. // If no properties were supplied, it means this was an 'allprops'
  751. // request, and we use a default set of properties.
  752. $allProperties = count($propertyNames)===0;
  753. if ($allProperties) {
  754. // Default list of propertyNames, when all properties were requested.
  755. $propertyNames = [
  756. '{DAV:}getlastmodified',
  757. '{DAV:}getcontentlength',
  758. '{DAV:}resourcetype',
  759. '{DAV:}quota-used-bytes',
  760. '{DAV:}quota-available-bytes',
  761. '{DAV:}getetag',
  762. '{DAV:}getcontenttype',
  763. ];
  764. }
  765. // If the resourceType was not part of the list, we manually add it
  766. // and mark it for removal. We need to know the resourcetype in order
  767. // to make certain decisions about the entry.
  768. // WebDAV dictates we should add a / and the end of href's for collections
  769. $removeRT = false;
  770. if (!in_array('{DAV:}resourcetype',$propertyNames)) {
  771. $propertyNames[] = '{DAV:}resourcetype';
  772. $removeRT = true;
  773. }
  774. $result = $this->emit('beforeGetProperties',[$path, $node, &$propertyNames, &$newProperties]);
  775. // If this method explicitly returned false, we must ignore this
  776. // node as it is inaccessible.
  777. if ($result===false) return;
  778. if (count($propertyNames) > 0) {
  779. if ($node instanceof IProperties) {
  780. $nodeProperties = $node->getProperties($propertyNames);
  781. // The getProperties method may give us too much,
  782. // properties, in case the implementor was lazy.
  783. //
  784. // So as we loop through this list, we will only take the
  785. // properties that were actually requested and discard the
  786. // rest.
  787. foreach($propertyNames as $k=>$propertyName) {
  788. if (isset($nodeProperties[$propertyName])) {
  789. unset($propertyNames[$k]);
  790. $newProperties[200][$propertyName] = $nodeProperties[$propertyName];
  791. }
  792. }
  793. }
  794. }
  795. foreach($propertyNames as $prop) {
  796. if (isset($newProperties[200][$prop])) continue;
  797. switch($prop) {
  798. case '{DAV:}getlastmodified' : if ($node->getLastModified()) $newProperties[200][$prop] = new Property\GetLastModified($node->getLastModified()); break;
  799. case '{DAV:}getcontentlength' :
  800. if ($node instanceof IFile) {
  801. $size = $node->getSize();
  802. if (!is_null($size)) {
  803. $newProperties[200][$prop] = (int)$node->getSize();
  804. }
  805. }
  806. break;
  807. case '{DAV:}quota-used-bytes' :
  808. if ($node instanceof IQuota) {
  809. $quotaInfo = $node->getQuotaInfo();
  810. $newProperties[200][$prop] = $quotaInfo[0];
  811. }
  812. break;
  813. case '{DAV:}quota-available-bytes' :
  814. if ($node instanceof IQuota) {
  815. $quotaInfo = $node->getQuotaInfo();
  816. $newProperties[200][$prop] = $quotaInfo[1];
  817. }
  818. break;
  819. case '{DAV:}getetag' : if ($node instanceof IFile && $etag = $node->getETag()) $newProperties[200][$prop] = $etag; break;
  820. case '{DAV:}getcontenttype' : if ($node instanceof IFile && $ct = $node->getContentType()) $newProperties[200][$prop] = $ct; break;
  821. case '{DAV:}supported-report-set' :
  822. $reports = [];
  823. foreach($this->plugins as $plugin) {
  824. $reports = array_merge($reports, $plugin->getSupportedReportSet($path));
  825. }
  826. $newProperties[200][$prop] = new Property\SupportedReportSet($reports);
  827. break;
  828. case '{DAV:}resourcetype' :
  829. $newProperties[200]['{DAV:}resourcetype'] = new Property\ResourceType();
  830. foreach($this->resourceTypeMapping as $className => $resourceType) {
  831. if ($node instanceof $className) $newProperties[200]['{DAV:}resourcetype']->add($resourceType);
  832. }
  833. break;
  834. }
  835. // If we were unable to find the property, we will list it as 404.
  836. if (!$allProperties && !isset($newProperties[200][$prop])) $newProperties[404][$prop] = null;
  837. }
  838. $this->emit('afterGetProperties',[trim($path,'/'),&$newProperties, $node]);
  839. $newProperties['href'] = trim($path,'/');
  840. // Its is a WebDAV recommendation to add a trailing slash to collectionnames.
  841. // Apple's iCal also requires a trailing slash for principals (rfc 3744), though this is non-standard.
  842. if ($path!='' && isset($newProperties[200]['{DAV:}resourcetype'])) {
  843. $rt = $newProperties[200]['{DAV:}resourcetype'];
  844. if ($rt->is('{DAV:}collection') || $rt->is('{DAV:}principal')) {
  845. $newProperties['href'] .='/';
  846. }
  847. }
  848. // If the resourcetype property was manually added to the requested property list,
  849. // we will remove it again.
  850. if ($removeRT) unset($newProperties[200]['{DAV:}resourcetype']);
  851. return $newProperties;
  852. }
  853. /**
  854. * This method is invoked by sub-systems creating a new file.
  855. *
  856. * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin).
  857. * It was important to get this done through a centralized function,
  858. * allowing plugins to intercept this using the beforeCreateFile event.
  859. *
  860. * This method will return true if the file was actually created
  861. *
  862. * @param string $uri
  863. * @param resource $data
  864. * @param string $etag
  865. * @return bool
  866. */
  867. public function createFile($uri,$data, &$etag = null) {
  868. list($dir,$name) = URLUtil::splitPath($uri);
  869. if (!$this->emit('beforeBind',[$uri])) return false;
  870. $parent = $this->tree->getNodeForPath($dir);
  871. if (!$parent instanceof ICollection) {
  872. throw new Exception\Conflict('Files can only be created as children of collections');
  873. }
  874. // It is possible for an event handler to modify the content of the
  875. // body, before it gets written. If this is the case, $modified
  876. // should be set to true.
  877. //
  878. // If $modified is true, we must not send back an etag.
  879. $modified = false;
  880. if (!$this->emit('beforeCreateFile',[$uri, &$data, $parent, &$modified])) return false;
  881. $etag = $parent->createFile($name,$data);
  882. if ($modified) $etag = null;
  883. $this->tree->markDirty($dir . '/' . $name);
  884. $this->emit('afterBind',[$uri]);
  885. $this->emit('afterCreateFile',[$uri, $parent]);
  886. return true;
  887. }
  888. /**
  889. * This method is invoked by sub-systems creating a new directory.
  890. *
  891. * @param string $uri
  892. * @return void
  893. */
  894. public function createDirectory($uri) {
  895. $this->createCollection($uri,['{DAV:}collection'], []);
  896. }
  897. /**
  898. * Use this method to create a new collection
  899. *
  900. * The {DAV:}resourcetype is specified using the resourceType array.
  901. * At the very least it must contain {DAV:}collection.
  902. *
  903. * The properties array can contain a list of additional properties.
  904. *
  905. * @param string $uri The new uri
  906. * @param array $resourceType The resourceType(s)
  907. * @param array $properties A list of properties
  908. * @return array|null
  909. */
  910. public function createCollection($uri, array $resourceType, array $properties) {
  911. list($parentUri,$newName) = URLUtil::splitPath($uri);
  912. // Making sure {DAV:}collection was specified as resourceType
  913. if (!in_array('{DAV:}collection', $resourceType)) {
  914. throw new Exception\InvalidResourceType('The resourceType for this collection must at least include {DAV:}collection');
  915. }
  916. // Making sure the parent exists
  917. try {
  918. $parent = $this->tree->getNodeForPath($parentUri);
  919. } catch (Exception\NotFound $e) {
  920. throw new Exception\Conflict('Parent node does not exist');
  921. }
  922. // Making sure the parent is a collection
  923. if (!$parent instanceof ICollection) {
  924. throw new Exception\Conflict('Parent node is not a collection');
  925. }
  926. // Making sure the child does not already exist
  927. try {
  928. $parent->getChild($newName);
  929. // If we got here.. it means there's already a node on that url, and we need to throw a 405
  930. throw new Exception\MethodNotAllowed('The resource you tried to create already exists');
  931. } catch (Exception\NotFound $e) {
  932. // This is correct
  933. }
  934. if (!$this->emit('beforeBind',[$uri])) return;
  935. // There are 2 modes of operation. The standard collection
  936. // creates the directory, and then updates properties
  937. // the extended collection can create it directly.
  938. if ($parent instanceof IExtendedCollection) {
  939. $parent->createExtendedCollection($newName, $resourceType, $properties);
  940. } else {
  941. // No special resourcetypes are supported
  942. if (count($resourceType)>1) {
  943. throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.');
  944. }
  945. $parent->createDirectory($newName);
  946. $rollBack = false;
  947. $exception = null;
  948. $errorResult = null;
  949. if (count($properties)>0) {
  950. try {
  951. $errorResult = $this->updateProperties($uri, $properties);
  952. if (!isset($errorResult[200])) {
  953. $rollBack = true;
  954. }
  955. } catch (Exception $e) {
  956. $rollBack = true;
  957. $exception = $e;
  958. }
  959. }
  960. if ($rollBack) {
  961. if (!$this->emit('beforeUnbind',[$uri])) return;
  962. $this->tree->delete($uri);
  963. // Re-throwing exception
  964. if ($exception) throw $exception;
  965. return $errorResult;
  966. }
  967. }
  968. $this->tree->markDirty($parentUri);
  969. $this->emit('afterBind',[$uri]);
  970. }
  971. /**
  972. * This method updates a resource's properties
  973. *
  974. * The properties array must be a list of properties. Array-keys are
  975. * property names in clarknotation, array-values are it's values.
  976. * If a property must be deleted, the value should be null.
  977. *
  978. * Note that this request should either completely succeed, or
  979. * completely fail.
  980. *
  981. * The response is an array with statuscodes for keys, which in turn
  982. * contain arrays with propertynames. This response can be used
  983. * to generate a multistatus body.
  984. *
  985. * @param string $uri
  986. * @param array $properties
  987. * @return array
  988. */
  989. public function updateProperties($uri, array $properties) {
  990. // we'll start by grabbing the node, this will throw the appropriate
  991. // exceptions if it doesn't.
  992. $node = $this->tree->getNodeForPath($uri);
  993. $result = [
  994. 200 => [],
  995. 403 => [],
  996. 424 => [],
  997. ];
  998. $remainingProperties = $properties;
  999. $hasError = false;
  1000. // Running through all properties to make sure none of them are protected
  1001. if (!$hasError) foreach($properties as $propertyName => $value) {
  1002. if(in_array($propertyName, $this->protectedProperties)) {
  1003. $result[403][$propertyName] = null;
  1004. unset($remainingProperties[$propertyName]);
  1005. $hasError = true;
  1006. }
  1007. }
  1008. if (!$hasError) {
  1009. // Allowing plugins to take care of property updating
  1010. $hasError = !$this->emit('updateProperties', [
  1011. &$remainingProperties,
  1012. &$result,
  1013. $node
  1014. ]);
  1015. }
  1016. // If the node is not an instance of Sabre\DAV\IProperties, every
  1017. // property is 403 Forbidden
  1018. if (!$hasError && count($remainingProperties) && !($node instanceof IProperties)) {
  1019. $hasError = true;
  1020. foreach($properties as $propertyName=> $value) {
  1021. $result[403][$propertyName] = null;
  1022. }
  1023. $remainingProperties = [];
  1024. }
  1025. // Only if there were no errors we may attempt to update the resource
  1026. if (!$hasError) {
  1027. if (count($remainingProperties)>0) {
  1028. $updateResult = $node->updateProperties($remainingProperties);
  1029. if ($updateResult===true) {
  1030. // success
  1031. foreach($remainingProperties as $propertyName=>$value) {
  1032. $result[200][$propertyName] = null;
  1033. }
  1034. } elseif ($updateResult===false) {
  1035. // The node failed to update the properties for an
  1036. // unknown reason
  1037. foreach($remainingProperties as $propertyName=>$value) {
  1038. $result[403][$propertyName] = null;
  1039. }
  1040. } elseif (is_array($updateResult)) {
  1041. // The node has detailed update information
  1042. // We need to merge the results with the earlier results.
  1043. foreach($updateResult as $status => $props) {
  1044. if (is_array($props)) {
  1045. if (!isset($result[$status]))
  1046. $result[$status] = [];
  1047. $result[$status] = array_merge($result[$status], $updateResult[$status]);
  1048. }
  1049. }
  1050. } else {
  1051. throw new Exception('Invalid result from updateProperties');
  1052. }
  1053. $remainingProperties = [];
  1054. }
  1055. }
  1056. foreach($remainingProperties as $propertyName=>$value) {
  1057. // if there are remaining properties, it must mean
  1058. // there's a dependency failure
  1059. $result[424][$propertyName] = null;
  1060. }
  1061. // Removing empty array values
  1062. foreach($result as $status=>$props) {
  1063. if (count($props)===0) unset($result[$status]);
  1064. }
  1065. $result['href'] = $uri;
  1066. return $result;
  1067. }
  1068. /**
  1069. * This method checks the main HTTP preconditions.
  1070. *
  1071. * Currently these are:
  1072. * * If-Match
  1073. * * If-None-Match
  1074. * * If-Modified-Since
  1075. * * If-Unmodified-Since
  1076. *
  1077. * The method will return true if all preconditions are met
  1078. * The method will return false, or throw an exception if preconditions
  1079. * failed. If false is returned the operation should be aborted, and
  1080. * the appropriate HTTP response headers are already set.
  1081. *
  1082. * Normally this method will throw 412 Precondition Failed for failures
  1083. * related to If-None-Match, If-Match and If-Unmodified Since. It will
  1084. * set the status to 304 Not Modified for If-Modified_since.
  1085. *
  1086. * If the $handleAsGET argument is set to true, it will also return 304
  1087. * Not Modified for failure of the If-None-Match precondition. This is the
  1088. * desired behaviour for HTTP GET and HTTP HEAD requests.
  1089. *
  1090. * @param bool $handleAsGET
  1091. * @return bool
  1092. */
  1093. public function checkPreconditions($handleAsGET = false) {
  1094. $uri = $this->getRequestUri();
  1095. $node = null;
  1096. $lastMod = null;
  1097. $etag = null;
  1098. if ($ifMatch = $this->httpRequest->getHeader('If-Match')) {
  1099. // If-Match contains an entity tag. Only if the entity-tag
  1100. // matches we are allowed to make the request succeed.
  1101. // If the entity-tag is '*' we are only allowed to make the
  1102. // request succeed if a resource exists at that url.
  1103. try {
  1104. $node = $this->tree->getNodeForPath($uri);
  1105. } catch (Exception\NotFound $e) {
  1106. throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist','If-Match');
  1107. }
  1108. // Only need to check entity tags if they are not *
  1109. if ($ifMatch!=='*') {
  1110. // There can be multiple etags
  1111. $ifMatch = explode(',',$ifMatch);
  1112. $haveMatch = false;
  1113. foreach($ifMatch as $ifMatchItem) {
  1114. // Stripping any extra spaces
  1115. $ifMatchItem = trim($ifMatchItem,' ');
  1116. $etag = $node->getETag();
  1117. if ($etag===$ifMatchItem) {
  1118. $haveMatch = true;
  1119. } else {
  1120. // Evolution has a bug where it sometimes prepends the "
  1121. // with a \. This is our workaround.
  1122. if (str_replace('\\"','"', $ifMatchItem) === $etag) {
  1123. $haveMatch = true;
  1124. }
  1125. }
  1126. }
  1127. if (!$haveMatch) {
  1128. throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.','If-Match');
  1129. }
  1130. }
  1131. }
  1132. if ($ifNoneMatch = $this->httpRequest->getHeader('If-None-Match')) {
  1133. // The If-None-Match header contains an etag.
  1134. // Only if the ETag does not match the current ETag, the request will succeed
  1135. // The header can also contain *, in which case the request
  1136. // will only succeed if the entity does not exist at all.
  1137. $nodeExists = true;
  1138. if (!$node) {
  1139. try {
  1140. $node = $this->tree->getNodeForPath($uri);
  1141. } catch (Exception\NotFound $e) {
  1142. $nodeExists = false;
  1143. }
  1144. }
  1145. if ($nodeExists) {
  1146. $haveMatch = false;
  1147. if ($ifNoneMatch==='*') $haveMatch = true;
  1148. else {
  1149. // There might be multiple etags
  1150. $ifNoneMatch = explode(',', $ifNoneMatch);
  1151. $etag = $node->getETag();
  1152. foreach($ifNoneMatch as $ifNoneMatchItem) {
  1153. // Stripping any extra spaces
  1154. $ifNoneMatchItem = trim($ifNoneMatchItem,' ');
  1155. if ($etag===$ifNoneMatchItem) $haveMatch = true;
  1156. }
  1157. }
  1158. if ($haveMatch) {
  1159. if ($handleAsGET) {
  1160. $this->httpResponse->setStatus(304);
  1161. return false;
  1162. } else {
  1163. throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).','If-None-Match');
  1164. }
  1165. }
  1166. }
  1167. }
  1168. if (!$ifNoneMatch && ($ifModifiedSince = $this->httpRequest->getHeader('If-Modified-Since'))) {
  1169. // The If-Modified-Since header contains a date. We
  1170. // will only return the entity if it has been changed since
  1171. // that date. If it hasn't been changed, we return a 304
  1172. // header
  1173. // Note that this header only has to be checked if there was no If-None-Match header
  1174. // as per the HTTP spec.
  1175. $date = HTTP\Util::parseHTTPDate($ifModifiedSince);
  1176. if ($date) {
  1177. if (is_null($node)) {
  1178. $node = $this->tree->getNodeForPath($uri);
  1179. }
  1180. $lastMod = $node->getLastModified();
  1181. if ($lastMod) {
  1182. $lastMod = new \DateTime('@' . $lastMod);
  1183. if ($lastMod <= $date) {
  1184. $this->httpResponse->setStatus(304);
  1185. $this->httpResponse->setHeader('Last-Modified', HTTP\Util::toHTTPDate($lastMod));
  1186. return false;
  1187. }
  1188. }
  1189. }
  1190. }
  1191. if ($ifUnmodifiedSince = $this->httpRequest->getHeader('If-Unmodified-Since')) {
  1192. // The If-Unmodified-Since will allow allow the request if the
  1193. // entity has not changed since the specified date.
  1194. $date = HTTP\Util::parseHTTPDate($ifUnmodifiedSince);
  1195. // We must only check the date if it's valid
  1196. if ($date) {
  1197. if (is_null($node)) {
  1198. $node = $this->tree->getNodeForPath($uri);
  1199. }
  1200. $lastMod = $node->getLastModified();
  1201. if ($lastMod) {
  1202. $lastMod = new \DateTime('@' . $lastMod);
  1203. if ($lastMod > $date) {
  1204. throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.','If-Unmodified-Since');
  1205. }
  1206. }
  1207. }
  1208. }
  1209. // Now the hardest, the If: header. The If: header can contain multiple
  1210. // urls, etags and so-called 'state tokens'.
  1211. //
  1212. // Examples of state tokens include lock-tokens (as defined in rfc4918)
  1213. // and sync-tokens (as defined in rfc6578).
  1214. //
  1215. // The only proper way to deal with these, is to emit events, that a
  1216. // Sync and Lock plugin can pick up.
  1217. $ifConditions = $this->getIfConditions();
  1218. foreach($ifConditions as $kk => $ifCondition) {
  1219. foreach($ifCondition['tokens'] as $ii => $token) {
  1220. $ifConditions[$kk]['tokens'][$ii]['validToken'] = false;
  1221. }
  1222. }
  1223. // Plugins are responsible for validating all the tokens.
  1224. // If a plugin deemed a token 'valid', it will set 'validToken' to
  1225. // true.
  1226. $this->emit('validateTokens', [ &$ifConditions ]);
  1227. // Now we're going to analyze the result.
  1228. // Every ifCondition needs to validate to true, so we exit as soon as
  1229. // we have an invalid condition.
  1230. foreach($ifConditions as $ifCondition) {
  1231. $uri = $ifCondition['uri'];
  1232. $tokens = $ifCondition['tokens'];
  1233. // We only need 1 valid token for the condition to succeed.
  1234. foreach($tokens as $token) {
  1235. $tokenValid = $token['validToken'] || !$token['token'];
  1236. $etagValid = false;
  1237. if (!$token['etag']) {
  1238. $etagValid = true;
  1239. }
  1240. // Checking the etag, only if the token was already deamed
  1241. // valid and there is one.
  1242. if ($token['etag'] && $tokenValid) {
  1243. // The token was valid, and there was an etag.. We must
  1244. // grab the current etag and check it.
  1245. $node = $this->tree->getNodeForPath($uri);
  1246. $etagValid = $node->getETag() == $token['etag'];
  1247. }
  1248. if (($tokenValid && $etagValid) ^ $token['negate']) {
  1249. // Both were valid, so we can go to the next condition.
  1250. continue 2;
  1251. }
  1252. }
  1253. // If we ended here, it means there was no valid etag + token
  1254. // combination found for the current condition. This means we fail!
  1255. throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for ' . $uri, 'If');
  1256. }
  1257. return true;
  1258. }
  1259. /**
  1260. * This method is created to extract information from the WebDAV HTTP 'If:' header
  1261. *
  1262. * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information
  1263. * The function will return an array, containing structs with the following keys
  1264. *
  1265. * * uri - the uri the condition applies to.
  1266. * * tokens - The lock token. another 2 dimensional array containing 3 elements
  1267. *
  1268. * Example 1:
  1269. *
  1270. * If: (<opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>)
  1271. *
  1272. * Would result in:
  1273. *
  1274. * [
  1275. * [
  1276. * 'uri' => '/request/uri',
  1277. * 'tokens' => [
  1278. * [
  1279. * [
  1280. * 'negate' => false,
  1281. * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2',
  1282. * 'etag' => ""
  1283. * ]
  1284. * ]
  1285. * ],
  1286. * ]
  1287. * ]
  1288. *
  1289. * Example 2:
  1290. *
  1291. * If: </path/> (Not <opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> ["Im An ETag"]) (["Another ETag"]) </path2/> (Not ["Path2 ETag"])
  1292. *
  1293. * Would result in:
  1294. *
  1295. * [
  1296. * [
  1297. * 'uri' => 'path',
  1298. * 'tokens' => [
  1299. * [
  1300. * [
  1301. * 'negate' => true,
  1302. * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2',
  1303. * 'etag' => '"Im An ETag"'
  1304. * ],
  1305. * [
  1306. * 'negate' => false,
  1307. * 'token' => '',
  1308. * 'etag' => '"Another ETag"'
  1309. * ]
  1310. * ]
  1311. * ],
  1312. * ],
  1313. * [
  1314. * 'uri' => 'path2',
  1315. * 'tokens' => [
  1316. * [
  1317. * [
  1318. * 'negate' => true,
  1319. * 'token' => '',
  1320. * 'etag' => '"Path2 ETag"'
  1321. * ]
  1322. * ]
  1323. * ],
  1324. * ],
  1325. * ]
  1326. *
  1327. * @return array
  1328. */
  1329. public function getIfConditions() {
  1330. $header = $this->httpRequest->getHeader('If');
  1331. if (!$header) return [];
  1332. $matches = [];
  1333. $regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im';
  1334. preg_match_all($regex,$header,$matches,PREG_SET_ORDER);
  1335. $conditions = [];
  1336. foreach($matches as $match) {
  1337. // If there was no uri specified in this match, and there were
  1338. // already conditions parsed, we add the condition to the list of
  1339. // conditions for the previous uri.
  1340. if (!$match['uri'] && count($conditions)) {
  1341. $conditions[count($conditions)-1]['tokens'][] = [
  1342. 'negate' => $match['not']?true:false,
  1343. 'token' => $match['token'],
  1344. 'etag' => isset($match['etag'])?$match['etag']:''
  1345. ];
  1346. } else {
  1347. if (!$match['uri']) {
  1348. $realUri = $this->getRequestUri();
  1349. } else {
  1350. $realUri = $this->calculateUri($match['uri']);
  1351. }
  1352. $conditions[] = [
  1353. 'uri' => $realUri,
  1354. 'tokens' => [
  1355. [
  1356. 'negate' => $match['not']?true:false,
  1357. 'token' => $match['token'],
  1358. 'etag' => isset($match['etag'])?$match['etag']:''
  1359. ]
  1360. ],
  1361. ];
  1362. }
  1363. }
  1364. return $conditions;
  1365. }
  1366. // }}}
  1367. // {{{ XML Readers & Writers
  1368. /**
  1369. * Generates a WebDAV propfind response body based on a list of nodes.
  1370. *
  1371. * If 'strip404s' is set to true, all 404 responses will be removed.
  1372. *
  1373. * @param array $fileProperties The list with nodes
  1374. * @param bool strip404s
  1375. * @return string
  1376. */
  1377. public function generateMultiStatus(array $fileProperties, $strip404s = false) {
  1378. $dom = new \DOMDocument('1.0','utf-8');
  1379. //$dom->formatOutput = true;
  1380. $multiStatus = $dom->createElement('d:multistatus');
  1381. $dom->appendChild($multiStatus);
  1382. // Adding in default namespaces
  1383. foreach($this->xmlNamespaces as $namespace=>$prefix) {
  1384. $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
  1385. }
  1386. foreach($fileProperties as $entry) {
  1387. $href = $entry['href'];
  1388. unset($entry['href']);
  1389. if ($strip404s && isset($entry[404])) {
  1390. unset($entry[404]);
  1391. }
  1392. $response = new Property\Response($href,$entry);
  1393. $response->serialize($this,$multiStatus);
  1394. }
  1395. return $dom->saveXML();
  1396. }
  1397. /**
  1398. * This method parses a PropPatch request
  1399. *
  1400. * PropPatch changes the properties for a resource. This method
  1401. * returns a list of properties.
  1402. *
  1403. * The keys in the returned array contain the property name (e.g.: {DAV:}displayname,
  1404. * and the value contains the property value. If a property is to be removed the value
  1405. * will be null.
  1406. *
  1407. * @param string $body xml body
  1408. * @return array list of properties in need of updating or deletion
  1409. */
  1410. public function parsePropPatchRequest($body) {
  1411. //We'll need to change the DAV namespace declaration to something else in order to make it parsable
  1412. $dom = XMLUtil::loadDOMDocument($body);
  1413. $newProperties = [];
  1414. foreach($dom->firstChild->childNodes as $child) {
  1415. if ($child->nodeType !== XML_ELEMENT_NODE) continue;
  1416. $operation = XMLUtil::toClarkNotation($child);
  1417. if ($operation!=='{DAV:}set' && $operation!=='{DAV:}remove') continue;
  1418. $innerProperties = XMLUtil::parseProperties($child, $this->propertyMap);
  1419. foreach($innerProperties as $propertyName=>$propertyValue) {
  1420. if ($operation==='{DAV:}remove') {
  1421. $propertyValue = null;
  1422. }
  1423. $newProperties[$propertyName] = $propertyValue;
  1424. }
  1425. }
  1426. return $newProperties;
  1427. }
  1428. /**
  1429. * This method parses the PROPFIND request and returns its information
  1430. *
  1431. * This will either be a list of properties, or an empty array; in which case
  1432. * an {DAV:}allprop was requested.
  1433. *
  1434. * @param string $body
  1435. * @return array
  1436. */
  1437. public function parsePropFindRequest($body) {
  1438. // If the propfind body was empty, it means IE is requesting 'all' properties
  1439. if (!$body) return [];
  1440. $dom = XMLUtil::loadDOMDocument($body);
  1441. $elem = $dom->getElementsByTagNameNS('urn:DAV','propfind')->item(0);
  1442. if (is_null($elem)) throw new Exception\UnsupportedMediaType('We could not find a {DAV:}propfind element in the xml request body');
  1443. return array_keys(XMLUtil::parseProperties($elem));
  1444. }
  1445. // }}}
  1446. }