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