1<?php
2
3declare(strict_types=1);
4
5/**
6 * @copyright Copyright (c) 2016, ownCloud, Inc.
7 *
8 * @author Christoph Wurst <christoph@winzerhof-wurst.at>
9 * @author Joas Schilling <coding@schilljs.com>
10 * @author Roeland Jago Douma <roeland@famdouma.nl>
11 * @author Vincent Petry <vincent@nextcloud.com>
12 *
13 * @license AGPL-3.0
14 *
15 * This code is free software: you can redistribute it and/or modify
16 * it under the terms of the GNU Affero General Public License, version 3,
17 * as published by the Free Software Foundation.
18 *
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU Affero General Public License for more details.
23 *
24 * You should have received a copy of the GNU Affero General Public License, version 3,
25 * along with this program. If not, see <http://www.gnu.org/licenses/>
26 *
27 */
28namespace OC\SystemTag;
29
30use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
31use OCP\DB\QueryBuilder\IQueryBuilder;
32use OCP\IDBConnection;
33use OCP\SystemTag\ISystemTag;
34use OCP\SystemTag\ISystemTagManager;
35use OCP\SystemTag\ISystemTagObjectMapper;
36use OCP\SystemTag\MapperEvent;
37use OCP\SystemTag\TagNotFoundException;
38use Symfony\Component\EventDispatcher\EventDispatcherInterface;
39
40class SystemTagObjectMapper implements ISystemTagObjectMapper {
41	public const RELATION_TABLE = 'systemtag_object_mapping';
42
43	/** @var ISystemTagManager */
44	protected $tagManager;
45
46	/** @var IDBConnection */
47	protected $connection;
48
49	/** @var EventDispatcherInterface */
50	protected $dispatcher;
51
52	/**
53	 * Constructor.
54	 *
55	 * @param IDBConnection $connection database connection
56	 * @param ISystemTagManager $tagManager system tag manager
57	 * @param EventDispatcherInterface $dispatcher
58	 */
59	public function __construct(IDBConnection $connection, ISystemTagManager $tagManager, EventDispatcherInterface $dispatcher) {
60		$this->connection = $connection;
61		$this->tagManager = $tagManager;
62		$this->dispatcher = $dispatcher;
63	}
64
65	/**
66	 * {@inheritdoc}
67	 */
68	public function getTagIdsForObjects($objIds, string $objectType): array {
69		if (!\is_array($objIds)) {
70			$objIds = [$objIds];
71		} elseif (empty($objIds)) {
72			return [];
73		}
74
75		$query = $this->connection->getQueryBuilder();
76		$query->select(['systemtagid', 'objectid'])
77			->from(self::RELATION_TABLE)
78			->where($query->expr()->in('objectid', $query->createParameter('objectids')))
79			->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype')))
80			->setParameter('objectids', $objIds, IQueryBuilder::PARAM_STR_ARRAY)
81			->setParameter('objecttype', $objectType)
82			->addOrderBy('objectid', 'ASC')
83			->addOrderBy('systemtagid', 'ASC');
84
85		$mapping = [];
86		foreach ($objIds as $objId) {
87			$mapping[$objId] = [];
88		}
89
90		$result = $query->execute();
91		while ($row = $result->fetch()) {
92			$objectId = $row['objectid'];
93			$mapping[$objectId][] = $row['systemtagid'];
94		}
95
96		$result->closeCursor();
97
98		return $mapping;
99	}
100
101	/**
102	 * {@inheritdoc}
103	 */
104	public function getObjectIdsForTags($tagIds, string $objectType, int $limit = 0, string $offset = ''): array {
105		if (!\is_array($tagIds)) {
106			$tagIds = [$tagIds];
107		}
108
109		$this->assertTagsExist($tagIds);
110
111		$query = $this->connection->getQueryBuilder();
112		$query->selectDistinct('objectid')
113			->from(self::RELATION_TABLE)
114			->where($query->expr()->in('systemtagid', $query->createNamedParameter($tagIds, IQueryBuilder::PARAM_INT_ARRAY)))
115			->andWhere($query->expr()->eq('objecttype', $query->createNamedParameter($objectType)));
116
117		if ($limit) {
118			if (\count($tagIds) !== 1) {
119				throw new \InvalidArgumentException('Limit is only allowed with a single tag');
120			}
121
122			$query->setMaxResults($limit)
123				->orderBy('objectid', 'ASC');
124
125			if ($offset !== '') {
126				$query->andWhere($query->expr()->gt('objectid', $query->createNamedParameter($offset)));
127			}
128		}
129
130		$objectIds = [];
131
132		$result = $query->execute();
133		while ($row = $result->fetch()) {
134			$objectIds[] = $row['objectid'];
135		}
136
137		return $objectIds;
138	}
139
140	/**
141	 * {@inheritdoc}
142	 */
143	public function assignTags(string $objId, string $objectType, $tagIds) {
144		if (!\is_array($tagIds)) {
145			$tagIds = [$tagIds];
146		}
147
148		$this->assertTagsExist($tagIds);
149
150		$query = $this->connection->getQueryBuilder();
151		$query->insert(self::RELATION_TABLE)
152			->values([
153				'objectid' => $query->createNamedParameter($objId),
154				'objecttype' => $query->createNamedParameter($objectType),
155				'systemtagid' => $query->createParameter('tagid'),
156			]);
157
158		$tagsAssigned = [];
159		foreach ($tagIds as $tagId) {
160			try {
161				$query->setParameter('tagid', $tagId);
162				$query->execute();
163				$tagsAssigned[] = $tagId;
164			} catch (UniqueConstraintViolationException $e) {
165				// ignore existing relations
166			}
167		}
168
169		if (empty($tagsAssigned)) {
170			return;
171		}
172
173		$this->dispatcher->dispatch(MapperEvent::EVENT_ASSIGN, new MapperEvent(
174			MapperEvent::EVENT_ASSIGN,
175			$objectType,
176			$objId,
177			$tagsAssigned
178		));
179	}
180
181	/**
182	 * {@inheritdoc}
183	 */
184	public function unassignTags(string $objId, string $objectType, $tagIds) {
185		if (!\is_array($tagIds)) {
186			$tagIds = [$tagIds];
187		}
188
189		$this->assertTagsExist($tagIds);
190
191		$query = $this->connection->getQueryBuilder();
192		$query->delete(self::RELATION_TABLE)
193			->where($query->expr()->eq('objectid', $query->createParameter('objectid')))
194			->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype')))
195			->andWhere($query->expr()->in('systemtagid', $query->createParameter('tagids')))
196			->setParameter('objectid', $objId)
197			->setParameter('objecttype', $objectType)
198			->setParameter('tagids', $tagIds, IQueryBuilder::PARAM_INT_ARRAY)
199			->execute();
200
201		$this->dispatcher->dispatch(MapperEvent::EVENT_UNASSIGN, new MapperEvent(
202			MapperEvent::EVENT_UNASSIGN,
203			$objectType,
204			$objId,
205			$tagIds
206		));
207	}
208
209	/**
210	 * {@inheritdoc}
211	 */
212	public function haveTag($objIds, string $objectType, string $tagId, bool $all = true): bool {
213		$this->assertTagsExist([$tagId]);
214
215		if (!\is_array($objIds)) {
216			$objIds = [$objIds];
217		}
218
219		$query = $this->connection->getQueryBuilder();
220
221		if (!$all) {
222			// If we only need one entry, we make the query lighter, by not
223			// counting the elements
224			$query->select('*')
225				->setMaxResults(1);
226		} else {
227			$query->select($query->func()->count($query->expr()->literal(1)));
228		}
229
230		$query->from(self::RELATION_TABLE)
231			->where($query->expr()->in('objectid', $query->createParameter('objectids')))
232			->andWhere($query->expr()->eq('objecttype', $query->createParameter('objecttype')))
233			->andWhere($query->expr()->eq('systemtagid', $query->createParameter('tagid')))
234			->setParameter('objectids', $objIds, IQueryBuilder::PARAM_STR_ARRAY)
235			->setParameter('tagid', $tagId)
236			->setParameter('objecttype', $objectType);
237
238		$result = $query->execute();
239		$row = $result->fetch(\PDO::FETCH_NUM);
240		$result->closeCursor();
241
242		if ($all) {
243			return ((int)$row[0] === \count($objIds));
244		}
245
246		return (bool) $row;
247	}
248
249	/**
250	 * Asserts that all the given tag ids exist.
251	 *
252	 * @param string[] $tagIds tag ids to check
253	 *
254	 * @throws \OCP\SystemTag\TagNotFoundException if at least one tag did not exist
255	 */
256	private function assertTagsExist($tagIds) {
257		$tags = $this->tagManager->getTagsByIds($tagIds);
258		if (\count($tags) !== \count($tagIds)) {
259			// at least one tag missing, bail out
260			$foundTagIds = array_map(
261				function (ISystemTag $tag) {
262					return $tag->getId();
263				},
264				$tags
265			);
266			$missingTagIds = array_diff($tagIds, $foundTagIds);
267			throw new TagNotFoundException(
268				'Tags not found', 0, null, $missingTagIds
269			);
270		}
271	}
272}
273