1<?php 2 3namespace Sabre\DAVACL; 4 5use Sabre\DAV; 6use Sabre\DAV\INode; 7use Sabre\DAV\Exception\BadRequest; 8use Sabre\HTTP\RequestInterface; 9use Sabre\HTTP\ResponseInterface; 10use Sabre\Uri; 11 12/** 13 * SabreDAV ACL Plugin 14 * 15 * This plugin provides functionality to enforce ACL permissions. 16 * ACL is defined in RFC3744. 17 * 18 * In addition it also provides support for the {DAV:}current-user-principal 19 * property, defined in RFC5397 and the {DAV:}expand-property report, as 20 * defined in RFC3253. 21 * 22 * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). 23 * @author Evert Pot (http://evertpot.com/) 24 * @license http://sabre.io/license/ Modified BSD License 25 */ 26class Plugin extends DAV\ServerPlugin { 27 28 /** 29 * Recursion constants 30 * 31 * This only checks the base node 32 */ 33 const R_PARENT = 1; 34 35 /** 36 * Recursion constants 37 * 38 * This checks every node in the tree 39 */ 40 const R_RECURSIVE = 2; 41 42 /** 43 * Recursion constants 44 * 45 * This checks every parentnode in the tree, but not leaf-nodes. 46 */ 47 const R_RECURSIVEPARENTS = 3; 48 49 /** 50 * Reference to server object. 51 * 52 * @var Sabre\DAV\Server 53 */ 54 protected $server; 55 56 /** 57 * List of urls containing principal collections. 58 * Modify this if your principals are located elsewhere. 59 * 60 * @var array 61 */ 62 public $principalCollectionSet = [ 63 'principals', 64 ]; 65 66 /** 67 * By default ACL is only enforced for nodes that have ACL support (the 68 * ones that implement IACL). For any other node, access is 69 * always granted. 70 * 71 * To override this behaviour you can turn this setting off. This is useful 72 * if you plan to fully support ACL in the entire tree. 73 * 74 * @var bool 75 */ 76 public $allowAccessToNodesWithoutACL = true; 77 78 /** 79 * By default nodes that are inaccessible by the user, can still be seen 80 * in directory listings (PROPFIND on parent with Depth: 1) 81 * 82 * In certain cases it's desirable to hide inaccessible nodes. Setting this 83 * to true will cause these nodes to be hidden from directory listings. 84 * 85 * @var bool 86 */ 87 public $hideNodesFromListings = false; 88 89 /** 90 * This list of properties are the properties a client can search on using 91 * the {DAV:}principal-property-search report. 92 * 93 * The keys are the property names, values are descriptions. 94 * 95 * @var array 96 */ 97 public $principalSearchPropertySet = [ 98 '{DAV:}displayname' => 'Display name', 99 '{http://sabredav.org/ns}email-address' => 'Email address', 100 ]; 101 102 /** 103 * Any principal uri's added here, will automatically be added to the list 104 * of ACL's. They will effectively receive {DAV:}all privileges, as a 105 * protected privilege. 106 * 107 * @var array 108 */ 109 public $adminPrincipals = []; 110 111 /** 112 * Returns a list of features added by this plugin. 113 * 114 * This list is used in the response of a HTTP OPTIONS request. 115 * 116 * @return array 117 */ 118 function getFeatures() { 119 120 return ['access-control', 'calendarserver-principal-property-search']; 121 122 } 123 124 /** 125 * Returns a list of available methods for a given url 126 * 127 * @param string $uri 128 * @return array 129 */ 130 function getMethods($uri) { 131 132 return ['ACL']; 133 134 } 135 136 /** 137 * Returns a plugin name. 138 * 139 * Using this name other plugins will be able to access other plugins 140 * using Sabre\DAV\Server::getPlugin 141 * 142 * @return string 143 */ 144 function getPluginName() { 145 146 return 'acl'; 147 148 } 149 150 /** 151 * Returns a list of reports this plugin supports. 152 * 153 * This will be used in the {DAV:}supported-report-set property. 154 * Note that you still need to subscribe to the 'report' event to actually 155 * implement them 156 * 157 * @param string $uri 158 * @return array 159 */ 160 function getSupportedReportSet($uri) { 161 162 return [ 163 '{DAV:}expand-property', 164 '{DAV:}principal-property-search', 165 '{DAV:}principal-search-property-set', 166 ]; 167 168 } 169 170 171 /** 172 * Checks if the current user has the specified privilege(s). 173 * 174 * You can specify a single privilege, or a list of privileges. 175 * This method will throw an exception if the privilege is not available 176 * and return true otherwise. 177 * 178 * @param string $uri 179 * @param array|string $privileges 180 * @param int $recursion 181 * @param bool $throwExceptions if set to false, this method won't throw exceptions. 182 * @throws Sabre\DAVACL\Exception\NeedPrivileges 183 * @return bool 184 */ 185 function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true) { 186 187 if (!is_array($privileges)) $privileges = [$privileges]; 188 189 $acl = $this->getCurrentUserPrivilegeSet($uri); 190 191 if (is_null($acl)) { 192 if ($this->allowAccessToNodesWithoutACL) { 193 return true; 194 } else { 195 if ($throwExceptions) 196 throw new Exception\NeedPrivileges($uri, $privileges); 197 else 198 return false; 199 200 } 201 } 202 203 $failed = []; 204 foreach ($privileges as $priv) { 205 206 if (!in_array($priv, $acl)) { 207 $failed[] = $priv; 208 } 209 210 } 211 212 if ($failed) { 213 if ($throwExceptions) 214 throw new Exception\NeedPrivileges($uri, $failed); 215 else 216 return false; 217 } 218 return true; 219 220 } 221 222 /** 223 * Returns the standard users' principal. 224 * 225 * This is one authorative principal url for the current user. 226 * This method will return null if the user wasn't logged in. 227 * 228 * @return string|null 229 */ 230 function getCurrentUserPrincipal() { 231 232 $authPlugin = $this->server->getPlugin('auth'); 233 if (is_null($authPlugin)) return null; 234 /** @var $authPlugin Sabre\DAV\Auth\Plugin */ 235 236 return $authPlugin->getCurrentPrincipal(); 237 238 } 239 240 241 /** 242 * Returns a list of principals that's associated to the current 243 * user, either directly or through group membership. 244 * 245 * @return array 246 */ 247 function getCurrentUserPrincipals() { 248 249 $currentUser = $this->getCurrentUserPrincipal(); 250 251 if (is_null($currentUser)) return []; 252 253 return array_merge( 254 [$currentUser], 255 $this->getPrincipalMembership($currentUser) 256 ); 257 258 } 259 260 /** 261 * This array holds a cache for all the principals that are associated with 262 * a single principal. 263 * 264 * @var array 265 */ 266 protected $principalMembershipCache = []; 267 268 269 /** 270 * Returns all the principal groups the specified principal is a member of. 271 * 272 * @param string $principal 273 * @return array 274 */ 275 function getPrincipalMembership($mainPrincipal) { 276 277 // First check our cache 278 if (isset($this->principalMembershipCache[$mainPrincipal])) { 279 return $this->principalMembershipCache[$mainPrincipal]; 280 } 281 282 $check = [$mainPrincipal]; 283 $principals = []; 284 285 while (count($check)) { 286 287 $principal = array_shift($check); 288 289 $node = $this->server->tree->getNodeForPath($principal); 290 if ($node instanceof IPrincipal) { 291 foreach ($node->getGroupMembership() as $groupMember) { 292 293 if (!in_array($groupMember, $principals)) { 294 295 $check[] = $groupMember; 296 $principals[] = $groupMember; 297 298 } 299 300 } 301 302 } 303 304 } 305 306 // Store the result in the cache 307 $this->principalMembershipCache[$mainPrincipal] = $principals; 308 309 return $principals; 310 311 } 312 313 /** 314 * Returns the supported privilege structure for this ACL plugin. 315 * 316 * See RFC3744 for more details. Currently we default on a simple, 317 * standard structure. 318 * 319 * You can either get the list of privileges by a uri (path) or by 320 * specifying a Node. 321 * 322 * @param string|INode $node 323 * @return array 324 */ 325 function getSupportedPrivilegeSet($node) { 326 327 if (is_string($node)) { 328 $node = $this->server->tree->getNodeForPath($node); 329 } 330 331 if ($node instanceof IACL) { 332 $result = $node->getSupportedPrivilegeSet(); 333 334 if ($result) 335 return $result; 336 } 337 338 return self::getDefaultSupportedPrivilegeSet(); 339 340 } 341 342 /** 343 * Returns a fairly standard set of privileges, which may be useful for 344 * other systems to use as a basis. 345 * 346 * @return array 347 */ 348 static function getDefaultSupportedPrivilegeSet() { 349 350 return [ 351 'privilege' => '{DAV:}all', 352 'abstract' => true, 353 'aggregates' => [ 354 [ 355 'privilege' => '{DAV:}read', 356 'aggregates' => [ 357 [ 358 'privilege' => '{DAV:}read-acl', 359 'abstract' => false, 360 ], 361 [ 362 'privilege' => '{DAV:}read-current-user-privilege-set', 363 'abstract' => false, 364 ], 365 ], 366 ], // {DAV:}read 367 [ 368 'privilege' => '{DAV:}write', 369 'aggregates' => [ 370 [ 371 'privilege' => '{DAV:}write-acl', 372 'abstract' => false, 373 ], 374 [ 375 'privilege' => '{DAV:}write-properties', 376 'abstract' => false, 377 ], 378 [ 379 'privilege' => '{DAV:}write-content', 380 'abstract' => false, 381 ], 382 [ 383 'privilege' => '{DAV:}bind', 384 'abstract' => false, 385 ], 386 [ 387 'privilege' => '{DAV:}unbind', 388 'abstract' => false, 389 ], 390 [ 391 'privilege' => '{DAV:}unlock', 392 'abstract' => false, 393 ], 394 ], 395 ], // {DAV:}write 396 ], 397 ]; // {DAV:}all 398 399 } 400 401 /** 402 * Returns the supported privilege set as a flat list 403 * 404 * This is much easier to parse. 405 * 406 * The returned list will be index by privilege name. 407 * The value is a struct containing the following properties: 408 * - aggregates 409 * - abstract 410 * - concrete 411 * 412 * @param string|INode $node 413 * @return array 414 */ 415 final function getFlatPrivilegeSet($node) { 416 417 $privs = $this->getSupportedPrivilegeSet($node); 418 419 $fpsTraverse = null; 420 $fpsTraverse = function($priv, $concrete, &$flat) use (&$fpsTraverse) { 421 422 $myPriv = [ 423 'privilege' => $priv['privilege'], 424 'abstract' => isset($priv['abstract']) && $priv['abstract'], 425 'aggregates' => [], 426 'concrete' => isset($priv['abstract']) && $priv['abstract'] ? $concrete : $priv['privilege'], 427 ]; 428 429 if (isset($priv['aggregates'])) { 430 431 foreach ($priv['aggregates'] as $subPriv) { 432 433 $myPriv['aggregates'][] = $subPriv['privilege']; 434 435 } 436 437 } 438 439 $flat[$priv['privilege']] = $myPriv; 440 441 if (isset($priv['aggregates'])) { 442 443 foreach ($priv['aggregates'] as $subPriv) { 444 445 $fpsTraverse($subPriv, $myPriv['concrete'], $flat); 446 447 } 448 449 } 450 451 }; 452 453 $flat = []; 454 $fpsTraverse($privs, null, $flat); 455 456 return $flat; 457 458 } 459 460 /** 461 * Returns the full ACL list. 462 * 463 * Either a uri or a INode may be passed. 464 * 465 * null will be returned if the node doesn't support ACLs. 466 * 467 * @param string|DAV\INode $node 468 * @return array 469 */ 470 function getACL($node) { 471 472 if (is_string($node)) { 473 $node = $this->server->tree->getNodeForPath($node); 474 } 475 if (!$node instanceof IACL) { 476 return null; 477 } 478 $acl = $node->getACL(); 479 foreach ($this->adminPrincipals as $adminPrincipal) { 480 $acl[] = [ 481 'principal' => $adminPrincipal, 482 'privilege' => '{DAV:}all', 483 'protected' => true, 484 ]; 485 } 486 return $acl; 487 488 } 489 490 /** 491 * Returns a list of privileges the current user has 492 * on a particular node. 493 * 494 * Either a uri or a DAV\INode may be passed. 495 * 496 * null will be returned if the node doesn't support ACLs. 497 * 498 * @param string|DAV\INode $node 499 * @return array 500 */ 501 function getCurrentUserPrivilegeSet($node) { 502 503 if (is_string($node)) { 504 $node = $this->server->tree->getNodeForPath($node); 505 } 506 507 $acl = $this->getACL($node); 508 509 if (is_null($acl)) return null; 510 511 $principals = $this->getCurrentUserPrincipals(); 512 513 $collected = []; 514 515 foreach ($acl as $ace) { 516 517 $principal = $ace['principal']; 518 519 switch ($principal) { 520 521 case '{DAV:}owner' : 522 $owner = $node->getOwner(); 523 if ($owner && in_array($owner, $principals)) { 524 $collected[] = $ace; 525 } 526 break; 527 528 529 // 'all' matches for every user 530 case '{DAV:}all' : 531 532 // 'authenticated' matched for every user that's logged in. 533 // Since it's not possible to use ACL while not being logged 534 // in, this is also always true. 535 case '{DAV:}authenticated' : 536 $collected[] = $ace; 537 break; 538 539 // 'unauthenticated' can never occur either, so we simply 540 // ignore these. 541 case '{DAV:}unauthenticated' : 542 break; 543 544 default : 545 if (in_array($ace['principal'], $principals)) { 546 $collected[] = $ace; 547 } 548 break; 549 550 } 551 552 553 } 554 555 // Now we deduct all aggregated privileges. 556 $flat = $this->getFlatPrivilegeSet($node); 557 558 $collected2 = []; 559 while (count($collected)) { 560 561 $current = array_pop($collected); 562 $collected2[] = $current['privilege']; 563 564 foreach ($flat[$current['privilege']]['aggregates'] as $subPriv) { 565 $collected2[] = $subPriv; 566 $collected[] = $flat[$subPriv]; 567 } 568 569 } 570 571 return array_values(array_unique($collected2)); 572 573 } 574 575 576 /** 577 * Returns a principal based on its uri. 578 * 579 * Returns null if the principal could not be found. 580 * 581 * @param string $uri 582 * @return null|string 583 */ 584 function getPrincipalByUri($uri) { 585 586 $result = null; 587 $collections = $this->principalCollectionSet; 588 foreach ($collections as $collection) { 589 590 $principalCollection = $this->server->tree->getNodeForPath($collection); 591 if (!$principalCollection instanceof IPrincipalCollection) { 592 // Not a principal collection, we're simply going to ignore 593 // this. 594 continue; 595 } 596 597 $result = $principalCollection->findByUri($uri); 598 if ($result) { 599 return $result; 600 } 601 602 } 603 604 } 605 606 /** 607 * Principal property search 608 * 609 * This method can search for principals matching certain values in 610 * properties. 611 * 612 * This method will return a list of properties for the matched properties. 613 * 614 * @param array $searchProperties The properties to search on. This is a 615 * key-value list. The keys are property 616 * names, and the values the strings to 617 * match them on. 618 * @param array $requestedProperties This is the list of properties to 619 * return for every match. 620 * @param string $collectionUri The principal collection to search on. 621 * If this is ommitted, the standard 622 * principal collection-set will be used. 623 * @param string $test "allof" to use AND to search the 624 * properties. 'anyof' for OR. 625 * @return array This method returns an array structure similar to 626 * Sabre\DAV\Server::getPropertiesForPath. Returned 627 * properties are index by a HTTP status code. 628 */ 629 function principalSearch(array $searchProperties, array $requestedProperties, $collectionUri = null, $test = 'allof') { 630 631 if (!is_null($collectionUri)) { 632 $uris = [$collectionUri]; 633 } else { 634 $uris = $this->principalCollectionSet; 635 } 636 637 $lookupResults = []; 638 foreach ($uris as $uri) { 639 640 $principalCollection = $this->server->tree->getNodeForPath($uri); 641 if (!$principalCollection instanceof IPrincipalCollection) { 642 // Not a principal collection, we're simply going to ignore 643 // this. 644 continue; 645 } 646 647 $results = $principalCollection->searchPrincipals($searchProperties, $test); 648 foreach ($results as $result) { 649 $lookupResults[] = rtrim($uri, '/') . '/' . $result; 650 } 651 652 } 653 654 $matches = []; 655 656 foreach ($lookupResults as $lookupResult) { 657 658 list($matches[]) = $this->server->getPropertiesForPath($lookupResult, $requestedProperties, 0); 659 660 } 661 662 return $matches; 663 664 } 665 666 /** 667 * Sets up the plugin 668 * 669 * This method is automatically called by the server class. 670 * 671 * @param DAV\Server $server 672 * @return void 673 */ 674 function initialize(DAV\Server $server) { 675 676 $this->server = $server; 677 $server->on('propFind', [$this, 'propFind'], 20); 678 $server->on('beforeMethod', [$this, 'beforeMethod'], 20); 679 $server->on('beforeBind', [$this, 'beforeBind'], 20); 680 $server->on('beforeUnbind', [$this, 'beforeUnbind'], 20); 681 $server->on('propPatch', [$this, 'propPatch']); 682 $server->on('beforeUnlock', [$this, 'beforeUnlock'], 20); 683 $server->on('report', [$this, 'report']); 684 $server->on('method:ACL', [$this, 'httpAcl']); 685 $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); 686 687 array_push($server->protectedProperties, 688 '{DAV:}alternate-URI-set', 689 '{DAV:}principal-URL', 690 '{DAV:}group-membership', 691 '{DAV:}principal-collection-set', 692 '{DAV:}current-user-principal', 693 '{DAV:}supported-privilege-set', 694 '{DAV:}current-user-privilege-set', 695 '{DAV:}acl', 696 '{DAV:}acl-restrictions', 697 '{DAV:}inherited-acl-set', 698 '{DAV:}owner', 699 '{DAV:}group' 700 ); 701 702 // Automatically mapping nodes implementing IPrincipal to the 703 // {DAV:}principal resourcetype. 704 $server->resourceTypeMapping['Sabre\\DAVACL\\IPrincipal'] = '{DAV:}principal'; 705 706 // Mapping the group-member-set property to the HrefList property 707 // class. 708 $server->xml->elementMap['{DAV:}group-member-set'] = 'Sabre\\DAV\\Xml\\Property\\Href'; 709 $server->xml->elementMap['{DAV:}acl'] = 'Sabre\\DAVACL\\Xml\\Property\\Acl'; 710 $server->xml->elementMap['{DAV:}expand-property'] = 'Sabre\\DAVACL\\Xml\\Request\\ExpandPropertyReport'; 711 $server->xml->elementMap['{DAV:}principal-property-search'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalPropertySearchReport'; 712 $server->xml->elementMap['{DAV:}principal-search-property-set'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalSearchPropertySetReport'; 713 714 } 715 716 /* {{{ Event handlers */ 717 718 /** 719 * Triggered before any method is handled 720 * 721 * @param RequestInterface $request 722 * @param ResponseInterface $response 723 * @return void 724 */ 725 function beforeMethod(RequestInterface $request, ResponseInterface $response) { 726 727 $method = $request->getMethod(); 728 $path = $request->getPath(); 729 730 $exists = $this->server->tree->nodeExists($path); 731 732 // If the node doesn't exists, none of these checks apply 733 if (!$exists) return; 734 735 switch ($method) { 736 737 case 'GET' : 738 case 'HEAD' : 739 case 'OPTIONS' : 740 // For these 3 we only need to know if the node is readable. 741 $this->checkPrivileges($path, '{DAV:}read'); 742 break; 743 744 case 'PUT' : 745 case 'LOCK' : 746 case 'UNLOCK' : 747 // This method requires the write-content priv if the node 748 // already exists, and bind on the parent if the node is being 749 // created. 750 // The bind privilege is handled in the beforeBind event. 751 $this->checkPrivileges($path, '{DAV:}write-content'); 752 break; 753 754 755 case 'PROPPATCH' : 756 $this->checkPrivileges($path, '{DAV:}write-properties'); 757 break; 758 759 case 'ACL' : 760 $this->checkPrivileges($path, '{DAV:}write-acl'); 761 break; 762 763 case 'COPY' : 764 case 'MOVE' : 765 // Copy requires read privileges on the entire source tree. 766 // If the target exists write-content normally needs to be 767 // checked, however, we're deleting the node beforehand and 768 // creating a new one after, so this is handled by the 769 // beforeUnbind event. 770 // 771 // The creation of the new node is handled by the beforeBind 772 // event. 773 // 774 // If MOVE is used beforeUnbind will also be used to check if 775 // the sourcenode can be deleted. 776 $this->checkPrivileges($path, '{DAV:}read', self::R_RECURSIVE); 777 778 break; 779 780 } 781 782 } 783 784 /** 785 * Triggered before a new node is created. 786 * 787 * This allows us to check permissions for any operation that creates a 788 * new node, such as PUT, MKCOL, MKCALENDAR, LOCK, COPY and MOVE. 789 * 790 * @param string $uri 791 * @return void 792 */ 793 function beforeBind($uri) { 794 795 list($parentUri) = Uri\split($uri); 796 $this->checkPrivileges($parentUri, '{DAV:}bind'); 797 798 } 799 800 /** 801 * Triggered before a node is deleted 802 * 803 * This allows us to check permissions for any operation that will delete 804 * an existing node. 805 * 806 * @param string $uri 807 * @return void 808 */ 809 function beforeUnbind($uri) { 810 811 list($parentUri) = Uri\split($uri); 812 $this->checkPrivileges($parentUri, '{DAV:}unbind', self::R_RECURSIVEPARENTS); 813 814 } 815 816 /** 817 * Triggered before a node is unlocked. 818 * 819 * @param string $uri 820 * @param DAV\Locks\LockInfo $lock 821 * @TODO: not yet implemented 822 * @return void 823 */ 824 function beforeUnlock($uri, DAV\Locks\LockInfo $lock) { 825 826 827 } 828 829 /** 830 * Triggered before properties are looked up in specific nodes. 831 * 832 * @param DAV\PropFind $propFind 833 * @param DAV\INode $node 834 * @param array $requestedProperties 835 * @param array $returnedProperties 836 * @TODO really should be broken into multiple methods, or even a class. 837 * @return bool 838 */ 839 function propFind(DAV\PropFind $propFind, DAV\INode $node) { 840 841 $path = $propFind->getPath(); 842 843 // Checking the read permission 844 if (!$this->checkPrivileges($path, '{DAV:}read', self::R_PARENT, false)) { 845 // User is not allowed to read properties 846 847 // Returning false causes the property-fetching system to pretend 848 // that the node does not exist, and will cause it to be hidden 849 // from listings such as PROPFIND or the browser plugin. 850 if ($this->hideNodesFromListings) { 851 return false; 852 } 853 854 // Otherwise we simply mark every property as 403. 855 foreach ($propFind->getRequestedProperties() as $requestedProperty) { 856 $propFind->set($requestedProperty, null, 403); 857 } 858 859 return; 860 861 } 862 863 /* Adding principal properties */ 864 if ($node instanceof IPrincipal) { 865 866 $propFind->handle('{DAV:}alternate-URI-set', function() use ($node) { 867 return new DAV\Xml\Property\Href($node->getAlternateUriSet()); 868 }); 869 $propFind->handle('{DAV:}principal-URL', function() use ($node) { 870 return new DAV\Xml\Property\Href($node->getPrincipalUrl() . '/'); 871 }); 872 $propFind->handle('{DAV:}group-member-set', function() use ($node) { 873 $members = $node->getGroupMemberSet(); 874 foreach ($members as $k => $member) { 875 $members[$k] = rtrim($member, '/') . '/'; 876 } 877 return new DAV\Xml\Property\Href($members); 878 }); 879 $propFind->handle('{DAV:}group-membership', function() use ($node) { 880 $members = $node->getGroupMembership(); 881 foreach ($members as $k => $member) { 882 $members[$k] = rtrim($member, '/') . '/'; 883 } 884 return new DAV\Xml\Property\Href($members); 885 }); 886 $propFind->handle('{DAV:}displayname', [$node, 'getDisplayName']); 887 888 } 889 890 $propFind->handle('{DAV:}principal-collection-set', function() { 891 892 $val = $this->principalCollectionSet; 893 // Ensuring all collections end with a slash 894 foreach ($val as $k => $v) $val[$k] = $v . '/'; 895 return new DAV\Xml\Property\Href($val); 896 897 }); 898 $propFind->handle('{DAV:}current-user-principal', function() { 899 if ($url = $this->getCurrentUserPrincipal()) { 900 return new Xml\Property\Principal(Xml\Property\Principal::HREF, $url . '/'); 901 } else { 902 return new Xml\Property\Principal(Xml\Property\Principal::UNAUTHENTICATED); 903 } 904 }); 905 $propFind->handle('{DAV:}supported-privilege-set', function() use ($node) { 906 return new Xml\Property\SupportedPrivilegeSet($this->getSupportedPrivilegeSet($node)); 907 }); 908 $propFind->handle('{DAV:}current-user-privilege-set', function() use ($node, $propFind, $path) { 909 if (!$this->checkPrivileges($path, '{DAV:}read-current-user-privilege-set', self::R_PARENT, false)) { 910 $propFind->set('{DAV:}current-user-privilege-set', null, 403); 911 } else { 912 $val = $this->getCurrentUserPrivilegeSet($node); 913 if (!is_null($val)) { 914 return new Xml\Property\CurrentUserPrivilegeSet($val); 915 } 916 } 917 }); 918 $propFind->handle('{DAV:}acl', function() use ($node, $propFind, $path) { 919 /* The ACL property contains all the permissions */ 920 if (!$this->checkPrivileges($path, '{DAV:}read-acl', self::R_PARENT, false)) { 921 $propFind->set('{DAV:}acl', null, 403); 922 } else { 923 $acl = $this->getACL($node); 924 if (!is_null($acl)) { 925 return new Xml\Property\Acl($this->getACL($node)); 926 } 927 } 928 }); 929 $propFind->handle('{DAV:}acl-restrictions', function() { 930 return new Xml\Property\AclRestrictions(); 931 }); 932 933 /* Adding ACL properties */ 934 if ($node instanceof IACL) { 935 $propFind->handle('{DAV:}owner', function() use ($node) { 936 return new DAV\Xml\Property\Href($node->getOwner() . '/'); 937 }); 938 } 939 940 } 941 942 /** 943 * This method intercepts PROPPATCH methods and make sure the 944 * group-member-set is updated correctly. 945 * 946 * @param string $path 947 * @param DAV\PropPatch $propPatch 948 * @return void 949 */ 950 function propPatch($path, DAV\PropPatch $propPatch) { 951 952 $propPatch->handle('{DAV:}group-member-set', function($value) use ($path) { 953 if (is_null($value)) { 954 $memberSet = []; 955 } elseif ($value instanceof DAV\Xml\Property\Href) { 956 $memberSet = array_map( 957 [$this->server, 'calculateUri'], 958 $value->getHrefs() 959 ); 960 } else { 961 throw new DAV\Exception('The group-member-set property MUST be an instance of Sabre\DAV\Property\HrefList or null'); 962 } 963 $node = $this->server->tree->getNodeForPath($path); 964 if (!($node instanceof IPrincipal)) { 965 // Fail 966 return false; 967 } 968 969 $node->setGroupMemberSet($memberSet); 970 // We must also clear our cache, just in case 971 972 $this->principalMembershipCache = []; 973 974 return true; 975 }); 976 977 } 978 979 /** 980 * This method handles HTTP REPORT requests 981 * 982 * @param string $reportName 983 * @param mixed $report 984 * @return bool 985 */ 986 function report($reportName, $report) { 987 988 switch ($reportName) { 989 990 case '{DAV:}principal-property-search' : 991 $this->server->transactionType = 'report-principal-property-search'; 992 $this->principalPropertySearchReport($report); 993 return false; 994 case '{DAV:}principal-search-property-set' : 995 $this->server->transactionType = 'report-principal-search-property-set'; 996 $this->principalSearchPropertySetReport($report); 997 return false; 998 case '{DAV:}expand-property' : 999 $this->server->transactionType = 'report-expand-property'; 1000 $this->expandPropertyReport($report); 1001 return false; 1002 1003 } 1004 1005 } 1006 1007 /** 1008 * This method is responsible for handling the 'ACL' event. 1009 * 1010 * @param RequestInterface $request 1011 * @param ResponseInterface $response 1012 * @return bool 1013 */ 1014 function httpAcl(RequestInterface $request, ResponseInterface $response) { 1015 1016 $path = $request->getPath(); 1017 $body = $request->getBodyAsString(); 1018 1019 if (!$body) { 1020 throw new DAV\Exception\BadRequest('XML body expected in ACL request'); 1021 } 1022 1023 $acl = $this->server->xml->expect('{DAV:}acl', $body); 1024 $newAcl = $acl->getPrivileges(); 1025 1026 // Normalizing urls 1027 foreach ($newAcl as $k => $newAce) { 1028 $newAcl[$k]['principal'] = $this->server->calculateUri($newAce['principal']); 1029 } 1030 $node = $this->server->tree->getNodeForPath($path); 1031 1032 if (!$node instanceof IACL) { 1033 throw new DAV\Exception\MethodNotAllowed('This node does not support the ACL method'); 1034 } 1035 1036 $oldAcl = $this->getACL($node); 1037 1038 $supportedPrivileges = $this->getFlatPrivilegeSet($node); 1039 1040 /* Checking if protected principals from the existing principal set are 1041 not overwritten. */ 1042 foreach ($oldAcl as $oldAce) { 1043 1044 if (!isset($oldAce['protected']) || !$oldAce['protected']) continue; 1045 1046 $found = false; 1047 foreach ($newAcl as $newAce) { 1048 if ( 1049 $newAce['privilege'] === $oldAce['privilege'] && 1050 $newAce['principal'] === $oldAce['principal'] && 1051 $newAce['protected'] 1052 ) 1053 $found = true; 1054 } 1055 1056 if (!$found) 1057 throw new Exception\AceConflict('This resource contained a protected {DAV:}ace, but this privilege did not occur in the ACL request'); 1058 1059 } 1060 1061 foreach ($newAcl as $newAce) { 1062 1063 // Do we recognize the privilege 1064 if (!isset($supportedPrivileges[$newAce['privilege']])) { 1065 throw new Exception\NotSupportedPrivilege('The privilege you specified (' . $newAce['privilege'] . ') is not recognized by this server'); 1066 } 1067 1068 if ($supportedPrivileges[$newAce['privilege']]['abstract']) { 1069 throw new Exception\NoAbstract('The privilege you specified (' . $newAce['privilege'] . ') is an abstract privilege'); 1070 } 1071 1072 // Looking up the principal 1073 try { 1074 $principal = $this->server->tree->getNodeForPath($newAce['principal']); 1075 } catch (DAV\Exception\NotFound $e) { 1076 throw new Exception\NotRecognizedPrincipal('The specified principal (' . $newAce['principal'] . ') does not exist'); 1077 } 1078 if (!($principal instanceof IPrincipal)) { 1079 throw new Exception\NotRecognizedPrincipal('The specified uri (' . $newAce['principal'] . ') is not a principal'); 1080 } 1081 1082 } 1083 $node->setACL($newAcl); 1084 1085 $response->setStatus(200); 1086 1087 // Breaking the event chain, because we handled this method. 1088 return false; 1089 1090 } 1091 1092 /* }}} */ 1093 1094 /* Reports {{{ */ 1095 1096 /** 1097 * The expand-property report is defined in RFC3253 section 3-8. 1098 * 1099 * This report is very similar to a standard PROPFIND. The difference is 1100 * that it has the additional ability to look at properties containing a 1101 * {DAV:}href element, follow that property and grab additional elements 1102 * there. 1103 * 1104 * Other rfc's, such as ACL rely on this report, so it made sense to put 1105 * it in this plugin. 1106 * 1107 * @param Xml\Request\ExpandPropertyReport $report 1108 * @return void 1109 */ 1110 protected function expandPropertyReport($report) { 1111 1112 $depth = $this->server->getHTTPDepth(0); 1113 $requestUri = $this->server->getRequestUri(); 1114 1115 $result = $this->expandProperties($requestUri, $report->properties, $depth); 1116 1117 $xml = $this->server->xml->write( 1118 '{DAV:}multistatus', 1119 new DAV\Xml\Response\MultiStatus($result), 1120 $this->server->getBaseUri() 1121 ); 1122 $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); 1123 $this->server->httpResponse->setStatus(207); 1124 $this->server->httpResponse->setBody($xml); 1125 1126 } 1127 1128 /** 1129 * This method expands all the properties and returns 1130 * a list with property values 1131 * 1132 * @param array $path 1133 * @param array $requestedProperties the list of required properties 1134 * @param int $depth 1135 * @return array 1136 */ 1137 protected function expandProperties($path, array $requestedProperties, $depth) { 1138 1139 $foundProperties = $this->server->getPropertiesForPath($path, array_keys($requestedProperties), $depth); 1140 1141 $result = []; 1142 1143 foreach ($foundProperties as $node) { 1144 1145 foreach ($requestedProperties as $propertyName => $childRequestedProperties) { 1146 1147 // We're only traversing if sub-properties were requested 1148 if (count($childRequestedProperties) === 0) continue; 1149 1150 // We only have to do the expansion if the property was found 1151 // and it contains an href element. 1152 if (!array_key_exists($propertyName, $node[200])) continue; 1153 1154 if (!$node[200][$propertyName] instanceof DAV\Xml\Property\Href) { 1155 continue; 1156 } 1157 1158 $childHrefs = $node[200][$propertyName]->getHrefs(); 1159 $childProps = []; 1160 1161 foreach ($childHrefs as $href) { 1162 // Gathering the result of the children 1163 $childProps[] = [ 1164 'name' => '{DAV:}response', 1165 'value' => $this->expandProperties($href, $childRequestedProperties, 0)[0] 1166 ]; 1167 } 1168 1169 // Replacing the property with its expannded form. 1170 $node[200][$propertyName] = $childProps; 1171 1172 } 1173 $result[] = new DAV\Xml\Element\Response($node['href'], $node); 1174 1175 } 1176 1177 return $result; 1178 1179 } 1180 1181 /** 1182 * principalSearchPropertySetReport 1183 * 1184 * This method responsible for handing the 1185 * {DAV:}principal-search-property-set report. This report returns a list 1186 * of properties the client may search on, using the 1187 * {DAV:}principal-property-search report. 1188 * 1189 * @param Xml\Request\PrincipalSearchPropertySetReport $report 1190 * @return void 1191 */ 1192 protected function principalSearchPropertySetReport($report) { 1193 1194 $httpDepth = $this->server->getHTTPDepth(0); 1195 if ($httpDepth !== 0) { 1196 throw new DAV\Exception\BadRequest('This report is only defined when Depth: 0'); 1197 } 1198 1199 $writer = $this->server->xml->getWriter(); 1200 $writer->openMemory(); 1201 $writer->startDocument(); 1202 1203 $writer->startElement('{DAV:}principal-search-property-set'); 1204 1205 foreach ($this->principalSearchPropertySet as $propertyName => $description) { 1206 1207 $writer->startElement('{DAV:}principal-search-property'); 1208 $writer->startElement('{DAV:}prop'); 1209 1210 $writer->writeElement($propertyName); 1211 1212 $writer->endElement(); // prop 1213 1214 if ($description) { 1215 $writer->write([[ 1216 'name' => '{DAV:}description', 1217 'value' => $description, 1218 'attributes' => ['xml:lang' => 'en'] 1219 ]]); 1220 } 1221 1222 $writer->endElement(); // principal-search-property 1223 1224 1225 } 1226 1227 $writer->endElement(); // principal-search-property-set 1228 1229 $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); 1230 $this->server->httpResponse->setStatus(200); 1231 $this->server->httpResponse->setBody($writer->outputMemory()); 1232 1233 } 1234 1235 /** 1236 * principalPropertySearchReport 1237 * 1238 * This method is responsible for handing the 1239 * {DAV:}principal-property-search report. This report can be used for 1240 * clients to search for groups of principals, based on the value of one 1241 * or more properties. 1242 * 1243 * @param Xml\Request\PrincipalPropertySearchReport $report 1244 * @return void 1245 */ 1246 protected function principalPropertySearchReport($report) { 1247 1248 $uri = null; 1249 if (!$report->applyToPrincipalCollectionSet) { 1250 $uri = $this->server->httpRequest->getPath(); 1251 } 1252 if ($this->server->getHttpDepth('0') !== 0) { 1253 throw new BadRequest('Depth must be 0'); 1254 } 1255 $result = $this->principalSearch( 1256 $report->searchProperties, 1257 $report->properties, 1258 $uri, 1259 $report->test 1260 ); 1261 1262 $prefer = $this->server->getHTTPPrefer(); 1263 1264 $this->server->httpResponse->setStatus(207); 1265 $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); 1266 $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); 1267 $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, $prefer['return'] === 'minimal')); 1268 1269 } 1270 1271 /* }}} */ 1272 1273 /** 1274 * This method is used to generate HTML output for the 1275 * DAV\Browser\Plugin. This allows us to generate an interface users 1276 * can use to create new calendars. 1277 * 1278 * @param DAV\INode $node 1279 * @param string $output 1280 * @return bool 1281 */ 1282 function htmlActionsPanel(DAV\INode $node, &$output) { 1283 1284 if (!$node instanceof PrincipalCollection) 1285 return; 1286 1287 $output .= '<tr><td colspan="2"><form method="post" action=""> 1288 <h3>Create new principal</h3> 1289 <input type="hidden" name="sabreAction" value="mkcol" /> 1290 <input type="hidden" name="resourceType" value="{DAV:}principal" /> 1291 <label>Name (uri):</label> <input type="text" name="name" /><br /> 1292 <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br /> 1293 <label>Email address:</label> <input type="text" name="{http://sabredav*DOT*org/ns}email-address" /><br /> 1294 <input type="submit" value="create" /> 1295 </form> 1296 </td></tr>'; 1297 1298 return false; 1299 1300 } 1301 1302 /** 1303 * Returns a bunch of meta-data about the plugin. 1304 * 1305 * Providing this information is optional, and is mainly displayed by the 1306 * Browser plugin. 1307 * 1308 * The description key in the returned array may contain html and will not 1309 * be sanitized. 1310 * 1311 * @return array 1312 */ 1313 function getPluginInfo() { 1314 1315 return [ 1316 'name' => $this->getPluginName(), 1317 'description' => 'Adds support for WebDAV ACL (rfc3744)', 1318 'link' => 'http://sabre.io/dav/acl/', 1319 ]; 1320 1321 } 1322} 1323