1<?php
2
3namespace Sabre\DAV;
4
5use Sabre\DAV\Exception\BadRequest;
6use Sabre\HTTP\RequestInterface;
7use Sabre\HTTP\ResponseInterface;
8use Sabre\Xml\ParseException;
9
10/**
11 * The core plugin provides all the basic features for a WebDAV server.
12 *
13 * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
14 * @author Evert Pot (http://evertpot.com/)
15 * @license http://sabre.io/license/ Modified BSD License
16 */
17class CorePlugin extends ServerPlugin {
18
19    /**
20     * Reference to server object.
21     *
22     * @var Server
23     */
24    protected $server;
25
26    /**
27     * Sets up the plugin
28     *
29     * @param Server $server
30     * @return void
31     */
32    function initialize(Server $server) {
33
34        $this->server = $server;
35        $server->on('method:GET',       [$this, 'httpGet']);
36        $server->on('method:OPTIONS',   [$this, 'httpOptions']);
37        $server->on('method:HEAD',      [$this, 'httpHead']);
38        $server->on('method:DELETE',    [$this, 'httpDelete']);
39        $server->on('method:PROPFIND',  [$this, 'httpPropFind']);
40        $server->on('method:PROPPATCH', [$this, 'httpPropPatch']);
41        $server->on('method:PUT',       [$this, 'httpPut']);
42        $server->on('method:MKCOL',     [$this, 'httpMkcol']);
43        $server->on('method:MOVE',      [$this, 'httpMove']);
44        $server->on('method:COPY',      [$this, 'httpCopy']);
45        $server->on('method:REPORT',    [$this, 'httpReport']);
46
47        $server->on('propPatch',        [$this, 'propPatchProtectedPropertyCheck'], 90);
48        $server->on('propPatch',        [$this, 'propPatchNodeUpdate'], 200);
49        $server->on('propFind',         [$this, 'propFind']);
50        $server->on('propFind',         [$this, 'propFindNode'], 120);
51        $server->on('propFind',         [$this, 'propFindLate'], 200);
52
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    function getPluginName() {
64
65        return 'core';
66
67    }
68
69    /**
70     * This is the default implementation for the GET method.
71     *
72     * @param RequestInterface $request
73     * @param ResponseInterface $response
74     * @return bool
75     */
76    function httpGet(RequestInterface $request, ResponseInterface $response) {
77
78        $path = $request->getPath();
79        $node = $this->server->tree->getNodeForPath($path, 0);
80
81        if (!$node instanceof IFile) return;
82
83        $body = $node->get();
84
85        // Converting string into stream, if needed.
86        if (is_string($body)) {
87            $stream = fopen('php://temp', 'r+');
88            fwrite($stream, $body);
89            rewind($stream);
90            $body = $stream;
91        }
92
93        /*
94         * TODO: getetag, getlastmodified, getsize should also be used using
95         * this method
96         */
97        $httpHeaders = $this->server->getHTTPHeaders($path);
98
99        /* ContentType needs to get a default, because many webservers will otherwise
100         * default to text/html, and we don't want this for security reasons.
101         */
102        if (!isset($httpHeaders['Content-Type'])) {
103            $httpHeaders['Content-Type'] = 'application/octet-stream';
104        }
105
106
107        if (isset($httpHeaders['Content-Length'])) {
108
109            $nodeSize = $httpHeaders['Content-Length'];
110
111            // Need to unset Content-Length, because we'll handle that during figuring out the range
112            unset($httpHeaders['Content-Length']);
113
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
128            // if IfRange is parsable as a date we'll treat it as a DateTime
129            // otherwise, we must treat it as an etag.
130            try {
131                $ifRangeDate = new \DateTime($ifRange);
132
133                // It's a date. We must check if the entity is modified since
134                // the specified date.
135                if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true;
136                else {
137                    $modified = new \DateTime($httpHeaders['Last-Modified']);
138                    if ($modified > $ifRangeDate) $ignoreRangeHeader = true;
139                }
140
141            } catch (\Exception $e) {
142
143                // It's an entity. We can do a simple comparison.
144                if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true;
145                elseif ($httpHeaders['ETag'] !== $ifRange) $ignoreRangeHeader = true;
146            }
147        }
148
149        // We're only going to support HTTP ranges if the backend provided a filesize
150        if (!$ignoreRangeHeader && $nodeSize && $range) {
151
152            // Determining the exact byte offsets
153            if (!is_null($range[0])) {
154
155                $start = $range[0];
156                $end = $range[1] ? $range[1] : $nodeSize - 1;
157                if ($start >= $nodeSize)
158                    throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')');
159
160                if ($end < $start) throw new Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')');
161                if ($end >= $nodeSize) $end = $nodeSize - 1;
162
163            } else {
164
165                $start = $nodeSize - $range[1];
166                $end  = $nodeSize - 1;
167
168                if ($start < 0) $start = 0;
169
170            }
171
172            // for a seekable $body stream we simply set the pointer
173            // for a non-seekable $body stream we read and discard just the
174            // right amount of data
175            if (stream_get_meta_data($body)['seekable']) {
176                fseek($body, $start, SEEK_SET);
177            } else {
178                $consumeBlock = 8192;
179                for ($consumed = 0; $start - $consumed > 0;){
180                    if (feof($body)) throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $start . ') exceeded the size of the entity (' . $consumed . ')');
181                    $consumed += strlen(fread($body, min($start - $consumed, $consumeBlock)));
182                }
183            }
184
185            $response->setHeader('Content-Length', $end - $start + 1);
186            $response->setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $nodeSize);
187            $response->setStatus(206);
188            $response->setBody($body);
189
190        } else {
191
192            if ($nodeSize) $response->setHeader('Content-Length', $nodeSize);
193            $response->setStatus(200);
194            $response->setBody($body);
195
196        }
197        // Sending back false will interupt the event chain and tell the server
198        // we've handled this method.
199        return false;
200
201    }
202
203    /**
204     * HTTP OPTIONS
205     *
206     * @param RequestInterface $request
207     * @param ResponseInterface $response
208     * @return bool
209     */
210    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 interupt the event chain and tell the server
228        // we've handled this method.
229        return false;
230
231    }
232
233    /**
234     * HTTP HEAD
235     *
236     * This method is normally used to take a peak at a url, and only get the
237     * HTTP response headers, without the body. This is used by clients to
238     * determine if a remote file was changed, so they can use a local cached
239     * version, instead of downloading it again
240     *
241     * @param RequestInterface $request
242     * @param ResponseInterface $response
243     * @return bool
244     */
245    function httpHead(RequestInterface $request, ResponseInterface $response) {
246
247        // This is implemented by changing the HEAD request to a GET request,
248        // and dropping the response body.
249        $subRequest = clone $request;
250        $subRequest->setMethod('GET');
251
252        try {
253            $this->server->invokeMethod($subRequest, $response, false);
254            $response->setBody('');
255        } catch (Exception\NotImplemented $e) {
256            // Some clients may do HEAD requests on collections, however, GET
257            // requests and HEAD requests _may_ not be defined on a collection,
258            // which would trigger a 501.
259            // This breaks some clients though, so we're transforming these
260            // 501s into 200s.
261            $response->setStatus(200);
262            $response->setBody('');
263            $response->setHeader('Content-Type', 'text/plain');
264            $response->setHeader('X-Sabre-Real-Status', $e->getHTTPCode());
265        }
266
267        // Sending back false will interupt the event chain and tell the server
268        // we've handled this method.
269        return false;
270
271    }
272
273    /**
274     * HTTP Delete
275     *
276     * The HTTP delete method, deletes a given uri
277     *
278     * @param RequestInterface $request
279     * @param ResponseInterface $response
280     * @return void
281     */
282    function httpDelete(RequestInterface $request, ResponseInterface $response) {
283
284        $path = $request->getPath();
285
286        if (!$this->server->emit('beforeUnbind', [$path])) return false;
287        $this->server->tree->delete($path);
288        $this->server->emit('afterUnbind', [$path]);
289
290        $response->setStatus(204);
291        $response->setHeader('Content-Length', '0');
292
293        // Sending back false will interupt the event chain and tell the server
294        // we've handled this method.
295        return false;
296
297    }
298
299    /**
300     * WebDAV PROPFIND
301     *
302     * This WebDAV method requests information about an uri resource, or a list of resources
303     * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value
304     * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory)
305     *
306     * The request body contains an XML data structure that has a list of properties the client understands
307     * The response body is also an xml document, containing information about every uri resource and the requested properties
308     *
309     * It has to return a HTTP 207 Multi-status status code
310     *
311     * @param RequestInterface $request
312     * @param ResponseInterface $response
313     * @return void
314     */
315    function httpPropFind(RequestInterface $request, ResponseInterface $response) {
316
317        $path = $request->getPath();
318
319        $requestBody = $request->getBodyAsString();
320        if (strlen($requestBody)) {
321            try {
322                $propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody);
323            } catch (ParseException $e) {
324                throw new BadRequest($e->getMessage(), null, $e);
325            }
326        } else {
327            $propFindXml = new Xml\Request\PropFind();
328            $propFindXml->allProp = true;
329            $propFindXml->properties = [];
330        }
331
332        $depth = $this->server->getHTTPDepth(1);
333        // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled
334        if (!$this->server->enablePropfindDepthInfinity && $depth != 0) $depth = 1;
335
336        $newProperties = $this->server->getPropertiesForPath($path, $propFindXml->properties, $depth);
337
338        // This is a multi-status response
339        $response->setStatus(207);
340        $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
341        $response->setHeader('Vary', 'Brief,Prefer');
342
343        // Normally this header is only needed for OPTIONS responses, however..
344        // iCal seems to also depend on these being set for PROPFIND. Since
345        // this is not harmful, we'll add it.
346        $features = ['1', '3', 'extended-mkcol'];
347        foreach ($this->server->getPlugins() as $plugin) {
348            $features = array_merge($features, $plugin->getFeatures());
349        }
350        $response->setHeader('DAV', implode(', ', $features));
351
352        $prefer = $this->server->getHTTPPrefer();
353        $minimal = $prefer['return'] === 'minimal';
354
355        $data = $this->server->generateMultiStatus($newProperties, $minimal);
356        $response->setBody($data);
357
358        // Sending back false will interupt the event chain and tell the server
359        // we've handled this method.
360        return false;
361
362    }
363
364    /**
365     * WebDAV PROPPATCH
366     *
367     * This method is called to update properties on a Node. The request is an XML body with all the mutations.
368     * In this XML body it is specified which properties should be set/updated and/or deleted
369     *
370     * @param RequestInterface $request
371     * @param ResponseInterface $response
372     * @return bool
373     */
374    function httpPropPatch(RequestInterface $request, ResponseInterface $response) {
375
376        $path = $request->getPath();
377
378        try {
379            $propPatch = $this->server->xml->expect('{DAV:}propertyupdate', $request->getBody());
380        } catch (ParseException $e) {
381            throw new BadRequest($e->getMessage(), null, $e);
382        }
383        $newProperties = $propPatch->properties;
384
385        $result = $this->server->updateProperties($path, $newProperties);
386
387        $prefer = $this->server->getHTTPPrefer();
388        $response->setHeader('Vary', 'Brief,Prefer');
389
390        if ($prefer['return'] === 'minimal') {
391
392            // If return-minimal is specified, we only have to check if the
393            // request was succesful, and don't need to return the
394            // multi-status.
395            $ok = true;
396            foreach ($result as $prop => $code) {
397                if ((int)$code > 299) {
398                    $ok = false;
399                }
400            }
401
402            if ($ok) {
403
404                $response->setStatus(204);
405                return false;
406
407            }
408
409        }
410
411        $response->setStatus(207);
412        $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
413
414
415        // Reorganizing the result for generateMultiStatus
416        $multiStatus = [];
417        foreach ($result as $propertyName => $code) {
418            if (isset($multiStatus[$code])) {
419                $multiStatus[$code][$propertyName] = null;
420            } else {
421                $multiStatus[$code] = [$propertyName => null];
422            }
423        }
424        $multiStatus['href'] = $path;
425
426        $response->setBody(
427            $this->server->generateMultiStatus([$multiStatus])
428        );
429
430        // Sending back false will interupt the event chain and tell the server
431        // we've handled this method.
432        return false;
433
434    }
435
436    /**
437     * HTTP PUT method
438     *
439     * This HTTP method updates a file, or creates a new one.
440     *
441     * 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
442     *
443     * @param RequestInterface $request
444     * @param ResponseInterface $response
445     * @return bool
446     */
447    function httpPut(RequestInterface $request, ResponseInterface $response) {
448
449        $body = $request->getBodyAsStream();
450        $path = $request->getPath();
451
452        // Intercepting Content-Range
453        if ($request->getHeader('Content-Range')) {
454            /*
455               An origin server that allows PUT on a given target resource MUST send
456               a 400 (Bad Request) response to a PUT request that contains a
457               Content-Range header field.
458
459               Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4
460            */
461            throw new Exception\BadRequest('Content-Range on PUT requests are forbidden.');
462        }
463
464        // Intercepting the Finder problem
465        if (($expected = $request->getHeader('X-Expected-Entity-Length')) && $expected > 0) {
466
467            /*
468            Many webservers will not cooperate well with Finder PUT requests,
469            because it uses 'Chunked' transfer encoding for the request body.
470
471            The symptom of this problem is that Finder sends files to the
472            server, but they arrive as 0-length files in PHP.
473
474            If we don't do anything, the user might think they are uploading
475            files successfully, but they end up empty on the server. Instead,
476            we throw back an error if we detect this.
477
478            The reason Finder uses Chunked, is because it thinks the files
479            might change as it's being uploaded, and therefore the
480            Content-Length can vary.
481
482            Instead it sends the X-Expected-Entity-Length header with the size
483            of the file at the very start of the request. If this header is set,
484            but we don't get a request body we will fail the request to
485            protect the end-user.
486            */
487
488            // Only reading first byte
489            $firstByte = fread($body, 1);
490            if (strlen($firstByte) !== 1) {
491                throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.');
492            }
493
494            // The body needs to stay intact, so we copy everything to a
495            // temporary stream.
496
497            $newBody = fopen('php://temp', 'r+');
498            fwrite($newBody, $firstByte);
499            stream_copy_to_stream($body, $newBody);
500            rewind($newBody);
501
502            $body = $newBody;
503
504        }
505
506        if ($this->server->tree->nodeExists($path)) {
507
508            $node = $this->server->tree->getNodeForPath($path);
509
510            // If the node is a collection, we'll deny it
511            if (!($node instanceof IFile)) throw new Exception\Conflict('PUT is not allowed on non-files.');
512
513            if (!$this->server->updateFile($path, $body, $etag)) {
514                return false;
515            }
516
517            $response->setHeader('Content-Length', '0');
518            if ($etag) $response->setHeader('ETag', $etag);
519            $response->setStatus(204);
520
521        } else {
522
523            $etag = null;
524            // If we got here, the resource didn't exist yet.
525            if (!$this->server->createFile($path, $body, $etag)) {
526                // For one reason or another the file was not created.
527                return false;
528            }
529
530            $response->setHeader('Content-Length', '0');
531            if ($etag) $response->setHeader('ETag', $etag);
532            $response->setStatus(201);
533
534        }
535
536        // Sending back false will interupt the event chain and tell the server
537        // we've handled this method.
538        return false;
539
540    }
541
542
543    /**
544     * WebDAV MKCOL
545     *
546     * The MKCOL method is used to create a new collection (directory) on the server
547     *
548     * @param RequestInterface $request
549     * @param ResponseInterface $response
550     * @return bool
551     */
552    function httpMkcol(RequestInterface $request, ResponseInterface $response) {
553
554        $requestBody = $request->getBodyAsString();
555        $path = $request->getPath();
556
557        if ($requestBody) {
558
559            $contentType = $request->getHeader('Content-Type');
560            if (strpos($contentType, 'application/xml') !== 0 && strpos($contentType, 'text/xml') !== 0) {
561
562                // We must throw 415 for unsupported mkcol bodies
563                throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type');
564
565            }
566
567            try {
568                $mkcol = $this->server->xml->expect('{DAV:}mkcol', $requestBody);
569            } catch (\Sabre\Xml\ParseException $e) {
570                throw new Exception\BadRequest($e->getMessage(), null, $e);
571            }
572
573            $properties = $mkcol->getProperties();
574
575            if (!isset($properties['{DAV:}resourcetype']))
576                throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property');
577
578            $resourceType = $properties['{DAV:}resourcetype']->getValue();
579            unset($properties['{DAV:}resourcetype']);
580
581        } else {
582
583            $properties = [];
584            $resourceType = ['{DAV:}collection'];
585
586        }
587
588        $mkcol = new MkCol($resourceType, $properties);
589
590        $result = $this->server->createCollection($path, $mkcol);
591
592        if (is_array($result)) {
593            $response->setStatus(207);
594            $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
595
596            $response->setBody(
597                $this->server->generateMultiStatus([$result])
598            );
599
600        } else {
601            $response->setHeader('Content-Length', '0');
602            $response->setStatus(201);
603        }
604
605        // Sending back false will interupt the event chain and tell the server
606        // we've handled this method.
607        return false;
608
609    }
610
611    /**
612     * WebDAV HTTP MOVE method
613     *
614     * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo
615     *
616     * @param RequestInterface $request
617     * @param ResponseInterface $response
618     * @return bool
619     */
620    function httpMove(RequestInterface $request, ResponseInterface $response) {
621
622        $path = $request->getPath();
623
624        $moveInfo = $this->server->getCopyAndMoveInfo($request);
625
626        if ($moveInfo['destinationExists']) {
627
628            if (!$this->server->emit('beforeUnbind', [$moveInfo['destination']])) return false;
629
630        }
631        if (!$this->server->emit('beforeUnbind',[$path])) return false;
632        if (!$this->server->emit('beforeBind',[$moveInfo['destination']])) return false;
633        if (!$this->server->emit('beforeMove', [$path, $moveInfo['destination']])) return false;
634
635        if ($moveInfo['destinationExists']) {
636
637            $this->server->tree->delete($moveInfo['destination']);
638            $this->server->emit('afterUnbind', [$moveInfo['destination']]);
639
640        }
641
642        $this->server->tree->move($path, $moveInfo['destination']);
643
644        // Its important afterMove is called before afterUnbind, because it
645        // allows systems to transfer data from one path to another.
646        // PropertyStorage uses this. If afterUnbind was first, it would clean
647        // up all the properties before it has a chance.
648        $this->server->emit('afterMove', [$path, $moveInfo['destination']]);
649        $this->server->emit('afterUnbind', [$path]);
650        $this->server->emit('afterBind', [$moveInfo['destination']]);
651
652        // If a resource was overwritten we should send a 204, otherwise a 201
653        $response->setHeader('Content-Length', '0');
654        $response->setStatus($moveInfo['destinationExists'] ? 204 : 201);
655
656        // Sending back false will interupt the event chain and tell the server
657        // we've handled this method.
658        return false;
659
660    }
661
662    /**
663     * WebDAV HTTP COPY method
664     *
665     * This method copies one uri to a different uri, and works much like the MOVE request
666     * A lot of the actual request processing is done in getCopyMoveInfo
667     *
668     * @param RequestInterface $request
669     * @param ResponseInterface $response
670     * @return bool
671     */
672    function httpCopy(RequestInterface $request, ResponseInterface $response) {
673
674        $path = $request->getPath();
675
676        $copyInfo = $this->server->getCopyAndMoveInfo($request);
677
678        if ($copyInfo['destinationExists']) {
679            if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) return false;
680            $this->server->tree->delete($copyInfo['destination']);
681
682        }
683        if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) return false;
684        $this->server->tree->copy($path, $copyInfo['destination']);
685        $this->server->emit('afterBind', [$copyInfo['destination']]);
686
687        // If a resource was overwritten we should send a 204, otherwise a 201
688        $response->setHeader('Content-Length', '0');
689        $response->setStatus($copyInfo['destinationExists'] ? 204 : 201);
690
691        // Sending back false will interupt the event chain and tell the server
692        // we've handled this method.
693        return false;
694
695
696    }
697
698    /**
699     * HTTP REPORT method implementation
700     *
701     * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253)
702     * It's used in a lot of extensions, so it made sense to implement it into the core.
703     *
704     * @param RequestInterface $request
705     * @param ResponseInterface $response
706     * @return bool
707     */
708    function httpReport(RequestInterface $request, ResponseInterface $response) {
709
710        $path = $request->getPath();
711
712        $result = $this->server->xml->parse(
713            $request->getBody(),
714            $request->getUrl(),
715            $rootElementName
716        );
717
718        if ($this->server->emit('report', [$rootElementName, $result, $path])) {
719
720            // If emit returned true, it means the report was not supported
721            throw new Exception\ReportNotSupported();
722
723        }
724
725        // Sending back false will interupt the event chain and tell the server
726        // we've handled this method.
727        return false;
728
729    }
730
731    /**
732     * This method is called during property updates.
733     *
734     * Here we check if a user attempted to update a protected property and
735     * ensure that the process fails if this is the case.
736     *
737     * @param string $path
738     * @param PropPatch $propPatch
739     * @return void
740     */
741    function propPatchProtectedPropertyCheck($path, PropPatch $propPatch) {
742
743        // Comparing the mutation list to the list of propetected properties.
744        $mutations = $propPatch->getMutations();
745
746        $protected = array_intersect(
747            $this->server->protectedProperties,
748            array_keys($mutations)
749        );
750
751        if ($protected) {
752            $propPatch->setResultCode($protected, 403);
753        }
754
755    }
756
757    /**
758     * This method is called during property updates.
759     *
760     * Here we check if a node implements IProperties and let the node handle
761     * updating of (some) properties.
762     *
763     * @param string $path
764     * @param PropPatch $propPatch
765     * @return void
766     */
767    function propPatchNodeUpdate($path, PropPatch $propPatch) {
768
769        // This should trigger a 404 if the node doesn't exist.
770        $node = $this->server->tree->getNodeForPath($path);
771
772        if ($node instanceof IProperties) {
773            $node->propPatch($propPatch);
774        }
775
776    }
777
778    /**
779     * This method is called when properties are retrieved.
780     *
781     * Here we add all the default properties.
782     *
783     * @param PropFind $propFind
784     * @param INode $node
785     * @return void
786     */
787    function propFind(PropFind $propFind, INode $node) {
788
789        $propFind->handle('{DAV:}getlastmodified', function() use ($node) {
790            $lm = $node->getLastModified();
791            if ($lm) {
792                return new Xml\Property\GetLastModified($lm);
793            }
794        });
795
796        if ($node instanceof IFile) {
797            $propFind->handle('{DAV:}getcontentlength', [$node, 'getSize']);
798            $propFind->handle('{DAV:}getetag', [$node, 'getETag']);
799            $propFind->handle('{DAV:}getcontenttype', [$node, 'getContentType']);
800        }
801
802        if ($node instanceof IQuota) {
803            $quotaInfo = null;
804            $propFind->handle('{DAV:}quota-used-bytes', function() use (&$quotaInfo, $node) {
805                $quotaInfo = $node->getQuotaInfo();
806                return $quotaInfo[0];
807            });
808            $propFind->handle('{DAV:}quota-available-bytes', function() use (&$quotaInfo, $node) {
809                if (!$quotaInfo) {
810                    $quotaInfo = $node->getQuotaInfo();
811                }
812                return $quotaInfo[1];
813            });
814        }
815
816        $propFind->handle('{DAV:}supported-report-set', function() use ($propFind) {
817            $reports = [];
818            foreach ($this->server->getPlugins() as $plugin) {
819                $reports = array_merge($reports, $plugin->getSupportedReportSet($propFind->getPath()));
820            }
821            return new Xml\Property\SupportedReportSet($reports);
822        });
823        $propFind->handle('{DAV:}resourcetype', function() use ($node) {
824            return new Xml\Property\ResourceType($this->server->getResourceTypeForNode($node));
825        });
826        $propFind->handle('{DAV:}supported-method-set', function() use ($propFind) {
827            return new Xml\Property\SupportedMethodSet(
828                $this->server->getAllowedMethods($propFind->getPath())
829            );
830        });
831
832    }
833
834    /**
835     * Fetches properties for a node.
836     *
837     * This event is called a bit later, so plugins have a chance first to
838     * populate the result.
839     *
840     * @param PropFind $propFind
841     * @param INode $node
842     * @return void
843     */
844    function propFindNode(PropFind $propFind, INode $node) {
845
846        if ($node instanceof IProperties && $propertyNames = $propFind->get404Properties()) {
847
848            $nodeProperties = $node->getProperties($propertyNames);
849            foreach ($propertyNames as $propertyName) {
850                if (array_key_exists($propertyName, $nodeProperties)) {
851                    $propFind->set($propertyName, $nodeProperties[$propertyName], 200);
852                }
853            }
854
855        }
856
857    }
858
859    /**
860     * This method is called when properties are retrieved.
861     *
862     * This specific handler is called very late in the process, because we
863     * want other systems to first have a chance to handle the properties.
864     *
865     * @param PropFind $propFind
866     * @param INode $node
867     * @return void
868     */
869    function propFindLate(PropFind $propFind, INode $node) {
870
871        $propFind->handle('{http://calendarserver.org/ns/}getctag', function() use ($propFind) {
872
873            // If we already have a sync-token from the current propFind
874            // request, we can re-use that.
875            $val = $propFind->get('{http://sabredav.org/ns}sync-token');
876            if ($val) return $val;
877
878            $val = $propFind->get('{DAV:}sync-token');
879            if ($val && is_scalar($val)) {
880                return $val;
881            }
882            if ($val && $val instanceof Xml\Property\Href) {
883                return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX));
884            }
885
886            // If we got here, the earlier two properties may simply not have
887            // been part of the earlier request. We're going to fetch them.
888            $result = $this->server->getProperties($propFind->getPath(), [
889                '{http://sabredav.org/ns}sync-token',
890                '{DAV:}sync-token',
891            ]);
892
893            if (isset($result['{http://sabredav.org/ns}sync-token'])) {
894                return $result['{http://sabredav.org/ns}sync-token'];
895            }
896            if (isset($result['{DAV:}sync-token'])) {
897                $val = $result['{DAV:}sync-token'];
898                if (is_scalar($val)) {
899                    return $val;
900                } elseif ($val instanceof Xml\Property\Href) {
901                    return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX));
902                }
903            }
904
905        });
906
907    }
908
909    /**
910     * Returns a bunch of meta-data about the plugin.
911     *
912     * Providing this information is optional, and is mainly displayed by the
913     * Browser plugin.
914     *
915     * The description key in the returned array may contain html and will not
916     * be sanitized.
917     *
918     * @return array
919     */
920    function getPluginInfo() {
921
922        return [
923            'name'        => $this->getPluginName(),
924            '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.',
925            'link'        => null,
926        ];
927
928    }
929}
930