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