Tags.php 19 KB

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