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