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