diff options
Diffstat (limited to 'lib/private/SystemTag')
-rw-r--r-- | lib/private/SystemTag/ManagerFactory.php | 57 | ||||
-rw-r--r-- | lib/private/SystemTag/SystemTag.php | 103 | ||||
-rw-r--r-- | lib/private/SystemTag/SystemTagManager.php | 222 | ||||
-rw-r--r-- | lib/private/SystemTag/SystemTagObjectMapper.php | 209 | ||||
-rw-r--r-- | lib/private/SystemTag/SystemTagsInFilesDetector.php | 55 |
5 files changed, 370 insertions, 276 deletions
diff --git a/lib/private/SystemTag/ManagerFactory.php b/lib/private/SystemTag/ManagerFactory.php index ca0508fe19b..4d3a1396529 100644 --- a/lib/private/SystemTag/ManagerFactory.php +++ b/lib/private/SystemTag/ManagerFactory.php @@ -1,32 +1,19 @@ <?php declare(strict_types=1); - /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\SystemTag; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAppConfig; +use OCP\IDBConnection; +use OCP\IGroupManager; use OCP\IServerContainer; +use OCP\IUserSession; use OCP\SystemTag\ISystemTagManager; use OCP\SystemTag\ISystemTagManagerFactory; use OCP\SystemTag\ISystemTagObjectMapper; @@ -39,32 +26,25 @@ use OCP\SystemTag\ISystemTagObjectMapper; */ class ManagerFactory implements ISystemTagManagerFactory { /** - * Server container - * - * @var IServerContainer - */ - private $serverContainer; - - /** * Constructor for the system tag manager factory - * - * @param IServerContainer $serverContainer server container */ - public function __construct(IServerContainer $serverContainer) { - $this->serverContainer = $serverContainer; + public function __construct( + private IServerContainer $serverContainer, + ) { } /** * Creates and returns an instance of the system tag manager * - * @return ISystemTagManager * @since 9.0.0 */ public function getManager(): ISystemTagManager { return new SystemTagManager( - $this->serverContainer->getDatabaseConnection(), - $this->serverContainer->getGroupManager(), - $this->serverContainer->getEventDispatcher() + $this->serverContainer->get(IDBConnection::class), + $this->serverContainer->get(IGroupManager::class), + $this->serverContainer->get(IEventDispatcher::class), + $this->serverContainer->get(IUserSession::class), + $this->serverContainer->get(IAppConfig::class), ); } @@ -72,14 +52,13 @@ class ManagerFactory implements ISystemTagManagerFactory { * Creates and returns an instance of the system tag object * mapper * - * @return ISystemTagObjectMapper * @since 9.0.0 */ public function getObjectMapper(): ISystemTagObjectMapper { return new SystemTagObjectMapper( - $this->serverContainer->getDatabaseConnection(), + $this->serverContainer->get(IDBConnection::class), $this->getManager(), - $this->serverContainer->getEventDispatcher() + $this->serverContainer->get(IEventDispatcher::class), ); } } diff --git a/lib/private/SystemTag/SystemTag.php b/lib/private/SystemTag/SystemTag.php index da6d4bd4b11..1a573dabeaa 100644 --- a/lib/private/SystemTag/SystemTag.php +++ b/lib/private/SystemTag/SystemTag.php @@ -1,110 +1,59 @@ <?php declare(strict_types=1); - /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Johannes Leuker <j.leuker@hosting.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\SystemTag; use OCP\SystemTag\ISystemTag; class SystemTag implements ISystemTag { - /** - * @var string - */ - private $id; - - /** - * @var string - */ - private $name; - - /** - * @var bool - */ - private $userVisible; - - /** - * @var bool - */ - private $userAssignable; - - /** - * Constructor. - * - * @param string $id tag id - * @param string $name tag name - * @param bool $userVisible whether the tag is user visible - * @param bool $userAssignable whether the tag is user assignable - */ - public function __construct(string $id, string $name, bool $userVisible, bool $userAssignable) { - $this->id = $id; - $this->name = $name; - $this->userVisible = $userVisible; - $this->userAssignable = $userAssignable; + public function __construct( + private string $id, + private string $name, + private bool $userVisible, + private bool $userAssignable, + private ?string $etag = null, + private ?string $color = null, + ) { } - /** - * {@inheritdoc} - */ public function getId(): string { return $this->id; } - /** - * {@inheritdoc} - */ public function getName(): string { return $this->name; } - /** - * {@inheritdoc} - */ public function isUserVisible(): bool { return $this->userVisible; } - /** - * {@inheritdoc} - */ public function isUserAssignable(): bool { return $this->userAssignable; } - /** - * {@inheritdoc} - */ public function getAccessLevel(): int { - if ($this->userVisible) { - if ($this->userAssignable) { - return self::ACCESS_LEVEL_PUBLIC; - } else { - return self::ACCESS_LEVEL_RESTRICTED; - } - } else { + if (!$this->userVisible) { return self::ACCESS_LEVEL_INVISIBLE; } + + if (!$this->userAssignable) { + return self::ACCESS_LEVEL_RESTRICTED; + } + + return self::ACCESS_LEVEL_PUBLIC; + } + + public function getETag(): ?string { + return $this->etag; + } + + public function getColor(): ?string { + return $this->color; } } diff --git a/lib/private/SystemTag/SystemTagManager.php b/lib/private/SystemTag/SystemTagManager.php index 4524aeaf7bc..4b421fa033a 100644 --- a/lib/private/SystemTag/SystemTagManager.php +++ b/lib/private/SystemTag/SystemTagManager.php @@ -1,45 +1,28 @@ <?php declare(strict_types=1); - /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\SystemTag; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAppConfig; use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IUser; +use OCP\IUserSession; use OCP\SystemTag\ISystemTag; use OCP\SystemTag\ISystemTagManager; use OCP\SystemTag\ManagerEvent; use OCP\SystemTag\TagAlreadyExistsException; +use OCP\SystemTag\TagCreationForbiddenException; use OCP\SystemTag\TagNotFoundException; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use OCP\SystemTag\TagUpdateForbiddenException; /** * Manager class for system tags @@ -48,38 +31,18 @@ class SystemTagManager implements ISystemTagManager { public const TAG_TABLE = 'systemtag'; public const TAG_GROUP_TABLE = 'systemtag_group'; - /** @var IDBConnection */ - protected $connection; - - /** @var EventDispatcherInterface */ - protected $dispatcher; - - /** @var IGroupManager */ - protected $groupManager; - /** * Prepared query for selecting tags directly - * - * @var \OCP\DB\QueryBuilder\IQueryBuilder */ - private $selectTagQuery; + private IQueryBuilder $selectTagQuery; - /** - * Constructor. - * - * @param IDBConnection $connection database connection - * @param IGroupManager $groupManager - * @param EventDispatcherInterface $dispatcher - */ public function __construct( - IDBConnection $connection, - IGroupManager $groupManager, - EventDispatcherInterface $dispatcher + protected IDBConnection $connection, + protected IGroupManager $groupManager, + protected IEventDispatcher $dispatcher, + private IUserSession $userSession, + private IAppConfig $appConfig, ) { - $this->connection = $connection; - $this->groupManager = $groupManager; - $this->dispatcher = $dispatcher; - $query = $this->connection->getQueryBuilder(); $this->selectTagQuery = $query->select('*') ->from(self::TAG_TABLE) @@ -88,10 +51,7 @@ class SystemTagManager implements ISystemTagManager { ->andWhere($query->expr()->eq('editable', $query->createParameter('editable'))); } - /** - * {@inheritdoc} - */ - public function getTagsByIds($tagIds): array { + public function getTagsByIds($tagIds, ?IUser $user = null): array { if (!\is_array($tagIds)) { $tagIds = [$tagIds]; } @@ -116,7 +76,12 @@ class SystemTagManager implements ISystemTagManager { $result = $query->execute(); while ($row = $result->fetch()) { - $tags[$row['id']] = $this->createSystemTagFromRow($row); + $tag = $this->createSystemTagFromRow($row); + if ($user && !$this->canUserSeeTag($tag, $user)) { + // if a user is given, hide invisible tags + continue; + } + $tags[$row['id']] = $tag; } $result->closeCursor(); @@ -130,9 +95,6 @@ class SystemTagManager implements ISystemTagManager { return $tags; } - /** - * {@inheritdoc} - */ public function getAllTags($visibilityFilter = null, $nameSearchPattern = null): array { $tags = []; @@ -146,9 +108,9 @@ class SystemTagManager implements ISystemTagManager { if (!empty($nameSearchPattern)) { $query->andWhere( - $query->expr()->like( + $query->expr()->iLike( 'name', - $query->createNamedParameter('%' . $this->connection->escapeLikeParameter($nameSearchPattern). '%') + $query->createNamedParameter('%' . $this->connection->escapeLikeParameter($nameSearchPattern) . '%') ) ); } @@ -158,7 +120,7 @@ class SystemTagManager implements ISystemTagManager { ->addOrderBy('visibility', 'ASC') ->addOrderBy('editable', 'ASC'); - $result = $query->execute(); + $result = $query->executeQuery(); while ($row = $result->fetch()) { $tags[$row['id']] = $this->createSystemTagFromRow($row); } @@ -168,12 +130,11 @@ class SystemTagManager implements ISystemTagManager { return $tags; } - /** - * {@inheritdoc} - */ public function getTag(string $tagName, bool $userVisible, bool $userAssignable): ISystemTag { + // Length of name column is 64 + $truncatedTagName = substr($tagName, 0, 64); $result = $this->selectTagQuery - ->setParameter('name', $tagName) + ->setParameter('name', $truncatedTagName) ->setParameter('visibility', $userVisible ? 1 : 0) ->setParameter('editable', $userAssignable ? 1 : 0) ->execute(); @@ -182,30 +143,43 @@ class SystemTagManager implements ISystemTagManager { $result->closeCursor(); if (!$row) { throw new TagNotFoundException( - 'Tag ("' . $tagName . '", '. $userVisible . ', ' . $userAssignable . ') does not exist' + 'Tag ("' . $truncatedTagName . '", ' . $userVisible . ', ' . $userAssignable . ') does not exist' ); } return $this->createSystemTagFromRow($row); } - /** - * {@inheritdoc} - */ public function createTag(string $tagName, bool $userVisible, bool $userAssignable): ISystemTag { + $user = $this->userSession->getUser(); + if (!$this->canUserCreateTag($user)) { + throw new TagCreationForbiddenException(); + } + + // Check if tag already exists (case-insensitive) + $existingTags = $this->getAllTags(null, $tagName); + foreach ($existingTags as $existingTag) { + if (mb_strtolower($existingTag->getName()) === mb_strtolower($tagName)) { + throw new TagAlreadyExistsException('Tag ' . $tagName . ' already exists'); + } + } + + // Length of name column is 64 + $truncatedTagName = substr($tagName, 0, 64); $query = $this->connection->getQueryBuilder(); $query->insert(self::TAG_TABLE) ->values([ - 'name' => $query->createNamedParameter($tagName), + 'name' => $query->createNamedParameter($truncatedTagName), 'visibility' => $query->createNamedParameter($userVisible ? 1 : 0), 'editable' => $query->createNamedParameter($userAssignable ? 1 : 0), + 'etag' => $query->createNamedParameter(md5((string)time())), ]); try { $query->execute(); } catch (UniqueConstraintViolationException $e) { throw new TagAlreadyExistsException( - 'Tag ("' . $tagName . '", '. $userVisible . ', ' . $userAssignable . ') already exists', + 'Tag ("' . $truncatedTagName . '", ' . $userVisible . ', ' . $userAssignable . ') already exists', 0, $e ); @@ -215,7 +189,7 @@ class SystemTagManager implements ISystemTagManager { $tag = new SystemTag( (string)$tagId, - $tagName, + $truncatedTagName, $userVisible, $userAssignable ); @@ -227,10 +201,13 @@ class SystemTagManager implements ISystemTagManager { return $tag; } - /** - * {@inheritdoc} - */ - public function updateTag(string $tagId, string $newName, bool $userVisible, bool $userAssignable) { + public function updateTag( + string $tagId, + string $newName, + bool $userVisible, + bool $userAssignable, + ?string $color, + ): void { try { $tags = $this->getTagsByIds($tagId); } catch (TagNotFoundException $e) { @@ -239,24 +216,45 @@ class SystemTagManager implements ISystemTagManager { ); } + $user = $this->userSession->getUser(); + if (!$this->canUserUpdateTag($user)) { + throw new TagUpdateForbiddenException(); + } + $beforeUpdate = array_shift($tags); + // Length of name column is 64 + $newName = trim($newName); + $truncatedNewName = substr($newName, 0, 64); $afterUpdate = new SystemTag( $tagId, - $newName, + $truncatedNewName, $userVisible, - $userAssignable + $userAssignable, + $beforeUpdate->getETag(), + $color ); + // Check if tag already exists (case-insensitive) + $existingTags = $this->getAllTags(null, $truncatedNewName); + foreach ($existingTags as $existingTag) { + if (mb_strtolower($existingTag->getName()) === mb_strtolower($truncatedNewName) + && $existingTag->getId() !== $tagId) { + throw new TagAlreadyExistsException('Tag ' . $truncatedNewName . ' already exists'); + } + } + $query = $this->connection->getQueryBuilder(); $query->update(self::TAG_TABLE) ->set('name', $query->createParameter('name')) ->set('visibility', $query->createParameter('visibility')) ->set('editable', $query->createParameter('editable')) + ->set('color', $query->createParameter('color')) ->where($query->expr()->eq('id', $query->createParameter('tagid'))) - ->setParameter('name', $newName) + ->setParameter('name', $truncatedNewName) ->setParameter('visibility', $userVisible ? 1 : 0) ->setParameter('editable', $userAssignable ? 1 : 0) - ->setParameter('tagid', $tagId); + ->setParameter('tagid', $tagId) + ->setParameter('color', $color); try { if ($query->execute() === 0) { @@ -266,7 +264,7 @@ class SystemTagManager implements ISystemTagManager { } } catch (UniqueConstraintViolationException $e) { throw new TagAlreadyExistsException( - 'Tag ("' . $newName . '", '. $userVisible . ', ' . $userAssignable . ') already exists', + 'Tag ("' . $newName . '", ' . $userVisible . ', ' . $userAssignable . ') already exists', 0, $e ); @@ -277,10 +275,7 @@ class SystemTagManager implements ISystemTagManager { )); } - /** - * {@inheritdoc} - */ - public function deleteTags($tagIds) { + public function deleteTags($tagIds): void { if (!\is_array($tagIds)) { $tagIds = [$tagIds]; } @@ -329,10 +324,11 @@ class SystemTagManager implements ISystemTagManager { } } - /** - * {@inheritdoc} - */ - public function canUserAssignTag(ISystemTag $tag, IUser $user): bool { + public function canUserAssignTag(ISystemTag $tag, ?IUser $user): bool { + if ($user === null) { + return false; + } + // early check to avoid unneeded group lookups if ($tag->isUserAssignable() && $tag->isUserVisible()) { return true; @@ -357,14 +353,39 @@ class SystemTagManager implements ISystemTagManager { return false; } - /** - * {@inheritdoc} - */ - public function canUserSeeTag(ISystemTag $tag, IUser $user): bool { + public function canUserCreateTag(?IUser $user): bool { + if ($user === null) { + // If no user given, allows only calls from CLI + return \OC::$CLI; + } + + if ($this->appConfig->getValueBool('systemtags', 'restrict_creation_to_admin', false) === false) { + return true; + } + + return $this->groupManager->isAdmin($user->getUID()); + } + + public function canUserUpdateTag(?IUser $user): bool { + // We currently have no different permissions for updating tags than for creating them + return $this->canUserCreateTag($user); + } + + public function canUserSeeTag(ISystemTag $tag, ?IUser $user): bool { + // If no user, then we only show public tags + if (!$user && $tag->getAccessLevel() === ISystemTag::ACCESS_LEVEL_PUBLIC) { + return true; + } + if ($tag->isUserVisible()) { return true; } + // if not returned yet, and user is not logged in, then the tag is not visible + if ($user === null) { + return false; + } + if ($this->groupManager->isAdmin($user->getUID())) { return true; } @@ -372,14 +393,11 @@ class SystemTagManager implements ISystemTagManager { return false; } - private function createSystemTagFromRow($row) { - return new SystemTag((string)$row['id'], $row['name'], (bool)$row['visibility'], (bool)$row['editable']); + private function createSystemTagFromRow($row): SystemTag { + return new SystemTag((string)$row['id'], $row['name'], (bool)$row['visibility'], (bool)$row['editable'], $row['etag'], $row['color']); } - /** - * {@inheritdoc} - */ - public function setTagGroups(ISystemTag $tag, array $groupIds) { + public function setTagGroups(ISystemTag $tag, array $groupIds): void { // delete relationships first $this->connection->beginTransaction(); try { @@ -410,9 +428,6 @@ class SystemTagManager implements ISystemTagManager { } } - /** - * {@inheritdoc} - */ public function getTagGroups(ISystemTag $tag): array { $groupIds = []; $query = $this->connection->getQueryBuilder(); @@ -430,4 +445,5 @@ class SystemTagManager implements ISystemTagManager { return $groupIds; } + } diff --git a/lib/private/SystemTag/SystemTagObjectMapper.php b/lib/private/SystemTag/SystemTagObjectMapper.php index 5a09a1754f2..1fa5975dafb 100644 --- a/lib/private/SystemTag/SystemTagObjectMapper.php +++ b/lib/private/SystemTag/SystemTagObjectMapper.php @@ -3,63 +3,30 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\SystemTag; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IDBConnection; use OCP\SystemTag\ISystemTag; use OCP\SystemTag\ISystemTagManager; use OCP\SystemTag\ISystemTagObjectMapper; use OCP\SystemTag\MapperEvent; use OCP\SystemTag\TagNotFoundException; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; class SystemTagObjectMapper implements ISystemTagObjectMapper { public const RELATION_TABLE = 'systemtag_object_mapping'; - /** @var ISystemTagManager */ - protected $tagManager; - - /** @var IDBConnection */ - protected $connection; - - /** @var EventDispatcherInterface */ - protected $dispatcher; - - /** - * Constructor. - * - * @param IDBConnection $connection database connection - * @param ISystemTagManager $tagManager system tag manager - * @param EventDispatcherInterface $dispatcher - */ - public function __construct(IDBConnection $connection, ISystemTagManager $tagManager, EventDispatcherInterface $dispatcher) { - $this->connection = $connection; - $this->tagManager = $tagManager; - $this->dispatcher = $dispatcher; + public function __construct( + protected IDBConnection $connection, + protected ISystemTagManager $tagManager, + protected IEventDispatcher $dispatcher, + ) { } /** @@ -77,24 +44,25 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { ->from(self::RELATION_TABLE) ->where($query->expr()->in('objectid', $query->createParameter('objectids'))) ->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype'))) - ->setParameter('objectids', $objIds, IQueryBuilder::PARAM_STR_ARRAY) ->setParameter('objecttype', $objectType) ->addOrderBy('objectid', 'ASC') ->addOrderBy('systemtagid', 'ASC'); - + $chunks = array_chunk($objIds, 900, false); $mapping = []; foreach ($objIds as $objId) { $mapping[$objId] = []; } + foreach ($chunks as $chunk) { + $query->setParameter('objectids', $chunk, IQueryBuilder::PARAM_STR_ARRAY); + $result = $query->executeQuery(); + while ($row = $result->fetch()) { + $objectId = $row['objectid']; + $mapping[$objectId][] = $row['systemtagid']; + } - $result = $query->execute(); - while ($row = $result->fetch()) { - $objectId = $row['objectid']; - $mapping[$objectId][] = $row['systemtagid']; + $result->closeCursor(); } - $result->closeCursor(); - return $mapping; } @@ -129,10 +97,11 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { $objectIds = []; - $result = $query->execute(); + $result = $query->executeQuery(); while ($row = $result->fetch()) { $objectIds[] = $row['objectid']; } + $result->closeCursor(); return $objectIds; } @@ -140,12 +109,33 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { /** * {@inheritdoc} */ - public function assignTags(string $objId, string $objectType, $tagIds) { + public function assignTags(string $objId, string $objectType, $tagIds): void { if (!\is_array($tagIds)) { $tagIds = [$tagIds]; } $this->assertTagsExist($tagIds); + $this->connection->beginTransaction(); + + $query = $this->connection->getQueryBuilder(); + $query->select('systemtagid') + ->from(self::RELATION_TABLE) + ->where($query->expr()->in('systemtagid', $query->createNamedParameter($tagIds, IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($query->expr()->eq('objecttype', $query->createNamedParameter($objectType))) + ->andWhere($query->expr()->eq('objectid', $query->createNamedParameter($objId))); + $result = $query->executeQuery(); + $rows = $result->fetchAll(); + $existingTags = []; + foreach ($rows as $row) { + $existingTags[] = $row['systemtagid']; + } + //filter only tags that do not exist in db + $tagIds = array_diff($tagIds, $existingTags); + if (empty($tagIds)) { + // no tags to insert so return here + $this->connection->commit(); + return; + } $query = $this->connection->getQueryBuilder(); $query->insert(self::RELATION_TABLE) @@ -166,6 +156,9 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { } } + $this->updateEtagForTags($tagIds); + + $this->connection->commit(); if (empty($tagsAssigned)) { return; } @@ -181,7 +174,7 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { /** * {@inheritdoc} */ - public function unassignTags(string $objId, string $objectType, $tagIds) { + public function unassignTags(string $objId, string $objectType, $tagIds): void { if (!\is_array($tagIds)) { $tagIds = [$tagIds]; } @@ -196,7 +189,9 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { ->setParameter('objectid', $objId) ->setParameter('objecttype', $objectType) ->setParameter('tagids', $tagIds, IQueryBuilder::PARAM_INT_ARRAY) - ->execute(); + ->executeStatement(); + + $this->updateEtagForTags($tagIds); $this->dispatcher->dispatch(MapperEvent::EVENT_UNASSIGN, new MapperEvent( MapperEvent::EVENT_UNASSIGN, @@ -207,6 +202,21 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { } /** + * Update the etag for the given tags. + * + * @param string[] $tagIds + */ + private function updateEtagForTags(array $tagIds): void { + // Update etag after assigning tags + $md5 = md5(json_encode(time())); + $query = $this->connection->getQueryBuilder(); + $query->update('systemtag') + ->set('etag', $query->createNamedParameter($md5)) + ->where($query->expr()->in('id', $query->createNamedParameter($tagIds, IQueryBuilder::PARAM_INT_ARRAY))); + $query->execute(); + } + + /** * {@inheritdoc} */ public function haveTag($objIds, string $objectType, string $tagId, bool $all = true): bool { @@ -235,7 +245,7 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { ->setParameter('tagid', $tagId) ->setParameter('objecttype', $objectType); - $result = $query->execute(); + $result = $query->executeQuery(); $row = $result->fetch(\PDO::FETCH_NUM); $result->closeCursor(); @@ -243,7 +253,7 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { return ((int)$row[0] === \count($objIds)); } - return (bool) $row; + return (bool)$row; } /** @@ -253,7 +263,7 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { * * @throws \OCP\SystemTag\TagNotFoundException if at least one tag did not exist */ - private function assertTagsExist($tagIds) { + private function assertTagsExist(array $tagIds): void { $tags = $this->tagManager->getTagsByIds($tagIds); if (\count($tags) !== \count($tagIds)) { // at least one tag missing, bail out @@ -269,4 +279,89 @@ class SystemTagObjectMapper implements ISystemTagObjectMapper { ); } } + + /** + * {@inheritdoc} + */ + public function setObjectIdsForTag(string $tagId, string $objectType, array $objectIds): void { + $currentObjectIds = $this->getObjectIdsForTags($tagId, $objectType); + $removedObjectIds = array_diff($currentObjectIds, $objectIds); + $addedObjectIds = array_diff($objectIds, $currentObjectIds); + + $this->connection->beginTransaction(); + $query = $this->connection->getQueryBuilder(); + $query->delete(self::RELATION_TABLE) + ->where($query->expr()->eq('systemtagid', $query->createNamedParameter($tagId, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('objecttype', $query->createNamedParameter($objectType))) + ->executeStatement(); + $this->connection->commit(); + + foreach ($removedObjectIds as $objectId) { + $this->dispatcher->dispatch(MapperEvent::EVENT_UNASSIGN, new MapperEvent( + MapperEvent::EVENT_UNASSIGN, + $objectType, + (string)$objectId, + [(int)$tagId] + )); + } + + if (empty($objectIds)) { + return; + } + + $this->connection->beginTransaction(); + $query = $this->connection->getQueryBuilder(); + $query->insert(self::RELATION_TABLE) + ->values([ + 'systemtagid' => $query->createNamedParameter($tagId, IQueryBuilder::PARAM_INT), + 'objecttype' => $query->createNamedParameter($objectType), + 'objectid' => $query->createParameter('objectid'), + ]); + + foreach (array_unique($objectIds) as $objectId) { + $query->setParameter('objectid', (string)$objectId); + $query->executeStatement(); + } + + $this->updateEtagForTags([$tagId]); + $this->connection->commit(); + + // Dispatch assign events for new object ids + foreach ($addedObjectIds as $objectId) { + $this->dispatcher->dispatch(MapperEvent::EVENT_ASSIGN, new MapperEvent( + MapperEvent::EVENT_ASSIGN, + $objectType, + (string)$objectId, + [(int)$tagId] + )); + } + + // Dispatch unassign events for removed object ids + foreach ($removedObjectIds as $objectId) { + $this->dispatcher->dispatch(MapperEvent::EVENT_UNASSIGN, new MapperEvent( + MapperEvent::EVENT_UNASSIGN, + $objectType, + (string)$objectId, + [(int)$tagId] + )); + } + } + + /** + * {@inheritdoc} + */ + public function getAvailableObjectTypes(): array { + $query = $this->connection->getQueryBuilder(); + $query->selectDistinct('objecttype') + ->from(self::RELATION_TABLE); + + $result = $query->executeQuery(); + $objectTypes = []; + while ($row = $result->fetch()) { + $objectTypes[] = $row['objecttype']; + } + $result->closeCursor(); + + return $objectTypes; + } } diff --git a/lib/private/SystemTag/SystemTagsInFilesDetector.php b/lib/private/SystemTag/SystemTagsInFilesDetector.php new file mode 100644 index 00000000000..9268b7ab098 --- /dev/null +++ b/lib/private/SystemTag/SystemTagsInFilesDetector.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\SystemTag; + +use OC\Files\Cache\QuerySearchHelper; +use OC\Files\Node\Root; +use OC\Files\Search\SearchBinaryOperator; +use OC\Files\Search\SearchComparison; +use OC\Files\Search\SearchQuery; +use OCP\Files\Folder; +use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; + +class SystemTagsInFilesDetector { + public function __construct( + protected QuerySearchHelper $searchHelper, + ) { + } + + public function detectAssignedSystemTagsIn( + Folder $folder, + string $filteredMediaType = '', + int $limit = 0, + int $offset = 0, + ): array { + $operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'systemtag', '%'); + // Currently query has to have exactly one search condition. If no media type is provided, + // we fall back to the presence of a system tag. + if ($filteredMediaType !== '') { + $mimeOperator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $filteredMediaType . '/%'); + $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$operator, $mimeOperator]); + } + + $query = new SearchQuery($operator, $limit, $offset, []); + [$caches, ] = $this->searchHelper->getCachesAndMountPointsForSearch( + $this->getRootFolder($folder), + $folder->getPath(), + ); + return $this->searchHelper->findUsedTagsInCaches($query, $caches); + } + + protected function getRootFolder(?Folder $folder): Root { + if ($folder instanceof Root) { + return $folder; + } elseif ($folder === null) { + throw new \LogicException('Could not climb up to root folder'); + } + return $this->getRootFolder($folder->getParent()); + } +} |