From 847c687a06ccd968936e5e6ed249840180c8e187 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Tue, 14 Nov 2023 10:01:29 -0100 Subject: [PATCH] metadata must be set as editable for PROPPATCH Signed-off-by: Maxence Lange --- apps/dav/lib/Connector/Sabre/FilesPlugin.php | 154 +++++++++++++----- .../FilesMetadata/FilesMetadataManager.php | 19 ++- lib/private/FilesMetadata/MetadataQuery.php | 4 +- .../FilesMetadata/Model/FilesMetadata.php | 34 +++- .../Model/MetadataValueWrapper.php | 28 +++- .../FilesMetadata/IFilesMetadataManager.php | 16 +- .../FilesMetadata/Model/IFilesMetadata.php | 28 +++- .../Model/IMetadataValueWrapper.php | 40 ++++- 8 files changed, 265 insertions(+), 58 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index c52805c95488d..8c467f11d0cb0 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -35,10 +35,10 @@ namespace OCA\DAV\Connector\Sabre; use OC\AppFramework\Http\Request; -use OC\FilesMetadata\Model\MetadataValueWrapper; use OCP\Constants; use OCP\Files\ForbiddenException; use OCP\Files\StorageNotAvailableException; +use OCP\FilesMetadata\Exceptions\FilesMetadataException; use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException; use OCP\FilesMetadata\IFilesMetadataManager; use OCP\FilesMetadata\Model\IMetadataValueWrapper; @@ -530,9 +530,34 @@ public function handleUpdateProperties($path, PropPatch $propPatch) { return true; }); + $this->handleUpdatePropertiesMetadata($propPatch, $node); - /** @var IFilesMetadataManager */ - $filesMetadataManager = \OCP\Server::get(IFilesMetadataManager::class); + /** + * Disable modification of the displayname property for files and + * folders via PROPPATCH. See PROPFIND for more information. + */ + $propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) { + return 403; + }); + } + + + /** + * handle the update of metadata from PROPPATCH requests + * + * @param PropPatch $propPatch + * @param Node $node + * + * @throws FilesMetadataException + */ + private function handleUpdatePropertiesMetadata(PropPatch $propPatch, Node $node): void { + $userId = $this->userSession->getUser()?->getUID(); + if (null === $userId) { + return; + } + + $accessRight = $this->getMetadataFileAccessRight($node, $userId); + $filesMetadataManager = $this->initFilesMetadataManager(); $knownMetadata = $filesMetadataManager->getKnownMetadata(); foreach ($propPatch->getRemainingMutations() as $mutation) { @@ -540,55 +565,96 @@ public function handleUpdateProperties($path, PropPatch $propPatch) { continue; } - $propPatch->handle($mutation, function (mixed $value) use ($knownMetadata, $node, $mutation, $filesMetadataManager): bool { - $metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true); - $metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX)); + $propPatch->handle( + $mutation, + function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool { + $metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true); + $metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX)); - // If the metadata is unknown, it defaults to string. - try { - $type = $knownMetadata->getType($metadataKey); - } catch (FilesMetadataNotFoundException) { - $type = IMetadataValueWrapper::TYPE_STRING; - } + // confirm metadata key is editable via PROPPATCH + if ($knownMetadata->getEditPermission($metadataKey) < $accessRight) { + throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node'); + } + + // If the metadata is unknown, it defaults to string. + try { + $type = $knownMetadata->getType($metadataKey); + } catch (FilesMetadataNotFoundException) { + $type = IMetadataValueWrapper::TYPE_STRING; + } + + switch ($type) { + case IMetadataValueWrapper::TYPE_STRING: + $metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); + break; + case IMetadataValueWrapper::TYPE_INT: + $metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); + break; + case IMetadataValueWrapper::TYPE_FLOAT: + $metadata->setFloat($metadataKey, $value); + break; + case IMetadataValueWrapper::TYPE_BOOL: + $metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); + break; + case IMetadataValueWrapper::TYPE_ARRAY: + $metadata->setArray($metadataKey, $value); + break; + case IMetadataValueWrapper::TYPE_STRING_LIST: + $metadata->setStringList( + $metadataKey, $value, $knownMetadata->isIndex($metadataKey) + ); + break; + case IMetadataValueWrapper::TYPE_INT_LIST: + $metadata->setIntList( + $metadataKey, $value, $knownMetadata->isIndex($metadataKey) + ); + break; + } + + $filesMetadataManager->saveMetadata($metadata); - switch ($type) { - case IMetadataValueWrapper::TYPE_STRING: - $metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); - break; - case IMetadataValueWrapper::TYPE_INT: - $metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); - break; - case IMetadataValueWrapper::TYPE_FLOAT: - $metadata->setFloat($metadataKey, $value); - break; - case IMetadataValueWrapper::TYPE_BOOL: - $metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); - break; - case IMetadataValueWrapper::TYPE_ARRAY: - $metadata->setArray($metadataKey, $value); - break; - case IMetadataValueWrapper::TYPE_STRING_LIST: - $metadata->setStringList($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); - break; - case IMetadataValueWrapper::TYPE_INT_LIST: - $metadata->setIntList($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); - break; + return true; } + ); + } + } - $filesMetadataManager->saveMetadata($metadata); - return true; - }); + /** + * init default internal metadata + * + * @return IFilesMetadataManager + */ + private function initFilesMetadataManager(): IFilesMetadataManager { + /** @var IFilesMetadataManager $manager */ + $manager = \OCP\Server::get(IFilesMetadataManager::class); + $manager->initMetadata('files-live-photo', IMetadataValueWrapper::TYPE_STRING, false, IMetadataValueWrapper::EDIT_REQ_OWNERSHIP); + + return $manager; + } + + /** + * based on owner and shares, returns the bottom limit to update related metadata + * + * @param Node $node + * @param string $userId + * + * @return int + */ + private function getMetadataFileAccessRight(Node $node, string $userId): int { + if ($node->getOwner()?->getUID() === $userId) { + return IMetadataValueWrapper::EDIT_REQ_OWNERSHIP; + } else { + $filePermissions = $node->getSharePermissions($userId); + if ($filePermissions & Constants::PERMISSION_UPDATE) { + return IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION; + } } - /** - * Disable modification of the displayname property for files and - * folders via PROPPATCH. See PROPFIND for more information. - */ - $propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) { - return 403; - }); + return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION; } + + /** * @param string $filePath * @param \Sabre\DAV\INode $node diff --git a/lib/private/FilesMetadata/FilesMetadataManager.php b/lib/private/FilesMetadata/FilesMetadataManager.php index e9d6e677bafda..f90693359950e 100644 --- a/lib/private/FilesMetadata/FilesMetadataManager.php +++ b/lib/private/FilesMetadata/FilesMetadataManager.php @@ -254,6 +254,7 @@ public function getKnownMetadata(): IFilesMetadata { * @param string $key metadata key * @param string $type metadata type * @param bool $indexed TRUE if metadata can be search + * @param int $editPermission remote edit permission via Webdav PROPPATCH * * @inheritDoc * @since 28.0.0 @@ -264,19 +265,31 @@ public function getKnownMetadata(): IFilesMetadata { * @see IMetadataValueWrapper::TYPE_STRING_LIST * @see IMetadataValueWrapper::TYPE_INT_LIST * @see IMetadataValueWrapper::TYPE_STRING + * @see IMetadataValueWrapper::EDIT_FORBIDDEN + * @see IMetadataValueWrapper::EDIT_REQ_OWNERSHIP + * @see IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION + * @see IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION */ - public function initMetadata(string $key, string $type, bool $indexed): void { + public function initMetadata( + string $key, + string $type, + bool $indexed = false, + int $editPermission = IMetadataValueWrapper::EDIT_FORBIDDEN + ): void { $current = $this->getKnownMetadata(); try { - if ($current->getType($key) === $type && $indexed === $current->isIndex($key)) { + if ($current->getType($key) === $type + && $indexed === $current->isIndex($key) + && $editPermission === $current->getEditPermission($key)) { return; // if key exists, with same type and indexed, we do nothing. } } catch (FilesMetadataNotFoundException) { // if value does not exist, we keep on the writing of course } - $current->import([$key => ['type' => $type, 'indexed' => $indexed]]); + $current->import([$key => ['type' => $type, 'indexed' => $indexed, 'editPermission' => $editPermission]]); $this->config->setAppValue('core', self::CONFIG_KEY, json_encode($current)); + $this->all = $current; } /** diff --git a/lib/private/FilesMetadata/MetadataQuery.php b/lib/private/FilesMetadata/MetadataQuery.php index 56ea90465b26b..9c3e63e5e22df 100644 --- a/lib/private/FilesMetadata/MetadataQuery.php +++ b/lib/private/FilesMetadata/MetadataQuery.php @@ -104,7 +104,7 @@ public function joinIndex(string $metadataKey, bool $enforce = false): string { $andX->add($expr->eq($this->getMetadataKeyField($metadataKey), $this->queryBuilder->createNamedParameter($metadataKey))); if ($enforce) { - $this->queryBuilder->rightJoin( + $this->queryBuilder->innerJoin( $this->fileTableAlias, IndexRequestService::TABLE_METADATA_INDEX, $aliasIndex, @@ -125,7 +125,7 @@ public function joinIndex(string $metadataKey, bool $enforce = false): string { /** * @throws FilesMetadataNotFoundException */ - public function joinedTableAlias(string $metadataKey): string { + private function joinedTableAlias(string $metadataKey): string { if (!array_key_exists($metadataKey, $this->knownJoinedIndex)) { throw new FilesMetadataNotFoundException('table related to ' . $metadataKey . ' not initiated, you need to use leftJoin() first.'); } diff --git a/lib/private/FilesMetadata/Model/FilesMetadata.php b/lib/private/FilesMetadata/Model/FilesMetadata.php index b10de55579c75..629b537dabee9 100644 --- a/lib/private/FilesMetadata/Model/FilesMetadata.php +++ b/lib/private/FilesMetadata/Model/FilesMetadata.php @@ -124,6 +124,38 @@ public function isIndex(string $key): bool { return $this->metadata[$key]?->isIndexed() ?? false; } + /** + * @param string $key metadata key + * + * @inheritDoc + * @return int edit permission + * @throws FilesMetadataNotFoundException + * @since 28.0.0 + */ + public function getEditPermission(string $key): int { + if (!array_key_exists($key, $this->metadata)) { + throw new FilesMetadataNotFoundException(); + } + + return $this->metadata[$key]->getEditPermission(); + } + + /** + * @param string $key metadata key + * @param int $permission edit permission + * + * @inheritDoc + * @throws FilesMetadataNotFoundException + * @since 28.0.0 + */ + public function setEditPermission(string $key, int $permission): void { + if (!array_key_exists($key, $this->metadata)) { + throw new FilesMetadataNotFoundException(); + } + + $this->metadata[$key]->setEditPermission($permission); + } + /** * @param string $key metadata key * @@ -582,7 +614,7 @@ public function importFromDatabase(array $data, string $prefix = ''): IFilesMeta JSON_THROW_ON_ERROR ) ); - } catch (JsonException $e) { + } catch (JsonException) { throw new FilesMetadataNotFoundException(); } } diff --git a/lib/private/FilesMetadata/Model/MetadataValueWrapper.php b/lib/private/FilesMetadata/Model/MetadataValueWrapper.php index 159cd1e6fd11f..90f1554180dd7 100644 --- a/lib/private/FilesMetadata/Model/MetadataValueWrapper.php +++ b/lib/private/FilesMetadata/Model/MetadataValueWrapper.php @@ -39,6 +39,7 @@ class MetadataValueWrapper implements IMetadataValueWrapper { /** @var string|int|float|bool|array|string[]|int[] */ private mixed $value = null; private bool $indexed = false; + private int $editPermission = self::EDIT_FORBIDDEN; /** * @param string $type value type @@ -371,6 +372,28 @@ public function isIndexed(): bool { return $this->indexed; } + /** + * @param int $permission edit permission + * + * @inheritDoc + * @return self + * @since 28.0.0 + */ + public function setEditPermission(int $permission): self { + $this->editPermission = $permission; + + return $this; + } + + /** + * @inheritDoc + * @return int edit permission + * @since 28.0.0 + */ + public function getEditPermission(): int { + return $this->editPermission; + } + /** * @param array $data serialized version of the object * @@ -383,7 +406,7 @@ public function import(array $data): self { $this->value = $data['value'] ?? null; $this->type = $data['type'] ?? ''; $this->setIndexed($data['indexed'] ?? false); - + $this->setEditPermission($data['editPermission'] ?? self::EDIT_FORBIDDEN); return $this; } @@ -391,7 +414,8 @@ public function jsonSerialize(bool $emptyValues = false): array { return [ 'value' => ($emptyValues) ? null : $this->value, 'type' => $this->getType(), - 'indexed' => $this->isIndexed() + 'indexed' => $this->isIndexed(), + 'editPermission' => $this->getEditPermission() ]; } } diff --git a/lib/public/FilesMetadata/IFilesMetadataManager.php b/lib/public/FilesMetadata/IFilesMetadataManager.php index 78ea020e96734..6664c049d917c 100644 --- a/lib/public/FilesMetadata/IFilesMetadataManager.php +++ b/lib/public/FilesMetadata/IFilesMetadataManager.php @@ -30,6 +30,7 @@ use OCP\FilesMetadata\Exceptions\FilesMetadataException; use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException; use OCP\FilesMetadata\Model\IFilesMetadata; +use OCP\FilesMetadata\Model\IMetadataValueWrapper; /** * Manager for FilesMetadata; manage files' metadata. @@ -133,7 +134,20 @@ public function getKnownMetadata(): IFilesMetadata; * @param string $key metadata key * @param string $type metadata type * @param bool $indexed TRUE if metadata can be search + * @param int $editPermission remote edit permission via Webdav PROPPATCH + * + * @see IMetadataValueWrapper::TYPE_INT + * @see IMetadataValueWrapper::TYPE_FLOAT + * @see IMetadataValueWrapper::TYPE_BOOL + * @see IMetadataValueWrapper::TYPE_ARRAY + * @see IMetadataValueWrapper::TYPE_STRING_LIST + * @see IMetadataValueWrapper::TYPE_INT_LIST + * @see IMetadataValueWrapper::TYPE_STRING + * @see IMetadataValueWrapper::EDIT_FORBIDDEN + * @see IMetadataValueWrapper::EDIT_REQ_OWNERSHIP + * @see IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION + * @see IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION * @since 28.0.0 */ - public function initMetadata(string $key, string $type, bool $indexed): void; + public function initMetadata(string $key, string $type, bool $indexed, int $editPermission): void; } diff --git a/lib/public/FilesMetadata/Model/IFilesMetadata.php b/lib/public/FilesMetadata/Model/IFilesMetadata.php index e5b6ff1bce03d..7697a2f37adea 100644 --- a/lib/public/FilesMetadata/Model/IFilesMetadata.php +++ b/lib/public/FilesMetadata/Model/IFilesMetadata.php @@ -37,12 +37,14 @@ * "mymeta": { * "value": "this is a test", * "type": "string", - * "indexed": false + * "indexed": false, + * "editPermission": 1 * }, * "myapp-anothermeta": { * "value": 42, * "type": "int", - * "indexed": true + * "indexed": true, + * "editPermission": 0 * } * } * @@ -110,6 +112,28 @@ public function getIndexes(): array; */ public function isIndex(string $key): bool; + /** + * set remote edit permission + * (Webdav PROPPATCH) + * + * @param string $key metadata key + * @param int $permission remote edit permission + * + * @since 28.0.0 + */ + public function setEditPermission(string $key, int $permission): void; + + /** + * returns remote edit permission + * (Webdav PROPPATCH) + * + * @param string $key metadata key + * + * @return int + * @since 28.0.0 + */ + public function getEditPermission(string $key): int; + /** * returns string value for a metadata key * diff --git a/lib/public/FilesMetadata/Model/IMetadataValueWrapper.php b/lib/public/FilesMetadata/Model/IMetadataValueWrapper.php index 577ff87433002..d34cd070c8b2c 100644 --- a/lib/public/FilesMetadata/Model/IMetadataValueWrapper.php +++ b/lib/public/FilesMetadata/Model/IMetadataValueWrapper.php @@ -37,17 +37,31 @@ * @since 28.0.0 */ interface IMetadataValueWrapper extends JsonSerializable { - /** - * @since 28.0.0 - */ + /** @since 28.0.0 */ public const TYPE_STRING = 'string'; + /** @since 28.0.0 */ public const TYPE_INT = 'int'; + /** @since 28.0.0 */ public const TYPE_FLOAT = 'float'; + /** @since 28.0.0 */ public const TYPE_BOOL = 'bool'; + /** @since 28.0.0 */ public const TYPE_ARRAY = 'array'; + /** @since 28.0.0 */ public const TYPE_STRING_LIST = 'string[]'; + /** @since 28.0.0 */ public const TYPE_INT_LIST = 'int[]'; + /** @since 28.0.0 */ + public const EDIT_FORBIDDEN = 0; + /** @since 28.0.0 */ + public const EDIT_REQ_OWNERSHIP = 1; + /** @since 28.0.0 */ + public const EDIT_REQ_WRITE_PERMISSION = 2; + /** @since 28.0.0 */ + public const EDIT_REQ_READ_PERMISSION = 3; + + /** * Unless a call of import() to deserialize an object is expected, a valid value type is needed here. * @@ -287,6 +301,26 @@ public function setIndexed(bool $indexed): self; */ public function isIndexed(): bool; + /** + * set remote edit permission + * (Webdav PROPPATCH) + * + * @param int $permission edit permission + * + * @return self + * @since 28.0.0 + */ + public function setEditPermission(int $permission): self; + + /** + * get remote edit permission + * (Webdav PROPPATCH) + * + * @return int edit permission + * @since 28.0.0 + */ + public function getEditPermission(): int; + /** * deserialize the object from a json *