Browse Source

feat(dav): update a principal's schedule-default-calendar-URL

Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
tags/v29.0.0beta1
Richard Steinmetz 2 months ago
parent
commit
53ef6c5f71
No account linked to committer's email address

+ 7
- 0
apps/dav/lib/CalDAV/Schedule/Plugin.php View File

@@ -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,
);
}

/**

+ 1
- 0
apps/dav/lib/Connector/Sabre/Principal.php View File

@@ -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;
}


+ 1
- 0
apps/dav/lib/Connector/Sabre/ServerFactory.php View File

@@ -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()

+ 135
- 4
apps/dav/lib/DAV/CustomPropertiesBackend.php View File

@@ -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')

+ 1
- 0
apps/dav/lib/Server.php View File

@@ -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()

+ 1
- 0
apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php View File

@@ -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

+ 195
- 5
apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php View File

@@ -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/')]],
];
}


+ 10
- 0
build/integration/dav_features/caldav.feature View File

@@ -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/"

+ 149
- 0
build/integration/features/bootstrap/CalDavContext.php View File

@@ -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…
Cancel
Save