You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Tags.php 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  6. * @author Bernhard Reiter <ockham@raz.or.at>
  7. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  8. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  9. * @author Joas Schilling <coding@schilljs.com>
  10. * @author Morris Jobke <hey@morrisjobke.de>
  11. * @author Robin Appelman <robin@icewind.nl>
  12. * @author Roeland Jago Douma <roeland@famdouma.nl>
  13. * @author Thomas Tanghus <thomas@tanghus.net>
  14. * @author Vincent Petry <vincent@nextcloud.com>
  15. *
  16. * @license AGPL-3.0
  17. *
  18. * This code is free software: you can redistribute it and/or modify
  19. * it under the terms of the GNU Affero General Public License, version 3,
  20. * as published by the Free Software Foundation.
  21. *
  22. * This program is distributed in the hope that it will be useful,
  23. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  24. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  25. * GNU Affero General Public License for more details.
  26. *
  27. * You should have received a copy of the GNU Affero General Public License, version 3,
  28. * along with this program. If not, see <http://www.gnu.org/licenses/>
  29. *
  30. */
  31. namespace OC;
  32. use OC\Tagging\Tag;
  33. use OC\Tagging\TagMapper;
  34. use OCP\DB\Exception;
  35. use OCP\DB\QueryBuilder\IQueryBuilder;
  36. use OCP\IDBConnection;
  37. use OCP\ITags;
  38. use OCP\Share_Backend;
  39. use Psr\Log\LoggerInterface;
  40. class Tags implements ITags {
  41. /**
  42. * Used for storing objectid/categoryname pairs while rescanning.
  43. */
  44. private static array $relations = [];
  45. private string $type;
  46. private string $user;
  47. private IDBConnection $db;
  48. private LoggerInterface $logger;
  49. private array $tags = [];
  50. /**
  51. * Are we including tags for shared items?
  52. */
  53. private bool $includeShared = false;
  54. /**
  55. * The current user, plus any owners of the items shared with the current
  56. * user, if $this->includeShared === true.
  57. */
  58. private array $owners = [];
  59. /**
  60. * The Mapper we are using to communicate our Tag objects to the database.
  61. */
  62. private TagMapper $mapper;
  63. /**
  64. * The sharing backend for objects of $this->type. Required if
  65. * $this->includeShared === true to determine ownership of items.
  66. */
  67. private ?Share_Backend $backend = null;
  68. public const TAG_TABLE = 'vcategory';
  69. public const RELATION_TABLE = 'vcategory_to_object';
  70. /**
  71. * Constructor.
  72. *
  73. * @param TagMapper $mapper Instance of the TagMapper abstraction layer.
  74. * @param string $user The user whose data the object will operate on.
  75. * @param string $type The type of items for which tags will be loaded.
  76. * @param array $defaultTags Tags that should be created at construction.
  77. *
  78. * since 20.0.0 $includeShared isn't used anymore
  79. */
  80. public function __construct(TagMapper $mapper, string $user, string $type, LoggerInterface $logger, IDBConnection $connection, array $defaultTags = []) {
  81. $this->mapper = $mapper;
  82. $this->user = $user;
  83. $this->type = $type;
  84. $this->owners = [$this->user];
  85. $this->tags = $this->mapper->loadTags($this->owners, $this->type);
  86. $this->db = $connection;
  87. $this->logger = $logger;
  88. if (count($defaultTags) > 0 && count($this->tags) === 0) {
  89. $this->addMultiple($defaultTags, true);
  90. }
  91. }
  92. /**
  93. * Check if any tags are saved for this type and user.
  94. *
  95. * @return boolean
  96. */
  97. public function isEmpty(): bool {
  98. return count($this->tags) === 0;
  99. }
  100. /**
  101. * Returns an array mapping a given tag's properties to its values:
  102. * ['id' => 0, 'name' = 'Tag', 'owner' = 'User', 'type' => 'tagtype']
  103. *
  104. * @param string $id The ID of the tag that is going to be mapped
  105. * @return array|false
  106. */
  107. public function getTag(string $id) {
  108. $key = $this->getTagById($id);
  109. if ($key !== false) {
  110. return $this->tagMap($this->tags[$key]);
  111. }
  112. return false;
  113. }
  114. /**
  115. * Get the tags for a specific user.
  116. *
  117. * This returns an array with maps containing each tag's properties:
  118. * [
  119. * ['id' => 0, 'name' = 'First tag', 'owner' = 'User', 'type' => 'tagtype'],
  120. * ['id' => 1, 'name' = 'Shared tag', 'owner' = 'Other user', 'type' => 'tagtype'],
  121. * ]
  122. *
  123. * @return array<array-key, array{id: int, name: string}>
  124. */
  125. public function getTags(): array {
  126. if (!count($this->tags)) {
  127. return [];
  128. }
  129. usort($this->tags, function ($a, $b) {
  130. return strnatcasecmp($a->getName(), $b->getName());
  131. });
  132. $tagMap = [];
  133. foreach ($this->tags as $tag) {
  134. if ($tag->getName() !== ITags::TAG_FAVORITE) {
  135. $tagMap[] = $this->tagMap($tag);
  136. }
  137. }
  138. return $tagMap;
  139. }
  140. /**
  141. * Return only the tags owned by the given user, omitting any tags shared
  142. * by other users.
  143. *
  144. * @param string $user The user whose tags are to be checked.
  145. * @return array An array of Tag objects.
  146. */
  147. public function getTagsForUser(string $user): array {
  148. return array_filter($this->tags,
  149. function ($tag) use ($user) {
  150. return $tag->getOwner() === $user;
  151. }
  152. );
  153. }
  154. /**
  155. * Get the list of tags for the given ids.
  156. *
  157. * @param array $objIds array of object ids
  158. * @return array|false of tags id as key to array of tag names
  159. * or false if an error occurred
  160. */
  161. public function getTagsForObjects(array $objIds) {
  162. $entries = [];
  163. try {
  164. $chunks = array_chunk($objIds, 900, false);
  165. $qb = $this->db->getQueryBuilder();
  166. $qb->select('category', 'categoryid', 'objid')
  167. ->from(self::RELATION_TABLE, 'r')
  168. ->join('r', self::TAG_TABLE, 't', $qb->expr()->eq('r.categoryid', 't.id'))
  169. ->where($qb->expr()->eq('uid', $qb->createParameter('uid')))
  170. ->andWhere($qb->expr()->eq('r.type', $qb->createParameter('type')))
  171. ->andWhere($qb->expr()->in('objid', $qb->createParameter('chunk')));
  172. foreach ($chunks as $chunk) {
  173. $qb->setParameter('uid', $this->user, IQueryBuilder::PARAM_STR);
  174. $qb->setParameter('type', $this->type, IQueryBuilder::PARAM_STR);
  175. $qb->setParameter('chunk', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
  176. $result = $qb->executeQuery();
  177. while ($row = $result->fetch()) {
  178. $objId = (int)$row['objid'];
  179. if (!isset($entries[$objId])) {
  180. $entries[$objId] = [];
  181. }
  182. $entries[$objId][] = $row['category'];
  183. }
  184. $result->closeCursor();
  185. }
  186. } catch (\Exception $e) {
  187. $this->logger->error($e->getMessage(), [
  188. 'exception' => $e,
  189. 'app' => 'core',
  190. ]);
  191. return false;
  192. }
  193. return $entries;
  194. }
  195. /**
  196. * Get the a list if items tagged with $tag.
  197. *
  198. * Throws an exception if the tag could not be found.
  199. *
  200. * @param string $tag Tag id or name.
  201. * @return int[]|false An array of object ids or false on error.
  202. * @throws \Exception
  203. */
  204. public function getIdsForTag($tag) {
  205. $tagId = false;
  206. if (is_numeric($tag)) {
  207. $tagId = $tag;
  208. } elseif (is_string($tag)) {
  209. $tag = trim($tag);
  210. if ($tag === '') {
  211. $this->logger->debug(__METHOD__ . ' Cannot use empty tag names', ['app' => 'core']);
  212. return false;
  213. }
  214. $tagId = $this->getTagId($tag);
  215. }
  216. if ($tagId === false) {
  217. $l10n = \OCP\Util::getL10N('core');
  218. throw new \Exception(
  219. $l10n->t('Could not find category "%s"', [$tag])
  220. );
  221. }
  222. $ids = [];
  223. try {
  224. $qb = $this->db->getQueryBuilder();
  225. $qb->select('objid')
  226. ->from(self::RELATION_TABLE)
  227. ->where($qb->expr()->eq('categoryid', $qb->createNamedParameter($tagId, IQueryBuilder::PARAM_STR)));
  228. $result = $qb->executeQuery();
  229. } catch (Exception $e) {
  230. $this->logger->error($e->getMessage(), [
  231. 'app' => 'core',
  232. 'exception' => $e,
  233. ]);
  234. return false;
  235. }
  236. while ($row = $result->fetch()) {
  237. $ids[] = (int)$row['objid'];
  238. }
  239. $result->closeCursor();
  240. return $ids;
  241. }
  242. /**
  243. * Checks whether a tag is saved for the given user,
  244. * disregarding the ones shared with him or her.
  245. *
  246. * @param string $name The tag name to check for.
  247. * @param string $user The user whose tags are to be checked.
  248. */
  249. public function userHasTag(string $name, string $user): bool {
  250. $key = $this->array_searchi($name, $this->getTagsForUser($user));
  251. return ($key !== false) ? $this->tags[$key]->getId() : false;
  252. }
  253. /**
  254. * Checks whether a tag is saved for or shared with the current user.
  255. *
  256. * @param string $name The tag name to check for.
  257. */
  258. public function hasTag(string $name): bool {
  259. return $this->getTagId($name) !== false;
  260. }
  261. /**
  262. * Add a new tag.
  263. *
  264. * @param string $name A string with a name of the tag
  265. * @return false|int the id of the added tag or false on error.
  266. */
  267. public function add(string $name) {
  268. $name = trim($name);
  269. if ($name === '') {
  270. $this->logger->debug(__METHOD__ . ' Cannot add an empty tag', ['app' => 'core']);
  271. return false;
  272. }
  273. if ($this->userHasTag($name, $this->user)) {
  274. // TODO use unique db properties instead of an additional check
  275. $this->logger->debug(__METHOD__ . ' Tag with name already exists', ['app' => 'core']);
  276. return false;
  277. }
  278. try {
  279. $tag = new Tag($this->user, $this->type, $name);
  280. $tag = $this->mapper->insert($tag);
  281. $this->tags[] = $tag;
  282. } catch (\Exception $e) {
  283. $this->logger->error($e->getMessage(), [
  284. 'exception' => $e,
  285. 'app' => 'core',
  286. ]);
  287. return false;
  288. }
  289. $this->logger->debug(__METHOD__ . ' Added an tag with ' . $tag->getId(), ['app' => 'core']);
  290. return $tag->getId();
  291. }
  292. /**
  293. * Rename tag.
  294. *
  295. * @param string|integer $from The name or ID of the existing tag
  296. * @param string $to The new name of the tag.
  297. * @return bool
  298. */
  299. public function rename($from, string $to): bool {
  300. $from = trim($from);
  301. $to = trim($to);
  302. if ($to === '' || $from === '') {
  303. $this->logger->debug(__METHOD__ . 'Cannot use an empty tag names', ['app' => 'core']);
  304. return false;
  305. }
  306. if (is_numeric($from)) {
  307. $key = $this->getTagById($from);
  308. } else {
  309. $key = $this->getTagByName($from);
  310. }
  311. if ($key === false) {
  312. $this->logger->debug(__METHOD__ . 'Tag ' . $from . 'does not exist', ['app' => 'core']);
  313. return false;
  314. }
  315. $tag = $this->tags[$key];
  316. if ($this->userHasTag($to, $tag->getOwner())) {
  317. $this->logger->debug(__METHOD__ . 'A tag named' . $to . 'already exists for user' . $tag->getOwner(), ['app' => 'core']);
  318. return false;
  319. }
  320. try {
  321. $tag->setName($to);
  322. $this->tags[$key] = $this->mapper->update($tag);
  323. } catch (\Exception $e) {
  324. $this->logger->error($e->getMessage(), [
  325. 'exception' => $e,
  326. 'app' => 'core',
  327. ]);
  328. return false;
  329. }
  330. return true;
  331. }
  332. /**
  333. * Add a list of new tags.
  334. *
  335. * @param string|string[] $names A string with a name or an array of strings containing
  336. * the name(s) of the tag(s) to add.
  337. * @param bool $sync When true, save the tags
  338. * @param int|null $id int Optional object id to add to this|these tag(s)
  339. * @return bool Returns false on error.
  340. */
  341. public function addMultiple($names, bool $sync = false, ?int $id = null): bool {
  342. if (!is_array($names)) {
  343. $names = [$names];
  344. }
  345. $names = array_map('trim', $names);
  346. array_filter($names);
  347. $newones = [];
  348. foreach ($names as $name) {
  349. if (!$this->hasTag($name) && $name !== '') {
  350. $newones[] = new Tag($this->user, $this->type, $name);
  351. }
  352. if (!is_null($id)) {
  353. // Insert $objectid, $categoryid pairs if not exist.
  354. self::$relations[] = ['objid' => $id, 'tag' => $name];
  355. }
  356. }
  357. $this->tags = array_merge($this->tags, $newones);
  358. if ($sync === true) {
  359. $this->save();
  360. }
  361. return true;
  362. }
  363. /**
  364. * Save the list of tags and their object relations
  365. */
  366. protected function save(): void {
  367. foreach ($this->tags as $tag) {
  368. try {
  369. if (!$this->mapper->tagExists($tag)) {
  370. $this->mapper->insert($tag);
  371. }
  372. } catch (\Exception $e) {
  373. $this->logger->error($e->getMessage(), [
  374. 'exception' => $e,
  375. 'app' => 'core',
  376. ]);
  377. }
  378. }
  379. // reload tags to get the proper ids.
  380. $this->tags = $this->mapper->loadTags($this->owners, $this->type);
  381. $this->logger->debug(__METHOD__ . 'tags' . print_r($this->tags, true), ['app' => 'core']);
  382. // Loop through temporarily cached objectid/tagname pairs
  383. // and save relations.
  384. $tags = $this->tags;
  385. // For some reason this is needed or array_search(i) will return 0..?
  386. ksort($tags);
  387. foreach (self::$relations as $relation) {
  388. $tagId = $this->getTagId($relation['tag']);
  389. $this->logger->debug(__METHOD__ . 'catid ' . $relation['tag'] . ' ' . $tagId, ['app' => 'core']);
  390. if ($tagId) {
  391. $qb = $this->db->getQueryBuilder();
  392. $qb->insert(self::RELATION_TABLE)
  393. ->values([
  394. 'objid' => $qb->createNamedParameter($relation['objid'], IQueryBuilder::PARAM_INT),
  395. 'categoryid' => $qb->createNamedParameter($tagId, IQueryBuilder::PARAM_INT),
  396. 'type' => $qb->createNamedParameter($this->type),
  397. ]);
  398. try {
  399. $qb->executeStatement();
  400. } catch (Exception $e) {
  401. $this->logger->error($e->getMessage(), [
  402. 'exception' => $e,
  403. 'app' => 'core',
  404. ]);
  405. }
  406. }
  407. }
  408. self::$relations = []; // reset
  409. }
  410. /**
  411. * Delete tag/object relations from the db
  412. *
  413. * @param array $ids The ids of the objects
  414. * @return boolean Returns false on error.
  415. */
  416. public function purgeObjects(array $ids): bool {
  417. if (count($ids) === 0) {
  418. // job done ;)
  419. return true;
  420. }
  421. $updates = $ids;
  422. $qb = $this->db->getQueryBuilder();
  423. $qb->delete(self::RELATION_TABLE)
  424. ->where($qb->expr()->in('objid', $qb->createNamedParameter($ids)));
  425. try {
  426. $qb->executeStatement();
  427. } catch (Exception $e) {
  428. $this->logger->error($e->getMessage(), [
  429. 'app' => 'core',
  430. 'exception' => $e,
  431. ]);
  432. return false;
  433. }
  434. return true;
  435. }
  436. /**
  437. * Get favorites for an object type
  438. *
  439. * @return array|false An array of object ids.
  440. */
  441. public function getFavorites() {
  442. if (!$this->userHasTag(ITags::TAG_FAVORITE, $this->user)) {
  443. return [];
  444. }
  445. try {
  446. return $this->getIdsForTag(ITags::TAG_FAVORITE);
  447. } catch (\Exception $e) {
  448. \OCP\Server::get(LoggerInterface::class)->error(
  449. $e->getMessage(),
  450. [
  451. 'app' => 'core',
  452. 'exception' => $e,
  453. ]
  454. );
  455. return [];
  456. }
  457. }
  458. /**
  459. * Add an object to favorites
  460. *
  461. * @param int $objid The id of the object
  462. * @return boolean
  463. */
  464. public function addToFavorites($objid) {
  465. if (!$this->userHasTag(ITags::TAG_FAVORITE, $this->user)) {
  466. $this->add(ITags::TAG_FAVORITE);
  467. }
  468. return $this->tagAs($objid, ITags::TAG_FAVORITE);
  469. }
  470. /**
  471. * Remove an object from favorites
  472. *
  473. * @param int $objid The id of the object
  474. * @return boolean
  475. */
  476. public function removeFromFavorites($objid) {
  477. return $this->unTag($objid, ITags::TAG_FAVORITE);
  478. }
  479. /**
  480. * Creates a tag/object relation.
  481. *
  482. * @param int $objid The id of the object
  483. * @param string $tag The id or name of the tag
  484. * @return boolean Returns false on error.
  485. */
  486. public function tagAs($objid, $tag) {
  487. if (is_string($tag) && !is_numeric($tag)) {
  488. $tag = trim($tag);
  489. if ($tag === '') {
  490. $this->logger->debug(__METHOD__.', Cannot add an empty tag');
  491. return false;
  492. }
  493. if (!$this->hasTag($tag)) {
  494. $this->add($tag);
  495. }
  496. $tagId = $this->getTagId($tag);
  497. } else {
  498. $tagId = $tag;
  499. }
  500. $qb = $this->db->getQueryBuilder();
  501. $qb->insert(self::RELATION_TABLE)
  502. ->values([
  503. 'objid' => $qb->createNamedParameter($objid, IQueryBuilder::PARAM_INT),
  504. 'categoryid' => $qb->createNamedParameter($tagId, IQueryBuilder::PARAM_INT),
  505. 'type' => $qb->createNamedParameter($this->type, IQueryBuilder::PARAM_STR),
  506. ]);
  507. try {
  508. $qb->executeStatement();
  509. } catch (\Exception $e) {
  510. \OCP\Server::get(LoggerInterface::class)->error($e->getMessage(), [
  511. 'app' => 'core',
  512. 'exception' => $e,
  513. ]);
  514. return false;
  515. }
  516. return true;
  517. }
  518. /**
  519. * Delete single tag/object relation from the db
  520. *
  521. * @param int $objid The id of the object
  522. * @param string $tag The id or name of the tag
  523. * @return boolean
  524. */
  525. public function unTag($objid, $tag) {
  526. if (is_string($tag) && !is_numeric($tag)) {
  527. $tag = trim($tag);
  528. if ($tag === '') {
  529. $this->logger->debug(__METHOD__.', Tag name is empty');
  530. return false;
  531. }
  532. $tagId = $this->getTagId($tag);
  533. } else {
  534. $tagId = $tag;
  535. }
  536. try {
  537. $qb = $this->db->getQueryBuilder();
  538. $qb->delete(self::RELATION_TABLE)
  539. ->where($qb->expr()->andX(
  540. $qb->expr()->eq('objid', $qb->createNamedParameter($objid)),
  541. $qb->expr()->eq('categoryid', $qb->createNamedParameter($tagId)),
  542. $qb->expr()->eq('type', $qb->createNamedParameter($this->type)),
  543. ))->executeStatement();
  544. } catch (\Exception $e) {
  545. $this->logger->error($e->getMessage(), [
  546. 'app' => 'core',
  547. 'exception' => $e,
  548. ]);
  549. return false;
  550. }
  551. return true;
  552. }
  553. /**
  554. * Delete tags from the database.
  555. *
  556. * @param string[]|integer[] $names An array of tags (names or IDs) to delete
  557. * @return bool Returns false on error
  558. */
  559. public function delete($names) {
  560. if (!is_array($names)) {
  561. $names = [$names];
  562. }
  563. $names = array_map('trim', $names);
  564. array_filter($names);
  565. $this->logger->debug(__METHOD__ . ', before: ' . print_r($this->tags, true));
  566. foreach ($names as $name) {
  567. $id = null;
  568. if (is_numeric($name)) {
  569. $key = $this->getTagById($name);
  570. } else {
  571. $key = $this->getTagByName($name);
  572. }
  573. if ($key !== false) {
  574. $tag = $this->tags[$key];
  575. $id = $tag->getId();
  576. unset($this->tags[$key]);
  577. $this->mapper->delete($tag);
  578. } else {
  579. $this->logger->error(__METHOD__ . 'Cannot delete tag ' . $name . ': not found.');
  580. }
  581. if (!is_null($id) && $id !== false) {
  582. try {
  583. $qb = $this->db->getQueryBuilder();
  584. $qb->delete(self::RELATION_TABLE)
  585. ->where($qb->expr()->eq('categoryid', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
  586. ->executeStatement();
  587. } catch (\Exception $e) {
  588. $this->logger->error($e->getMessage(), [
  589. 'app' => 'core',
  590. 'exception' => $e,
  591. ]);
  592. return false;
  593. }
  594. }
  595. }
  596. return true;
  597. }
  598. // case-insensitive array_search
  599. protected function array_searchi($needle, $haystack, $mem = 'getName') {
  600. if (!is_array($haystack)) {
  601. return false;
  602. }
  603. return array_search(strtolower($needle), array_map(
  604. function ($tag) use ($mem) {
  605. return strtolower(call_user_func([$tag, $mem]));
  606. }, $haystack)
  607. );
  608. }
  609. /**
  610. * Get a tag's ID.
  611. *
  612. * @param string $name The tag name to look for.
  613. * @return string|bool The tag's id or false if no matching tag is found.
  614. */
  615. private function getTagId($name) {
  616. $key = $this->array_searchi($name, $this->tags);
  617. if ($key !== false) {
  618. return $this->tags[$key]->getId();
  619. }
  620. return false;
  621. }
  622. /**
  623. * Get a tag by its name.
  624. *
  625. * @param string $name The tag name.
  626. * @return integer|bool The tag object's offset within the $this->tags
  627. * array or false if it doesn't exist.
  628. */
  629. private function getTagByName($name) {
  630. return $this->array_searchi($name, $this->tags, 'getName');
  631. }
  632. /**
  633. * Get a tag by its ID.
  634. *
  635. * @param string $id The tag ID to look for.
  636. * @return integer|bool The tag object's offset within the $this->tags
  637. * array or false if it doesn't exist.
  638. */
  639. private function getTagById($id) {
  640. return $this->array_searchi($id, $this->tags, 'getId');
  641. }
  642. /**
  643. * Returns an array mapping a given tag's properties to its values:
  644. * ['id' => 0, 'name' = 'Tag', 'owner' = 'User', 'type' => 'tagtype']
  645. *
  646. * @param Tag $tag The tag that is going to be mapped
  647. * @return array
  648. */
  649. private function tagMap(Tag $tag) {
  650. return [
  651. 'id' => $tag->getId(),
  652. 'name' => $tag->getName(),
  653. 'owner' => $tag->getOwner(),
  654. 'type' => $tag->getType()
  655. ];
  656. }
  657. }