<?php
namespace Sabre\DAVACL;
use Sabre\DAV;
use Sabre\DAV\INode;
use Sabre\DAV\Exception\BadRequest;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\Uri;
/**
* SabreDAV ACL Plugin
*
* This plugin provides functionality to enforce ACL permissions.
* ACL is defined in RFC3744.
*
* In addition it also provides support for the {DAV:}current-user-principal
* property, defined in RFC5397 and the {DAV:}expand-property report, as
* defined in RFC3253.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Plugin extends DAV\ServerPlugin {
/**
* Recursion constants
*
* This only checks the base node
*/
const R_PARENT = 1;
/**
* Recursion constants
*
* This checks every node in the tree
*/
const R_RECURSIVE = 2;
/**
* Recursion constants
*
* This checks every parentnode in the tree, but not leaf-nodes.
*/
const R_RECURSIVEPARENTS = 3;
/**
* Reference to server object.
*
* @var Sabre\DAV\Server
*/
protected $server;
/**
* List of urls containing principal collections.
* Modify this if your principals are located elsewhere.
*
* @var array
*/
public $principalCollectionSet = [
'principals',
];
/**
* By default ACL is only enforced for nodes that have ACL support (the
* ones that implement IACL). For any other node, access is
* always granted.
*
* To override this behaviour you can turn this setting off. This is useful
* if you plan to fully support ACL in the entire tree.
*
* @var bool
*/
public $allowAccessToNodesWithoutACL = true;
/**
* By default nodes that are inaccessible by the user, can still be seen
* in directory listings (PROPFIND on parent with Depth: 1)
*
* In certain cases it's desirable to hide inaccessible nodes. Setting this
* to true will cause these nodes to be hidden from directory listings.
*
* @var bool
*/
public $hideNodesFromListings = false;
/**
* This list of properties are the properties a client can search on using
* the {DAV:}principal-property-search report.
*
* The keys are the property names, values are descriptions.
*
* @var array
*/
public $principalSearchPropertySet = [
'{DAV:}displayname' => '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 .= '<tr><td colspan="2"><form method="post" action="">
<h3>Create new principal</h3>
<input type="hidden" name="sabreAction" value="mkcol" />
<input type="hidden" name="resourceType" value="{DAV:}principal" />
<label>Name (uri):</label> <input type="text" name="name" /><br />
<label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
<label>Email address:</label> <input type="text" name="{http://sabredav*DOT*org/ns}email-address" /><br />
<input type="submit" value="create" />
</form>
</td></tr>';
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/',
];
}
}