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) 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 $server->on('exception', [$this, 'exception']); 54 55 } 56 57 /** 58 * Returns a plugin name. 59 * 60 * Using this name other plugins will be able to access other plugins 61 * using DAV\Server::getPlugin 62 * 63 * @return string 64 */ 65 function getPluginName() { 66 67 return 'core'; 68 69 } 70 71 /** 72 * This is the default implementation for the GET method. 73 * 74 * @param RequestInterface $request 75 * @param ResponseInterface $response 76 * @return bool 77 */ 78 function httpGet(RequestInterface $request, ResponseInterface $response) { 79 80 $path = $request->getPath(); 81 $node = $this->server->tree->getNodeForPath($path); 82 83 if (!$node instanceof IFile) return; 84 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 * TODO: getetag, getlastmodified, getsize should also be used using 97 * this method 98 */ 99 $httpHeaders = $this->server->getHTTPHeaders($path); 100 101 /* ContentType needs to get a default, because many webservers will otherwise 102 * default to text/html, and we don't want this for security reasons. 103 */ 104 if (!isset($httpHeaders['Content-Type'])) { 105 $httpHeaders['Content-Type'] = 'application/octet-stream'; 106 } 107 108 109 if (isset($httpHeaders['Content-Length'])) { 110 111 $nodeSize = $httpHeaders['Content-Length']; 112 113 // Need to unset Content-Length, because we'll handle that during figuring out the range 114 unset($httpHeaders['Content-Length']); 115 116 } else { 117 $nodeSize = null; 118 } 119 120 $response->addHeaders($httpHeaders); 121 122 $range = $this->server->getHTTPRange(); 123 $ifRange = $request->getHeader('If-Range'); 124 $ignoreRangeHeader = false; 125 126 // If ifRange is set, and range is specified, we first need to check 127 // the precondition. 128 if ($nodeSize && $range && $ifRange) { 129 130 // if IfRange is parsable as a date we'll treat it as a DateTime 131 // otherwise, we must treat it as an etag. 132 try { 133 $ifRangeDate = new \DateTime($ifRange); 134 135 // It's a date. We must check if the entity is modified since 136 // the specified date. 137 if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true; 138 else { 139 $modified = new \DateTime($httpHeaders['Last-Modified']); 140 if ($modified > $ifRangeDate) $ignoreRangeHeader = true; 141 } 142 143 } catch (\Exception $e) { 144 145 // It's an entity. We can do a simple comparison. 146 if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true; 147 elseif ($httpHeaders['ETag'] !== $ifRange) $ignoreRangeHeader = true; 148 } 149 } 150 151 // We're only going to support HTTP ranges if the backend provided a filesize 152 if (!$ignoreRangeHeader && $nodeSize && $range) { 153 154 // Determining the exact byte offsets 155 if (!is_null($range[0])) { 156 157 $start = $range[0]; 158 $end = $range[1] ? $range[1] : $nodeSize - 1; 159 if ($start >= $nodeSize) 160 throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')'); 161 162 if ($end < $start) throw new Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')'); 163 if ($end >= $nodeSize) $end = $nodeSize - 1; 164 165 } else { 166 167 $start = $nodeSize - $range[1]; 168 $end = $nodeSize - 1; 169 170 if ($start < 0) $start = 0; 171 172 } 173 174 // Streams may advertise themselves as seekable, but still not 175 // actually allow fseek. We'll manually go forward in the stream 176 // if fseek failed. 177 if (!stream_get_meta_data($body)['seekable'] || fseek($body, $start, SEEK_SET) === -1) { 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 interrupt 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 interrupt 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 interrupt 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 interrupt 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->getPropertiesIteratorForPath($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 interrupt 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 successful, 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 interrupt 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 interrupt 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 interrupt 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 interrupt 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 (!$this->server->emit('beforeBind', [$copyInfo['destination']])) return false; 679 if ($copyInfo['destinationExists']) { 680 if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) return false; 681 $this->server->tree->delete($copyInfo['destination']); 682 } 683 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 interrupt 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 interrupt 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 protected 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 ($nodeProperties as $propertyName => $propertyValue) { 850 $propFind->set($propertyName, $propertyValue, 200); 851 } 852 853 } 854 855 } 856 857 /** 858 * This method is called when properties are retrieved. 859 * 860 * This specific handler is called very late in the process, because we 861 * want other systems to first have a chance to handle the properties. 862 * 863 * @param PropFind $propFind 864 * @param INode $node 865 * @return void 866 */ 867 function propFindLate(PropFind $propFind, INode $node) { 868 869 $propFind->handle('{http://calendarserver.org/ns/}getctag', function() use ($propFind) { 870 871 // If we already have a sync-token from the current propFind 872 // request, we can re-use that. 873 $val = $propFind->get('{http://sabredav.org/ns}sync-token'); 874 if ($val) return $val; 875 876 $val = $propFind->get('{DAV:}sync-token'); 877 if ($val && is_scalar($val)) { 878 return $val; 879 } 880 if ($val && $val instanceof Xml\Property\Href) { 881 return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); 882 } 883 884 // If we got here, the earlier two properties may simply not have 885 // been part of the earlier request. We're going to fetch them. 886 $result = $this->server->getProperties($propFind->getPath(), [ 887 '{http://sabredav.org/ns}sync-token', 888 '{DAV:}sync-token', 889 ]); 890 891 if (isset($result['{http://sabredav.org/ns}sync-token'])) { 892 return $result['{http://sabredav.org/ns}sync-token']; 893 } 894 if (isset($result['{DAV:}sync-token'])) { 895 $val = $result['{DAV:}sync-token']; 896 if (is_scalar($val)) { 897 return $val; 898 } elseif ($val instanceof Xml\Property\Href) { 899 return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); 900 } 901 } 902 903 }); 904 905 } 906 907 /** 908 * Listens for exception events, and automatically logs them. 909 * 910 * @param Exception $e 911 */ 912 function exception($e) { 913 914 $logLevel = \Psr\Log\LogLevel::CRITICAL; 915 if ($e instanceof \Sabre\DAV\Exception) { 916 // If it's a standard sabre/dav exception, it means we have a http 917 // status code available. 918 $code = $e->getHTTPCode(); 919 920 if ($code >= 400 && $code < 500) { 921 // user error 922 $logLevel = \Psr\Log\LogLevel::INFO; 923 } else { 924 // Server-side error. We mark it's as an error, but it's not 925 // critical. 926 $logLevel = \Psr\Log\LogLevel::ERROR; 927 } 928 } 929 930 $this->server->getLogger()->log( 931 $logLevel, 932 'Uncaught exception', 933 [ 934 'exception' => $e, 935 ] 936 ); 937 } 938 939 /** 940 * Returns a bunch of meta-data about the plugin. 941 * 942 * Providing this information is optional, and is mainly displayed by the 943 * Browser plugin. 944 * 945 * The description key in the returned array may contain html and will not 946 * be sanitized. 947 * 948 * @return array 949 */ 950 function getPluginInfo() { 951 952 return [ 953 'name' => $this->getPluginName(), 954 '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.', 955 'link' => null, 956 ]; 957 958 } 959} 960