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