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