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

Implement systemtags through webdav #37609

Merged
merged 2 commits into from
Apr 28, 2023
Merged
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
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 1 addition & 5 deletions apps/dav/lib/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
73 changes: 73 additions & 0 deletions apps/dav/lib/SystemTag/SystemTagList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
/**
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
*
* @author Robin Appelman <robin@icewind.nl>
*
* @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 <http://www.gnu.org/licenses/>
*/
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',
Fixed Show fixed Hide fixed
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();
}
}
}
5 changes: 5 additions & 0 deletions apps/dav/lib/SystemTag/SystemTagMappingNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ public function getName() {
* @param string $name The new name
*
* @throws MethodNotAllowed not allowed to rename node
*
* @return never
*/
public function setName($name) {
throw new MethodNotAllowed();
Expand All @@ -145,13 +147,16 @@ public function setName($name) {
/**
* Returns null, not supported
*
* @return null
*/
public function getLastModified() {
return null;
}

/**
* Delete tag to object association
*
* @return void
*/
public function delete() {
try {
Expand Down
9 changes: 8 additions & 1 deletion apps/dav/lib/SystemTag/SystemTagNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ public function getSystemTag() {
* @param string $name The new name
*
* @throws MethodNotAllowed not allowed to rename node
*
* @return never
*/
public function setName($name) {
throw new MethodNotAllowed();
Expand All @@ -114,11 +116,12 @@ public function setName($name) {
* @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');
Expand Down Expand Up @@ -151,11 +154,15 @@ public function update($name, $userVisible, $userAssignable) {
/**
* Returns null, not supported
*
* @return null
*/
public function getLastModified() {
return null;
}

/**
* @return void
*/
public function delete() {
try {
if (!$this->isAdmin) {
Expand Down
107 changes: 99 additions & 8 deletions apps/dav/lib/SystemTag/SystemTagPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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<int, string[]> */
private array $cachedTagMappings = [];
/** @var array<string, ISystemTag> */
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;
}

/**
Expand Down Expand Up @@ -215,11 +226,18 @@ private function createTag($data, $contentType = 'application/json') {
*
* @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;
}
Expand Down Expand Up @@ -260,6 +278,79 @@ 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))) {
$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));
Fixed Show fixed Hide fixed

$uncachedTagIds = array_filter($tagIds, function(string $tagId): bool {
return !isset($this->cachedTags[$tagId]);
});
Fixed Show fixed Hide fixed

if (count($uncachedTagIds)) {
$retrievedTags = $this->tagManager->getTagsByIds($uncachedTagIds);
foreach ($retrievedTags as $tag) {
$this->cachedTags[$tag->getId()] = $tag;
Fixed Show fixed Hide fixed
}
$tags += $retrievedTags;
}

return array_filter($tags, function(ISystemTag $tag) use ($user) {
return $this->tagManager->canUserSeeTag($tag, $user);
Fixed Show fixed Hide fixed
});
}

/**
* Updates tag attributes
*
Expand Down
25 changes: 24 additions & 1 deletion apps/dav/lib/SystemTag/SystemTagsByIdCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,28 @@ private function isAdmin() {
/**
* @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');
}

/**
* @param string $name
*
* @return never
*/
public function createDirectory($name) {
throw new Forbidden('Permission denied to create collections');
}

/**
* @param string $name
*
* @return SystemTagNode
*/
public function getChild($name) {
try {
Expand All @@ -115,6 +122,11 @@ public function getChild($name) {
}
}

/**
* @return SystemTagNode[]
*
* @psalm-return array<SystemTagNode>
*/
public function getChildren() {
$visibilityFilter = true;
if ($this->isAdmin()) {
Expand Down Expand Up @@ -145,22 +157,33 @@ public function childExists($name) {
}
}

/**
* @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');
}

/**
* Returns the last modification time, as a unix timestamp
*
* @return int
* @return null
*/
public function getLastModified() {
return null;
Expand Down
Loading