diff options
-rw-r--r-- | db_structure.xml | 124 | ||||
-rw-r--r-- | lib/private/server.php | 29 | ||||
-rw-r--r-- | lib/private/systemtag/systemtag.php | 90 | ||||
-rw-r--r-- | lib/private/systemtag/systemtagmanager.php | 264 | ||||
-rw-r--r-- | lib/private/systemtag/systemtagobjectmapper.php | 213 | ||||
-rw-r--r-- | lib/public/iservercontainer.php | 18 | ||||
-rw-r--r-- | lib/public/systemtag/isystemtag.php | 2 | ||||
-rw-r--r-- | lib/public/systemtag/isystemtagmanager.php (renamed from lib/public/systemtag/isystemtagsmanager.php) | 10 | ||||
-rw-r--r-- | lib/public/systemtag/isystemtagobjectmapper.php | 10 | ||||
-rw-r--r-- | tests/lib/systemtag/systemtagmanager.php | 396 | ||||
-rw-r--r-- | tests/lib/systemtag/systemtagobjectmapper.php | 327 |
11 files changed, 1478 insertions, 5 deletions
diff --git a/db_structure.xml b/db_structure.xml index 8135746233e..637e95d6ef4 100644 --- a/db_structure.xml +++ b/db_structure.xml @@ -1064,6 +1064,130 @@ </table> <table> + <!-- + List of system-wide tags + --> + <name>*dbprefix*systemtag</name> + + <declaration> + + <field> + <name>id</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <autoincrement>1</autoincrement> + <unsigned>true</unsigned> + <length>4</length> + </field> + + <!-- Tag name --> + <field> + <name>name</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>64</length> + </field> + + <!-- Visibility: 0 user-not-visible, 1 user-visible --> + <field> + <name>visibility</name> + <type>integer</type> + <default>1</default> + <notnull>true</notnull> + <length>1</length> + </field> + + <!-- Editable: 0 user-not-editable, 1 user-editable --> + <field> + <name>editable</name> + <type>integer</type> + <default>1</default> + <notnull>true</notnull> + <length>1</length> + </field> + + <index> + <name>tag_ident</name> + <unique>true</unique> + <field> + <name>name</name> + <sorting>ascending</sorting> + </field> + <field> + <name>visibility</name> + <sorting>ascending</sorting> + </field> + <field> + <name>editable</name> + <sorting>ascending</sorting> + </field> + </index> + + </declaration> + </table> + + <table> + + <!-- + System tag to object associations per object type. + --> + <name>*dbprefix*systemtag_object_mapping</name> + + <declaration> + + <!-- object id (ex: file id for files)--> + <field> + <name>objectid</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <unsigned>true</unsigned> + <length>4</length> + </field> + + <!-- object type (ex: "files")--> + <field> + <name>objecttype</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>64</length> + </field> + + <!-- Foreign Key systemtag::id --> + <field> + <name>systemtagid</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <unsigned>true</unsigned> + <length>4</length> + </field> + + <index> + <unique>true</unique> + <name>mapping</name> + <field> + <name>objecttype</name> + <sorting>ascending</sorting> + </field> + <field> + <name>objectid</name> + <sorting>ascending</sorting> + </field> + <field> + <name>systemtagid</name> + <sorting>ascending</sorting> + </field> + </index> + + </declaration> + + </table> + + <table> <!-- Namespaced Key-Value Store for arbitrary data. diff --git a/lib/private/server.php b/lib/private/server.php index ea5937dfea4..8171a00b3d1 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -138,6 +138,12 @@ class Server extends SimpleContainer implements IServerContainer { $tagMapper = $c->query('TagMapper'); return new TagManager($tagMapper, $c->getUserSession()); }); + $this->registerService('SystemTagManager', function (Server $c) { + return new SystemTag\SystemTagManager($c->getDatabaseConnection()); + }); + $this->registerService('SystemTagObjectMapper', function (Server $c) { + return new SystemTag\SystemTagObjectMapper($c->getDatabaseConnection(), $c->getSystemTagManager()); + }); $this->registerService('RootFolder', function (Server $c) { // TODO: get user and user manager from container as well $user = \OC_User::getUser(); @@ -582,6 +588,29 @@ class Server extends SimpleContainer implements IServerContainer { } /** + * Returns the system-tag manager + * + * @return \OCP\SystemTag\ISystemTagManager + * + * @since 9.0.0 + */ + public function getSystemTagManager() { + return $this->query('SystemTagManager'); + } + + /** + * Returns the system-tag object mapper + * + * @return \OCP\SystemTag\ISystemTagObjectMapper + * + * @since 9.0.0 + */ + public function getSystemTagObjectMapper() { + return $this->query('SystemTagObjectMapper'); + } + + + /** * Returns the avatar manager, used for avatar functionality * * @return \OCP\IAvatarManager diff --git a/lib/private/systemtag/systemtag.php b/lib/private/systemtag/systemtag.php new file mode 100644 index 00000000000..b79cc33cfb2 --- /dev/null +++ b/lib/private/systemtag/systemtag.php @@ -0,0 +1,90 @@ +<?php +/** + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @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 OC\SystemTag; + +use \OCP\IDBConnection; + +class SystemTag implements \OCP\SystemTag\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($id, $name, $userVisible, $userAssignable) { + $this->id = $id; + $this->name = $name; + $this->userVisible = $userVisible; + $this->userAssignable = $userAssignable; + } + + /** + * {@inheritdoc} + */ + public function getId() { + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function getName() { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function isUserVisible() { + return $this->userVisible; + } + + /** + * {@inheritdoc} + */ + public function isUserAssignable() { + return $this->userAssignable; + } +} diff --git a/lib/private/systemtag/systemtagmanager.php b/lib/private/systemtag/systemtagmanager.php new file mode 100644 index 00000000000..e307825ecfa --- /dev/null +++ b/lib/private/systemtag/systemtagmanager.php @@ -0,0 +1,264 @@ +<?php +/** + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @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 OC\SystemTag; + +use \OCP\IDBConnection; +use \OCP\SystemTag\TagNotFoundException; +use \OCP\SystemTag\TagAlreadyExistsException; + +use \Doctrine\DBAL\Exception\UniqueConstraintViolationException; + +class SystemTagManager implements \OCP\SystemTag\ISystemTagManager { + + const TAG_TABLE = 'systemtag'; + + /** + * @var IDBConnection + */ + private $connection; + + /** + * Prepared query for selecting tags directly + * + * @var \OCP\DB\QueryBuilder\IQueryBuilder + */ + private $selectTagQuery; + + /** + * Constructor. + * + * @param IDBConnection $connection database connection + */ + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + + $query = $this->connection->getQueryBuilder(); + $this->selectTagQuery = $query->select('*') + ->from(self::TAG_TABLE) + ->where($query->expr()->eq('name', $query->createParameter('name'))) + ->andWhere($query->expr()->eq('visibility', $query->createParameter('visibility'))) + ->andWhere($query->expr()->eq('editable', $query->createParameter('editable'))); + } + + /** + * {@inheritdoc} + */ + public function getTagsById($tagIds) { + if (!is_array($tagIds)) { + $tagIds = [$tagIds]; + } + + $tags = []; + + // note: not all databases will fail if it's a string or starts with a number + foreach ($tagIds as $tagId) { + if (!is_numeric($tagId)) { + throw new \InvalidArgumentException('Tag id must be integer'); + } + } + + $query = $this->connection->getQueryBuilder(); + $query->select('*') + ->from(self::TAG_TABLE) + ->where($query->expr()->in('id', $query->createParameter('tagids'))) + ->addOrderBy('name', 'ASC') + ->addOrderBy('visibility', 'ASC') + ->addOrderBy('editable', 'ASC') + ->setParameter('tagids', $tagIds, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY); + + $result = $query->execute(); + while ($row = $result->fetch()) { + $tags[$row['id']] = $this->createSystemTagFromRow($row); + } + + $result->closeCursor(); + + if (count($tags) !== count($tagIds)) { + throw new TagNotFoundException( + 'Tag(s) with id(s) ' . json_encode(array_diff($tagIds, array_keys($tags))) . ' not found' + ); + } + + return $tags; + } + + /** + * {@inheritdoc} + */ + public function getAllTags($visibilityFilter = null, $nameSearchPattern = null) { + $tags = []; + + $query = $this->connection->getQueryBuilder(); + $query->select('*') + ->from(self::TAG_TABLE); + + if (!is_null($visibilityFilter)) { + $query->andWhere($query->expr()->eq('visibility', $query->createNamedParameter((int)$visibilityFilter))); + } + + if (!empty($nameSearchPattern)) { + $query->andWhere( + $query->expr()->like( + 'name', + $query->expr()->literal('%' . $this->connection->escapeLikeParameter($nameSearchPattern). '%') + ) + ); + } + + $query + ->addOrderBy('name', 'ASC') + ->addOrderBy('visibility', 'ASC') + ->addOrderBy('editable', 'ASC'); + + $result = $query->execute(); + while ($row = $result->fetch()) { + $tags[$row['id']] = $this->createSystemTagFromRow($row); + } + + $result->closeCursor(); + + return $tags; + } + + /** + * {@inheritdoc} + */ + public function getTag($tagName, $userVisible, $userAssignable) { + $userVisible = (int)$userVisible; + $userAssignable = (int)$userAssignable; + + $result = $this->selectTagQuery + ->setParameter('name', $tagName) + ->setParameter('visibility', $userVisible) + ->setParameter('editable', $userAssignable) + ->execute(); + + $row = $result->fetch(); + $result->closeCursor(); + if (!$row) { + throw new TagNotFoundException( + 'Tag ("' . $tagName . '", '. $userVisible . ', ' . $userAssignable . ') does not exist' + ); + } + + return $this->createSystemTagFromRow($row); + } + + /** + * {@inheritdoc} + */ + public function createTag($tagName, $userVisible, $userAssignable) { + $userVisible = (int)$userVisible; + $userAssignable = (int)$userAssignable; + + $query = $this->connection->getQueryBuilder(); + $query->insert(self::TAG_TABLE) + ->values([ + 'name' => $query->createNamedParameter($tagName), + 'visibility' => $query->createNamedParameter($userVisible), + 'editable' => $query->createNamedParameter($userAssignable), + ]); + + try { + $query->execute(); + } catch (UniqueConstraintViolationException $e) { + throw new TagAlreadyExistsException( + 'Tag ("' . $tagName . '", '. $userVisible . ', ' . $userAssignable . ') already exists', + 0, + $e + ); + } + + $tagId = $this->connection->lastInsertId('*PREFIX*' . self::TAG_TABLE); + + return new SystemTag( + (int)$tagId, + $tagName, + (bool)$userVisible, + (bool)$userAssignable + ); + } + + /** + * {@inheritdoc} + */ + public function updateTag($tagId, $tagName, $userVisible, $userAssignable) { + $userVisible = (int)$userVisible; + $userAssignable = (int)$userAssignable; + + $query = $this->connection->getQueryBuilder(); + $query->update(self::TAG_TABLE) + ->set('name', $query->createParameter('name')) + ->set('visibility', $query->createParameter('visibility')) + ->set('editable', $query->createParameter('editable')) + ->where($query->expr()->eq('id', $query->createParameter('tagid'))) + ->setParameter('name', $tagName) + ->setParameter('visibility', $userVisible) + ->setParameter('editable', $userAssignable) + ->setParameter('tagid', $tagId); + + try { + if ($query->execute() === 0) { + throw new TagNotFoundException( + 'Tag ("' . $tagName . '", '. $userVisible . ', ' . $userAssignable . ') does not exist' + ); + } + } catch (UniqueConstraintViolationException $e) { + throw new TagAlreadyExistsException( + 'Tag ("' . $tagName . '", '. $userVisible . ', ' . $userAssignable . ') already exists', + 0, + $e + ); + } + } + + /** + * {@inheritdoc} + */ + public function deleteTags($tagIds) { + if (!is_array($tagIds)) { + $tagIds = [$tagIds]; + } + + // delete relationships first + $query = $this->connection->getQueryBuilder(); + $query->delete(SystemTagObjectMapper::RELATION_TABLE) + ->where($query->expr()->in('systemtagid', $query->createParameter('tagids'))) + ->setParameter('tagids', $tagIds, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY) + ->execute(); + + $query = $this->connection->getQueryBuilder(); + $query->delete(self::TAG_TABLE) + ->where($query->expr()->in('id', $query->createParameter('tagids'))) + ->setParameter('tagids', $tagIds, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY); + + if ($query->execute() === 0) { + throw new TagNotFoundException( + 'Tag does not exist' + ); + } + } + + private function createSystemTagFromRow($row) { + return new SystemTag((int)$row['id'], $row['name'], (bool)$row['visibility'], (bool)$row['editable']); + } +} diff --git a/lib/private/systemtag/systemtagobjectmapper.php b/lib/private/systemtag/systemtagobjectmapper.php new file mode 100644 index 00000000000..f2adc2e3ebd --- /dev/null +++ b/lib/private/systemtag/systemtagobjectmapper.php @@ -0,0 +1,213 @@ +<?php +/** + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @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 OC\SystemTag; + +use \OCP\SystemTag\ISystemTagManager; +use \OCP\IDBConnection; +use \OCP\SystemTag\TagNotFoundException; +use \Doctrine\DBAL\Exception\UniqueConstraintViolationException; + +class SystemTagObjectMapper implements \OCP\SystemTag\ISystemTagObjectMapper { + + const RELATION_TABLE = 'systemtag_object_mapping'; + + /** + * @var ISystemTagManager + */ + private $tagManager; + + /** + * @var IDBConnection + */ + private $connection; + + /** + * Constructor. + * + * @param IDBConnection $connection database connection + * @param ISystemTagManager $tagManager system tag manager + */ + public function __construct(IDBConnection $connection, ISystemTagManager $tagManager) { + $this->connection = $connection; + $this->tagManager = $tagManager; + } + + /** + * {@inheritdoc} + */ + public function getTagIdsForObjects($objIds, $objectType) { + if (!is_array($objIds)) { + $objIds = [$objIds]; + } + + $query = $this->connection->getQueryBuilder(); + $query->select(['systemtagid', 'objectid']) + ->from(self::RELATION_TABLE) + ->where($query->expr()->in('objectid', $query->createParameter('objectids'))) + ->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype'))) + ->setParameter('objectids', $objIds, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY) + ->setParameter('objecttype', $objectType) + ->addOrderBy('objectid', 'ASC') + ->addOrderBy('systemtagid', 'ASC'); + + $mapping = []; + foreach ($objIds as $objId) { + $mapping[$objId] = []; + } + + $result = $query->execute(); + while ($row = $result->fetch()) { + $objectId = $row['objectid']; + $mapping[$objectId][] = $row['systemtagid']; + } + + $result->closeCursor(); + + return $mapping; + } + + /** + * {@inheritdoc} + */ + public function getObjectIdsForTags($tagIds, $objectType) { + if (!is_array($tagIds)) { + $tagIds = [$tagIds]; + } + + $this->assertTagsExist($tagIds); + + $query = $this->connection->getQueryBuilder(); + $query->select($query->createFunction('DISTINCT(objectid)')) + ->from(self::RELATION_TABLE) + ->where($query->expr()->in('systemtagid', $query->createParameter('tagids'))) + ->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype'))) + ->setParameter('tagids', $tagIds, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY) + ->setParameter('objecttype', $objectType); + + $objectIds = []; + + $result = $query->execute(); + while ($row = $result->fetch()) { + $objectIds[] = $row['objectid']; + } + + return $objectIds; + } + + /** + * {@inheritdoc} + */ + public function assignTags($objId, $objectType, $tagIds) { + if (!is_array($tagIds)) { + $tagIds = [$tagIds]; + } + + $this->assertTagsExist($tagIds); + + $query = $this->connection->getQueryBuilder(); + $query->insert(self::RELATION_TABLE) + ->values([ + 'objectid' => $query->createNamedParameter($objId), + 'objecttype' => $query->createNamedParameter($objectType), + 'systemtagid' => $query->createParameter('tagid'), + ]); + + foreach ($tagIds as $tagId) { + try { + $query->setParameter('tagid', $tagId); + $query->execute(); + } catch (UniqueConstraintViolationException $e) { + // ignore existing relations + } + } + } + + /** + * {@inheritdoc} + */ + public function unassignTags($objId, $objectType, $tagIds) { + if (!is_array($tagIds)) { + $tagIds = [$tagIds]; + } + + $this->assertTagsExist($tagIds); + + $query = $this->connection->getQueryBuilder(); + $query->delete(self::RELATION_TABLE) + ->where($query->expr()->eq('objectid', $query->createParameter('objectid'))) + ->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype'))) + ->andWhere($query->expr()->in('systemtagid', $query->createParameter('tagids'))) + ->setParameter('objectid', $objId) + ->setParameter('objecttype', $objectType) + ->setParameter('tagids', $tagIds, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function haveTag($objIds, $objectType, $tagId, $all = true) { + $this->assertTagsExist([$tagId]); + + $query = $this->connection->getQueryBuilder(); + $query->select($query->createFunction('COUNT(1)')) + ->from(self::RELATION_TABLE) + ->where($query->expr()->in('objectid', $query->createParameter('objectids'))) + ->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype'))) + ->andWhere($query->expr()->eq('systemtagid', $query->createParameter('tagid'))) + ->setParameter('objectids', $objIds, \Doctrine\DBAL\Connection::PARAM_INT_ARRAY) + ->setParameter('tagid', $tagId) + ->setParameter('objecttype', $objectType) + ->setMaxResults(1); + + $result = $query->execute(); + $row = $result->fetch(\PDO::FETCH_NUM); + $result->closeCursor(); + if ($all) { + return ((int)$row[0] === count($objIds)); + } else { + return (int)$row[0] > 0; + } + } + + /** + * Asserts that all the given tag ids exist. + * + * @param string[] $tagIds tag ids to check + * + * @throws \OCP\SystemTag\TagNotFoundException if at least one tag did not exist + */ + private function assertTagsExist($tagIds) { + $tags = $this->tagManager->getTagsById($tagIds); + if (count($tags) !== count($tagIds)) { + // at least one tag missing, bail out + $foundTagIds = array_map( + function($tag) { + return $tag->getId(); + }, + $tags + ); + $missingTagIds = array_diff($tagIds, $foundTagIds); + throw new TagNotFoundException('Tags ' . json_encode($missingTagIds) . ' do not exist'); + } + } +} diff --git a/lib/public/iservercontainer.php b/lib/public/iservercontainer.php index d85f812b2e7..7cb2672254b 100644 --- a/lib/public/iservercontainer.php +++ b/lib/public/iservercontainer.php @@ -470,4 +470,22 @@ interface IServerContainer { * @since 8.2.0 */ public function getNotificationManager(); + + /** + * Returns the system-tag manager + * + * @return \OCP\SystemTag\ISystemTagManager + * + * @since 9.0.0 + */ + public function getSystemTagManager(); + + /** + * Returns the system-tag object mapper + * + * @return \OCP\SystemTag\ISystemTagObjectMapper + * + * @since 9.0.0 + */ + public function getSystemTagObjectMapper(); } diff --git a/lib/public/systemtag/isystemtag.php b/lib/public/systemtag/isystemtag.php index 76a812f38dc..26609fd8af7 100644 --- a/lib/public/systemtag/isystemtag.php +++ b/lib/public/systemtag/isystemtag.php @@ -62,7 +62,7 @@ interface ISystemTag { * * @since 9.0.0 */ - public function isUserAsssignable(); + public function isUserAssignable(); } diff --git a/lib/public/systemtag/isystemtagsmanager.php b/lib/public/systemtag/isystemtagmanager.php index df59cc48d52..2020ec52900 100644 --- a/lib/public/systemtag/isystemtagsmanager.php +++ b/lib/public/systemtag/isystemtagmanager.php @@ -31,9 +31,11 @@ interface ISystemTagManager { /** * Returns the tag objects matching the given tag ids. * - * @param array|string $tagIds The ID or array of IDs of the tags to retrieve + * @param array|string $tagIds id or array of unique ids of the tag to retrieve * - * @return \OCP\SystemTag\ISystemTag[] array of system tags or empty array if none found + * @return \OCP\SystemTag\ISystemTag[] array of system tags with tag id as key + * + * @throws \OCP\SystemTag\TagNotFoundException if at least one given tag id did no exist * * @since 9.0.0 */ @@ -72,14 +74,14 @@ interface ISystemTagManager { /** * Returns all known tags, optionally filtered by visibility. * - * @param bool $visibleOnly whether to only return user visible tags + * @param bool|null $visibilityFilter filter by visibility if non-null * @param string $nameSearchPattern optional search pattern for the tag name * * @return \OCP\SystemTag\ISystemTag[] array of system tags or empty array if none found * * @since 9.0.0 */ - public function getAllTags($visibleOnly = false, $nameSearchPattern = null); + public function getAllTags($visibilityFilter = null, $nameSearchPattern = null); /** * Updates the given tag diff --git a/lib/public/systemtag/isystemtagobjectmapper.php b/lib/public/systemtag/isystemtagobjectmapper.php index 8c6c27c4846..e2ac1fab124 100644 --- a/lib/public/systemtag/isystemtagobjectmapper.php +++ b/lib/public/systemtag/isystemtagobjectmapper.php @@ -69,6 +69,11 @@ interface ISystemTagObjectMapper { /** * Assign the given tags to the given object. * + * If at least one of the given tag ids doesn't exist, none of the tags + * will be assigned. + * + * If the relationship already existed, fail silently. + * * @param string $objId object id * @param string $objectType object type * @param string|array $tagIds tag id or array of tag ids to assign @@ -83,6 +88,11 @@ interface ISystemTagObjectMapper { /** * Unassign the given tags from the given object. * + * If at least one of the given tag ids doesn't exist, none of the tags + * will be unassigned. + * + * If the relationship did not exist in the first place, fail silently. + * * @param string $objId object id * @param string $objectType object type * @param string|array $tagIds tag id or array of tag ids to unassign diff --git a/tests/lib/systemtag/systemtagmanager.php b/tests/lib/systemtag/systemtagmanager.php new file mode 100644 index 00000000000..4a19d85b0c4 --- /dev/null +++ b/tests/lib/systemtag/systemtagmanager.php @@ -0,0 +1,396 @@ +<?php + +/** + * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + * +*/ + +namespace Test\SystemTag; + +use \OCP\SystemTag\ISystemTagManager; +use \OCP\SystemTag\TagNotFoundException; +use \OCP\SystemTag\TagAlreadyExistsException; +use \OCP\IDBConnection; + +class TestSystemTagManager extends \Test\TestCase { + + /** + * @var ISystemTagManager + **/ + private $tagManager; + + /** + * @var IDBConnection + */ + private $connection; + + public function setUp() { + parent::setUp(); + + $this->connection = \OC::$server->getDatabaseConnection(); + $this->tagManager = new \OC\SystemTag\SystemTagManager($this->connection); + } + + public function tearDown() { + $query = $this->connection->getQueryBuilder(); + $query->delete(\OC\SystemTag\SystemTagObjectMapper::RELATION_TABLE)->execute(); + $query->delete(\OC\SystemTag\SystemTagManager::TAG_TABLE)->execute(); + } + + public function getAllTagsDataProvider() { + return [ + [ + // no tags at all + [] + ], + [ + // simple + [ + ['one', false, false], + ['two', false, false], + ] + ], + [ + // duplicate names, different flags + [ + ['one', false, false], + ['one', true, false], + ['one', false, true], + ['one', true, true], + ['two', false, false], + ['two', false, true], + ] + ] + ]; + } + + /** + * @dataProvider getAllTagsDataProvider + */ + public function testGetAllTags($testTags) { + $testTagsById = []; + foreach ($testTags as $testTag) { + $tag = $this->tagManager->createTag($testTag[0], $testTag[1], $testTag[2]); + $testTagsById[$tag->getId()] = $tag; + } + + $tagList = $this->tagManager->getAllTags(); + + $this->assertCount(count($testTags), $tagList); + + foreach ($testTagsById as $testTagId => $testTag) { + $this->assertTrue(isset($tagList[$testTagId])); + $this->assertSameTag($tagList[$testTagId], $testTag); + } + } + + public function getAllTagsFilteredDataProvider() { + return [ + [ + [ + // no tags at all + ], + null, + null, + [] + ], + // filter by visibile only + [ + // none visible + [ + ['one', false, false], + ['two', false, false], + ], + true, + null, + [] + ], + [ + // one visible + [ + ['one', true, false], + ['two', false, false], + ], + true, + null, + [ + ['one', true, false], + ] + ], + [ + // one invisible + [ + ['one', true, false], + ['two', false, false], + ], + false, + null, + [ + ['two', false, false], + ] + ], + // filter by name pattern + [ + [ + ['one', true, false], + ['one', false, false], + ['two', true, false], + ], + null, + 'on', + [ + ['one', true, false], + ['one', false, false], + ] + ], + // filter by name pattern and visibility + [ + // one visible + [ + ['one', true, false], + ['two', true, false], + ['one', false, false], + ], + true, + 'on', + [ + ['one', true, false], + ] + ], + // filter by name pattern in the middle + [ + // one visible + [ + ['abcdefghi', true, false], + ['two', true, false], + ], + null, + 'def', + [ + ['abcdefghi', true, false], + ] + ] + ]; + } + + /** + * @dataProvider getAllTagsFilteredDataProvider + */ + public function testGetAllTagsFiltered($testTags, $visibilityFilter, $nameSearch, $expectedResults) { + foreach ($testTags as $testTag) { + $tag = $this->tagManager->createTag($testTag[0], $testTag[1], $testTag[2]); + } + + $testTagsById = []; + foreach ($expectedResults as $expectedTag) { + $tag = $this->tagManager->getTag($expectedTag[0], $expectedTag[1], $expectedTag[2]); + $testTagsById[$tag->getId()] = $tag; + } + + $tagList = $this->tagManager->getAllTags($visibilityFilter, $nameSearch); + + $this->assertCount(count($testTagsById), $tagList); + + foreach ($testTagsById as $testTagId => $testTag) { + $this->assertTrue(isset($tagList[$testTagId])); + $this->assertSameTag($tagList[$testTagId], $testTag); + } + } + + public function oneTagMultipleFlagsProvider() { + return [ + ['one', false, false], + ['one', true, false], + ['one', false, true], + ['one', true, true], + ]; + } + + /** + * @dataProvider oneTagMultipleFlagsProvider + * @expectedException \OCP\SystemTag\TagAlreadyExistsException + */ + public function testCreateDuplicate($name, $userVisible, $userAssignable) { + try { + $this->tagManager->createTag($name, $userVisible, $userAssignable); + } catch (\Exception $e) { + $this->assertTrue(false, 'No exception thrown for the first create call'); + } + $this->tagManager->createTag($name, $userVisible, $userAssignable); + } + + /** + * @dataProvider oneTagMultipleFlagsProvider + */ + public function testGetExistingTag($name, $userVisible, $userAssignable) { + $tag1 = $this->tagManager->createTag($name, $userVisible, $userAssignable); + $tag2 = $this->tagManager->getTag($name, $userVisible, $userAssignable); + + $this->assertSameTag($tag1, $tag2); + } + + /** + * @dataProvider oneTagMultipleFlagsProvider + */ + public function testGetExistingTagById($name, $userVisible, $userAssignable) { + $tag1 = $this->tagManager->createTag('one', true, false); + $tag2 = $this->tagManager->createTag('two', false, true); + + $tagList = $this->tagManager->getTagsById([$tag1->getId(), $tag2->getId()]); + + $this->assertCount(2, $tagList); + + $this->assertSameTag($tag1, $tagList[$tag1->getId()]); + $this->assertSameTag($tag2, $tagList[$tag2->getId()]); + } + + /** + * @expectedException \OCP\SystemTag\TagNotFoundException + */ + public function testGetNonExistingTag() { + $this->tagManager->getTag('nonexist', false, false); + } + + /** + * @expectedException \OCP\SystemTag\TagNotFoundException + */ + public function testGetNonExistingTagsById() { + $tag1 = $this->tagManager->createTag('one', true, false); + $this->tagManager->getTagsById([$tag1->getId(), 100, 101]); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testGetInvalidTagIdFormat() { + $tag1 = $this->tagManager->createTag('one', true, false); + $this->tagManager->getTagsById([$tag1->getId() . 'suffix']); + } + + public function updateTagProvider() { + return [ + [ + // update name + ['one', true, true], + ['two', true, true] + ], + [ + // update one flag + ['one', false, true], + ['one', true, true] + ], + [ + // update all flags + ['one', false, false], + ['one', true, true] + ], + [ + // update all + ['one', false, false], + ['two', true, true] + ], + ]; + } + + /** + * @dataProvider updateTagProvider + */ + public function testUpdateTag($tagCreate, $tagUpdated) { + $tag1 = $this->tagManager->createTag( + $tagCreate[0], + $tagCreate[1], + $tagCreate[2] + ); + $this->tagManager->updateTag( + $tag1->getId(), + $tagUpdated[0], + $tagUpdated[1], + $tagUpdated[2] + ); + $tag2 = $this->tagManager->getTag( + $tagUpdated[0], + $tagUpdated[1], + $tagUpdated[2] + ); + + $this->assertEquals($tag2->getId(), $tag1->getId()); + $this->assertEquals($tag2->getName(), $tagUpdated[0]); + $this->assertEquals($tag2->isUserVisible(), $tagUpdated[1]); + $this->assertEquals($tag2->isUserAssignable(), $tagUpdated[2]); + } + + /** + * @dataProvider updateTagProvider + * @expectedException \OCP\SystemTag\TagAlreadyExistsException + */ + public function testUpdateTagDuplicate($tagCreate, $tagUpdated) { + $this->tagManager->createTag( + $tagCreate[0], + $tagCreate[1], + $tagCreate[2] + ); + $tag2 = $this->tagManager->createTag( + $tagUpdated[0], + $tagUpdated[1], + $tagUpdated[2] + ); + + // update to match the first tag + $this->tagManager->updateTag( + $tag2->getId(), + $tagCreate[0], + $tagCreate[1], + $tagCreate[2] + ); + } + + public function testDeleteTags() { + $tag1 = $this->tagManager->createTag('one', true, false); + $tag2 = $this->tagManager->createTag('two', false, true); + + $this->tagManager->deleteTags([$tag1->getId(), $tag2->getId()]); + + $this->assertEmpty($this->tagManager->getAllTags()); + } + + /** + * @expectedException \OCP\SystemTag\TagNotFoundException + */ + public function testDeleteNonExistingTag() { + $this->tagManager->deleteTags([100]); + } + + public function testDeleteTagRemovesRelations() { + $tag1 = $this->tagManager->createTag('one', true, false); + $tag2 = $this->tagManager->createTag('two', true, true); + + $tagMapper = new \OC\SystemTag\SystemTagObjectMapper($this->connection, $this->tagManager); + + $tagMapper->assignTags(1, 'testtype', $tag1->getId()); + $tagMapper->assignTags(1, 'testtype', $tag2->getId()); + $tagMapper->assignTags(2, 'testtype', $tag1->getId()); + + $this->tagManager->deleteTags($tag1->getId()); + + $tagIdMapping = $tagMapper->getTagIdsForObjects( + [1, 2], + 'testtype' + ); + + $this->assertEquals([ + 1 => [$tag2->getId()], + 2 => [], + ], $tagIdMapping); + } + + private function assertSameTag($tag1, $tag2) { + $this->assertEquals($tag1->getId(), $tag2->getId()); + $this->assertEquals($tag1->getName(), $tag2->getName()); + $this->assertEquals($tag1->isUserVisible(), $tag2->isUserVisible()); + $this->assertEquals($tag1->isUserAssignable(), $tag2->isUserAssignable()); + } + +} diff --git a/tests/lib/systemtag/systemtagobjectmapper.php b/tests/lib/systemtag/systemtagobjectmapper.php new file mode 100644 index 00000000000..107496f9a15 --- /dev/null +++ b/tests/lib/systemtag/systemtagobjectmapper.php @@ -0,0 +1,327 @@ +<?php + +/** + * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + * +*/ + +namespace Test\SystemTag; + +use \OCP\SystemTag\ISystemTag; +use \OCP\SystemTag\ISystemTagManager; +use \OCP\SystemTag\ISystemTagObjectMapper; +use \OCP\SystemTag\TagNotFoundException; +use \OCP\SystemTag\TagAlreadyExistsException; +use \OCP\IDBConnection; +use \OC\SystemTag\SystemTag; + +class TestSystemTagObjectMapper extends \Test\TestCase { + + /** + * @var ISystemTagManager + **/ + private $tagManager; + + /** + * @var ISystemTagObjectMapper + **/ + private $tagMapper; + + /** + * @var IDBConnection + */ + private $connection; + + /** + * @var ISystemTag + */ + private $tag1; + + /** + * @var ISystemTag + */ + private $tag2; + + /** + * @var ISystemTag + */ + private $tag3; + + public function setUp() { + parent::setUp(); + + $this->connection = \OC::$server->getDatabaseConnection(); + + $this->tagManager = $this->getMockBuilder('OCP\SystemTag\ISystemTagManager') + ->getMock(); + + $this->tagMapper = new \OC\SystemTag\SystemTagObjectMapper($this->connection, $this->tagManager); + + $this->tag1 = new SystemTag(1, 'testtag1', false, false); + $this->tag2 = new SystemTag(2, 'testtag2', true, false); + $this->tag3 = new SystemTag(3, 'testtag3', false, false); + + $this->tagManager->expects($this->any()) + ->method('getTagsById') + ->will($this->returnCallback(function($tagIds) { + $result = []; + if (in_array(1, $tagIds)) { + $result[1] = $this->tag1; + } + if (in_array(2, $tagIds)) { + $result[2] = $this->tag2; + } + if (in_array(3, $tagIds)) { + $result[3] = $this->tag3; + } + return $result; + })); + + $this->tagMapper->assignTags(1, 'testtype', $this->tag1->getId()); + $this->tagMapper->assignTags(1, 'testtype', $this->tag2->getId()); + $this->tagMapper->assignTags(2, 'testtype', $this->tag1->getId()); + $this->tagMapper->assignTags(3, 'anothertype', $this->tag1->getId()); + } + + public function tearDown() { + $query = $this->connection->getQueryBuilder(); + $query->delete(\OC\SystemTag\SystemTagObjectMapper::RELATION_TABLE)->execute(); + $query->delete(\OC\SystemTag\SystemTagManager::TAG_TABLE)->execute(); + } + + public function testGetTagsForObjects() { + $tagIdMapping = $this->tagMapper->getTagIdsForObjects( + [1, 2, 3, 4], + 'testtype' + ); + + $this->assertEquals([ + 1 => [$this->tag1->getId(), $this->tag2->getId()], + 2 => [$this->tag1->getId()], + 3 => [], + 4 => [], + ], $tagIdMapping); + } + + public function testGetObjectsForTags() { + $objectIds = $this->tagMapper->getObjectIdsForTags( + [$this->tag1->getId(), $this->tag2->getId(), $this->tag3->getId()], + 'testtype' + ); + + $this->assertEquals([ + 1, + 2, + ], $objectIds); + } + + /** + * @expectedException \OCP\SystemTag\TagNotFoundException + */ + public function testGetObjectsForNonExistingTag() { + $this->tagMapper->getObjectIdsForTags( + [100], + 'testtype' + ); + } + + public function testAssignUnassignTags() { + $this->tagMapper->unassignTags(1, 'testtype', [$this->tag1->getId()]); + + $tagIdMapping = $this->tagMapper->getTagIdsForObjects(1, 'testtype'); + $this->assertEquals([ + 1 => [$this->tag2->getId()], + ], $tagIdMapping); + + $this->tagMapper->assignTags(1, 'testtype', [$this->tag1->getId()]); + $this->tagMapper->assignTags(1, 'testtype', $this->tag3->getId()); + + $tagIdMapping = $this->tagMapper->getTagIdsForObjects(1, 'testtype'); + + $this->assertEquals([ + 1 => [$this->tag1->getId(), $this->tag2->getId(), $this->tag3->getId()], + ], $tagIdMapping); + } + + public function testReAssignUnassignTags() { + // reassign tag1 + $this->tagMapper->assignTags(1, 'testtype', [$this->tag1->getId()]); + + // tag 3 was never assigned + $this->tagMapper->unassignTags(1, 'testtype', [$this->tag3->getId()]); + + $this->assertTrue(true, 'No error when reassigning/unassigning'); + } + + /** + * @expectedException \OCP\SystemTag\TagNotFoundException + */ + public function testAssignNonExistingTags() { + $this->tagMapper->assignTags(1, 'testtype', [100]); + } + + public function testAssignNonExistingTagInArray() { + $caught = false; + try { + $this->tagMapper->assignTags(1, 'testtype', [100, $this->tag3->getId()]); + } catch (TagNotFoundException $e) { + $caught = true; + } + + $this->assertTrue($caught, 'Exception thrown'); + + $tagIdMapping = $this->tagMapper->getTagIdsForObjects( + [1], + 'testtype' + ); + + $this->assertEquals([ + 1 => [$this->tag1->getId(), $this->tag2->getId()], + ], $tagIdMapping, 'None of the tags got assigned'); + } + + /** + * @expectedException \OCP\SystemTag\TagNotFoundException + */ + public function testUnassignNonExistingTags() { + $this->tagMapper->unassignTags(1, 'testtype', [100]); + } + + public function testUnassignNonExistingTagsInArray() { + $caught = false; + try { + $this->tagMapper->unassignTags(1, 'testtype', [100, $this->tag1->getId()]); + } catch (TagNotFoundException $e) { + $caught = true; + } + + $this->assertTrue($caught, 'Exception thrown'); + + $tagIdMapping = $this->tagMapper->getTagIdsForObjects( + [1], + 'testtype' + ); + + $this->assertEquals([ + 1 => [$this->tag1->getId(), $this->tag2->getId()], + ], $tagIdMapping, 'None of the tags got unassigned'); + } + + public function testHaveTagAllMatches() { + $this->assertTrue( + $this->tagMapper->haveTag( + [1], + 'testtype', + $this->tag1->getId(), + true + ), + 'object 1 has the tag tag1' + ); + + $this->assertTrue( + $this->tagMapper->haveTag( + [1, 2], + 'testtype', + $this->tag1->getId(), + true + ), + 'object 1 and object 2 ALL have the tag tag1' + ); + + $this->assertFalse( + $this->tagMapper->haveTag( + [1, 2], + 'testtype', + $this->tag2->getId(), + true + ), + 'object 1 has tag2 but not object 2, so not ALL of them' + ); + + $this->assertFalse( + $this->tagMapper->haveTag( + [2], + 'testtype', + $this->tag2->getId(), + true + ), + 'object 2 does not have tag2' + ); + + $this->assertFalse( + $this->tagMapper->haveTag( + [3], + 'testtype', + $this->tag2->getId(), + true + ), + 'object 3 does not have tag1 due to different type' + ); + } + + public function testHaveTagAtLeastOneMatch() { + $this->assertTrue( + $this->tagMapper->haveTag( + [1], + 'testtype', + $this->tag1->getId(), + false + ), + 'object1 has the tag tag1' + ); + + $this->assertTrue( + $this->tagMapper->haveTag( + [1, 2], + 'testtype', + $this->tag1->getId(), + false + ), + 'object 1 and object 2 both the tag tag1' + ); + + $this->assertTrue( + $this->tagMapper->haveTag( + [1, 2], + 'testtype', + $this->tag2->getId(), + false + ), + 'at least object 1 has the tag tag2' + ); + + $this->assertFalse( + $this->tagMapper->haveTag( + [2], + 'testtype', + $this->tag2->getId(), + false + ), + 'object 2 does not have tag2' + ); + + $this->assertFalse( + $this->tagMapper->haveTag( + [3], + 'testtype', + $this->tag2->getId(), + false + ), + 'object 3 does not have tag1 due to different type' + ); + } + + /** + * @expectedException \OCP\SystemTag\TagNotFoundException + */ + public function testHaveTagNonExisting() { + $this->tagMapper->haveTag( + [1], + 'testtype', + 100 + ); + } +} |