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