mirror of
https://github.com/nextcloud/server.git
synced 2026-04-28 01:28:08 -04:00
feat(dav): update a principal's schedule-default-calendar-URL
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
This commit is contained in:
parent
7cc20468f1
commit
53ef6c5f71
9 changed files with 500 additions and 9 deletions
|
|
@ -95,6 +95,13 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
|
|||
$server->on('propFind', [$this, 'propFindDefaultCalendarUrl'], 90);
|
||||
$server->on('afterWriteContent', [$this, 'dispatchSchedulingResponses']);
|
||||
$server->on('afterCreateFile', [$this, 'dispatchSchedulingResponses']);
|
||||
|
||||
// We allow mutating the default calendar URL through the CustomPropertiesBackend
|
||||
// (oc_properties table)
|
||||
$server->protectedProperties = array_filter(
|
||||
$server->protectedProperties,
|
||||
static fn (string $property) => $property !== self::SCHEDULE_DEFAULT_CALENDAR_URL,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -260,6 +260,7 @@ class Principal implements BackendInterface {
|
|||
* @return int
|
||||
*/
|
||||
public function updatePrincipal($path, PropPatch $propPatch) {
|
||||
// Updating schedule-default-calendar-URL is handled in CustomPropertiesBackend
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ class ServerFactory {
|
|||
$server->addPlugin(
|
||||
new \Sabre\DAV\PropertyStorage\Plugin(
|
||||
new \OCA\DAV\DAV\CustomPropertiesBackend(
|
||||
$server,
|
||||
$objectTree,
|
||||
$this->databaseConnection,
|
||||
$this->userSession->getUser()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
* @author Georg Ehrke <oc.list@georgehrke.com>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
||||
* @author Richard Steinmetz <richard@steinmetz.cloud>
|
||||
*
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
|
|
@ -31,11 +32,19 @@ use OCA\DAV\Connector\Sabre\FilesPlugin;
|
|||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUser;
|
||||
use Sabre\CalDAV\ICalendar;
|
||||
use Sabre\DAV\Exception as DavException;
|
||||
use Sabre\DAV\PropertyStorage\Backend\BackendInterface;
|
||||
use Sabre\DAV\PropFind;
|
||||
use Sabre\DAV\PropPatch;
|
||||
use Sabre\DAV\Server;
|
||||
use Sabre\DAV\Tree;
|
||||
use Sabre\DAV\Xml\Property\Complex;
|
||||
use Sabre\DAV\Xml\Property\Href;
|
||||
use Sabre\DAV\Xml\Property\LocalHref;
|
||||
use Sabre\Xml\ParseException;
|
||||
use Sabre\Xml\Service as XmlService;
|
||||
|
||||
use function array_intersect;
|
||||
|
||||
class CustomPropertiesBackend implements BackendInterface {
|
||||
|
|
@ -58,6 +67,11 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
*/
|
||||
public const PROPERTY_TYPE_OBJECT = 3;
|
||||
|
||||
/**
|
||||
* Value is stored as a {DAV:}href string.
|
||||
*/
|
||||
public const PROPERTY_TYPE_HREF = 4;
|
||||
|
||||
/**
|
||||
* Ignored properties
|
||||
*
|
||||
|
|
@ -105,6 +119,15 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
*/
|
||||
private const PUBLISHED_READ_ONLY_PROPERTIES = [
|
||||
'{urn:ietf:params:xml:ns:caldav}calendar-availability',
|
||||
'{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
|
||||
];
|
||||
|
||||
/**
|
||||
* Map of custom XML elements to parse when trying to deserialize an instance of
|
||||
* \Sabre\DAV\Xml\Property\Complex to find a more specialized PROPERTY_TYPE_*
|
||||
*/
|
||||
private const COMPLEX_XML_ELEMENT_MAP = [
|
||||
'{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => Href::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -129,19 +152,29 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
*/
|
||||
private $userCache = [];
|
||||
|
||||
private Server $server;
|
||||
private XmlService $xmlService;
|
||||
|
||||
/**
|
||||
* @param Tree $tree node tree
|
||||
* @param IDBConnection $connection database connection
|
||||
* @param IUser $user owner of the tree and properties
|
||||
*/
|
||||
public function __construct(
|
||||
Server $server,
|
||||
Tree $tree,
|
||||
IDBConnection $connection,
|
||||
IUser $user,
|
||||
) {
|
||||
$this->server = $server;
|
||||
$this->tree = $tree;
|
||||
$this->connection = $connection;
|
||||
$this->user = $user;
|
||||
$this->xmlService = new XmlService();
|
||||
$this->xmlService->elementMap = array_merge(
|
||||
$this->xmlService->elementMap,
|
||||
self::COMPLEX_XML_ELEMENT_MAP,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -199,6 +232,21 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
}
|
||||
}
|
||||
|
||||
// substr of principals/users/ => path is a user principal
|
||||
// two '/' => this a principal collection (and not some child object)
|
||||
if (str_starts_with($path, 'principals/users/') && substr_count($path, '/') === 2) {
|
||||
$allRequestedProps = $propFind->getRequestedProperties();
|
||||
$customProperties = [
|
||||
'{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
|
||||
];
|
||||
|
||||
foreach ($customProperties as $customProperty) {
|
||||
if (in_array($customProperty, $allRequestedProps, true)) {
|
||||
$requestedProps[] = $customProperty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($requestedProps)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -211,9 +259,19 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
// First fetch the published properties (set by another user), then get the ones set by
|
||||
// the current user. If both are set then the latter as priority.
|
||||
foreach ($this->getPublishedProperties($path, $requestedProps) as $propName => $propValue) {
|
||||
try {
|
||||
$this->validateProperty($path, $propName, $propValue);
|
||||
} catch (DavException $e) {
|
||||
continue;
|
||||
}
|
||||
$propFind->set($propName, $propValue);
|
||||
}
|
||||
foreach ($this->getUserProperties($path, $requestedProps) as $propName => $propValue) {
|
||||
try {
|
||||
$this->validateProperty($path, $propName, $propValue);
|
||||
} catch (DavException $e) {
|
||||
continue;
|
||||
}
|
||||
$propFind->set($propName, $propValue);
|
||||
}
|
||||
}
|
||||
|
|
@ -264,6 +322,30 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
$statement->closeCursor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the value of a property. Will throw if a value is invalid.
|
||||
*
|
||||
* @throws DavException The value of the property is invalid
|
||||
*/
|
||||
private function validateProperty(string $path, string $propName, mixed $propValue): void {
|
||||
switch ($propName) {
|
||||
case '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL':
|
||||
/** @var Href $propValue */
|
||||
$href = $propValue->getHref();
|
||||
if ($href === null) {
|
||||
throw new DavException('Href is empty');
|
||||
}
|
||||
|
||||
// $path is the principal here as this prop is only set on principals
|
||||
$node = $this->tree->getNodeForPath($href);
|
||||
if (!($node instanceof ICalendar) || $node->getOwner() !== $path) {
|
||||
throw new DavException('No such calendar');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param string[] $requestedProperties
|
||||
|
|
@ -393,7 +475,11 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
->executeStatement();
|
||||
}
|
||||
} else {
|
||||
[$value, $valueType] = $this->encodeValueForDatabase($propertyValue);
|
||||
[$value, $valueType] = $this->encodeValueForDatabase(
|
||||
$path,
|
||||
$propertyName,
|
||||
$propertyValue,
|
||||
);
|
||||
$dbParameters['propertyValue'] = $value;
|
||||
$dbParameters['valueType'] = $valueType;
|
||||
|
||||
|
|
@ -436,15 +522,38 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return array
|
||||
* @throws ParseException If parsing a \Sabre\DAV\Xml\Property\Complex value fails
|
||||
* @throws DavException If the property value is invalid
|
||||
*/
|
||||
private function encodeValueForDatabase($value): array {
|
||||
private function encodeValueForDatabase(string $path, string $name, mixed $value): array {
|
||||
// Try to parse a more specialized property type first
|
||||
if ($value instanceof Complex) {
|
||||
$xml = $this->xmlService->write($name, [$value], $this->server->getBaseUri());
|
||||
$value = $this->xmlService->parse($xml, $this->server->getBaseUri()) ?? $value;
|
||||
}
|
||||
|
||||
if ($name === '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL') {
|
||||
$value = $this->encodeDefaultCalendarUrl($value);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->validateProperty($path, $name, $value);
|
||||
} catch (DavException $e) {
|
||||
throw new DavException(
|
||||
"Property \"$name\" has an invalid value: " . $e->getMessage(),
|
||||
0,
|
||||
$e,
|
||||
);
|
||||
}
|
||||
|
||||
if (is_scalar($value)) {
|
||||
$valueType = self::PROPERTY_TYPE_STRING;
|
||||
} elseif ($value instanceof Complex) {
|
||||
$valueType = self::PROPERTY_TYPE_XML;
|
||||
$value = $value->getXml();
|
||||
} elseif ($value instanceof Href) {
|
||||
$valueType = self::PROPERTY_TYPE_HREF;
|
||||
$value = $value->getHref();
|
||||
} else {
|
||||
$valueType = self::PROPERTY_TYPE_OBJECT;
|
||||
$value = serialize($value);
|
||||
|
|
@ -459,6 +568,8 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
switch ($valueType) {
|
||||
case self::PROPERTY_TYPE_XML:
|
||||
return new Complex($value);
|
||||
case self::PROPERTY_TYPE_HREF:
|
||||
return new Href($value);
|
||||
case self::PROPERTY_TYPE_OBJECT:
|
||||
return unserialize($value);
|
||||
case self::PROPERTY_TYPE_STRING:
|
||||
|
|
@ -467,6 +578,26 @@ class CustomPropertiesBackend implements BackendInterface {
|
|||
}
|
||||
}
|
||||
|
||||
private function encodeDefaultCalendarUrl(Href $value): Href {
|
||||
$href = $value->getHref();
|
||||
if ($href === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!str_starts_with($href, '/')) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
try {
|
||||
// Build path relative to the dav base URI to be used later to find the node
|
||||
$value = new LocalHref($this->server->calculateUri($href) . '/');
|
||||
} catch (DavException\Forbidden) {
|
||||
// Not existing calendars will be handled later when the value is validated
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function createDeleteQuery(): IQueryBuilder {
|
||||
$deleteQuery = $this->connection->getQueryBuilder();
|
||||
$deleteQuery->delete('properties')
|
||||
|
|
|
|||
|
|
@ -276,6 +276,7 @@ class Server {
|
|||
$this->server->addPlugin(
|
||||
new \Sabre\DAV\PropertyStorage\Plugin(
|
||||
new CustomPropertiesBackend(
|
||||
$this->server,
|
||||
$this->server->tree,
|
||||
\OC::$server->getDatabaseConnection(),
|
||||
\OC::$server->getUserSession()->getUser()
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ class CustomPropertiesBackendTest extends \Test\TestCase {
|
|||
->willReturn($userId);
|
||||
|
||||
$this->plugin = new \OCA\DAV\DAV\CustomPropertiesBackend(
|
||||
$this->server,
|
||||
$this->tree,
|
||||
\OC::$server->getDatabaseConnection(),
|
||||
$this->user
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
* @author Morris Jobke <hey@morrisjobke.de>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
* @author Richard Steinmetz <richard@steinmetz.cloud>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
|
|
@ -28,17 +29,29 @@
|
|||
namespace OCA\DAV\Tests\DAV;
|
||||
|
||||
use OCA\DAV\DAV\CustomPropertiesBackend;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUser;
|
||||
use Sabre\CalDAV\ICalendar;
|
||||
use Sabre\DAV\Exception\NotFound;
|
||||
use Sabre\DAV\PropFind;
|
||||
use Sabre\DAV\PropPatch;
|
||||
use Sabre\DAV\Server;
|
||||
use Sabre\DAV\Tree;
|
||||
use Sabre\DAV\Xml\Property\Href;
|
||||
use Sabre\DAVACL\IACL;
|
||||
use Sabre\DAVACL\IPrincipal;
|
||||
use Test\TestCase;
|
||||
|
||||
/**
|
||||
* @group DB
|
||||
*/
|
||||
class CustomPropertiesBackendTest extends TestCase {
|
||||
private const BASE_URI = '/remote.php/dav/';
|
||||
|
||||
/** @var Server | \PHPUnit\Framework\MockObject\MockObject */
|
||||
private $server;
|
||||
|
||||
/** @var Tree | \PHPUnit\Framework\MockObject\MockObject */
|
||||
private $tree;
|
||||
|
||||
|
|
@ -54,6 +67,9 @@ class CustomPropertiesBackendTest extends TestCase {
|
|||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->server = $this->createMock(Server::class);
|
||||
$this->server->method('getBaseUri')
|
||||
->willReturn(self::BASE_URI);
|
||||
$this->tree = $this->createMock(Tree::class);
|
||||
$this->user = $this->createMock(IUser::class);
|
||||
$this->user->method('getUID')
|
||||
|
|
@ -62,9 +78,10 @@ class CustomPropertiesBackendTest extends TestCase {
|
|||
$this->dbConnection = \OC::$server->getDatabaseConnection();
|
||||
|
||||
$this->backend = new CustomPropertiesBackend(
|
||||
$this->server,
|
||||
$this->tree,
|
||||
$this->dbConnection,
|
||||
$this->user
|
||||
$this->user,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +107,13 @@ class CustomPropertiesBackendTest extends TestCase {
|
|||
}
|
||||
}
|
||||
|
||||
protected function insertProp(string $user, string $path, string $name, string $value) {
|
||||
protected function insertProp(string $user, string $path, string $name, mixed $value) {
|
||||
$type = CustomPropertiesBackend::PROPERTY_TYPE_STRING;
|
||||
if ($value instanceof Href) {
|
||||
$value = $value->getHref();
|
||||
$type = CustomPropertiesBackend::PROPERTY_TYPE_HREF;
|
||||
}
|
||||
|
||||
$query = $this->dbConnection->getQueryBuilder();
|
||||
$query->insert('properties')
|
||||
->values([
|
||||
|
|
@ -98,13 +121,14 @@ class CustomPropertiesBackendTest extends TestCase {
|
|||
'propertypath' => $query->createNamedParameter($this->formatPath($path)),
|
||||
'propertyname' => $query->createNamedParameter($name),
|
||||
'propertyvalue' => $query->createNamedParameter($value),
|
||||
'valuetype' => $query->createNamedParameter($type, IQueryBuilder::PARAM_INT)
|
||||
]);
|
||||
$query->execute();
|
||||
}
|
||||
|
||||
protected function getProps(string $user, string $path) {
|
||||
$query = $this->dbConnection->getQueryBuilder();
|
||||
$query->select('propertyname', 'propertyvalue')
|
||||
$query->select('propertyname', 'propertyvalue', 'valuetype')
|
||||
->from('properties')
|
||||
->where($query->expr()->eq('userid', $query->createNamedParameter($user)))
|
||||
->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($this->formatPath($path))));
|
||||
|
|
@ -112,7 +136,11 @@ class CustomPropertiesBackendTest extends TestCase {
|
|||
$result = $query->execute();
|
||||
$data = [];
|
||||
while ($row = $result->fetch()) {
|
||||
$data[$row['propertyname']] = $row['propertyvalue'];
|
||||
$value = $row['propertyvalue'];
|
||||
if ((int)$row['valuetype'] === CustomPropertiesBackend::PROPERTY_TYPE_HREF) {
|
||||
$value = new Href($value);
|
||||
}
|
||||
$data[$row['propertyname']] = $value;
|
||||
}
|
||||
$result->closeCursor();
|
||||
|
||||
|
|
@ -122,9 +150,10 @@ class CustomPropertiesBackendTest extends TestCase {
|
|||
public function testPropFindNoDbCalls(): void {
|
||||
$db = $this->createMock(IDBConnection::class);
|
||||
$backend = new CustomPropertiesBackend(
|
||||
$this->server,
|
||||
$this->tree,
|
||||
$db,
|
||||
$this->user
|
||||
$this->user,
|
||||
);
|
||||
|
||||
$propFind = $this->createMock(PropFind::class);
|
||||
|
|
@ -186,10 +215,169 @@ class CustomPropertiesBackendTest extends TestCase {
|
|||
$this->assertEquals($props, $setProps);
|
||||
}
|
||||
|
||||
public function testPropFindPrincipalCall(): void {
|
||||
$this->tree->method('getNodeForPath')
|
||||
->willReturnCallback(function ($uri) {
|
||||
$node = $this->createMock(ICalendar::class);
|
||||
$node->method('getOwner')
|
||||
->willReturn('principals/users/dummy_user_42');
|
||||
return $node;
|
||||
});
|
||||
|
||||
$propFind = $this->createMock(PropFind::class);
|
||||
$propFind->method('get404Properties')
|
||||
->with()
|
||||
->willReturn([
|
||||
'{DAV:}getcontentlength',
|
||||
'{DAV:}getcontenttype',
|
||||
'{DAV:}getetag',
|
||||
'{abc}def',
|
||||
]);
|
||||
|
||||
$propFind->method('getRequestedProperties')
|
||||
->with()
|
||||
->willReturn([
|
||||
'{DAV:}getcontentlength',
|
||||
'{DAV:}getcontenttype',
|
||||
'{DAV:}getetag',
|
||||
'{abc}def',
|
||||
'{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
|
||||
]);
|
||||
|
||||
$props = [
|
||||
'{abc}def' => 'a',
|
||||
'{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/admin/personal'),
|
||||
];
|
||||
$this->insertProps('dummy_user_42', 'principals/users/dummy_user_42', $props);
|
||||
|
||||
$setProps = [];
|
||||
$propFind->method('set')
|
||||
->willReturnCallback(function ($name, $value, $status) use (&$setProps): void {
|
||||
$setProps[$name] = $value;
|
||||
});
|
||||
|
||||
$this->backend->propFind('principals/users/dummy_user_42', $propFind);
|
||||
$this->assertEquals($props, $setProps);
|
||||
}
|
||||
|
||||
public function propFindPrincipalScheduleDefaultCalendarProviderUrlProvider(): array {
|
||||
// [ user, nodes, existingProps, requestedProps, returnedProps ]
|
||||
return [
|
||||
[ // Exists
|
||||
'dummy_user_42',
|
||||
['calendars/dummy_user_42/foo/' => ICalendar::class],
|
||||
['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')],
|
||||
['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
|
||||
['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')],
|
||||
],
|
||||
[ // Doesn't exist
|
||||
'dummy_user_42',
|
||||
['calendars/dummy_user_42/foo/' => ICalendar::class],
|
||||
['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/bar/')],
|
||||
['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
|
||||
[],
|
||||
],
|
||||
[ // No privilege
|
||||
'dummy_user_42',
|
||||
['calendars/user2/baz/' => ICalendar::class],
|
||||
['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/user2/baz/')],
|
||||
['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
|
||||
[],
|
||||
],
|
||||
[ // Not a calendar
|
||||
'dummy_user_42',
|
||||
['foo/dummy_user_42/bar/' => IACL::class],
|
||||
['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/dummy_user_42/bar/')],
|
||||
['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
|
||||
[],
|
||||
],
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider propFindPrincipalScheduleDefaultCalendarProviderUrlProvider
|
||||
*/
|
||||
public function testPropFindPrincipalScheduleDefaultCalendarUrl(
|
||||
string $user,
|
||||
array $nodes,
|
||||
array $existingProps,
|
||||
array $requestedProps,
|
||||
array $returnedProps,
|
||||
): void {
|
||||
$propFind = $this->createMock(PropFind::class);
|
||||
$propFind->method('get404Properties')
|
||||
->with()
|
||||
->willReturn([
|
||||
'{DAV:}getcontentlength',
|
||||
'{DAV:}getcontenttype',
|
||||
'{DAV:}getetag',
|
||||
]);
|
||||
|
||||
$propFind->method('getRequestedProperties')
|
||||
->with()
|
||||
->willReturn(array_merge([
|
||||
'{DAV:}getcontentlength',
|
||||
'{DAV:}getcontenttype',
|
||||
'{DAV:}getetag',
|
||||
'{abc}def',
|
||||
],
|
||||
$requestedProps,
|
||||
));
|
||||
|
||||
$this->server->method('calculateUri')
|
||||
->willReturnCallback(function ($uri) {
|
||||
if (!str_starts_with($uri, self::BASE_URI)) {
|
||||
return trim(substr($uri, strlen(self::BASE_URI)), '/');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
$this->tree->method('getNodeForPath')
|
||||
->willReturnCallback(function ($uri) use ($nodes) {
|
||||
if (str_starts_with($uri, 'principals/')) {
|
||||
return $this->createMock(IPrincipal::class);
|
||||
}
|
||||
if (array_key_exists($uri, $nodes)) {
|
||||
$owner = explode('/', $uri)[1];
|
||||
$node = $this->createMock($nodes[$uri]);
|
||||
$node->method('getOwner')
|
||||
->willReturn("principals/users/$owner");
|
||||
return $node;
|
||||
}
|
||||
throw new NotFound('Node not found');
|
||||
});
|
||||
|
||||
$this->insertProps($user, "principals/users/$user", $existingProps);
|
||||
|
||||
$setProps = [];
|
||||
$propFind->method('set')
|
||||
->willReturnCallback(function ($name, $value, $status) use (&$setProps): void {
|
||||
$setProps[$name] = $value;
|
||||
});
|
||||
|
||||
$this->backend->propFind("principals/users/$user", $propFind);
|
||||
$this->assertEquals($returnedProps, $setProps);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider propPatchProvider
|
||||
*/
|
||||
public function testPropPatch(string $path, array $existing, array $props, array $result): void {
|
||||
$this->server->method('calculateUri')
|
||||
->willReturnCallback(function ($uri) {
|
||||
if (str_starts_with($uri, self::BASE_URI)) {
|
||||
return trim(substr($uri, strlen(self::BASE_URI)), '/');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
$this->tree->method('getNodeForPath')
|
||||
->willReturnCallback(function ($uri) {
|
||||
$node = $this->createMock(ICalendar::class);
|
||||
$node->method('getOwner')
|
||||
->willReturn('principals/users/' . $this->user->getUID());
|
||||
return $node;
|
||||
});
|
||||
|
||||
$this->insertProps($this->user->getUID(), $path, $existing);
|
||||
$propPatch = new PropPatch($props);
|
||||
|
||||
|
|
@ -207,6 +395,8 @@ class CustomPropertiesBackendTest extends TestCase {
|
|||
['foo_bar_path_1337', ['{DAV:}displayname' => 'foo'], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']],
|
||||
['foo_bar_path_1337', ['{DAV:}displayname' => 'foo'], ['{DAV:}displayname' => null], []],
|
||||
[$longPath, [], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']],
|
||||
['principals/users/dummy_user_42', [], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')]],
|
||||
['principals/users/dummy_user_42', [], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href(self::BASE_URI . 'foo/bar/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')]],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,3 +75,13 @@ Feature: caldav
|
|||
Then The CalDAV HTTP status code should be "404"
|
||||
And The exception is "Sabre\DAV\Exception\NotFound"
|
||||
And The error message is "Node with name 'admin' could not be found"
|
||||
|
||||
Scenario: Update a principal's schedule-default-calendar-URL
|
||||
Given user "user0" exists
|
||||
And "user0" creates a calendar named "MyCalendar2"
|
||||
When "user0" updates property "{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL" to href "/remote.php/dav/calendars/user0/MyCalendar2/" of principal "users/user0" on the endpoint "/remote.php/dav/principals/"
|
||||
Then The CalDAV response should be multi status
|
||||
And The CalDAV response should contain a property "{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL"
|
||||
When "user0" requests principal "users/user0" on the endpoint "/remote.php/dav/principals/"
|
||||
Then The CalDAV response should be multi status
|
||||
And The CalDAV response should contain a property "{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL" with a href value "/remote.php/dav/calendars/user0/MyCalendar2/"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
* @author Lukas Reschke <lukas@statuscode.ch>
|
||||
* @author Phil Davis <phil.davis@inf.org>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Richard Steinmetz <richard@steinmetz.cloud>
|
||||
*
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
|
|
@ -105,6 +106,119 @@ class CalDavContext implements \Behat\Behat\Context\Context {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @When :user requests principal :principal on the endpoint :endpoint
|
||||
*/
|
||||
public function requestsPrincipal(string $user, string $principal, string $endpoint): void {
|
||||
$davUrl = $this->baseUrl . $endpoint . $principal;
|
||||
|
||||
$password = ($user === 'admin') ? 'admin' : '123456';
|
||||
try {
|
||||
$this->response = $this->client->request(
|
||||
'PROPFIND',
|
||||
$davUrl,
|
||||
[
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/xml; charset=UTF-8',
|
||||
'Depth' => 0,
|
||||
],
|
||||
'body' => '<x0:propfind xmlns:x0="DAV:"><x0:prop><x0:displayname/><x1:calendar-user-type xmlns:x1="urn:ietf:params:xml:ns:caldav"/><x1:calendar-user-address-set xmlns:x1="urn:ietf:params:xml:ns:caldav"/><x0:principal-URL/><x0:alternate-URI-set/><x2:email-address xmlns:x2="http://sabredav.org/ns"/><x3:language xmlns:x3="http://nextcloud.com/ns"/><x1:calendar-home-set xmlns:x1="urn:ietf:params:xml:ns:caldav"/><x1:schedule-inbox-URL xmlns:x1="urn:ietf:params:xml:ns:caldav"/><x1:schedule-outbox-URL xmlns:x1="urn:ietf:params:xml:ns:caldav"/><x1:schedule-default-calendar-URL xmlns:x1="urn:ietf:params:xml:ns:caldav"/><x3:resource-type xmlns:x3="http://nextcloud.com/ns"/><x3:resource-vehicle-type xmlns:x3="http://nextcloud.com/ns"/><x3:resource-vehicle-make xmlns:x3="http://nextcloud.com/ns"/><x3:resource-vehicle-model xmlns:x3="http://nextcloud.com/ns"/><x3:resource-vehicle-is-electric xmlns:x3="http://nextcloud.com/ns"/><x3:resource-vehicle-range xmlns:x3="http://nextcloud.com/ns"/><x3:resource-vehicle-seating-capacity xmlns:x3="http://nextcloud.com/ns"/><x3:resource-contact-person xmlns:x3="http://nextcloud.com/ns"/><x3:resource-contact-person-vcard xmlns:x3="http://nextcloud.com/ns"/><x3:room-type xmlns:x3="http://nextcloud.com/ns"/><x3:room-seating-capacity xmlns:x3="http://nextcloud.com/ns"/><x3:room-building-address xmlns:x3="http://nextcloud.com/ns"/><x3:room-building-story xmlns:x3="http://nextcloud.com/ns"/><x3:room-building-room-number xmlns:x3="http://nextcloud.com/ns"/><x3:room-features xmlns:x3="http://nextcloud.com/ns"/><x0:principal-collection-set/><x0:supported-report-set/></x0:prop></x0:propfind>',
|
||||
'auth' => [
|
||||
$user,
|
||||
$password,
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (\GuzzleHttp\Exception\ClientException $e) {
|
||||
$this->response = $e->getResponse();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then The CalDAV response should contain a property :key
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function theCaldavResponseShouldContainAProperty(string $key): void {
|
||||
/** @var \Sabre\DAV\Xml\Response\MultiStatus $multiStatus */
|
||||
$multiStatus = $this->responseXml['value'];
|
||||
$responses = $multiStatus->getResponses()[0]->getResponseProperties();
|
||||
if (!isset($responses[200])) {
|
||||
throw new \Exception(
|
||||
sprintf(
|
||||
'Expected code 200 got [%s]',
|
||||
implode(',', array_keys($responses)),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$props = $responses[200];
|
||||
if (!array_key_exists($key, $props)) {
|
||||
throw new \Exception(
|
||||
sprintf(
|
||||
'Expected property %s in %s',
|
||||
$key,
|
||||
json_encode($props, JSON_PRETTY_PRINT),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then The CalDAV response should contain a property :key with a href value :value
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function theCaldavResponseShouldContainAPropertyWithHrefValue(
|
||||
string $key,
|
||||
string $value,
|
||||
): void {
|
||||
/** @var \Sabre\DAV\Xml\Response\MultiStatus $multiStatus */
|
||||
$multiStatus = $this->responseXml['value'];
|
||||
$responses = $multiStatus->getResponses()[0]->getResponseProperties();
|
||||
if (!isset($responses[200])) {
|
||||
throw new \Exception(
|
||||
sprintf(
|
||||
'Expected code 200 got [%s]',
|
||||
implode(',', array_keys($responses)),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$props = $responses[200];
|
||||
if (!array_key_exists($key, $props)) {
|
||||
throw new \Exception("Cannot find property \"$key\"");
|
||||
}
|
||||
|
||||
$actualValue = $props[$key]->getHref();
|
||||
if ($actualValue !== $value) {
|
||||
throw new \Exception("Property \"$key\" found with value \"$actualValue\", expected \"$value\"");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then The CalDAV response should be multi status
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function theCaldavResponseShouldBeMultiStatus(): void {
|
||||
if (207 !== $this->response->getStatusCode()) {
|
||||
throw new \Exception(
|
||||
sprintf(
|
||||
'Expected code 207 got %s',
|
||||
$this->response->getStatusCode()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$body = $this->response->getBody()->getContents();
|
||||
if ($body && substr($body, 0, 1) === '<') {
|
||||
$reader = new Sabre\Xml\Reader();
|
||||
$reader->xml($body);
|
||||
$reader->elementMap['{DAV:}multistatus'] = \Sabre\DAV\Xml\Response\MultiStatus::class;
|
||||
$reader->elementMap['{DAV:}response'] = \Sabre\DAV\Xml\Element\Response::class;
|
||||
$reader->elementMap['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'] = \Sabre\DAV\Xml\Property\Href::class;
|
||||
$this->responseXml = $reader->parse();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then The CalDAV HTTP status code should be :code
|
||||
* @param int $code
|
||||
|
|
@ -258,4 +372,39 @@ class CalDavContext implements \Behat\Behat\Context\Context {
|
|||
$this->response = $e->getResponse();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given :user updates property :key to href :value of principal :principal on the endpoint :endpoint
|
||||
*/
|
||||
public function updatesHrefPropertyOfPrincipal(
|
||||
string $user,
|
||||
string $key,
|
||||
string $value,
|
||||
string $principal,
|
||||
string $endpoint,
|
||||
): void {
|
||||
$davUrl = $this->baseUrl . $endpoint . $principal;
|
||||
$password = ($user === 'admin') ? 'admin' : '123456';
|
||||
|
||||
$propPatch = new \Sabre\DAV\Xml\Request\PropPatch();
|
||||
$propPatch->properties = [$key => new \Sabre\DAV\Xml\Property\Href($value)];
|
||||
|
||||
$xml = new \Sabre\Xml\Service();
|
||||
$body = $xml->write('{DAV:}propertyupdate', $propPatch, '/');
|
||||
|
||||
$this->response = $this->client->request(
|
||||
'PROPPATCH',
|
||||
$davUrl,
|
||||
[
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/xml; charset=UTF-8',
|
||||
],
|
||||
'body' => $body,
|
||||
'auth' => [
|
||||
$user,
|
||||
$password,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue