diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 6745ffe41b451..db7de8c9ac6d3 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -304,6 +304,7 @@ '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 302a424d08ea6..c29d93d72a833 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 4be149ac44034..909bcaa71e8f9 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -208,11 +208,7 @@ public function __construct(IRequest $request, string $baseUri) { } // 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 0000000000000..67d33b9701ead --- /dev/null +++ b/apps/dav/lib/SystemTag/SystemTagList.php @@ -0,0 +1,73 @@ + + * @author Thomas Müller + * @author Vincent Petry + * + * @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; + } + + public function getTags() { + 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/SystemTagPlugin.php b/apps/dav/lib/SystemTag/SystemTagPlugin.php index c21935edfdc3a..5fe1c01357103 100644 --- a/apps/dav/lib/SystemTag/SystemTagPlugin.php +++ b/apps/dav/lib/SystemTag/SystemTagPlugin.php @@ -25,10 +25,13 @@ */ namespace OCA\DAV\SystemTag; +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\Node; use OCP\IGroupManager; 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 +59,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 +81,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; } /** @@ -220,6 +230,11 @@ 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 +275,68 @@ public function handleGetProperties( }); } + private function propfindForFile(PropFind $propFind, Node $node): void { + if ($node instanceof Directory + && $propFind->getDepth() !== 0 + && !is_null($propFind->getStatus(self::SYSTEM_TAGS_PROPERTYNAME))) { + // note: pre-fetching only supported for depth <= 1 + $folderContent = $node->getNode()->getDirectoryListing(); + $fileIds[] = (int)$node->getId(); + foreach ($folderContent as $info) { + $fileIds[] = (int)$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) { + $tags = $this->getTagsForFile($node->getId()); + return new SystemTagList($tags, $this->tagManager, $this->userSession->getUser()); + }); + } + + /** + * @param int $fileId + * @return ISystemTag[] + */ + private function getTagsForFile(int $fileId): array { + $user = $this->userSession->getUser(); + 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($tagId) { + return $this->cachedTags[$tagId] ?? null; + }, $tagIds)); + + $uncachedTagIds = array_filter($tagIds, function($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/tests/unit/SystemTag/SystemTagPluginTest.php b/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php index 291aa45ad0ef7..199bf28fb7da2 100644 --- a/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php +++ b/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php @@ -36,6 +36,7 @@ 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,8 @@ class SystemTagPluginTest extends \Test\TestCase { */ private $plugin; + private ISystemTagObjectMapper $tagMapper; + protected function setUp(): void { parent::setUp(); $this->tree = $this->getMockBuilder(Tree::class) @@ -108,11 +111,13 @@ protected function setUp(): void { ->expects($this->any()) ->method('isLoggedIn') ->willReturn(true); + $this->tagMapper = $this->getMockBuilder(ISystemTagObjectMapper::class); $this->plugin = new \OCA\DAV\SystemTag\SystemTagPlugin( $this->tagManager, $this->groupManager, - $this->userSession + $this->userSession, + $this->tagMapper ); $this->plugin->initialize($this->server); } @@ -233,7 +238,7 @@ public function testGetProperties(ISystemTag $systemTag, $groups, $requestedProp $this->assertEquals($expectedProperties, $result[200]); } - + public function testGetPropertiesForbidden(): void { $this->expectException(\Sabre\DAV\Exception\Forbidden::class); @@ -330,7 +335,7 @@ public function testUpdatePropertiesAdmin(): void { $this->assertEquals(200, $result[self::USERVISIBLE_PROPERTYNAME]); } - + public function testUpdatePropertiesForbidden(): void { $this->expectException(\Sabre\DAV\Exception\Forbidden::class); @@ -537,7 +542,7 @@ public function testCreateTagInByIdCollection($userVisible, $userAssignable, $gr ->method('createTag') ->with('Test', $userVisible, $userAssignable) ->willReturn($systemTag); - + if (!empty($groups)) { $this->tagManager->expects($this->once()) ->method('setTagGroups') @@ -658,7 +663,7 @@ public function testCreateTagInMappingCollection(): void { $this->plugin->httpPost($request, $response); } - + public function testCreateTagToUnknownNode(): void { $this->expectException(\Sabre\DAV\Exception\NotFound::class);