1<?php 2 3namespace Sabre\DAV; 4 5use Psr\Log\LoggerAwareInterface; 6use Psr\Log\LoggerAwareTrait; 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 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 foreach ($this->tree->getChildren($path) as $childNode) { 897 $subPropFind = clone $propFind; 898 $subPropFind->setDepth($newDepth); 899 if ($path !== '') { 900 $subPath = $path . '/' . $childNode->getName(); 901 } else { 902 $subPath = $childNode->getName(); 903 } 904 $subPropFind->setPath($subPath); 905 906 yield [ 907 $subPropFind, 908 $childNode 909 ]; 910 911 if (($newDepth === self::DEPTH_INFINITY || $newDepth >= 1) && $childNode instanceof ICollection) { 912 foreach ($this->generatePathNodes($subPropFind) as $subItem) { 913 yield $subItem; 914 } 915 } 916 917 } 918 } 919 920 /** 921 * Returns a list of properties for a given path 922 * 923 * The path that should be supplied should have the baseUrl stripped out 924 * The list of properties should be supplied in Clark notation. If the list is empty 925 * 'allprops' is assumed. 926 * 927 * If a depth of 1 is requested child elements will also be returned. 928 * 929 * @param string $path 930 * @param array $propertyNames 931 * @param int $depth 932 * @return array 933 * 934 * @deprecated Use getPropertiesIteratorForPath() instead (as it's more memory efficient) 935 * @see getPropertiesIteratorForPath() 936 */ 937 function getPropertiesForPath($path, $propertyNames = [], $depth = 0) { 938 939 return iterator_to_array($this->getPropertiesIteratorForPath($path, $propertyNames, $depth)); 940 941 } 942 /** 943 * Returns a list of properties for a given path 944 * 945 * The path that should be supplied should have the baseUrl stripped out 946 * The list of properties should be supplied in Clark notation. If the list is empty 947 * 'allprops' is assumed. 948 * 949 * If a depth of 1 is requested child elements will also be returned. 950 * 951 * @param string $path 952 * @param array $propertyNames 953 * @param int $depth 954 * @return \Iterator 955 */ 956 function getPropertiesIteratorForPath($path, $propertyNames = [], $depth = 0) { 957 958 // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled 959 if (!$this->enablePropfindDepthInfinity && $depth != 0) $depth = 1; 960 961 $path = trim($path, '/'); 962 963 $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS; 964 $propFind = new PropFind($path, (array)$propertyNames, $depth, $propFindType); 965 966 $parentNode = $this->tree->getNodeForPath($path); 967 968 $propFindRequests = [[ 969 $propFind, 970 $parentNode 971 ]]; 972 973 if (($depth > 0 || $depth === self::DEPTH_INFINITY) && $parentNode instanceof ICollection) { 974 $propFindRequests = $this->generatePathNodes(clone $propFind, current($propFindRequests)); 975 } 976 977 foreach ($propFindRequests as $propFindRequest) { 978 979 list($propFind, $node) = $propFindRequest; 980 $r = $this->getPropertiesByNode($propFind, $node); 981 if ($r) { 982 $result = $propFind->getResultForMultiStatus(); 983 $result['href'] = $propFind->getPath(); 984 985 // WebDAV recommends adding a slash to the path, if the path is 986 // a collection. 987 // Furthermore, iCal also demands this to be the case for 988 // principals. This is non-standard, but we support it. 989 $resourceType = $this->getResourceTypeForNode($node); 990 if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { 991 $result['href'] .= '/'; 992 } 993 yield $result; 994 } 995 996 } 997 998 } 999 1000 /** 1001 * Returns a list of properties for a list of paths. 1002 * 1003 * The path that should be supplied should have the baseUrl stripped out 1004 * The list of properties should be supplied in Clark notation. If the list is empty 1005 * 'allprops' is assumed. 1006 * 1007 * The result is returned as an array, with paths for it's keys. 1008 * The result may be returned out of order. 1009 * 1010 * @param array $paths 1011 * @param array $propertyNames 1012 * @return array 1013 */ 1014 function getPropertiesForMultiplePaths(array $paths, array $propertyNames = []) { 1015 1016 $result = [ 1017 ]; 1018 1019 $nodes = $this->tree->getMultipleNodes($paths); 1020 1021 foreach ($nodes as $path => $node) { 1022 1023 $propFind = new PropFind($path, $propertyNames); 1024 $r = $this->getPropertiesByNode($propFind, $node); 1025 if ($r) { 1026 $result[$path] = $propFind->getResultForMultiStatus(); 1027 $result[$path]['href'] = $path; 1028 1029 $resourceType = $this->getResourceTypeForNode($node); 1030 if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { 1031 $result[$path]['href'] .= '/'; 1032 } 1033 } 1034 1035 } 1036 1037 return $result; 1038 1039 } 1040 1041 1042 /** 1043 * Determines all properties for a node. 1044 * 1045 * This method tries to grab all properties for a node. This method is used 1046 * internally getPropertiesForPath and a few others. 1047 * 1048 * It could be useful to call this, if you already have an instance of your 1049 * target node and simply want to run through the system to get a correct 1050 * list of properties. 1051 * 1052 * @param PropFind $propFind 1053 * @param INode $node 1054 * @return bool 1055 */ 1056 function getPropertiesByNode(PropFind $propFind, INode $node) { 1057 1058 return $this->emit('propFind', [$propFind, $node]); 1059 1060 } 1061 1062 /** 1063 * This method is invoked by sub-systems creating a new file. 1064 * 1065 * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin). 1066 * It was important to get this done through a centralized function, 1067 * allowing plugins to intercept this using the beforeCreateFile event. 1068 * 1069 * This method will return true if the file was actually created 1070 * 1071 * @param string $uri 1072 * @param resource $data 1073 * @param string $etag 1074 * @return bool 1075 */ 1076 function createFile($uri, $data, &$etag = null) { 1077 1078 list($dir, $name) = URLUtil::splitPath($uri); 1079 1080 if (!$this->emit('beforeBind', [$uri])) return false; 1081 1082 $parent = $this->tree->getNodeForPath($dir); 1083 if (!$parent instanceof ICollection) { 1084 throw new Exception\Conflict('Files can only be created as children of collections'); 1085 } 1086 1087 // It is possible for an event handler to modify the content of the 1088 // body, before it gets written. If this is the case, $modified 1089 // should be set to true. 1090 // 1091 // If $modified is true, we must not send back an ETag. 1092 $modified = false; 1093 if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) return false; 1094 1095 $etag = $parent->createFile($name, $data); 1096 1097 if ($modified) $etag = null; 1098 1099 $this->tree->markDirty($dir . '/' . $name); 1100 1101 $this->emit('afterBind', [$uri]); 1102 $this->emit('afterCreateFile', [$uri, $parent]); 1103 1104 return true; 1105 } 1106 1107 /** 1108 * This method is invoked by sub-systems updating a file. 1109 * 1110 * This method will return true if the file was actually updated 1111 * 1112 * @param string $uri 1113 * @param resource $data 1114 * @param string $etag 1115 * @return bool 1116 */ 1117 function updateFile($uri, $data, &$etag = null) { 1118 1119 $node = $this->tree->getNodeForPath($uri); 1120 1121 // It is possible for an event handler to modify the content of the 1122 // body, before it gets written. If this is the case, $modified 1123 // should be set to true. 1124 // 1125 // If $modified is true, we must not send back an ETag. 1126 $modified = false; 1127 if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) return false; 1128 1129 $etag = $node->put($data); 1130 if ($modified) $etag = null; 1131 $this->emit('afterWriteContent', [$uri, $node]); 1132 1133 return true; 1134 } 1135 1136 1137 1138 /** 1139 * This method is invoked by sub-systems creating a new directory. 1140 * 1141 * @param string $uri 1142 * @return void 1143 */ 1144 function createDirectory($uri) { 1145 1146 $this->createCollection($uri, new MkCol(['{DAV:}collection'], [])); 1147 1148 } 1149 1150 /** 1151 * Use this method to create a new collection 1152 * 1153 * @param string $uri The new uri 1154 * @param MkCol $mkCol 1155 * @return array|null 1156 */ 1157 function createCollection($uri, MkCol $mkCol) { 1158 1159 list($parentUri, $newName) = URLUtil::splitPath($uri); 1160 1161 // Making sure the parent exists 1162 try { 1163 $parent = $this->tree->getNodeForPath($parentUri); 1164 1165 } catch (Exception\NotFound $e) { 1166 throw new Exception\Conflict('Parent node does not exist'); 1167 1168 } 1169 1170 // Making sure the parent is a collection 1171 if (!$parent instanceof ICollection) { 1172 throw new Exception\Conflict('Parent node is not a collection'); 1173 } 1174 1175 // Making sure the child does not already exist 1176 try { 1177 $parent->getChild($newName); 1178 1179 // If we got here.. it means there's already a node on that url, and we need to throw a 405 1180 throw new Exception\MethodNotAllowed('The resource you tried to create already exists'); 1181 1182 } catch (Exception\NotFound $e) { 1183 // NotFound is the expected behavior. 1184 } 1185 1186 1187 if (!$this->emit('beforeBind', [$uri])) return; 1188 1189 if ($parent instanceof IExtendedCollection) { 1190 1191 /** 1192 * If the parent is an instance of IExtendedCollection, it means that 1193 * we can pass the MkCol object directly as it may be able to store 1194 * properties immediately. 1195 */ 1196 $parent->createExtendedCollection($newName, $mkCol); 1197 1198 } else { 1199 1200 /** 1201 * If the parent is a standard ICollection, it means only 1202 * 'standard' collections can be created, so we should fail any 1203 * MKCOL operation that carries extra resourcetypes. 1204 */ 1205 if (count($mkCol->getResourceType()) > 1) { 1206 throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.'); 1207 } 1208 1209 $parent->createDirectory($newName); 1210 1211 } 1212 1213 // If there are any properties that have not been handled/stored, 1214 // we ask the 'propPatch' event to handle them. This will allow for 1215 // example the propertyStorage system to store properties upon MKCOL. 1216 if ($mkCol->getRemainingMutations()) { 1217 $this->emit('propPatch', [$uri, $mkCol]); 1218 } 1219 $success = $mkCol->commit(); 1220 1221 if (!$success) { 1222 $result = $mkCol->getResult(); 1223 1224 $formattedResult = [ 1225 'href' => $uri, 1226 ]; 1227 1228 foreach ($result as $propertyName => $status) { 1229 1230 if (!isset($formattedResult[$status])) { 1231 $formattedResult[$status] = []; 1232 } 1233 $formattedResult[$status][$propertyName] = null; 1234 1235 } 1236 return $formattedResult; 1237 } 1238 1239 $this->tree->markDirty($parentUri); 1240 $this->emit('afterBind', [$uri]); 1241 1242 } 1243 1244 /** 1245 * This method updates a resource's properties 1246 * 1247 * The properties array must be a list of properties. Array-keys are 1248 * property names in clarknotation, array-values are it's values. 1249 * If a property must be deleted, the value should be null. 1250 * 1251 * Note that this request should either completely succeed, or 1252 * completely fail. 1253 * 1254 * The response is an array with properties for keys, and http status codes 1255 * as their values. 1256 * 1257 * @param string $path 1258 * @param array $properties 1259 * @return array 1260 */ 1261 function updateProperties($path, array $properties) { 1262 1263 $propPatch = new PropPatch($properties); 1264 $this->emit('propPatch', [$path, $propPatch]); 1265 $propPatch->commit(); 1266 1267 return $propPatch->getResult(); 1268 1269 } 1270 1271 /** 1272 * This method checks the main HTTP preconditions. 1273 * 1274 * Currently these are: 1275 * * If-Match 1276 * * If-None-Match 1277 * * If-Modified-Since 1278 * * If-Unmodified-Since 1279 * 1280 * The method will return true if all preconditions are met 1281 * The method will return false, or throw an exception if preconditions 1282 * failed. If false is returned the operation should be aborted, and 1283 * the appropriate HTTP response headers are already set. 1284 * 1285 * Normally this method will throw 412 Precondition Failed for failures 1286 * related to If-None-Match, If-Match and If-Unmodified Since. It will 1287 * set the status to 304 Not Modified for If-Modified_since. 1288 * 1289 * @param RequestInterface $request 1290 * @param ResponseInterface $response 1291 * @return bool 1292 */ 1293 function checkPreconditions(RequestInterface $request, ResponseInterface $response) { 1294 1295 $path = $request->getPath(); 1296 $node = null; 1297 $lastMod = null; 1298 $etag = null; 1299 1300 if ($ifMatch = $request->getHeader('If-Match')) { 1301 1302 // If-Match contains an entity tag. Only if the entity-tag 1303 // matches we are allowed to make the request succeed. 1304 // If the entity-tag is '*' we are only allowed to make the 1305 // request succeed if a resource exists at that url. 1306 try { 1307 $node = $this->tree->getNodeForPath($path); 1308 } catch (Exception\NotFound $e) { 1309 throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match'); 1310 } 1311 1312 // Only need to check entity tags if they are not * 1313 if ($ifMatch !== '*') { 1314 1315 // There can be multiple ETags 1316 $ifMatch = explode(',', $ifMatch); 1317 $haveMatch = false; 1318 foreach ($ifMatch as $ifMatchItem) { 1319 1320 // Stripping any extra spaces 1321 $ifMatchItem = trim($ifMatchItem, ' '); 1322 1323 $etag = $node instanceof IFile ? $node->getETag() : null; 1324 if ($etag === $ifMatchItem) { 1325 $haveMatch = true; 1326 } else { 1327 // Evolution has a bug where it sometimes prepends the " 1328 // with a \. This is our workaround. 1329 if (str_replace('\\"', '"', $ifMatchItem) === $etag) { 1330 $haveMatch = true; 1331 } 1332 } 1333 1334 } 1335 if (!$haveMatch) { 1336 if ($etag) $response->setHeader('ETag', $etag); 1337 throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.', 'If-Match'); 1338 } 1339 } 1340 } 1341 1342 if ($ifNoneMatch = $request->getHeader('If-None-Match')) { 1343 1344 // The If-None-Match header contains an ETag. 1345 // Only if the ETag does not match the current ETag, the request will succeed 1346 // The header can also contain *, in which case the request 1347 // will only succeed if the entity does not exist at all. 1348 $nodeExists = true; 1349 if (!$node) { 1350 try { 1351 $node = $this->tree->getNodeForPath($path); 1352 } catch (Exception\NotFound $e) { 1353 $nodeExists = false; 1354 } 1355 } 1356 if ($nodeExists) { 1357 $haveMatch = false; 1358 if ($ifNoneMatch === '*') $haveMatch = true; 1359 else { 1360 1361 // There might be multiple ETags 1362 $ifNoneMatch = explode(',', $ifNoneMatch); 1363 $etag = $node instanceof IFile ? $node->getETag() : null; 1364 1365 foreach ($ifNoneMatch as $ifNoneMatchItem) { 1366 1367 // Stripping any extra spaces 1368 $ifNoneMatchItem = trim($ifNoneMatchItem, ' '); 1369 1370 if ($etag === $ifNoneMatchItem) $haveMatch = true; 1371 1372 } 1373 1374 } 1375 1376 if ($haveMatch) { 1377 if ($etag) $response->setHeader('ETag', $etag); 1378 if ($request->getMethod() === 'GET') { 1379 $response->setStatus(304); 1380 return false; 1381 } else { 1382 throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match'); 1383 } 1384 } 1385 } 1386 1387 } 1388 1389 if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) { 1390 1391 // The If-Modified-Since header contains a date. We 1392 // will only return the entity if it has been changed since 1393 // that date. If it hasn't been changed, we return a 304 1394 // header 1395 // Note that this header only has to be checked if there was no If-None-Match header 1396 // as per the HTTP spec. 1397 $date = HTTP\Util::parseHTTPDate($ifModifiedSince); 1398 1399 if ($date) { 1400 if (is_null($node)) { 1401 $node = $this->tree->getNodeForPath($path); 1402 } 1403 $lastMod = $node->getLastModified(); 1404 if ($lastMod) { 1405 $lastMod = new \DateTime('@' . $lastMod); 1406 if ($lastMod <= $date) { 1407 $response->setStatus(304); 1408 $response->setHeader('Last-Modified', HTTP\Util::toHTTPDate($lastMod)); 1409 return false; 1410 } 1411 } 1412 } 1413 } 1414 1415 if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) { 1416 1417 // The If-Unmodified-Since will allow allow the request if the 1418 // entity has not changed since the specified date. 1419 $date = HTTP\Util::parseHTTPDate($ifUnmodifiedSince); 1420 1421 // We must only check the date if it's valid 1422 if ($date) { 1423 if (is_null($node)) { 1424 $node = $this->tree->getNodeForPath($path); 1425 } 1426 $lastMod = $node->getLastModified(); 1427 if ($lastMod) { 1428 $lastMod = new \DateTime('@' . $lastMod); 1429 if ($lastMod > $date) { 1430 throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since'); 1431 } 1432 } 1433 } 1434 1435 } 1436 1437 // Now the hardest, the If: header. The If: header can contain multiple 1438 // urls, ETags and so-called 'state tokens'. 1439 // 1440 // Examples of state tokens include lock-tokens (as defined in rfc4918) 1441 // and sync-tokens (as defined in rfc6578). 1442 // 1443 // The only proper way to deal with these, is to emit events, that a 1444 // Sync and Lock plugin can pick up. 1445 $ifConditions = $this->getIfConditions($request); 1446 1447 foreach ($ifConditions as $kk => $ifCondition) { 1448 foreach ($ifCondition['tokens'] as $ii => $token) { 1449 $ifConditions[$kk]['tokens'][$ii]['validToken'] = false; 1450 } 1451 } 1452 1453 // Plugins are responsible for validating all the tokens. 1454 // If a plugin deemed a token 'valid', it will set 'validToken' to 1455 // true. 1456 $this->emit('validateTokens', [$request, &$ifConditions]); 1457 1458 // Now we're going to analyze the result. 1459 1460 // Every ifCondition needs to validate to true, so we exit as soon as 1461 // we have an invalid condition. 1462 foreach ($ifConditions as $ifCondition) { 1463 1464 $uri = $ifCondition['uri']; 1465 $tokens = $ifCondition['tokens']; 1466 1467 // We only need 1 valid token for the condition to succeed. 1468 foreach ($tokens as $token) { 1469 1470 $tokenValid = $token['validToken'] || !$token['token']; 1471 1472 $etagValid = false; 1473 if (!$token['etag']) { 1474 $etagValid = true; 1475 } 1476 // Checking the ETag, only if the token was already deemed 1477 // valid and there is one. 1478 if ($token['etag'] && $tokenValid) { 1479 1480 // The token was valid, and there was an ETag. We must 1481 // grab the current ETag and check it. 1482 $node = $this->tree->getNodeForPath($uri); 1483 $etagValid = $node instanceof IFile && $node->getETag() == $token['etag']; 1484 1485 } 1486 1487 1488 if (($tokenValid && $etagValid) ^ $token['negate']) { 1489 // Both were valid, so we can go to the next condition. 1490 continue 2; 1491 } 1492 1493 1494 } 1495 1496 // If we ended here, it means there was no valid ETag + token 1497 // combination found for the current condition. This means we fail! 1498 throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for ' . $uri, 'If'); 1499 1500 } 1501 1502 return true; 1503 1504 } 1505 1506 /** 1507 * This method is created to extract information from the WebDAV HTTP 'If:' header 1508 * 1509 * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information 1510 * The function will return an array, containing structs with the following keys 1511 * 1512 * * uri - the uri the condition applies to. 1513 * * tokens - The lock token. another 2 dimensional array containing 3 elements 1514 * 1515 * Example 1: 1516 * 1517 * If: (<opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>) 1518 * 1519 * Would result in: 1520 * 1521 * [ 1522 * [ 1523 * 'uri' => '/request/uri', 1524 * 'tokens' => [ 1525 * [ 1526 * [ 1527 * 'negate' => false, 1528 * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', 1529 * 'etag' => "" 1530 * ] 1531 * ] 1532 * ], 1533 * ] 1534 * ] 1535 * 1536 * Example 2: 1537 * 1538 * If: </path/> (Not <opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> ["Im An ETag"]) (["Another ETag"]) </path2/> (Not ["Path2 ETag"]) 1539 * 1540 * Would result in: 1541 * 1542 * [ 1543 * [ 1544 * 'uri' => 'path', 1545 * 'tokens' => [ 1546 * [ 1547 * [ 1548 * 'negate' => true, 1549 * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', 1550 * 'etag' => '"Im An ETag"' 1551 * ], 1552 * [ 1553 * 'negate' => false, 1554 * 'token' => '', 1555 * 'etag' => '"Another ETag"' 1556 * ] 1557 * ] 1558 * ], 1559 * ], 1560 * [ 1561 * 'uri' => 'path2', 1562 * 'tokens' => [ 1563 * [ 1564 * [ 1565 * 'negate' => true, 1566 * 'token' => '', 1567 * 'etag' => '"Path2 ETag"' 1568 * ] 1569 * ] 1570 * ], 1571 * ], 1572 * ] 1573 * 1574 * @param RequestInterface $request 1575 * @return array 1576 */ 1577 function getIfConditions(RequestInterface $request) { 1578 1579 $header = $request->getHeader('If'); 1580 if (!$header) return []; 1581 1582 $matches = []; 1583 1584 $regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im'; 1585 preg_match_all($regex, $header, $matches, PREG_SET_ORDER); 1586 1587 $conditions = []; 1588 1589 foreach ($matches as $match) { 1590 1591 // If there was no uri specified in this match, and there were 1592 // already conditions parsed, we add the condition to the list of 1593 // conditions for the previous uri. 1594 if (!$match['uri'] && count($conditions)) { 1595 $conditions[count($conditions) - 1]['tokens'][] = [ 1596 'negate' => $match['not'] ? true : false, 1597 'token' => $match['token'], 1598 'etag' => isset($match['etag']) ? $match['etag'] : '' 1599 ]; 1600 } else { 1601 1602 if (!$match['uri']) { 1603 $realUri = $request->getPath(); 1604 } else { 1605 $realUri = $this->calculateUri($match['uri']); 1606 } 1607 1608 $conditions[] = [ 1609 'uri' => $realUri, 1610 'tokens' => [ 1611 [ 1612 'negate' => $match['not'] ? true : false, 1613 'token' => $match['token'], 1614 'etag' => isset($match['etag']) ? $match['etag'] : '' 1615 ] 1616 ], 1617 1618 ]; 1619 } 1620 1621 } 1622 1623 return $conditions; 1624 1625 } 1626 1627 /** 1628 * Returns an array with resourcetypes for a node. 1629 * 1630 * @param INode $node 1631 * @return array 1632 */ 1633 function getResourceTypeForNode(INode $node) { 1634 1635 $result = []; 1636 foreach ($this->resourceTypeMapping as $className => $resourceType) { 1637 if ($node instanceof $className) $result[] = $resourceType; 1638 } 1639 return $result; 1640 1641 } 1642 1643 // }}} 1644 // {{{ XML Readers & Writers 1645 1646 1647 /** 1648 * Generates a WebDAV propfind response body based on a list of nodes. 1649 * 1650 * If 'strip404s' is set to true, all 404 responses will be removed. 1651 * 1652 * @param array|\Traversable $fileProperties The list with nodes 1653 * @param bool $strip404s 1654 * @return string 1655 */ 1656 function generateMultiStatus($fileProperties, $strip404s = false) { 1657 1658 $w = $this->xml->getWriter(); 1659 $w->openMemory(); 1660 $w->contextUri = $this->baseUri; 1661 $w->startDocument(); 1662 1663 $w->startElement('{DAV:}multistatus'); 1664 1665 foreach ($fileProperties as $entry) { 1666 1667 $href = $entry['href']; 1668 unset($entry['href']); 1669 if ($strip404s) { 1670 unset($entry[404]); 1671 } 1672 $response = new Xml\Element\Response( 1673 ltrim($href, '/'), 1674 $entry 1675 ); 1676 $w->write([ 1677 'name' => '{DAV:}response', 1678 'value' => $response 1679 ]); 1680 } 1681 $w->endElement(); 1682 1683 return $w->outputMemory(); 1684 1685 } 1686 1687} 1688