1<?php 2 3declare(strict_types=1); 4 5namespace Sabre\DAV; 6 7use Sabre\DAV\Exception\BadRequest; 8use Sabre\HTTP\RequestInterface; 9use Sabre\HTTP\ResponseInterface; 10use Sabre\Xml\ParseException; 11 12/** 13 * The core plugin provides all the basic features for a WebDAV server. 14 * 15 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 16 * @author Evert Pot (http://evertpot.com/) 17 * @license http://sabre.io/license/ Modified BSD License 18 */ 19class CorePlugin extends ServerPlugin 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 /* 97 * TODO: getetag, getlastmodified, getsize should also be used using 98 * this method 99 */ 100 $httpHeaders = $this->server->getHTTPHeaders($path); 101 102 /* ContentType needs to get a default, because many webservers will otherwise 103 * default to text/html, and we don't want this for security reasons. 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 /* 436 An origin server that allows PUT on a given target resource MUST send 437 a 400 (Bad Request) response to a PUT request that contains a 438 Content-Range header field. 439 440 Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4 441 */ 442 throw new Exception\BadRequest('Content-Range on PUT requests are forbidden.'); 443 } 444 445 // Intercepting the Finder problem 446 if (($expected = $request->getHeader('X-Expected-Entity-Length')) && $expected > 0) { 447 /* 448 Many webservers will not cooperate well with Finder PUT requests, 449 because it uses 'Chunked' transfer encoding for the request body. 450 451 The symptom of this problem is that Finder sends files to the 452 server, but they arrive as 0-length files in PHP. 453 454 If we don't do anything, the user might think they are uploading 455 files successfully, but they end up empty on the server. Instead, 456 we throw back an error if we detect this. 457 458 The reason Finder uses Chunked, is because it thinks the files 459 might change as it's being uploaded, and therefore the 460 Content-Length can vary. 461 462 Instead it sends the X-Expected-Entity-Length header with the size 463 of the file at the very start of the request. If this header is set, 464 but we don't get a request body we will fail the request to 465 protect the end-user. 466 */ 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 ($copyInfo['destinationExists']) { 649 if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) { 650 return false; 651 } 652 $this->server->tree->delete($copyInfo['destination']); 653 } 654 655 $this->server->tree->copy($path, $copyInfo['destination']); 656 $this->server->emit('afterBind', [$copyInfo['destination']]); 657 658 // If a resource was overwritten we should send a 204, otherwise a 201 659 $response->setHeader('Content-Length', '0'); 660 $response->setStatus($copyInfo['destinationExists'] ? 204 : 201); 661 662 // Sending back false will interrupt the event chain and tell the server 663 // we've handled this method. 664 return false; 665 } 666 667 /** 668 * HTTP REPORT method implementation. 669 * 670 * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253) 671 * It's used in a lot of extensions, so it made sense to implement it into the core. 672 * 673 * @return bool 674 */ 675 public function httpReport(RequestInterface $request, ResponseInterface $response) 676 { 677 $path = $request->getPath(); 678 679 $result = $this->server->xml->parse( 680 $request->getBody(), 681 $request->getUrl(), 682 $rootElementName 683 ); 684 685 if ($this->server->emit('report', [$rootElementName, $result, $path])) { 686 // If emit returned true, it means the report was not supported 687 throw new Exception\ReportNotSupported(); 688 } 689 690 // Sending back false will interrupt the event chain and tell the server 691 // we've handled this method. 692 return false; 693 } 694 695 /** 696 * This method is called during property updates. 697 * 698 * Here we check if a user attempted to update a protected property and 699 * ensure that the process fails if this is the case. 700 * 701 * @param string $path 702 */ 703 public function propPatchProtectedPropertyCheck($path, PropPatch $propPatch) 704 { 705 // Comparing the mutation list to the list of protected properties. 706 $mutations = $propPatch->getMutations(); 707 708 $protected = array_intersect( 709 $this->server->protectedProperties, 710 array_keys($mutations) 711 ); 712 713 if ($protected) { 714 $propPatch->setResultCode($protected, 403); 715 } 716 } 717 718 /** 719 * This method is called during property updates. 720 * 721 * Here we check if a node implements IProperties and let the node handle 722 * updating of (some) properties. 723 * 724 * @param string $path 725 */ 726 public function propPatchNodeUpdate($path, PropPatch $propPatch) 727 { 728 // This should trigger a 404 if the node doesn't exist. 729 $node = $this->server->tree->getNodeForPath($path); 730 731 if ($node instanceof IProperties) { 732 $node->propPatch($propPatch); 733 } 734 } 735 736 /** 737 * This method is called when properties are retrieved. 738 * 739 * Here we add all the default properties. 740 */ 741 public function propFind(PropFind $propFind, INode $node) 742 { 743 $propFind->handle('{DAV:}getlastmodified', function () use ($node) { 744 $lm = $node->getLastModified(); 745 if ($lm) { 746 return new Xml\Property\GetLastModified($lm); 747 } 748 }); 749 750 if ($node instanceof IFile) { 751 $propFind->handle('{DAV:}getcontentlength', [$node, 'getSize']); 752 $propFind->handle('{DAV:}getetag', [$node, 'getETag']); 753 $propFind->handle('{DAV:}getcontenttype', [$node, 'getContentType']); 754 } 755 756 if ($node instanceof IQuota) { 757 $quotaInfo = null; 758 $propFind->handle('{DAV:}quota-used-bytes', function () use (&$quotaInfo, $node) { 759 $quotaInfo = $node->getQuotaInfo(); 760 761 return $quotaInfo[0]; 762 }); 763 $propFind->handle('{DAV:}quota-available-bytes', function () use (&$quotaInfo, $node) { 764 if (!$quotaInfo) { 765 $quotaInfo = $node->getQuotaInfo(); 766 } 767 768 return $quotaInfo[1]; 769 }); 770 } 771 772 $propFind->handle('{DAV:}supported-report-set', function () use ($propFind) { 773 $reports = []; 774 foreach ($this->server->getPlugins() as $plugin) { 775 $reports = array_merge($reports, $plugin->getSupportedReportSet($propFind->getPath())); 776 } 777 778 return new Xml\Property\SupportedReportSet($reports); 779 }); 780 $propFind->handle('{DAV:}resourcetype', function () use ($node) { 781 return new Xml\Property\ResourceType($this->server->getResourceTypeForNode($node)); 782 }); 783 $propFind->handle('{DAV:}supported-method-set', function () use ($propFind) { 784 return new Xml\Property\SupportedMethodSet( 785 $this->server->getAllowedMethods($propFind->getPath()) 786 ); 787 }); 788 } 789 790 /** 791 * Fetches properties for a node. 792 * 793 * This event is called a bit later, so plugins have a chance first to 794 * populate the result. 795 */ 796 public function propFindNode(PropFind $propFind, INode $node) 797 { 798 if ($node instanceof IProperties && $propertyNames = $propFind->get404Properties()) { 799 $nodeProperties = $node->getProperties($propertyNames); 800 foreach ($nodeProperties as $propertyName => $propertyValue) { 801 $propFind->set($propertyName, $propertyValue, 200); 802 } 803 } 804 } 805 806 /** 807 * This method is called when properties are retrieved. 808 * 809 * This specific handler is called very late in the process, because we 810 * want other systems to first have a chance to handle the properties. 811 */ 812 public function propFindLate(PropFind $propFind, INode $node) 813 { 814 $propFind->handle('{http://calendarserver.org/ns/}getctag', function () use ($propFind) { 815 // If we already have a sync-token from the current propFind 816 // request, we can re-use that. 817 $val = $propFind->get('{http://sabredav.org/ns}sync-token'); 818 if ($val) { 819 return $val; 820 } 821 822 $val = $propFind->get('{DAV:}sync-token'); 823 if ($val && is_scalar($val)) { 824 return $val; 825 } 826 if ($val && $val instanceof Xml\Property\Href) { 827 return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); 828 } 829 830 // If we got here, the earlier two properties may simply not have 831 // been part of the earlier request. We're going to fetch them. 832 $result = $this->server->getProperties($propFind->getPath(), [ 833 '{http://sabredav.org/ns}sync-token', 834 '{DAV:}sync-token', 835 ]); 836 837 if (isset($result['{http://sabredav.org/ns}sync-token'])) { 838 return $result['{http://sabredav.org/ns}sync-token']; 839 } 840 if (isset($result['{DAV:}sync-token'])) { 841 $val = $result['{DAV:}sync-token']; 842 if (is_scalar($val)) { 843 return $val; 844 } elseif ($val instanceof Xml\Property\Href) { 845 return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); 846 } 847 } 848 }); 849 } 850 851 /** 852 * Listens for exception events, and automatically logs them. 853 * 854 * @param Exception $e 855 */ 856 public function exception($e) 857 { 858 $logLevel = \Psr\Log\LogLevel::CRITICAL; 859 if ($e instanceof \Sabre\DAV\Exception) { 860 // If it's a standard sabre/dav exception, it means we have a http 861 // status code available. 862 $code = $e->getHTTPCode(); 863 864 if ($code >= 400 && $code < 500) { 865 // user error 866 $logLevel = \Psr\Log\LogLevel::INFO; 867 } else { 868 // Server-side error. We mark it's as an error, but it's not 869 // critical. 870 $logLevel = \Psr\Log\LogLevel::ERROR; 871 } 872 } 873 874 $this->server->getLogger()->log( 875 $logLevel, 876 'Uncaught exception', 877 [ 878 'exception' => $e, 879 ] 880 ); 881 } 882 883 /** 884 * Returns a bunch of meta-data about the plugin. 885 * 886 * Providing this information is optional, and is mainly displayed by the 887 * Browser plugin. 888 * 889 * The description key in the returned array may contain html and will not 890 * be sanitized. 891 * 892 * @return array 893 */ 894 public function getPluginInfo() 895 { 896 return [ 897 'name' => $this->getPluginName(), 898 '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.', 899 'link' => null, 900 ]; 901 } 902} 903