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