Skip to content

Commit

Permalink
feat(dav): update a principal's schedule-default-calendar-URL
Browse files Browse the repository at this point in the history
Signed-off-by: Richard Steinmetz <[email protected]>
  • Loading branch information
st3iny committed Feb 27, 2024
1 parent 455a209 commit 69bd8be
Show file tree
Hide file tree
Showing 9 changed files with 500 additions and 9 deletions.
7 changes: 7 additions & 0 deletions apps/dav/lib/CalDAV/Schedule/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ public function initialize(Server $server) {
$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,
);
}

/**
Expand Down
1 change: 1 addition & 0 deletions apps/dav/lib/Connector/Sabre/Principal.php
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ public function getGroupMembership($principal, $needGroups = false) {
* @return int
*/
public function updatePrincipal($path, PropPatch $propPatch) {
// Updating schedule-default-calendar-URL is handled in CustomPropertiesBackend
return 0;
}

Expand Down
1 change: 1 addition & 0 deletions apps/dav/lib/Connector/Sabre/ServerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ public function createServer(string $baseUri,
$server->addPlugin(
new \Sabre\DAV\PropertyStorage\Plugin(
new \OCA\DAV\DAV\CustomPropertiesBackend(
$server,
$objectTree,
$this->databaseConnection,
$this->userSession->getUser()
Expand Down
139 changes: 135 additions & 4 deletions apps/dav/lib/DAV/CustomPropertiesBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* @author Georg Ehrke <[email protected]>
* @author Robin Appelman <[email protected]>
* @author Thomas Müller <[email protected]>
* @author Richard Steinmetz <[email protected]>
*
* @license AGPL-3.0
*
Expand All @@ -31,11 +32,19 @@
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 {
Expand All @@ -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
*
Expand Down Expand Up @@ -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,
];

/**
Expand All @@ -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,
);
}

/**
Expand Down Expand Up @@ -199,6 +232,21 @@ public function propFind($path, PropFind $propFind) {
}
}

// 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;
}
Expand All @@ -211,9 +259,19 @@ public function propFind($path, PropFind $propFind) {
// 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);
}
}
Expand Down Expand Up @@ -264,6 +322,30 @@ public function move($source, $destination) {
$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
Expand Down Expand Up @@ -393,7 +475,11 @@ private function updateProperties(string $path, array $properties): bool {
->executeStatement();
}
} else {
[$value, $valueType] = $this->encodeValueForDatabase($propertyValue);
[$value, $valueType] = $this->encodeValueForDatabase(
$path,
$propertyName,
$propertyValue,
);
$dbParameters['propertyValue'] = $value;
$dbParameters['valueType'] = $valueType;

Expand Down Expand Up @@ -436,15 +522,38 @@ private function formatPath(string $path): string {
}

/**
* @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);
Expand All @@ -459,6 +568,8 @@ private function decodeValueFromDatabase(string $value, int $valueType) {
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:
Expand All @@ -467,6 +578,26 @@ private function decodeValueFromDatabase(string $value, int $valueType) {
}
}

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')
Expand Down
1 change: 1 addition & 0 deletions apps/dav/lib/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ public function __construct(IRequest $request, string $baseUri) {
$this->server->addPlugin(
new \Sabre\DAV\PropertyStorage\Plugin(
new CustomPropertiesBackend(
$this->server,
$this->server->tree,
\OC::$server->getDatabaseConnection(),
\OC::$server->getUserSession()->getUser()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ protected function setUp(): void {
->willReturn($userId);

$this->plugin = new \OCA\DAV\DAV\CustomPropertiesBackend(
$this->server,
$this->tree,
\OC::$server->getDatabaseConnection(),
$this->user
Expand Down
Loading

0 comments on commit 69bd8be

Please sign in to comment.