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(systemtags): add bulk tagging action #48786

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions apps/dav/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -362,9 +362,11 @@
'OCA\\DAV\\SystemTag\\SystemTagList' => $baseDir . '/../lib/SystemTag/SystemTagList.php',
'OCA\\DAV\\SystemTag\\SystemTagMappingNode' => $baseDir . '/../lib/SystemTag/SystemTagMappingNode.php',
'OCA\\DAV\\SystemTag\\SystemTagNode' => $baseDir . '/../lib/SystemTag/SystemTagNode.php',
'OCA\\DAV\\SystemTag\\SystemTagObjectType' => $baseDir . '/../lib/SystemTag/SystemTagObjectType.php',
'OCA\\DAV\\SystemTag\\SystemTagPlugin' => $baseDir . '/../lib/SystemTag/SystemTagPlugin.php',
'OCA\\DAV\\SystemTag\\SystemTagsByIdCollection' => $baseDir . '/../lib/SystemTag/SystemTagsByIdCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsInUseCollection' => $baseDir . '/../lib/SystemTag/SystemTagsInUseCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectList' => $baseDir . '/../lib/SystemTag/SystemTagsObjectList.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectMappingCollection' => $baseDir . '/../lib/SystemTag/SystemTagsObjectMappingCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectTypeCollection' => $baseDir . '/../lib/SystemTag/SystemTagsObjectTypeCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsRelationsCollection' => $baseDir . '/../lib/SystemTag/SystemTagsRelationsCollection.php',
Expand Down
2 changes: 2 additions & 0 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,11 @@ class ComposerStaticInitDAV
'OCA\\DAV\\SystemTag\\SystemTagList' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagList.php',
'OCA\\DAV\\SystemTag\\SystemTagMappingNode' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagMappingNode.php',
'OCA\\DAV\\SystemTag\\SystemTagNode' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagNode.php',
'OCA\\DAV\\SystemTag\\SystemTagObjectType' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagObjectType.php',
'OCA\\DAV\\SystemTag\\SystemTagPlugin' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagPlugin.php',
'OCA\\DAV\\SystemTag\\SystemTagsByIdCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsByIdCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsInUseCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsInUseCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectList' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsObjectList.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectMappingCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsObjectMappingCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsObjectTypeCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsObjectTypeCollection.php',
'OCA\\DAV\\SystemTag\\SystemTagsRelationsCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsRelationsCollection.php',
Expand Down
6 changes: 1 addition & 5 deletions apps/dav/lib/RootCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,7 @@ public function __construct() {

$publicCalendarRoot = new PublicCalendarRoot($caldavBackend, $l10n, $config, $logger);

$systemTagCollection = new SystemTagsByIdCollection(
\OC::$server->getSystemTagManager(),
\OC::$server->getUserSession(),
$groupManager
);
$systemTagCollection = Server::get(SystemTagsByIdCollection::class);
$systemTagRelationsCollection = new SystemTagsRelationsCollection(
\OC::$server->getSystemTagManager(),
\OC::$server->getSystemTagObjectMapper(),
Expand Down
34 changes: 31 additions & 3 deletions apps/dav/lib/SystemTag/SystemTagNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
use OCP\IUser;
use OCP\SystemTag\ISystemTag;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\TagAlreadyExistsException;

use OCP\SystemTag\TagNotFoundException;
use Sabre\DAV\Exception\Conflict;
use Sabre\DAV\Exception\Forbidden;
Expand All @@ -21,7 +21,7 @@
/**
* DAV node representing a system tag, with the name being the tag id.
*/
class SystemTagNode implements \Sabre\DAV\INode {
class SystemTagNode implements \Sabre\DAV\ICollection {

protected int $numberOfFiles = -1;
protected int $referenceFileId = -1;
Expand All @@ -43,8 +43,9 @@ public function __construct(
/**
* Whether to allow permissions for admins
*/
protected $isAdmin,
protected bool $isAdmin,
protected ISystemTagManager $tagManager,
protected ISystemTagObjectMapper $tagMapper,
) {
}

Expand Down Expand Up @@ -164,4 +165,31 @@ public function getReferenceFileId(): int {
public function setReferenceFileId(int $referenceFileId): void {
$this->referenceFileId = $referenceFileId;
}

public function createFile($name, $data = null) {
throw new MethodNotAllowed();
}

public function createDirectory($name) {
throw new MethodNotAllowed();
}

public function getChild($name) {
return new SystemTagObjectType($this->tag, $name, $this->tagManager, $this->tagMapper);
}

public function childExists($name) {
$objectTypes = $this->tagMapper->getAvailableObjectTypes();
return in_array($name, $objectTypes);
}

public function getChildren() {
$objectTypes = $this->tagMapper->getAvailableObjectTypes();
return array_map(
function ($objectType) {
return new SystemTagObjectType($this->tag, $objectType, $this->tagManager, $this->tagMapper);
},
$objectTypes
);
}
}
81 changes: 81 additions & 0 deletions apps/dav/lib/SystemTag/SystemTagObjectType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\SystemTag;

use OCP\SystemTag\ISystemTag;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
use Sabre\DAV\Exception\MethodNotAllowed;

/**
* SystemTagObjectType property
* This property represent a type of object which tags are assigned to.
*/
class SystemTagObjectType implements \Sabre\DAV\IFile {
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';

/** @var string[] */
private array $objectsIds = [];

public function __construct(
private ISystemTag $tag,
private string $type,
private ISystemTagManager $tagManager,
private ISystemTagObjectMapper $tagMapper,
) {
}

/**
* Get the list of object ids that have this tag assigned.
*/
public function getObjectsIds(): array {
if (empty($this->objectsIds)) {
$this->objectsIds = $this->tagMapper->getObjectIdsForTags($this->tag->getId(), $this->type);
}

return $this->objectsIds;
}

/**
* Returns the system tag represented by this node
*
* @return ISystemTag system tag
*/
public function getSystemTag() {
return $this->tag;
}

public function getName() {
return $this->type;
}

public function getLastModified() {
return null;
}

public function getETag() {
return '"' . $this->tag->getETag() . '"';
}

public function setName($name) {
throw new MethodNotAllowed();
}
public function put($data) {
throw new MethodNotAllowed();
}
public function get() {
throw new MethodNotAllowed();
}
public function delete() {
throw new MethodNotAllowed();
}
public function getContentType() {
throw new MethodNotAllowed();
}
public function getSize() {
throw new MethodNotAllowed();
}
}
71 changes: 65 additions & 6 deletions apps/dav/lib/SystemTag/SystemTagPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace OCA\DAV\SystemTag;

use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Connector\Sabre\FilesPlugin;
use OCA\DAV\Connector\Sabre\Node;
use OCP\IGroupManager;
use OCP\IUser;
Expand All @@ -20,6 +21,7 @@
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Conflict;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\PreconditionFailed;
use Sabre\DAV\Exception\UnsupportedMediaType;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
Expand All @@ -37,6 +39,7 @@

// namespace
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
public const NS_NEXTCLOUD = 'http://nextcloud.org/ns';
public const ID_PROPERTYNAME = '{http://owncloud.org/ns}id';
public const DISPLAYNAME_PROPERTYNAME = '{http://owncloud.org/ns}display-name';
public const USERVISIBLE_PROPERTYNAME = '{http://owncloud.org/ns}user-visible';
Expand All @@ -45,7 +48,8 @@
public const CANASSIGN_PROPERTYNAME = '{http://owncloud.org/ns}can-assign';
public const SYSTEM_TAGS_PROPERTYNAME = '{http://nextcloud.org/ns}system-tags';
public const NUM_FILES_PROPERTYNAME = '{http://nextcloud.org/ns}files-assigned';
public const FILEID_PROPERTYNAME = '{http://nextcloud.org/ns}reference-fileid';
public const REFERENCE_FILEID_PROPERTYNAME = '{http://nextcloud.org/ns}reference-fileid';
public const OBJECTIDS_PROPERTYNAME = '{http://nextcloud.org/ns}object-ids';

/**
* @var \Sabre\DAV\Server $server
Expand Down Expand Up @@ -78,6 +82,9 @@
*/
public function initialize(\Sabre\DAV\Server $server) {
$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';
$server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc';

$server->xml->elementMap[self::OBJECTIDS_PROPERTYNAME] = SystemTagsObjectList::class;

$server->protectedProperties[] = self::ID_PROPERTYNAME;

Expand Down Expand Up @@ -202,7 +209,7 @@
return;
}

if (!($node instanceof SystemTagNode) && !($node instanceof SystemTagMappingNode)) {
if (!($node instanceof SystemTagNode) && !($node instanceof SystemTagMappingNode) && !($node instanceof SystemTagObjectType)) {
return;
}

Expand All @@ -211,6 +218,10 @@
$propFind->setPath(str_replace('systemtags-assigned/', 'systemtags/', $propFind->getPath()));
}

$propFind->handle(FilesPlugin::GETETAG_PROPERTYNAME, function () use ($node): string|null {
return $node->getSystemTag()->getETag();
});

$propFind->handle(self::ID_PROPERTYNAME, function () use ($node) {
return $node->getSystemTag()->getId();
});
Expand Down Expand Up @@ -251,9 +262,25 @@
return $node->getNumberOfFiles();
});

$propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node): int {
$propFind->handle(self::REFERENCE_FILEID_PROPERTYNAME, function () use ($node): int {
return $node->getReferenceFileId();
});

$propFind->handle(self::OBJECTIDS_PROPERTYNAME, function () use ($node): SystemTagsObjectList {
$objectTypes = $this->tagMapper->getAvailableObjectTypes();
$objects = [];
foreach ($objectTypes as $type) {
$systemTagObjectType = new SystemTagObjectType($node->getSystemTag(), $type, $this->tagManager, $this->tagMapper);
$objects = array_merge($objects, array_fill_keys($systemTagObjectType->getObjectsIds(), $type));
}
return new SystemTagsObjectList($objects);
});
}

if ($node instanceof SystemTagObjectType) {
$propFind->handle(self::OBJECTIDS_PROPERTYNAME, function () use ($node): SystemTagsObjectList {
return new SystemTagsObjectList(array_fill_keys($node->getObjectsIds(), $node->getName()));
});
}
}

