'Display name', '{http://sabredav.org/ns}email-address' => 'Email address', ]; /** * Any principal uri's added here, will automatically be added to the list * of ACL's. They will effectively receive {DAV:}all privileges, as a * protected privilege. * * @var array */ public $adminPrincipals = []; /** * Returns a list of features added by this plugin. * * This list is used in the response of a HTTP OPTIONS request. * * @return array */ function getFeatures() { return ['access-control', 'calendarserver-principal-property-search']; } /** * Returns a list of available methods for a given url * * @param string $uri * @return array */ function getMethods($uri) { return ['ACL']; } /** * Returns a plugin name. * * Using this name other plugins will be able to access other plugins * using Sabre\DAV\Server::getPlugin * * @return string */ function getPluginName() { return 'acl'; } /** * Returns a list of reports this plugin supports. * * This will be used in the {DAV:}supported-report-set property. * Note that you still need to subscribe to the 'report' event to actually * implement them * * @param string $uri * @return array */ function getSupportedReportSet($uri) { return [ '{DAV:}expand-property', '{DAV:}principal-property-search', '{DAV:}principal-search-property-set', ]; } /** * Checks if the current user has the specified privilege(s). * * You can specify a single privilege, or a list of privileges. * This method will throw an exception if the privilege is not available * and return true otherwise. * * @param string $uri * @param array|string $privileges * @param int $recursion * @param bool $throwExceptions if set to false, this method won't throw exceptions. * @throws Sabre\DAVACL\Exception\NeedPrivileges * @return bool */ function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true) { if (!is_array($privileges)) $privileges = [$privileges]; $acl = $this->getCurrentUserPrivilegeSet($uri); if (is_null($acl)) { if ($this->allowAccessToNodesWithoutACL) { return true; } else { if ($throwExceptions) throw new Exception\NeedPrivileges($uri, $privileges); else return false; } } $failed = []; foreach ($privileges as $priv) { if (!in_array($priv, $acl)) { $failed[] = $priv; } } if ($failed) { if ($throwExceptions) throw new Exception\NeedPrivileges($uri, $failed); else return false; } return true; } /** * Returns the standard users' principal. * * This is one authorative principal url for the current user. * This method will return null if the user wasn't logged in. * * @return string|null */ function getCurrentUserPrincipal() { $authPlugin = $this->server->getPlugin('auth'); if (is_null($authPlugin)) return null; /** @var $authPlugin Sabre\DAV\Auth\Plugin */ return $authPlugin->getCurrentPrincipal(); } /** * Returns a list of principals that's associated to the current * user, either directly or through group membership. * * @return array */ function getCurrentUserPrincipals() { $currentUser = $this->getCurrentUserPrincipal(); if (is_null($currentUser)) return []; return array_merge( [$currentUser], $this->getPrincipalMembership($currentUser) ); } /** * This array holds a cache for all the principals that are associated with * a single principal. * * @var array */ protected $principalMembershipCache = []; /** * Returns all the principal groups the specified principal is a member of. * * @param string $principal * @return array */ function getPrincipalMembership($mainPrincipal) { // First check our cache if (isset($this->principalMembershipCache[$mainPrincipal])) { return $this->principalMembershipCache[$mainPrincipal]; } $check = [$mainPrincipal]; $principals = []; while (count($check)) { $principal = array_shift($check); $node = $this->server->tree->getNodeForPath($principal); if ($node instanceof IPrincipal) { foreach ($node->getGroupMembership() as $groupMember) { if (!in_array($groupMember, $principals)) { $check[] = $groupMember; $principals[] = $groupMember; } } } } // Store the result in the cache $this->principalMembershipCache[$mainPrincipal] = $principals; return $principals; } /** * Returns the supported privilege structure for this ACL plugin. * * See RFC3744 for more details. Currently we default on a simple, * standard structure. * * You can either get the list of privileges by a uri (path) or by * specifying a Node. * * @param string|INode $node * @return array */ function getSupportedPrivilegeSet($node) { if (is_string($node)) { $node = $this->server->tree->getNodeForPath($node); } if ($node instanceof IACL) { $result = $node->getSupportedPrivilegeSet(); if ($result) return $result; } return self::getDefaultSupportedPrivilegeSet(); } /** * Returns a fairly standard set of privileges, which may be useful for * other systems to use as a basis. * * @return array */ static function getDefaultSupportedPrivilegeSet() { return [ 'privilege' => '{DAV:}all', 'abstract' => true, 'aggregates' => [ [ 'privilege' => '{DAV:}read', 'aggregates' => [ [ 'privilege' => '{DAV:}read-acl', 'abstract' => false, ], [ 'privilege' => '{DAV:}read-current-user-privilege-set', 'abstract' => false, ], ], ], // {DAV:}read [ 'privilege' => '{DAV:}write', 'aggregates' => [ [ 'privilege' => '{DAV:}write-acl', 'abstract' => false, ], [ 'privilege' => '{DAV:}write-properties', 'abstract' => false, ], [ 'privilege' => '{DAV:}write-content', 'abstract' => false, ], [ 'privilege' => '{DAV:}bind', 'abstract' => false, ], [ 'privilege' => '{DAV:}unbind', 'abstract' => false, ], [ 'privilege' => '{DAV:}unlock', 'abstract' => false, ], ], ], // {DAV:}write ], ]; // {DAV:}all } /** * Returns the supported privilege set as a flat list * * This is much easier to parse. * * The returned list will be index by privilege name. * The value is a struct containing the following properties: * - aggregates * - abstract * - concrete * * @param string|INode $node * @return array */ final function getFlatPrivilegeSet($node) { $privs = $this->getSupportedPrivilegeSet($node); $fpsTraverse = null; $fpsTraverse = function($priv, $concrete, &$flat) use (&$fpsTraverse) { $myPriv = [ 'privilege' => $priv['privilege'], 'abstract' => isset($priv['abstract']) && $priv['abstract'], 'aggregates' => [], 'concrete' => isset($priv['abstract']) && $priv['abstract'] ? $concrete : $priv['privilege'], ]; if (isset($priv['aggregates'])) { foreach ($priv['aggregates'] as $subPriv) { $myPriv['aggregates'][] = $subPriv['privilege']; } } $flat[$priv['privilege']] = $myPriv; if (isset($priv['aggregates'])) { foreach ($priv['aggregates'] as $subPriv) { $fpsTraverse($subPriv, $myPriv['concrete'], $flat); } } }; $flat = []; $fpsTraverse($privs, null, $flat); return $flat; } /** * Returns the full ACL list. * * Either a uri or a INode may be passed. * * null will be returned if the node doesn't support ACLs. * * @param string|DAV\INode $node * @return array */ function getACL($node) { if (is_string($node)) { $node = $this->server->tree->getNodeForPath($node); } if (!$node instanceof IACL) { return null; } $acl = $node->getACL(); foreach ($this->adminPrincipals as $adminPrincipal) { $acl[] = [ 'principal' => $adminPrincipal, 'privilege' => '{DAV:}all', 'protected' => true, ]; } return $acl; } /** * Returns a list of privileges the current user has * on a particular node. * * Either a uri or a DAV\INode may be passed. * * null will be returned if the node doesn't support ACLs. * * @param string|DAV\INode $node * @return array */ function getCurrentUserPrivilegeSet($node) { if (is_string($node)) { $node = $this->server->tree->getNodeForPath($node); } $acl = $this->getACL($node); if (is_null($acl)) return null; $principals = $this->getCurrentUserPrincipals(); $collected = []; foreach ($acl as $ace) { $principal = $ace['principal']; switch ($principal) { case '{DAV:}owner' : $owner = $node->getOwner(); if ($owner && in_array($owner, $principals)) { $collected[] = $ace; } break; // 'all' matches for every user case '{DAV:}all' : // 'authenticated' matched for every user that's logged in. // Since it's not possible to use ACL while not being logged // in, this is also always true. case '{DAV:}authenticated' : $collected[] = $ace; break; // 'unauthenticated' can never occur either, so we simply // ignore these. case '{DAV:}unauthenticated' : break; default : if (in_array($ace['principal'], $principals)) { $collected[] = $ace; } break; } } // Now we deduct all aggregated privileges. $flat = $this->getFlatPrivilegeSet($node); $collected2 = []; while (count($collected)) { $current = array_pop($collected); $collected2[] = $current['privilege']; foreach ($flat[$current['privilege']]['aggregates'] as $subPriv) { $collected2[] = $subPriv; $collected[] = $flat[$subPriv]; } } return array_values(array_unique($collected2)); } /** * Returns a principal based on its uri. * * Returns null if the principal could not be found. * * @param string $uri * @return null|string */ function getPrincipalByUri($uri) { $result = null; $collections = $this->principalCollectionSet; foreach ($collections as $collection) { $principalCollection = $this->server->tree->getNodeForPath($collection); if (!$principalCollection instanceof IPrincipalCollection) { // Not a principal collection, we're simply going to ignore // this. continue; } $result = $principalCollection->findByUri($uri); if ($result) { return $result; } } } /** * Principal property search * * This method can search for principals matching certain values in * properties. * * This method will return a list of properties for the matched properties. * * @param array $searchProperties The properties to search on. This is a * key-value list. The keys are property * names, and the values the strings to * match them on. * @param array $requestedProperties This is the list of properties to * return for every match. * @param string $collectionUri The principal collection to search on. * If this is ommitted, the standard * principal collection-set will be used. * @param string $test "allof" to use AND to search the * properties. 'anyof' for OR. * @return array This method returns an array structure similar to * Sabre\DAV\Server::getPropertiesForPath. Returned * properties are index by a HTTP status code. */ function principalSearch(array $searchProperties, array $requestedProperties, $collectionUri = null, $test = 'allof') { if (!is_null($collectionUri)) { $uris = [$collectionUri]; } else { $uris = $this->principalCollectionSet; } $lookupResults = []; foreach ($uris as $uri) { $principalCollection = $this->server->tree->getNodeForPath($uri); if (!$principalCollection instanceof IPrincipalCollection) { // Not a principal collection, we're simply going to ignore // this. continue; } $results = $principalCollection->searchPrincipals($searchProperties, $test); foreach ($results as $result) { $lookupResults[] = rtrim($uri, '/') . '/' . $result; } } $matches = []; foreach ($lookupResults as $lookupResult) { list($matches[]) = $this->server->getPropertiesForPath($lookupResult, $requestedProperties, 0); } return $matches; } /** * Sets up the plugin * * This method is automatically called by the server class. * * @param DAV\Server $server * @return void */ function initialize(DAV\Server $server) { $this->server = $server; $server->on('propFind', [$this, 'propFind'], 20); $server->on('beforeMethod', [$this, 'beforeMethod'], 20); $server->on('beforeBind', [$this, 'beforeBind'], 20); $server->on('beforeUnbind', [$this, 'beforeUnbind'], 20); $server->on('propPatch', [$this, 'propPatch']); $server->on('beforeUnlock', [$this, 'beforeUnlock'], 20); $server->on('report', [$this, 'report']); $server->on('method:ACL', [$this, 'httpAcl']); $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); array_push($server->protectedProperties, '{DAV:}alternate-URI-set', '{DAV:}principal-URL', '{DAV:}group-membership', '{DAV:}principal-collection-set', '{DAV:}current-user-principal', '{DAV:}supported-privilege-set', '{DAV:}current-user-privilege-set', '{DAV:}acl', '{DAV:}acl-restrictions', '{DAV:}inherited-acl-set', '{DAV:}owner', '{DAV:}group' ); // Automatically mapping nodes implementing IPrincipal to the // {DAV:}principal resourcetype. $server->resourceTypeMapping['Sabre\\DAVACL\\IPrincipal'] = '{DAV:}principal'; // Mapping the group-member-set property to the HrefList property // class. $server->xml->elementMap['{DAV:}group-member-set'] = 'Sabre\\DAV\\Xml\\Property\\Href'; $server->xml->elementMap['{DAV:}acl'] = 'Sabre\\DAVACL\\Xml\\Property\\Acl'; $server->xml->elementMap['{DAV:}expand-property'] = 'Sabre\\DAVACL\\Xml\\Request\\ExpandPropertyReport'; $server->xml->elementMap['{DAV:}principal-property-search'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalPropertySearchReport'; $server->xml->elementMap['{DAV:}principal-search-property-set'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalSearchPropertySetReport'; } /* {{{ Event handlers */ /** * Triggered before any method is handled * * @param RequestInterface $request * @param ResponseInterface $response * @return void */ function beforeMethod(RequestInterface $request, ResponseInterface $response) { $method = $request->getMethod(); $path = $request->getPath(); $exists = $this->server->tree->nodeExists($path); // If the node doesn't exists, none of these checks apply if (!$exists) return; switch ($method) { case 'GET' : case 'HEAD' : case 'OPTIONS' : // For these 3 we only need to know if the node is readable. $this->checkPrivileges($path, '{DAV:}read'); break; case 'PUT' : case 'LOCK' : case 'UNLOCK' : // This method requires the write-content priv if the node // already exists, and bind on the parent if the node is being // created. // The bind privilege is handled in the beforeBind event. $this->checkPrivileges($path, '{DAV:}write-content'); break; case 'PROPPATCH' : $this->checkPrivileges($path, '{DAV:}write-properties'); break; case 'ACL' : $this->checkPrivileges($path, '{DAV:}write-acl'); break; case 'COPY' : case 'MOVE' : // Copy requires read privileges on the entire source tree. // If the target exists write-content normally needs to be // checked, however, we're deleting the node beforehand and // creating a new one after, so this is handled by the // beforeUnbind event. // // The creation of the new node is handled by the beforeBind // event. // // If MOVE is used beforeUnbind will also be used to check if // the sourcenode can be deleted. $this->checkPrivileges($path, '{DAV:}read', self::R_RECURSIVE); break; } } /** * Triggered before a new node is created. * * This allows us to check permissions for any operation that creates a * new node, such as PUT, MKCOL, MKCALENDAR, LOCK, COPY and MOVE. * * @param string $uri * @return void */ function beforeBind($uri) { list($parentUri) = Uri\split($uri); $this->checkPrivileges($parentUri, '{DAV:}bind'); } /** * Triggered before a node is deleted * * This allows us to check permissions for any operation that will delete * an existing node. * * @param string $uri * @return void */ function beforeUnbind($uri) { list($parentUri) = Uri\split($uri); $this->checkPrivileges($parentUri, '{DAV:}unbind', self::R_RECURSIVEPARENTS); } /** * Triggered before a node is unlocked. * * @param string $uri * @param DAV\Locks\LockInfo $lock * @TODO: not yet implemented * @return void */ function beforeUnlock($uri, DAV\Locks\LockInfo $lock) { } /** * Triggered before properties are looked up in specific nodes. * * @param DAV\PropFind $propFind * @param DAV\INode $node * @param array $requestedProperties * @param array $returnedProperties * @TODO really should be broken into multiple methods, or even a class. * @return bool */ function propFind(DAV\PropFind $propFind, DAV\INode $node) { $path = $propFind->getPath(); // Checking the read permission if (!$this->checkPrivileges($path, '{DAV:}read', self::R_PARENT, false)) { // User is not allowed to read properties // Returning false causes the property-fetching system to pretend // that the node does not exist, and will cause it to be hidden // from listings such as PROPFIND or the browser plugin. if ($this->hideNodesFromListings) { return false; } // Otherwise we simply mark every property as 403. foreach ($propFind->getRequestedProperties() as $requestedProperty) { $propFind->set($requestedProperty, null, 403); } return; } /* Adding principal properties */ if ($node instanceof IPrincipal) { $propFind->handle('{DAV:}alternate-URI-set', function() use ($node) { return new DAV\Xml\Property\Href($node->getAlternateUriSet()); }); $propFind->handle('{DAV:}principal-URL', function() use ($node) { return new DAV\Xml\Property\Href($node->getPrincipalUrl() . '/'); }); $propFind->handle('{DAV:}group-member-set', function() use ($node) { $members = $node->getGroupMemberSet(); foreach ($members as $k => $member) { $members[$k] = rtrim($member, '/') . '/'; } return new DAV\Xml\Property\Href($members); }); $propFind->handle('{DAV:}group-membership', function() use ($node) { $members = $node->getGroupMembership(); foreach ($members as $k => $member) { $members[$k] = rtrim($member, '/') . '/'; } return new DAV\Xml\Property\Href($members); }); $propFind->handle('{DAV:}displayname', [$node, 'getDisplayName']); } $propFind->handle('{DAV:}principal-collection-set', function() { $val = $this->principalCollectionSet; // Ensuring all collections end with a slash foreach ($val as $k => $v) $val[$k] = $v . '/'; return new DAV\Xml\Property\Href($val); }); $propFind->handle('{DAV:}current-user-principal', function() { if ($url = $this->getCurrentUserPrincipal()) { return new Xml\Property\Principal(Xml\Property\Principal::HREF, $url . '/'); } else { return new Xml\Property\Principal(Xml\Property\Principal::UNAUTHENTICATED); } }); $propFind->handle('{DAV:}supported-privilege-set', function() use ($node) { return new Xml\Property\SupportedPrivilegeSet($this->getSupportedPrivilegeSet($node)); }); $propFind->handle('{DAV:}current-user-privilege-set', function() use ($node, $propFind, $path) { if (!$this->checkPrivileges($path, '{DAV:}read-current-user-privilege-set', self::R_PARENT, false)) { $propFind->set('{DAV:}current-user-privilege-set', null, 403); } else { $val = $this->getCurrentUserPrivilegeSet($node); if (!is_null($val)) { return new Xml\Property\CurrentUserPrivilegeSet($val); } } }); $propFind->handle('{DAV:}acl', function() use ($node, $propFind, $path) { /* The ACL property contains all the permissions */ if (!$this->checkPrivileges($path, '{DAV:}read-acl', self::R_PARENT, false)) { $propFind->set('{DAV:}acl', null, 403); } else { $acl = $this->getACL($node); if (!is_null($acl)) { return new Xml\Property\Acl($this->getACL($node)); } } }); $propFind->handle('{DAV:}acl-restrictions', function() { return new Xml\Property\AclRestrictions(); }); /* Adding ACL properties */ if ($node instanceof IACL) { $propFind->handle('{DAV:}owner', function() use ($node) { return new DAV\Xml\Property\Href($node->getOwner() . '/'); }); } } /** * This method intercepts PROPPATCH methods and make sure the * group-member-set is updated correctly. * * @param string $path * @param DAV\PropPatch $propPatch * @return void */ function propPatch($path, DAV\PropPatch $propPatch) { $propPatch->handle('{DAV:}group-member-set', function($value) use ($path) { if (is_null($value)) { $memberSet = []; } elseif ($value instanceof DAV\Xml\Property\Href) { $memberSet = array_map( [$this->server, 'calculateUri'], $value->getHrefs() ); } else { throw new DAV\Exception('The group-member-set property MUST be an instance of Sabre\DAV\Property\HrefList or null'); } $node = $this->server->tree->getNodeForPath($path); if (!($node instanceof IPrincipal)) { // Fail return false; } $node->setGroupMemberSet($memberSet); // We must also clear our cache, just in case $this->principalMembershipCache = []; return true; }); } /** * This method handles HTTP REPORT requests * * @param string $reportName * @param mixed $report * @param mixed $path * @return bool */ function report($reportName, $report, $path) { switch ($reportName) { case '{DAV:}principal-property-search' : $this->server->transactionType = 'report-principal-property-search'; $this->principalPropertySearchReport($report); return false; case '{DAV:}principal-search-property-set' : $this->server->transactionType = 'report-principal-search-property-set'; $this->principalSearchPropertySetReport($report); return false; case '{DAV:}expand-property' : $this->server->transactionType = 'report-expand-property'; $this->expandPropertyReport($report); return false; } } /** * This method is responsible for handling the 'ACL' event. * * @param RequestInterface $request * @param ResponseInterface $response * @return bool */ function httpAcl(RequestInterface $request, ResponseInterface $response) { $path = $request->getPath(); $body = $request->getBodyAsString(); if (!$body) { throw new DAV\Exception\BadRequest('XML body expected in ACL request'); } $acl = $this->server->xml->expect('{DAV:}acl', $body); $newAcl = $acl->getPrivileges(); // Normalizing urls foreach ($newAcl as $k => $newAce) { $newAcl[$k]['principal'] = $this->server->calculateUri($newAce['principal']); } $node = $this->server->tree->getNodeForPath($path); if (!$node instanceof IACL) { throw new DAV\Exception\MethodNotAllowed('This node does not support the ACL method'); } $oldAcl = $this->getACL($node); $supportedPrivileges = $this->getFlatPrivilegeSet($node); /* Checking if protected principals from the existing principal set are not overwritten. */ foreach ($oldAcl as $oldAce) { if (!isset($oldAce['protected']) || !$oldAce['protected']) continue; $found = false; foreach ($newAcl as $newAce) { if ( $newAce['privilege'] === $oldAce['privilege'] && $newAce['principal'] === $oldAce['principal'] && $newAce['protected'] ) $found = true; } if (!$found) throw new Exception\AceConflict('This resource contained a protected {DAV:}ace, but this privilege did not occur in the ACL request'); } foreach ($newAcl as $newAce) { // Do we recognize the privilege if (!isset($supportedPrivileges[$newAce['privilege']])) { throw new Exception\NotSupportedPrivilege('The privilege you specified (' . $newAce['privilege'] . ') is not recognized by this server'); } if ($supportedPrivileges[$newAce['privilege']]['abstract']) { throw new Exception\NoAbstract('The privilege you specified (' . $newAce['privilege'] . ') is an abstract privilege'); } // Looking up the principal try { $principal = $this->server->tree->getNodeForPath($newAce['principal']); } catch (DAV\Exception\NotFound $e) { throw new Exception\NotRecognizedPrincipal('The specified principal (' . $newAce['principal'] . ') does not exist'); } if (!($principal instanceof IPrincipal)) { throw new Exception\NotRecognizedPrincipal('The specified uri (' . $newAce['principal'] . ') is not a principal'); } } $node->setACL($newAcl); $response->setStatus(200); // Breaking the event chain, because we handled this method. return false; } /* }}} */ /* Reports {{{ */ /** * The expand-property report is defined in RFC3253 section 3-8. * * This report is very similar to a standard PROPFIND. The difference is * that it has the additional ability to look at properties containing a * {DAV:}href element, follow that property and grab additional elements * there. * * Other rfc's, such as ACL rely on this report, so it made sense to put * it in this plugin. * * @param Xml\Request\ExpandPropertyReport $report * @return void */ protected function expandPropertyReport($report) { $depth = $this->server->getHTTPDepth(0); $requestUri = $this->server->getRequestUri(); $result = $this->expandProperties($requestUri, $report->properties, $depth); $xml = $this->server->xml->write( '{DAV:}multistatus', new DAV\Xml\Response\MultiStatus($result), $this->server->getBaseUri() ); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->setStatus(207); $this->server->httpResponse->setBody($xml); } /** * This method expands all the properties and returns * a list with property values * * @param array $path * @param array $requestedProperties the list of required properties * @param int $depth * @return array */ protected function expandProperties($path, array $requestedProperties, $depth) { $foundProperties = $this->server->getPropertiesForPath($path, array_keys($requestedProperties), $depth); $result = []; foreach ($foundProperties as $node) { foreach ($requestedProperties as $propertyName => $childRequestedProperties) { // We're only traversing if sub-properties were requested if (count($childRequestedProperties) === 0) continue; // We only have to do the expansion if the property was found // and it contains an href element. if (!array_key_exists($propertyName, $node[200])) continue; if (!$node[200][$propertyName] instanceof DAV\Xml\Property\Href) { continue; } $childHrefs = $node[200][$propertyName]->getHrefs(); $childProps = []; foreach ($childHrefs as $href) { // Gathering the result of the children $childProps[] = [ 'name' => '{DAV:}response', 'value' => $this->expandProperties($href, $childRequestedProperties, 0)[0] ]; } // Replacing the property with its expannded form. $node[200][$propertyName] = $childProps; } $result[] = new DAV\Xml\Element\Response($node['href'], $node); } return $result; } /** * principalSearchPropertySetReport * * This method responsible for handing the * {DAV:}principal-search-property-set report. This report returns a list * of properties the client may search on, using the * {DAV:}principal-property-search report. * * @param Xml\Request\PrincipalSearchPropertySetReport $report * @return void */ protected function principalSearchPropertySetReport($report) { $httpDepth = $this->server->getHTTPDepth(0); if ($httpDepth !== 0) { throw new DAV\Exception\BadRequest('This report is only defined when Depth: 0'); } $writer = $this->server->xml->getWriter(); $writer->openMemory(); $writer->startDocument(); $writer->startElement('{DAV:}principal-search-property-set'); foreach ($this->principalSearchPropertySet as $propertyName => $description) { $writer->startElement('{DAV:}principal-search-property'); $writer->startElement('{DAV:}prop'); $writer->writeElement($propertyName); $writer->endElement(); // prop if ($description) { $writer->write([[ 'name' => '{DAV:}description', 'value' => $description, 'attributes' => ['xml:lang' => 'en'] ]]); } $writer->endElement(); // principal-search-property } $writer->endElement(); // principal-search-property-set $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->setStatus(200); $this->server->httpResponse->setBody($writer->outputMemory()); } /** * principalPropertySearchReport * * This method is responsible for handing the * {DAV:}principal-property-search report. This report can be used for * clients to search for groups of principals, based on the value of one * or more properties. * * @param Xml\Request\PrincipalPropertySearchReport $report * @return void */ protected function principalPropertySearchReport($report) { $uri = null; if (!$report->applyToPrincipalCollectionSet) { $uri = $this->server->httpRequest->getPath(); } if ($this->server->getHttpDepth('0') !== 0) { throw new BadRequest('Depth must be 0'); } $result = $this->principalSearch( $report->searchProperties, $report->properties, $uri, $report->test ); $prefer = $this->server->getHTTPPrefer(); $this->server->httpResponse->setStatus(207); $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, $prefer['return'] === 'minimal')); } /* }}} */ /** * This method is used to generate HTML output for the * DAV\Browser\Plugin. This allows us to generate an interface users * can use to create new calendars. * * @param DAV\INode $node * @param string $output * @return bool */ function htmlActionsPanel(DAV\INode $node, &$output) { if (!$node instanceof PrincipalCollection) return; $output .= '

Create new principal




'; return false; } /** * Returns a bunch of meta-data about the plugin. * * Providing this information is optional, and is mainly displayed by the * Browser plugin. * * The description key in the returned array may contain html and will not * be sanitized. * * @return array */ function getPluginInfo() { return [ 'name' => $this->getPluginName(), 'description' => 'Adds support for WebDAV ACL (rfc3744)', 'link' => 'http://sabre.io/dav/acl/', ]; } }