Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Dismissed Show dismissed Hide dismissed
Dismissed Show dismissed Hide dismissed
}

if ($name === '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL') {
$value = $this->encodeDefaultCalendarUrl($value);
Fixed Show fixed Hide fixed
Dismissed Show dismissed Hide dismissed
}

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
Loading