6 * This plugin provides functionality to enforce ACL permissions.
7 * ACL is defined in RFC3744.
9 * In addition it also provides support for the {DAV:}current-user-principal
10 * property, defined in RFC5397 and the {DAV:}expand-property report, as
15 * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved.
16 * @author Evert Pot (http://www.rooftopsolutions.nl/)
17 * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
19 class Sabre_DAVACL_Plugin extends Sabre_DAV_ServerPlugin {
24 * This only checks the base node
31 * This checks every node in the tree
33 const R_RECURSIVE = 2;
38 * This checks every parentnode in the tree, but not leaf-nodes.
40 const R_RECURSIVEPARENTS = 3;
43 * Reference to server object.
45 * @var Sabre_DAV_Server
50 * List of urls containing principal collections.
51 * Modify this if your principals are located elsewhere.
55 public $principalCollectionSet = array(
60 * By default ACL is only enforced for nodes that have ACL support (the
61 * ones that implement Sabre_DAVACL_IACL). For any other node, access is
64 * To override this behaviour you can turn this setting off. This is useful
65 * if you plan to fully support ACL in the entire tree.
69 public $allowAccessToNodesWithoutACL = true;
72 * By default nodes that are inaccessible by the user, can still be seen
73 * in directory listings (PROPFIND on parent with Depth: 1)
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.
80 public $hideNodesFromListings = false;
83 * This string is prepended to the username of the currently logged in
84 * user. This allows the plugin to determine the principal path based on
89 public $defaultUsernamePath = 'principals';
92 * This list of properties are the properties a client can search on using
93 * the {DAV:}principal-property-search report.
95 * The keys are the property names, values are descriptions.
99 public $principalSearchPropertySet = array(
100 '{DAV:}displayname' => 'Display name',
101 '{http://sabredav.org/ns}email-address' => 'Email address',
105 * Any principal uri's added here, will automatically be added to the list
106 * of ACL's. They will effectively receive {DAV:}all privileges, as a
107 * protected privilege.
111 public $adminPrincipals = array();
114 * Returns a list of features added by this plugin.
116 * This list is used in the response of a HTTP OPTIONS request.
120 public function getFeatures() {
122 return array('access-control');
127 * Returns a list of available methods for a given url
132 public function getMethods($uri) {
139 * Returns a plugin name.
141 * Using this name other plugins will be able to access other plugins
142 * using Sabre_DAV_Server::getPlugin
146 public function getPluginName() {
153 * Returns a list of reports this plugin supports.
155 * This will be used in the {DAV:}supported-report-set property.
156 * Note that you still need to subscribe to the 'report' event to actually
162 public function getSupportedReportSet($uri) {
165 '{DAV:}expand-property',
166 '{DAV:}principal-property-search',
167 '{DAV:}principal-search-property-set',
174 * Checks if the current user has the specified privilege(s).
176 * You can specify a single privilege, or a list of privileges.
177 * This method will throw an exception if the privilege is not available
178 * and return true otherwise.
181 * @param array|string $privileges
182 * @param int $recursion
183 * @param bool $throwExceptions if set to false, this method won't through exceptions.
184 * @throws Sabre_DAVACL_Exception_NeedPrivileges
187 public function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true) {
189 if (!is_array($privileges)) $privileges = array($privileges);
191 $acl = $this->getCurrentUserPrivilegeSet($uri);
194 if ($this->allowAccessToNodesWithoutACL) {
197 if ($throwExceptions)
198 throw new Sabre_DAVACL_Exception_NeedPrivileges($uri,$privileges);
206 foreach($privileges as $priv) {
208 if (!in_array($priv, $acl)) {
215 if ($throwExceptions)
216 throw new Sabre_DAVACL_Exception_NeedPrivileges($uri,$failed);
225 * Returns the standard users' principal.
227 * This is one authorative principal url for the current user.
228 * This method will return null if the user wasn't logged in.
230 * @return string|null
232 public function getCurrentUserPrincipal() {
234 $authPlugin = $this->server->getPlugin('auth');
235 if (is_null($authPlugin)) return null;
236 /** @var $authPlugin Sabre_DAV_Auth_Plugin */
238 $userName = $authPlugin->getCurrentUser();
239 if (!$userName) return null;
241 return $this->defaultUsernamePath . '/' . $userName;
246 * Returns a list of principals that's associated to the current
247 * user, either directly or through group membership.
251 public function getCurrentUserPrincipals() {
253 $currentUser = $this->getCurrentUserPrincipal();
255 if (is_null($currentUser)) return array();
257 $check = array($currentUser);
258 $principals = array($currentUser);
260 while(count($check)) {
262 $principal = array_shift($check);
264 $node = $this->server->tree->getNodeForPath($principal);
265 if ($node instanceof Sabre_DAVACL_IPrincipal) {
266 foreach($node->getGroupMembership() as $groupMember) {
268 if (!in_array($groupMember, $principals)) {
270 $check[] = $groupMember;
271 $principals[] = $groupMember;
286 * Returns the supported privilege structure for this ACL plugin.
288 * See RFC3744 for more details. Currently we default on a simple,
289 * standard structure.
291 * You can either get the list of privileges by a uri (path) or by
294 * @param string|Sabre_DAV_INode $node
297 public function getSupportedPrivilegeSet($node) {
299 if (is_string($node)) {
300 $node = $this->server->tree->getNodeForPath($node);
303 if ($node instanceof Sabre_DAVACL_IACL) {
304 $result = $node->getSupportedPrivilegeSet();
310 return self::getDefaultSupportedPrivilegeSet();
315 * Returns a fairly standard set of privileges, which may be useful for
316 * other systems to use as a basis.
320 static function getDefaultSupportedPrivilegeSet() {
323 'privilege' => '{DAV:}all',
325 'aggregates' => array(
327 'privilege' => '{DAV:}read',
328 'aggregates' => array(
330 'privilege' => '{DAV:}read-acl',
334 'privilege' => '{DAV:}read-current-user-privilege-set',
340 'privilege' => '{DAV:}write',
341 'aggregates' => array(
343 'privilege' => '{DAV:}write-acl',
347 'privilege' => '{DAV:}write-properties',
351 'privilege' => '{DAV:}write-content',
355 'privilege' => '{DAV:}bind',
359 'privilege' => '{DAV:}unbind',
363 'privilege' => '{DAV:}unlock',
374 * Returns the supported privilege set as a flat list
376 * This is much easier to parse.
378 * The returned list will be index by privilege name.
379 * The value is a struct containing the following properties:
384 * @param string|Sabre_DAV_INode $node
387 final public function getFlatPrivilegeSet($node) {
389 $privs = $this->getSupportedPrivilegeSet($node);
392 $this->getFPSTraverse($privs, null, $flat);
399 * Traverses the privilege set tree for reordering
401 * This function is solely used by getFlatPrivilegeSet, and would have been
402 * a closure if it wasn't for the fact I need to support PHP 5.2.
409 final private function getFPSTraverse($priv, $concrete, &$flat) {
412 'privilege' => $priv['privilege'],
413 'abstract' => isset($priv['abstract']) && $priv['abstract'],
414 'aggregates' => array(),
415 'concrete' => isset($priv['abstract']) && $priv['abstract']?$concrete:$priv['privilege'],
418 if (isset($priv['aggregates']))
419 foreach($priv['aggregates'] as $subPriv) $myPriv['aggregates'][] = $subPriv['privilege'];
421 $flat[$priv['privilege']] = $myPriv;
423 if (isset($priv['aggregates'])) {
425 foreach($priv['aggregates'] as $subPriv) {
427 $this->getFPSTraverse($subPriv, $myPriv['concrete'], $flat);
436 * Returns the full ACL list.
438 * Either a uri or a Sabre_DAV_INode may be passed.
440 * null will be returned if the node doesn't support ACLs.
442 * @param string|Sabre_DAV_INode $node
445 public function getACL($node) {
447 if (is_string($node)) {
448 $node = $this->server->tree->getNodeForPath($node);
450 if (!$node instanceof Sabre_DAVACL_IACL) {
453 $acl = $node->getACL();
454 foreach($this->adminPrincipals as $adminPrincipal) {
456 'principal' => $adminPrincipal,
457 'privilege' => '{DAV:}all',
466 * Returns a list of privileges the current user has
467 * on a particular node.
469 * Either a uri or a Sabre_DAV_INode may be passed.
471 * null will be returned if the node doesn't support ACLs.
473 * @param string|Sabre_DAV_INode $node
476 public function getCurrentUserPrivilegeSet($node) {
478 if (is_string($node)) {
479 $node = $this->server->tree->getNodeForPath($node);
482 $acl = $this->getACL($node);
484 if (is_null($acl)) return null;
486 $principals = $this->getCurrentUserPrincipals();
488 $collected = array();
490 foreach($acl as $ace) {
492 $principal = $ace['principal'];
497 $owner = $node->getOwner();
498 if ($owner && in_array($owner, $principals)) {
504 // 'all' matches for every user
507 // 'authenticated' matched for every user that's logged in.
508 // Since it's not possible to use ACL while not being logged
509 // in, this is also always true.
510 case '{DAV:}authenticated' :
514 // 'unauthenticated' can never occur either, so we simply
516 case '{DAV:}unauthenticated' :
520 if (in_array($ace['principal'], $principals)) {
531 // Now we deduct all aggregated privileges.
532 $flat = $this->getFlatPrivilegeSet($node);
534 $collected2 = array();
535 while(count($collected)) {
537 $current = array_pop($collected);
538 $collected2[] = $current['privilege'];
540 foreach($flat[$current['privilege']]['aggregates'] as $subPriv) {
541 $collected2[] = $subPriv;
542 $collected[] = $flat[$subPriv];
547 return array_values(array_unique($collected2));
552 * Principal property search
554 * This method can search for principals matching certain values in
557 * This method will return a list of properties for the matched properties.
559 * @param array $searchProperties The properties to search on. This is a
560 * key-value list. The keys are property
561 * names, and the values the strings to
563 * @param array $requestedProperties This is the list of properties to
564 * return for every match.
565 * @param string $collectionUri The principal collection to search on.
566 * If this is ommitted, the standard
567 * principal collection-set will be used.
568 * @return array This method returns an array structure similar to
569 * Sabre_DAV_Server::getPropertiesForPath. Returned
570 * properties are index by a HTTP status code.
573 public function principalSearch(array $searchProperties, array $requestedProperties, $collectionUri = null) {
575 if (!is_null($collectionUri)) {
576 $uris = array($collectionUri);
578 $uris = $this->principalCollectionSet;
581 $lookupResults = array();
582 foreach($uris as $uri) {
584 $principalCollection = $this->server->tree->getNodeForPath($uri);
585 if (!$principalCollection instanceof Sabre_DAVACL_AbstractPrincipalCollection) {
586 // Not a principal collection, we're simply going to ignore
591 $results = $principalCollection->searchPrincipals($searchProperties);
592 foreach($results as $result) {
593 $lookupResults[] = rtrim($uri,'/') . '/' . $result;
600 foreach($lookupResults as $lookupResult) {
602 list($matches[]) = $this->server->getPropertiesForPath($lookupResult, $requestedProperties, 0);
613 * This method is automatically called by the server class.
615 * @param Sabre_DAV_Server $server
618 public function initialize(Sabre_DAV_Server $server) {
620 $this->server = $server;
621 $server->subscribeEvent('beforeGetProperties',array($this,'beforeGetProperties'));
623 $server->subscribeEvent('beforeMethod', array($this,'beforeMethod'),20);
624 $server->subscribeEvent('beforeBind', array($this,'beforeBind'),20);
625 $server->subscribeEvent('beforeUnbind', array($this,'beforeUnbind'),20);
626 $server->subscribeEvent('updateProperties',array($this,'updateProperties'));
627 $server->subscribeEvent('beforeUnlock', array($this,'beforeUnlock'),20);
628 $server->subscribeEvent('report',array($this,'report'));
629 $server->subscribeEvent('unknownMethod', array($this, 'unknownMethod'));
631 array_push($server->protectedProperties,
632 '{DAV:}alternate-URI-set',
633 '{DAV:}principal-URL',
634 '{DAV:}group-membership',
635 '{DAV:}principal-collection-set',
636 '{DAV:}current-user-principal',
637 '{DAV:}supported-privilege-set',
638 '{DAV:}current-user-privilege-set',
640 '{DAV:}acl-restrictions',
641 '{DAV:}inherited-acl-set',
646 // Automatically mapping nodes implementing IPrincipal to the
647 // {DAV:}principal resourcetype.
648 $server->resourceTypeMapping['Sabre_DAVACL_IPrincipal'] = '{DAV:}principal';
650 // Mapping the group-member-set property to the HrefList property
652 $server->propertyMap['{DAV:}group-member-set'] = 'Sabre_DAV_Property_HrefList';
657 /* {{{ Event handlers */
660 * Triggered before any method is handled
662 * @param string $method
666 public function beforeMethod($method, $uri) {
668 $exists = $this->server->tree->nodeExists($uri);
670 // If the node doesn't exists, none of these checks apply
671 if (!$exists) return;
678 // For these 3 we only need to know if the node is readable.
679 $this->checkPrivileges($uri,'{DAV:}read');
685 // This method requires the write-content priv if the node
686 // already exists, and bind on the parent if the node is being
688 // The bind privilege is handled in the beforeBind event.
689 $this->checkPrivileges($uri,'{DAV:}write-content');
694 $this->checkPrivileges($uri,'{DAV:}write-properties');
698 $this->checkPrivileges($uri,'{DAV:}write-acl');
703 // Copy requires read privileges on the entire source tree.
704 // If the target exists write-content normally needs to be
705 // checked, however, we're deleting the node beforehand and
706 // creating a new one after, so this is handled by the
707 // beforeUnbind event.
709 // The creation of the new node is handled by the beforeBind
712 // If MOVE is used beforeUnbind will also be used to check if
713 // the sourcenode can be deleted.
714 $this->checkPrivileges($uri,'{DAV:}read',self::R_RECURSIVE);
723 * Triggered before a new node is created.
725 * This allows us to check permissions for any operation that creates a
726 * new node, such as PUT, MKCOL, MKCALENDAR, LOCK, COPY and MOVE.
731 public function beforeBind($uri) {
733 list($parentUri,$nodeName) = Sabre_DAV_URLUtil::splitPath($uri);
734 $this->checkPrivileges($parentUri,'{DAV:}bind');
739 * Triggered before a node is deleted
741 * This allows us to check permissions for any operation that will delete
747 public function beforeUnbind($uri) {
749 list($parentUri,$nodeName) = Sabre_DAV_URLUtil::splitPath($uri);
750 $this->checkPrivileges($parentUri,'{DAV:}unbind',self::R_RECURSIVEPARENTS);
755 * Triggered before a node is unlocked.
758 * @param Sabre_DAV_Locks_LockInfo $lock
759 * @TODO: not yet implemented
762 public function beforeUnlock($uri, Sabre_DAV_Locks_LockInfo $lock) {
768 * Triggered before properties are looked up in specific nodes.
771 * @param Sabre_DAV_INode $node
772 * @param array $requestedProperties
773 * @param array $returnedProperties
774 * @TODO really should be broken into multiple methods, or even a class.
777 public function beforeGetProperties($uri, Sabre_DAV_INode $node, &$requestedProperties, &$returnedProperties) {
779 // Checking the read permission
780 if (!$this->checkPrivileges($uri,'{DAV:}read',self::R_PARENT,false)) {
782 // User is not allowed to read properties
783 if ($this->hideNodesFromListings) {
787 // Marking all requested properties as '403'.
788 foreach($requestedProperties as $key=>$requestedProperty) {
789 unset($requestedProperties[$key]);
790 $returnedProperties[403][$requestedProperty] = null;
796 /* Adding principal properties */
797 if ($node instanceof Sabre_DAVACL_IPrincipal) {
799 if (false !== ($index = array_search('{DAV:}alternate-URI-set', $requestedProperties))) {
801 unset($requestedProperties[$index]);
802 $returnedProperties[200]['{DAV:}alternate-URI-set'] = new Sabre_DAV_Property_HrefList($node->getAlternateUriSet());
805 if (false !== ($index = array_search('{DAV:}principal-URL', $requestedProperties))) {
807 unset($requestedProperties[$index]);
808 $returnedProperties[200]['{DAV:}principal-URL'] = new Sabre_DAV_Property_Href($node->getPrincipalUrl() . '/');
811 if (false !== ($index = array_search('{DAV:}group-member-set', $requestedProperties))) {
813 unset($requestedProperties[$index]);
814 $returnedProperties[200]['{DAV:}group-member-set'] = new Sabre_DAV_Property_HrefList($node->getGroupMemberSet());
817 if (false !== ($index = array_search('{DAV:}group-membership', $requestedProperties))) {
819 unset($requestedProperties[$index]);
820 $returnedProperties[200]['{DAV:}group-membership'] = new Sabre_DAV_Property_HrefList($node->getGroupMembership());
824 if (false !== ($index = array_search('{DAV:}displayname', $requestedProperties))) {
826 $returnedProperties[200]['{DAV:}displayname'] = $node->getDisplayName();
831 if (false !== ($index = array_search('{DAV:}principal-collection-set', $requestedProperties))) {
833 unset($requestedProperties[$index]);
834 $val = $this->principalCollectionSet;
835 // Ensuring all collections end with a slash
836 foreach($val as $k=>$v) $val[$k] = $v . '/';
837 $returnedProperties[200]['{DAV:}principal-collection-set'] = new Sabre_DAV_Property_HrefList($val);
840 if (false !== ($index = array_search('{DAV:}current-user-principal', $requestedProperties))) {
842 unset($requestedProperties[$index]);
843 if ($url = $this->getCurrentUserPrincipal()) {
844 $returnedProperties[200]['{DAV:}current-user-principal'] = new Sabre_DAVACL_Property_Principal(Sabre_DAVACL_Property_Principal::HREF, $url . '/');
846 $returnedProperties[200]['{DAV:}current-user-principal'] = new Sabre_DAVACL_Property_Principal(Sabre_DAVACL_Property_Principal::UNAUTHENTICATED);
850 if (false !== ($index = array_search('{DAV:}supported-privilege-set', $requestedProperties))) {
852 unset($requestedProperties[$index]);
853 $returnedProperties[200]['{DAV:}supported-privilege-set'] = new Sabre_DAVACL_Property_SupportedPrivilegeSet($this->getSupportedPrivilegeSet($node));
856 if (false !== ($index = array_search('{DAV:}current-user-privilege-set', $requestedProperties))) {
858 if (!$this->checkPrivileges($uri, '{DAV:}read-current-user-privilege-set', self::R_PARENT, false)) {
859 $returnedProperties[403]['{DAV:}current-user-privilege-set'] = null;
860 unset($requestedProperties[$index]);
862 $val = $this->getCurrentUserPrivilegeSet($node);
863 if (!is_null($val)) {
864 unset($requestedProperties[$index]);
865 $returnedProperties[200]['{DAV:}current-user-privilege-set'] = new Sabre_DAVACL_Property_CurrentUserPrivilegeSet($val);
871 /* The ACL property contains all the permissions */
872 if (false !== ($index = array_search('{DAV:}acl', $requestedProperties))) {
874 if (!$this->checkPrivileges($uri, '{DAV:}read-acl', self::R_PARENT, false)) {
876 unset($requestedProperties[$index]);
877 $returnedProperties[403]['{DAV:}acl'] = null;
881 $acl = $this->getACL($node);
882 if (!is_null($acl)) {
883 unset($requestedProperties[$index]);
884 $returnedProperties[200]['{DAV:}acl'] = new Sabre_DAVACL_Property_Acl($this->getACL($node));
891 /* The acl-restrictions property contains information on how privileges
894 if (false !== ($index = array_search('{DAV:}acl-restrictions', $requestedProperties))) {
895 unset($requestedProperties[$index]);
896 $returnedProperties[200]['{DAV:}acl-restrictions'] = new Sabre_DAVACL_Property_AclRestrictions();
902 * This method intercepts PROPPATCH methods and make sure the
903 * group-member-set is updated correctly.
905 * @param array $propertyDelta
906 * @param array $result
907 * @param Sabre_DAV_INode $node
910 public function updateProperties(&$propertyDelta, &$result, Sabre_DAV_INode $node) {
912 if (!array_key_exists('{DAV:}group-member-set', $propertyDelta))
915 if (is_null($propertyDelta['{DAV:}group-member-set'])) {
916 $memberSet = array();
917 } elseif ($propertyDelta['{DAV:}group-member-set'] instanceof Sabre_DAV_Property_HrefList) {
918 $memberSet = $propertyDelta['{DAV:}group-member-set']->getHrefs();
920 throw new Sabre_DAV_Exception('The group-member-set property MUST be an instance of Sabre_DAV_Property_HrefList or null');
923 if (!($node instanceof Sabre_DAVACL_IPrincipal)) {
924 $result[403]['{DAV:}group-member-set'] = null;
925 unset($propertyDelta['{DAV:}group-member-set']);
927 // Returning false will stop the updateProperties process
931 $node->setGroupMemberSet($memberSet);
933 $result[200]['{DAV:}group-member-set'] = null;
934 unset($propertyDelta['{DAV:}group-member-set']);
939 * This method handles HTTP REPORT requests
941 * @param string $reportName
942 * @param DOMNode $dom
945 public function report($reportName, $dom) {
947 switch($reportName) {
949 case '{DAV:}principal-property-search' :
950 $this->principalPropertySearchReport($dom);
952 case '{DAV:}principal-search-property-set' :
953 $this->principalSearchPropertySetReport($dom);
955 case '{DAV:}expand-property' :
956 $this->expandPropertyReport($dom);
964 * This event is triggered for any HTTP method that is not known by the
967 * @param string $method
971 public function unknownMethod($method, $uri) {
973 if ($method!=='ACL') return;
975 $this->httpACL($uri);
981 * This method is responsible for handling the 'ACL' event.
986 public function httpACL($uri) {
988 $body = $this->server->httpRequest->getBody(true);
989 $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
992 Sabre_DAVACL_Property_Acl::unserialize($dom->firstChild)
996 foreach($newAcl as $k=>$newAce) {
997 $newAcl[$k]['principal'] = $this->server->calculateUri($newAce['principal']);
1000 $node = $this->server->tree->getNodeForPath($uri);
1002 if (!($node instanceof Sabre_DAVACL_IACL)) {
1003 throw new Sabre_DAV_Exception_MethodNotAllowed('This node does not support the ACL method');
1006 $oldAcl = $this->getACL($node);
1008 $supportedPrivileges = $this->getFlatPrivilegeSet($node);
1010 /* Checking if protected principals from the existing principal set are
1012 foreach($oldAcl as $oldAce) {
1014 if (!isset($oldAce['protected']) || !$oldAce['protected']) continue;
1017 foreach($newAcl as $newAce) {
1019 $newAce['privilege'] === $oldAce['privilege'] &&
1020 $newAce['principal'] === $oldAce['principal'] &&
1021 $newAce['protected']
1027 throw new Sabre_DAVACL_Exception_AceConflict('This resource contained a protected {DAV:}ace, but this privilege did not occur in the ACL request');
1031 foreach($newAcl as $newAce) {
1033 // Do we recognize the privilege
1034 if (!isset($supportedPrivileges[$newAce['privilege']])) {
1035 throw new Sabre_DAVACL_Exception_NotSupportedPrivilege('The privilege you specified (' . $newAce['privilege'] . ') is not recognized by this server');
1038 if ($supportedPrivileges[$newAce['privilege']]['abstract']) {
1039 throw new Sabre_DAVACL_Exception_NoAbstract('The privilege you specified (' . $newAce['privilege'] . ') is an abstract privilege');
1042 // Looking up the principal
1044 $principal = $this->server->tree->getNodeForPath($newAce['principal']);
1045 } catch (Sabre_DAV_Exception_NotFound $e) {
1046 throw new Sabre_DAVACL_Exception_NotRecognizedPrincipal('The specified principal (' . $newAce['principal'] . ') does not exist');
1048 if (!($principal instanceof Sabre_DAVACL_IPrincipal)) {
1049 throw new Sabre_DAVACL_Exception_NotRecognizedPrincipal('The specified uri (' . $newAce['principal'] . ') is not a principal');
1053 $node->setACL($newAcl);
1062 * The expand-property report is defined in RFC3253 section 3-8.
1064 * This report is very similar to a standard PROPFIND. The difference is
1065 * that it has the additional ability to look at properties containing a
1066 * {DAV:}href element, follow that property and grab additional elements
1069 * Other rfc's, such as ACL rely on this report, so it made sense to put
1070 * it in this plugin.
1072 * @param DOMElement $dom
1075 protected function expandPropertyReport($dom) {
1077 $requestedProperties = $this->parseExpandPropertyReportRequest($dom->firstChild->firstChild);
1078 $depth = $this->server->getHTTPDepth(0);
1079 $requestUri = $this->server->getRequestUri();
1081 $result = $this->expandProperties($requestUri,$requestedProperties,$depth);
1083 $dom = new DOMDocument('1.0','utf-8');
1084 $dom->formatOutput = true;
1085 $multiStatus = $dom->createElement('d:multistatus');
1086 $dom->appendChild($multiStatus);
1088 // Adding in default namespaces
1089 foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
1091 $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
1095 foreach($result as $response) {
1096 $response->serialize($this->server, $multiStatus);
1099 $xml = $dom->saveXML();
1100 $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
1101 $this->server->httpResponse->sendStatus(207);
1102 $this->server->httpResponse->sendBody($xml);
1107 * This method is used by expandPropertyReport to parse
1108 * out the entire HTTP request.
1110 * @param DOMElement $node
1113 protected function parseExpandPropertyReportRequest($node) {
1115 $requestedProperties = array();
1118 if (Sabre_DAV_XMLUtil::toClarkNotation($node)!=='{DAV:}property') continue;
1120 if ($node->firstChild) {
1122 $children = $this->parseExpandPropertyReportRequest($node->firstChild);
1126 $children = array();
1130 $namespace = $node->getAttribute('namespace');
1131 if (!$namespace) $namespace = 'DAV:';
1133 $propName = '{'.$namespace.'}' . $node->getAttribute('name');
1134 $requestedProperties[$propName] = $children;
1136 } while ($node = $node->nextSibling);
1138 return $requestedProperties;
1143 * This method expands all the properties and returns
1144 * a list with property values
1146 * @param array $path
1147 * @param array $requestedProperties the list of required properties
1151 protected function expandProperties($path, array $requestedProperties, $depth) {
1153 $foundProperties = $this->server->getPropertiesForPath($path, array_keys($requestedProperties), $depth);
1157 foreach($foundProperties as $node) {
1159 foreach($requestedProperties as $propertyName=>$childRequestedProperties) {
1161 // We're only traversing if sub-properties were requested
1162 if(count($childRequestedProperties)===0) continue;
1164 // We only have to do the expansion if the property was found
1165 // and it contains an href element.
1166 if (!array_key_exists($propertyName,$node[200])) continue;
1168 if ($node[200][$propertyName] instanceof Sabre_DAV_Property_IHref) {
1169 $hrefs = array($node[200][$propertyName]->getHref());
1170 } elseif ($node[200][$propertyName] instanceof Sabre_DAV_Property_HrefList) {
1171 $hrefs = $node[200][$propertyName]->getHrefs();
1174 $childProps = array();
1175 foreach($hrefs as $href) {
1176 $childProps = array_merge($childProps, $this->expandProperties($href, $childRequestedProperties, 0));
1178 $node[200][$propertyName] = new Sabre_DAV_Property_ResponseList($childProps);
1181 $result[] = new Sabre_DAV_Property_Response($path, $node);
1190 * principalSearchPropertySetReport
1192 * This method responsible for handing the
1193 * {DAV:}principal-search-property-set report. This report returns a list
1194 * of properties the client may search on, using the
1195 * {DAV:}principal-property-search report.
1197 * @param DOMDocument $dom
1200 protected function principalSearchPropertySetReport(DOMDocument $dom) {
1202 $httpDepth = $this->server->getHTTPDepth(0);
1203 if ($httpDepth!==0) {
1204 throw new Sabre_DAV_Exception_BadRequest('This report is only defined when Depth: 0');
1207 if ($dom->firstChild->hasChildNodes())
1208 throw new Sabre_DAV_Exception_BadRequest('The principal-search-property-set report element is not allowed to have child elements');
1210 $dom = new DOMDocument('1.0','utf-8');
1211 $dom->formatOutput = true;
1212 $root = $dom->createElement('d:principal-search-property-set');
1213 $dom->appendChild($root);
1214 // Adding in default namespaces
1215 foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
1217 $root->setAttribute('xmlns:' . $prefix,$namespace);
1221 $nsList = $this->server->xmlNamespaces;
1223 foreach($this->principalSearchPropertySet as $propertyName=>$description) {
1225 $psp = $dom->createElement('d:principal-search-property');
1226 $root->appendChild($psp);
1228 $prop = $dom->createElement('d:prop');
1229 $psp->appendChild($prop);
1232 preg_match('/^{([^}]*)}(.*)$/',$propertyName,$propName);
1234 $currentProperty = $dom->createElement($nsList[$propName[1]] . ':' . $propName[2]);
1235 $prop->appendChild($currentProperty);
1237 $descriptionElem = $dom->createElement('d:description');
1238 $descriptionElem->setAttribute('xml:lang','en');
1239 $descriptionElem->appendChild($dom->createTextNode($description));
1240 $psp->appendChild($descriptionElem);
1245 $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
1246 $this->server->httpResponse->sendStatus(200);
1247 $this->server->httpResponse->sendBody($dom->saveXML());
1252 * principalPropertySearchReport
1254 * This method is responsible for handing the
1255 * {DAV:}principal-property-search report. This report can be used for
1256 * clients to search for groups of principals, based on the value of one
1257 * or more properties.
1259 * @param DOMDocument $dom
1262 protected function principalPropertySearchReport(DOMDocument $dom) {
1264 list($searchProperties, $requestedProperties, $applyToPrincipalCollectionSet) = $this->parsePrincipalPropertySearchReportRequest($dom);
1267 if (!$applyToPrincipalCollectionSet) {
1268 $uri = $this->server->getRequestUri();
1270 $result = $this->principalSearch($searchProperties, $requestedProperties, $uri);
1272 $xml = $this->server->generateMultiStatus($result);
1273 $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
1274 $this->server->httpResponse->sendStatus(207);
1275 $this->server->httpResponse->sendBody($xml);
1280 * parsePrincipalPropertySearchReportRequest
1282 * This method parses the request body from a
1283 * {DAV:}principal-property-search report.
1285 * This method returns an array with two elements:
1286 * 1. an array with properties to search on, and their values
1287 * 2. a list of propertyvalues that should be returned for the request.
1289 * @param DOMDocument $dom
1292 protected function parsePrincipalPropertySearchReportRequest($dom) {
1294 $httpDepth = $this->server->getHTTPDepth(0);
1295 if ($httpDepth!==0) {
1296 throw new Sabre_DAV_Exception_BadRequest('This report is only defined when Depth: 0');
1299 $searchProperties = array();
1301 $applyToPrincipalCollectionSet = false;
1303 // Parsing the search request
1304 foreach($dom->firstChild->childNodes as $searchNode) {
1306 if (Sabre_DAV_XMLUtil::toClarkNotation($searchNode) == '{DAV:}apply-to-principal-collection-set') {
1307 $applyToPrincipalCollectionSet = true;
1310 if (Sabre_DAV_XMLUtil::toClarkNotation($searchNode)!=='{DAV:}property-search')
1313 $propertyName = null;
1314 $propertyValue = null;
1316 foreach($searchNode->childNodes as $childNode) {
1318 switch(Sabre_DAV_XMLUtil::toClarkNotation($childNode)) {
1321 $property = Sabre_DAV_XMLUtil::parseProperties($searchNode);
1323 $propertyName = key($property);
1326 case '{DAV:}match' :
1327 $propertyValue = $childNode->textContent;
1335 if (is_null($propertyName) || is_null($propertyValue))
1336 throw new Sabre_DAV_Exception_BadRequest('Invalid search request. propertyname: ' . $propertyName . '. propertvvalue: ' . $propertyValue);
1338 $searchProperties[$propertyName] = $propertyValue;
1342 return array($searchProperties, array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild)), $applyToPrincipalCollectionSet);