diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 6745ffe41b4..db7de8c9ac6 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -304,6 +304,7 @@ return array( 'OCA\\DAV\\Settings\\AvailabilitySettings' => $baseDir . '/../lib/Settings/AvailabilitySettings.php', 'OCA\\DAV\\Settings\\CalDAVSettings' => $baseDir . '/../lib/Settings/CalDAVSettings.php', 'OCA\\DAV\\Storage\\PublicOwnerWrapper' => $baseDir . '/../lib/Storage/PublicOwnerWrapper.php', + '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\\SystemTagPlugin' => $baseDir . '/../lib/SystemTag/SystemTagPlugin.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 302a424d08e..c29d93d72a8 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -319,6 +319,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Settings\\AvailabilitySettings' => __DIR__ . '/..' . '/../lib/Settings/AvailabilitySettings.php', 'OCA\\DAV\\Settings\\CalDAVSettings' => __DIR__ . '/..' . '/../lib/Settings/CalDAVSettings.php', 'OCA\\DAV\\Storage\\PublicOwnerWrapper' => __DIR__ . '/..' . '/../lib/Storage/PublicOwnerWrapper.php', + '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\\SystemTagPlugin' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagPlugin.php', diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 4be149ac440..909bcaa71e8 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -208,11 +208,7 @@ class Server { } // system tags plugins - $this->server->addPlugin(new SystemTagPlugin( - \OC::$server->getSystemTagManager(), - \OC::$server->getGroupManager(), - \OC::$server->getUserSession() - )); + $this->server->addPlugin(\OC::$server->get(SystemTagPlugin::class)); // comments plugin $this->server->addPlugin(new CommentsPlugin( diff --git a/apps/dav/lib/SystemTag/SystemTagList.php b/apps/dav/lib/SystemTag/SystemTagList.php new file mode 100644 index 00000000000..678c8042a39 --- /dev/null +++ b/apps/dav/lib/SystemTag/SystemTagList.php @@ -0,0 +1,73 @@ + + * + * @author Robin Appelman + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + */ +namespace OCA\DAV\SystemTag; + +use OCP\IUser; +use OCP\SystemTag\ISystemTag; +use OCP\SystemTag\ISystemTagManager; +use Sabre\Xml\Element; +use Sabre\Xml\Reader; +use Sabre\Xml\Writer; + +/** + * TagList property + * + * This property contains multiple "tag" elements, each containing a tag name. + */ +class SystemTagList implements Element { + public const NS_NEXTCLOUD = 'http://nextcloud.org/ns'; + + /** @var ISystemTag[] */ + private array $tags; + private ISystemTagManager $tagManager; + private IUser $user; + + public function __construct(array $tags, ISystemTagManager $tagManager, IUser $user) { + $this->tags = $tags; + $this->tagManager = $tagManager; + $this->user = $user; + } + + /** + * @return ISystemTag[] + */ + public function getTags(): array { + return $this->tags; + } + + public static function xmlDeserialize(Reader $reader): void { + // unsupported/unused + } + + public function xmlSerialize(Writer $writer): void { + foreach ($this->tags as $tag) { + $writer->startElement('{' . self::NS_NEXTCLOUD . '}system-tag'); + $writer->writeAttributes([ + SystemTagPlugin::CANASSIGN_PROPERTYNAME => $this->tagManager->canUserAssignTag($tag, $this->user) ? 'true' : 'false', + SystemTagPlugin::ID_PROPERTYNAME => $tag->getId(), + SystemTagPlugin::USERASSIGNABLE_PROPERTYNAME => $tag->isUserAssignable() ? 'true' : 'false', + SystemTagPlugin::USERVISIBLE_PROPERTYNAME => $tag->isUserVisible() ? 'true' : 'false', + ]); + $writer->write($tag->getName()); + $writer->endElement(); + } + } +} diff --git a/apps/dav/lib/SystemTag/SystemTagMappingNode.php b/apps/dav/lib/SystemTag/SystemTagMappingNode.php index 344ff1dbc70..9762b6e1db9 100644 --- a/apps/dav/lib/SystemTag/SystemTagMappingNode.php +++ b/apps/dav/lib/SystemTag/SystemTagMappingNode.php @@ -137,6 +137,8 @@ class SystemTagMappingNode implements \Sabre\DAV\INode { * @param string $name The new name * * @throws MethodNotAllowed not allowed to rename node + * + * @return never */ public function setName($name) { throw new MethodNotAllowed(); @@ -145,6 +147,7 @@ class SystemTagMappingNode implements \Sabre\DAV\INode { /** * Returns null, not supported * + * @return null */ public function getLastModified() { return null; @@ -152,6 +155,8 @@ class SystemTagMappingNode implements \Sabre\DAV\INode { /** * Delete tag to object association + * + * @return void */ public function delete() { try { diff --git a/apps/dav/lib/SystemTag/SystemTagNode.php b/apps/dav/lib/SystemTag/SystemTagNode.php index a31deb59a93..7310cdb19a2 100644 --- a/apps/dav/lib/SystemTag/SystemTagNode.php +++ b/apps/dav/lib/SystemTag/SystemTagNode.php @@ -103,6 +103,8 @@ class SystemTagNode implements \Sabre\DAV\INode { * @param string $name The new name * * @throws MethodNotAllowed not allowed to rename node + * + * @return never */ public function setName($name) { throw new MethodNotAllowed(); @@ -114,11 +116,12 @@ class SystemTagNode implements \Sabre\DAV\INode { * @param string $name new tag name * @param bool $userVisible user visible * @param bool $userAssignable user assignable + * * @throws NotFound whenever the given tag id does not exist * @throws Forbidden whenever there is no permission to update said tag * @throws Conflict whenever a tag already exists with the given attributes */ - public function update($name, $userVisible, $userAssignable) { + public function update($name, $userVisible, $userAssignable): void { try { if (!$this->tagManager->canUserSeeTag($this->tag, $this->user)) { throw new NotFound('Tag with id ' . $this->tag->getId() . ' does not exist'); @@ -151,11 +154,15 @@ class SystemTagNode implements \Sabre\DAV\INode { /** * Returns null, not supported * + * @return null */ public function getLastModified() { return null; } + /** + * @return void + */ public function delete() { try { if (!$this->isAdmin) { diff --git a/apps/dav/lib/SystemTag/SystemTagPlugin.php b/apps/dav/lib/SystemTag/SystemTagPlugin.php index c21935edfdc..c5c828cfbff 100644 --- a/apps/dav/lib/SystemTag/SystemTagPlugin.php +++ b/apps/dav/lib/SystemTag/SystemTagPlugin.php @@ -25,10 +25,14 @@ */ namespace OCA\DAV\SystemTag; +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\Node; use OCP\IGroupManager; +use OCP\IUser; use OCP\IUserSession; use OCP\SystemTag\ISystemTag; use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; use OCP\SystemTag\TagAlreadyExistsException; use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\Conflict; @@ -56,6 +60,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { public const USERASSIGNABLE_PROPERTYNAME = '{http://owncloud.org/ns}user-assignable'; public const GROUPS_PROPERTYNAME = '{http://owncloud.org/ns}groups'; public const CANASSIGN_PROPERTYNAME = '{http://owncloud.org/ns}can-assign'; + public const SYSTEM_TAGS_PROPERTYNAME = '{http://nextcloud.org/ns}system-tags'; /** * @var \Sabre\DAV\Server $server @@ -77,17 +82,23 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { */ protected $groupManager; - /** - * @param ISystemTagManager $tagManager tag manager - * @param IGroupManager $groupManager - * @param IUserSession $userSession - */ - public function __construct(ISystemTagManager $tagManager, - IGroupManager $groupManager, - IUserSession $userSession) { + /** @var array */ + private array $cachedTagMappings = []; + /** @var array */ + private array $cachedTags = []; + + private ISystemTagObjectMapper $tagMapper; + + public function __construct( + ISystemTagManager $tagManager, + IGroupManager $groupManager, + IUserSession $userSession, + ISystemTagObjectMapper $tagMapper, + ) { $this->tagManager = $tagManager; $this->userSession = $userSession; $this->groupManager = $groupManager; + $this->tagMapper = $tagMapper; } /** @@ -215,11 +226,18 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { * * @param PropFind $propFind * @param \Sabre\DAV\INode $node + * + * @return void */ public function handleGetProperties( PropFind $propFind, \Sabre\DAV\INode $node ) { + if ($node instanceof Node) { + $this->propfindForFile($propFind, $node); + return; + } + if (!($node instanceof SystemTagNode) && !($node instanceof SystemTagMappingNode)) { return; } @@ -260,6 +278,79 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { }); } + private function propfindForFile(PropFind $propFind, Node $node): void { + if ($node instanceof Directory + && $propFind->getDepth() !== 0 + && !is_null($propFind->getStatus(self::SYSTEM_TAGS_PROPERTYNAME))) { + $fileIds = [$node->getId()]; + + // note: pre-fetching only supported for depth <= 1 + $folderContent = $node->getNode()->getDirectoryListing(); + foreach ($folderContent as $info) { + $fileIds[] = $info->getId(); + } + + $tags = $this->tagMapper->getTagIdsForObjects($fileIds, 'files'); + + $this->cachedTagMappings = $this->cachedTagMappings + $tags; + $emptyFileIds = array_diff($fileIds, array_keys($tags)); + + // also cache the ones that were not found + foreach ($emptyFileIds as $fileId) { + $this->cachedTagMappings[$fileId] = []; + } + } + + $propFind->handle(self::SYSTEM_TAGS_PROPERTYNAME, function () use ($node) { + $user = $this->userSession->getUser(); + if ($user === null) { + return; + } + + $tags = $this->getTagsForFile($node->getId(), $user); + return new SystemTagList($tags, $this->tagManager, $user); + }); + } + + /** + * @param int $fileId + * @return ISystemTag[] + */ + private function getTagsForFile(int $fileId, IUser $user): array { + + if (isset($this->cachedTagMappings[$fileId])) { + $tagIds = $this->cachedTagMappings[$fileId]; + } else { + $tags = $this->tagMapper->getTagIdsForObjects([$fileId], 'files'); + $fileTags = current($tags); + if ($fileTags) { + $tagIds = $fileTags; + } else { + $tagIds = []; + } + } + + $tags = array_filter(array_map(function(string $tagId) { + return $this->cachedTags[$tagId] ?? null; + }, $tagIds)); + + $uncachedTagIds = array_filter($tagIds, function(string $tagId): bool { + return !isset($this->cachedTags[$tagId]); + }); + + if (count($uncachedTagIds)) { + $retrievedTags = $this->tagManager->getTagsByIds($uncachedTagIds); + foreach ($retrievedTags as $tag) { + $this->cachedTags[$tag->getId()] = $tag; + } + $tags += $retrievedTags; + } + + return array_filter($tags, function(ISystemTag $tag) use ($user) { + return $this->tagManager->canUserSeeTag($tag, $user); + }); + } + /** * Updates tag attributes * diff --git a/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php b/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php index 1256c58921e..86ccadf5f56 100644 --- a/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php +++ b/apps/dav/lib/SystemTag/SystemTagsByIdCollection.php @@ -84,7 +84,10 @@ class SystemTagsByIdCollection implements ICollection { /** * @param string $name * @param resource|string $data Initial payload + * * @throws Forbidden + * + * @return never */ public function createFile($name, $data = null) { throw new Forbidden('Cannot create tags by id'); @@ -92,6 +95,8 @@ class SystemTagsByIdCollection implements ICollection { /** * @param string $name + * + * @return never */ public function createDirectory($name) { throw new Forbidden('Permission denied to create collections'); @@ -99,6 +104,8 @@ class SystemTagsByIdCollection implements ICollection { /** * @param string $name + * + * @return SystemTagNode */ public function getChild($name) { try { @@ -115,6 +122,11 @@ class SystemTagsByIdCollection implements ICollection { } } + /** + * @return SystemTagNode[] + * + * @psalm-return array + */ public function getChildren() { $visibilityFilter = true; if ($this->isAdmin()) { @@ -145,14 +157,25 @@ class SystemTagsByIdCollection implements ICollection { } } + /** + * @return never + */ public function delete() { throw new Forbidden('Permission denied to delete this collection'); } + /** + * @return string + * + * @psalm-return 'systemtags' + */ public function getName() { return 'systemtags'; } + /** + * @return never + */ public function setName($name) { throw new Forbidden('Permission denied to rename this collection'); } @@ -160,7 +183,7 @@ class SystemTagsByIdCollection implements ICollection { /** * Returns the last modification time, as a unix timestamp * - * @return int + * @return null */ public function getLastModified() { return null; diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php b/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php index 8bb34182b0c..4d73c17d7dd 100644 --- a/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php +++ b/apps/dav/lib/SystemTag/SystemTagsObjectMappingCollection.php @@ -92,6 +92,9 @@ class SystemTagsObjectMappingCollection implements ICollection { $this->user = $user; } + /** + * @return void + */ public function createFile($name, $data = null) { $tagId = $name; try { @@ -110,10 +113,16 @@ class SystemTagsObjectMappingCollection implements ICollection { } } + /** + * @return never + */ public function createDirectory($name) { throw new Forbidden('Permission denied to create collections'); } + /** + * @return SystemTagMappingNode + */ public function getChild($tagName) { try { if ($this->tagMapper->haveTag([$this->objectId], $this->objectType, $tagName, true)) { @@ -131,6 +140,11 @@ class SystemTagsObjectMappingCollection implements ICollection { } } + /** + * @return SystemTagMappingNode[] + * + * @psalm-return list + */ public function getChildren() { $tagIds = current($this->tagMapper->getTagIdsForObjects([$this->objectId], $this->objectType)); if (empty($tagIds)) { @@ -168,6 +182,9 @@ class SystemTagsObjectMappingCollection implements ICollection { } } + /** + * @return never + */ public function delete() { throw new Forbidden('Permission denied to delete this collection'); } @@ -176,6 +193,9 @@ class SystemTagsObjectMappingCollection implements ICollection { return $this->objectId; } + /** + * @return never + */ public function setName($name) { throw new Forbidden('Permission denied to rename this collection'); } @@ -183,7 +203,7 @@ class SystemTagsObjectMappingCollection implements ICollection { /** * Returns the last modification time, as a unix timestamp * - * @return int + * @return null */ public function getLastModified() { return null; diff --git a/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php b/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php index 1ca45c32ce4..3fa40278cdb 100644 --- a/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php +++ b/apps/dav/lib/SystemTag/SystemTagsObjectTypeCollection.php @@ -98,7 +98,9 @@ class SystemTagsObjectTypeCollection implements ICollection { /** * @param string $name * @param resource|string $data Initial payload - * @return null|string + * + * @return never + * * @throws Forbidden */ public function createFile($name, $data = null) { @@ -107,7 +109,10 @@ class SystemTagsObjectTypeCollection implements ICollection { /** * @param string $name + * * @throws Forbidden + * + * @return never */ public function createDirectory($name) { throw new Forbidden('Permission denied to create collections'); @@ -133,6 +138,9 @@ class SystemTagsObjectTypeCollection implements ICollection { ); } + /** + * @return never + */ public function getChildren() { // do not list object ids throw new MethodNotAllowed(); @@ -148,6 +156,9 @@ class SystemTagsObjectTypeCollection implements ICollection { return call_user_func($this->childExistsFunction, $name); } + /** + * @return never + */ public function delete() { throw new Forbidden('Permission denied to delete this collection'); } @@ -158,7 +169,10 @@ class SystemTagsObjectTypeCollection implements ICollection { /** * @param string $name + * * @throws Forbidden + * + * @return never */ public function setName($name) { throw new Forbidden('Permission denied to rename this collection'); @@ -167,7 +181,7 @@ class SystemTagsObjectTypeCollection implements ICollection { /** * Returns the last modification time, as a unix timestamp * - * @return int + * @return null */ public function getLastModified() { return null; diff --git a/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php b/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php index 291aa45ad0e..8341c6ca009 100644 --- a/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php +++ b/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php @@ -36,6 +36,7 @@ use OCP\IUser; use OCP\IUserSession; use OCP\SystemTag\ISystemTag; use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; use OCP\SystemTag\TagAlreadyExistsException; use Sabre\DAV\Tree; use Sabre\HTTP\RequestInterface; @@ -84,6 +85,11 @@ class SystemTagPluginTest extends \Test\TestCase { */ private $plugin; + /** + * @var ISystemTagObjectMapper + */ + private $tagMapper; + protected function setUp(): void { parent::setUp(); $this->tree = $this->getMockBuilder(Tree::class) @@ -108,11 +114,14 @@ class SystemTagPluginTest extends \Test\TestCase { ->expects($this->any()) ->method('isLoggedIn') ->willReturn(true); + $this->tagMapper = $this->getMockBuilder(ISystemTagObjectMapper::class) + ->getMock(); $this->plugin = new \OCA\DAV\SystemTag\SystemTagPlugin( $this->tagManager, $this->groupManager, - $this->userSession + $this->userSession, + $this->tagMapper ); $this->plugin->initialize($this->server); } @@ -233,7 +242,7 @@ class SystemTagPluginTest extends \Test\TestCase { $this->assertEquals($expectedProperties, $result[200]); } - + public function testGetPropertiesForbidden(): void { $this->expectException(\Sabre\DAV\Exception\Forbidden::class); @@ -330,7 +339,7 @@ class SystemTagPluginTest extends \Test\TestCase { $this->assertEquals(200, $result[self::USERVISIBLE_PROPERTYNAME]); } - + public function testUpdatePropertiesForbidden(): void { $this->expectException(\Sabre\DAV\Exception\Forbidden::class); @@ -537,7 +546,7 @@ class SystemTagPluginTest extends \Test\TestCase { ->method('createTag') ->with('Test', $userVisible, $userAssignable) ->willReturn($systemTag); - + if (!empty($groups)) { $this->tagManager->expects($this->once()) ->method('setTagGroups') @@ -658,7 +667,7 @@ class SystemTagPluginTest extends \Test\TestCase { $this->plugin->httpPost($request, $response); } - + public function testCreateTagToUnknownNode(): void { $this->expectException(\Sabre\DAV\Exception\NotFound::class); diff --git a/lib/public/SystemTag/ISystemTagManager.php b/lib/public/SystemTag/ISystemTagManager.php index 9918e3a4f99..1cf7263b456 100644 --- a/lib/public/SystemTag/ISystemTagManager.php +++ b/lib/public/SystemTag/ISystemTagManager.php @@ -125,7 +125,7 @@ interface ISystemTagManager { * @param ISystemTag $tag tag to check permission for * @param IUser $user user to check permission for * - * @return true if the user is allowed to assign/unassign the tag, false otherwise + * @return bool true if the user is allowed to assign/unassign the tag, false otherwise * * @since 9.1.0 */ @@ -137,7 +137,7 @@ interface ISystemTagManager { * @param ISystemTag $tag tag to check permission for * @param IUser $user user to check permission for * - * @return true if the user can see the tag, false otherwise + * @return bool true if the user can see the tag, false otherwise * * @since 9.1.0 */