Expand Down Expand Up @@ -341,18 +368,50 @@
*/
public function handleUpdateProperties($path, PropPatch $propPatch) {
$node = $this->server->tree->getNodeForPath($path);
if (!($node instanceof SystemTagNode)) {
if (!($node instanceof SystemTagNode) && !($node instanceof SystemTagObjectType)) {
return;
}

$propPatch->handle([self::OBJECTIDS_PROPERTYNAME], function ($props) use ($node) {
if (!($node instanceof SystemTagObjectType)) {
return false;
}

if (isset($props[self::OBJECTIDS_PROPERTYNAME])) {
$propValue = $props[self::OBJECTIDS_PROPERTYNAME];
if (!($propValue instanceof SystemTagsObjectList) || count($propValue?->getObjects() ?: []) === 0) {

Check failure on line 382 in apps/dav/lib/SystemTag/SystemTagPlugin.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

TypeDoesNotContainNull

apps/dav/lib/SystemTag/SystemTagPlugin.php:382:64: TypeDoesNotContainNull: OCA\DAV\SystemTag\SystemTagsObjectList does not contain null (see https://psalm.dev/090)

Check failure on line 382 in apps/dav/lib/SystemTag/SystemTagPlugin.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

RedundantCondition

apps/dav/lib/SystemTag/SystemTagPlugin.php:382:64: RedundantCondition: Type OCA\DAV\SystemTag\SystemTagsObjectList for $propValue is never null (see https://psalm.dev/122)

Check failure on line 382 in apps/dav/lib/SystemTag/SystemTagPlugin.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

TypeDoesNotContainNull

apps/dav/lib/SystemTag/SystemTagPlugin.php:382:93: TypeDoesNotContainNull: Cannot resolve types for $propValue - OCA\DAV\SystemTag\SystemTagsObjectList does not contain null (see https://psalm.dev/090)
throw new BadRequest('Invalid object-ids property');
}

$objects = $propValue->getObjects();
$objectTypes = array_unique(array_values($objects));

if (count($objectTypes) !== 1 || $objectTypes[0] !== $node->getName()) {
throw new BadRequest('Invalid object-ids property. All object types must be of the same type: ' . $node->getName());
}

$this->tagMapper->setObjectIdsForTag($node->getSystemTag()->getId(), $node->getName(), array_keys($objects));
}

if ($props[self::OBJECTIDS_PROPERTYNAME] === null) {
$this->tagMapper->setObjectIdsForTag($node->getSystemTag()->getId(), $node->getName(), []);
}

return true;
});

$propPatch->handle([
self::DISPLAYNAME_PROPERTYNAME,
self::USERVISIBLE_PROPERTYNAME,
self::USERASSIGNABLE_PROPERTYNAME,
self::GROUPS_PROPERTYNAME,
self::NUM_FILES_PROPERTYNAME,
self::FILEID_PROPERTYNAME,
self::REFERENCE_FILEID_PROPERTYNAME,
], function ($props) use ($node) {
if (!($node instanceof SystemTagNode)) {
return false;
}

$tag = $node->getSystemTag();
$name = $tag->getName();
$userVisible = $tag->isUserVisible();
Expand Down Expand Up @@ -388,7 +447,7 @@
$this->tagManager->setTagGroups($tag, $groupIds);
}

if (isset($props[self::NUM_FILES_PROPERTYNAME]) || isset($props[self::FILEID_PROPERTYNAME])) {
if (isset($props[self::NUM_FILES_PROPERTYNAME]) || isset($props[self::REFERENCE_FILEID_PROPERTYNAME])) {
// read-only properties
throw new Forbidden();
}
Expand Down
4 changes: 3 additions & 1 deletion apps/dav/lib/SystemTag/SystemTagsByIdCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use OCP\IUserSession;
use OCP\SystemTag\ISystemTag;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\TagNotFoundException;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
Expand All @@ -30,6 +31,7 @@ public function __construct(
private ISystemTagManager $tagManager,
private IUserSession $userSession,
private IGroupManager $groupManager,
protected ISystemTagObjectMapper $tagMapper,
) {
}

Expand Down Expand Up @@ -162,6 +164,6 @@ public function getLastModified() {
* @return SystemTagNode
*/
private function makeNode(ISystemTag $tag) {
return new SystemTagNode($tag, $this->userSession->getUser(), $this->isAdmin(), $this->tagManager);
return new SystemTagNode($tag, $this->userSession->getUser(), $this->isAdmin(), $this->tagManager, $this->tagMapper);
}
}
8 changes: 5 additions & 3 deletions apps/dav/lib/SystemTag/SystemTagsInUseCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use OCP\Files\NotPermittedException;
use OCP\IUserSession;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\SimpleCollection;
Expand All @@ -28,6 +29,7 @@ public function __construct(
protected IUserSession $userSession,
protected IRootFolder $rootFolder,
protected ISystemTagManager $systemTagManager,
protected ISystemTagObjectMapper $tagMapper,
SystemTagsInFilesDetector $systemTagsInFilesDetector,
protected string $mediaType = '',
) {
Expand All @@ -46,7 +48,7 @@ public function getChild($name): self {
if ($this->mediaType !== '') {
throw new NotFound('Invalid media type');
}
return new self($this->userSession, $this->rootFolder, $this->systemTagManager, $this->systemTagsInFilesDetector, $name);
return new self($this->userSession, $this->rootFolder, $this->systemTagManager, $this->tagMapper, $this->systemTagsInFilesDetector, $name);
}

/**
Expand All @@ -71,9 +73,9 @@ public function getChildren(): array {
$result = $this->systemTagsInFilesDetector->detectAssignedSystemTagsIn($userFolder, $this->mediaType);
$children = [];
foreach ($result as $tagData) {
$tag = new SystemTag((string)$tagData['id'], $tagData['name'], (bool)$tagData['visibility'], (bool)$tagData['editable']);
$tag = new SystemTag((string)$tagData['id'], $tagData['name'], (bool)$tagData['visibility'], (bool)$tagData['editable'], $tagData['etag']);
// read only, so we can submit the isAdmin parameter as false generally
$node = new SystemTagNode($tag, $user, false, $this->systemTagManager);
$node = new SystemTagNode($tag, $user, false, $this->systemTagManager, $this->tagMapper);
$node->setNumberOfFiles((int)$tagData['number_files']);
$node->setReferenceFileId((int)$tagData['ref_file_id']);
$children[] = $node;
Expand Down
Loading
Loading