Server.php 55 KB

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