Signed-off-by: Maxence Lange <maxence@artificial-owl.com>tags/v28.0.0beta3
@@ -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 @@ class FilesPlugin extends ServerPlugin { | |||
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 @@ class FilesPlugin extends ServerPlugin { | |||
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 |
@@ -254,6 +254,7 @@ class FilesMetadataManager implements IFilesMetadataManager { | |||
* @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 @@ class FilesMetadataManager implements IFilesMetadataManager { | |||
* @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; | |||
} | |||
/** |
@@ -104,7 +104,7 @@ class MetadataQuery implements IMetadataQuery { | |||
$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 @@ class MetadataQuery implements IMetadataQuery { | |||
/** | |||
* @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.'); | |||
} |
@@ -124,6 +124,38 @@ class FilesMetadata implements IFilesMetadata { | |||
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 @@ class FilesMetadata implements IFilesMetadata { | |||
JSON_THROW_ON_ERROR | |||
) | |||
); | |||
} catch (JsonException $e) { | |||
} catch (JsonException) { | |||
throw new FilesMetadataNotFoundException(); | |||
} | |||
} |
@@ -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 @@ class MetadataValueWrapper implements IMetadataValueWrapper { | |||
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 @@ class MetadataValueWrapper implements IMetadataValueWrapper { | |||
$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 @@ class MetadataValueWrapper implements IMetadataValueWrapper { | |||
return [ | |||
'value' => ($emptyValues) ? null : $this->value, | |||
'type' => $this->getType(), | |||
'indexed' => $this->isIndexed() | |||
'indexed' => $this->isIndexed(), | |||
'editPermission' => $this->getEditPermission() | |||
]; | |||
} | |||
} |
@@ -30,6 +30,7 @@ use OCP\Files\Node; | |||
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 @@ interface IFilesMetadataManager { | |||
* @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; | |||
} |
@@ -37,12 +37,14 @@ use OCP\FilesMetadata\Exceptions\FilesMetadataTypeException; | |||
* "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 @@ interface IFilesMetadata extends JsonSerializable { | |||
*/ | |||
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 | |||
* |
@@ -37,17 +37,31 @@ use OCP\FilesMetadata\Exceptions\FilesMetadataTypeException; | |||
* @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 @@ interface IMetadataValueWrapper extends JsonSerializable { | |||
*/ | |||
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 | |||
* |