Tags.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Bernhard Reiter <ockham@raz.or.at>
  6. * @author derkostka <sebastian.kostka@gmail.com>
  7. * @author Joas Schilling <coding@schilljs.com>
  8. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  9. * @author Morris Jobke <hey@morrisjobke.de>
  10. * @author Robin Appelman <robin@icewind.nl>
  11. * @author Robin McCorkell <robin@mccorkell.me.uk>
  12. * @author Thomas Müller <thomas.mueller@tmit.eu>
  13. * @author Thomas Tanghus <thomas@tanghus.net>
  14. * @author Vincent Petry <pvince81@owncloud.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. /**
  32. * Class for easily tagging objects by their id
  33. *
  34. * A tag can be e.g. 'Family', 'Work', 'Chore', 'Special Occation' or
  35. * anything else that is either parsed from a vobject or that the user chooses
  36. * to add.
  37. * Tag names are not case-sensitive, but will be saved with the case they
  38. * are entered in. If a user already has a tag 'family' for a type, and
  39. * tries to add a tag named 'Family' it will be silently ignored.
  40. */
  41. namespace OC;
  42. use OC\Tagging\Tag;
  43. use OC\Tagging\TagMapper;
  44. use OCP\DB\QueryBuilder\IQueryBuilder;
  45. class Tags implements \OCP\ITags {
  46. /**
  47. * Tags
  48. *
  49. * @var array
  50. */
  51. private $tags = array();
  52. /**
  53. * Used for storing objectid/categoryname pairs while rescanning.
  54. *
  55. * @var array
  56. */
  57. private static $relations = array();
  58. /**
  59. * Type
  60. *
  61. * @var string
  62. */
  63. private $type;
  64. /**
  65. * User
  66. *
  67. * @var string
  68. */
  69. private $user;
  70. /**
  71. * Are we including tags for shared items?
  72. *
  73. * @var bool
  74. */
  75. private $includeShared = false;
  76. /**
  77. * The current user, plus any owners of the items shared with the current
  78. * user, if $this->includeShared === true.
  79. *
  80. * @var array
  81. */
  82. private $owners = array();
  83. /**
  84. * The Mapper we're using to communicate our Tag objects to the database.
  85. *
  86. * @var TagMapper
  87. */
  88. private $mapper;
  89. /**
  90. * The sharing backend for objects of $this->type. Required if
  91. * $this->includeShared === true to determine ownership of items.
  92. *
  93. * @var \OCP\Share_Backend
  94. */
  95. private $backend;
  96. const TAG_TABLE = '*PREFIX*vcategory';
  97. const RELATION_TABLE = '*PREFIX*vcategory_to_object';
  98. const TAG_FAVORITE = '_$!<Favorite>!$_';
  99. /**
  100. * Constructor.
  101. *
  102. * @param TagMapper $mapper Instance of the TagMapper abstraction layer.
  103. * @param string $user The user whose data the object will operate on.
  104. * @param string $type The type of items for which tags will be loaded.
  105. * @param array $defaultTags Tags that should be created at construction.
  106. * @param boolean $includeShared Whether to include tags for items shared with this user by others.
  107. */
  108. public function __construct(TagMapper $mapper, $user, $type, $defaultTags = array(), $includeShared = false) {
  109. $this->mapper = $mapper;
  110. $this->user = $user;
  111. $this->type = $type;
  112. $this->includeShared = $includeShared;
  113. $this->owners = array($this->user);
  114. if ($this->includeShared) {
  115. $this->owners = array_merge($this->owners, \OC\Share\Share::getSharedItemsOwners($this->user, $this->type, true));
  116. $this->backend = \OC\Share\Share::getBackend($this->type);
  117. }
  118. $this->tags = $this->mapper->loadTags($this->owners, $this->type);
  119. if(count($defaultTags) > 0 && count($this->tags) === 0) {
  120. $this->addMultiple($defaultTags, true);
  121. }
  122. }
  123. /**
  124. * Check if any tags are saved for this type and user.
  125. *
  126. * @return boolean.
  127. */
  128. public function isEmpty() {
  129. return count($this->tags) === 0;
  130. }
  131. /**
  132. * Returns an array mapping a given tag's properties to its values:
  133. * ['id' => 0, 'name' = 'Tag', 'owner' = 'User', 'type' => 'tagtype']
  134. *
  135. * @param string $id The ID of the tag that is going to be mapped
  136. * @return array|false
  137. */
  138. public function getTag($id) {
  139. $key = $this->getTagById($id);
  140. if ($key !== false) {
  141. return $this->tagMap($this->tags[$key]);
  142. }
  143. return false;
  144. }
  145. /**
  146. * Get the tags for a specific user.
  147. *
  148. * This returns an array with maps containing each tag's properties:
  149. * [
  150. * ['id' => 0, 'name' = 'First tag', 'owner' = 'User', 'type' => 'tagtype'],
  151. * ['id' => 1, 'name' = 'Shared tag', 'owner' = 'Other user', 'type' => 'tagtype'],
  152. * ]
  153. *
  154. * @return array
  155. */
  156. public function getTags() {
  157. if(!count($this->tags)) {
  158. return array();
  159. }
  160. usort($this->tags, function($a, $b) {
  161. return strnatcasecmp($a->getName(), $b->getName());
  162. });
  163. $tagMap = array();
  164. foreach($this->tags as $tag) {
  165. if($tag->getName() !== self::TAG_FAVORITE) {
  166. $tagMap[] = $this->tagMap($tag);
  167. }
  168. }
  169. return $tagMap;
  170. }
  171. /**
  172. * Return only the tags owned by the given user, omitting any tags shared
  173. * by other users.
  174. *
  175. * @param string $user The user whose tags are to be checked.
  176. * @return array An array of Tag objects.
  177. */
  178. public function getTagsForUser($user) {
  179. return array_filter($this->tags,
  180. function($tag) use($user) {
  181. return $tag->getOwner() === $user;
  182. }
  183. );
  184. }
  185. /**
  186. * Get the list of tags for the given ids.
  187. *
  188. * @param array $objIds array of object ids
  189. * @return array|boolean of tags id as key to array of tag names
  190. * or false if an error occurred
  191. */
  192. public function getTagsForObjects(array $objIds) {
  193. $entries = array();
  194. try {
  195. $conn = \OC::$server->getDatabaseConnection();
  196. $chunks = array_chunk($objIds, 900, false);
  197. foreach ($chunks as $chunk) {
  198. $result = $conn->executeQuery(
  199. 'SELECT `category`, `categoryid`, `objid` ' .
  200. 'FROM `' . self::RELATION_TABLE . '` r, `' . self::TAG_TABLE . '` ' .
  201. 'WHERE `categoryid` = `id` AND `uid` = ? AND r.`type` = ? AND `objid` IN (?)',
  202. array($this->user, $this->type, $chunk),
  203. array(null, null, IQueryBuilder::PARAM_INT_ARRAY)
  204. );
  205. while ($row = $result->fetch()) {
  206. $objId = (int)$row['objid'];
  207. if (!isset($entries[$objId])) {
  208. $entries[$objId] = array();
  209. }
  210. $entries[$objId][] = $row['category'];
  211. }
  212. if (\OCP\DB::isError($result)) {
  213. \OCP\Util::writeLog('core', __METHOD__. 'DB error: ' . \OCP\DB::getErrorMessage(), \OCP\Util::ERROR);
  214. return false;
  215. }
  216. }
  217. } catch(\Exception $e) {
  218. \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(),
  219. \OCP\Util::ERROR);
  220. return false;
  221. }
  222. return $entries;
  223. }
  224. /**
  225. * Get the a list if items tagged with $tag.
  226. *
  227. * Throws an exception if the tag could not be found.
  228. *
  229. * @param string $tag Tag id or name.
  230. * @return array|false An array of object ids or false on error.
  231. * @throws \Exception
  232. */
  233. public function getIdsForTag($tag) {
  234. $result = null;
  235. $tagId = false;
  236. if(is_numeric($tag)) {
  237. $tagId = $tag;
  238. } elseif(is_string($tag)) {
  239. $tag = trim($tag);
  240. if($tag === '') {
  241. \OCP\Util::writeLog('core', __METHOD__.', Cannot use empty tag names', \OCP\Util::DEBUG);
  242. return false;
  243. }
  244. $tagId = $this->getTagId($tag);
  245. }
  246. if($tagId === false) {
  247. $l10n = \OC::$server->getL10N('core');
  248. throw new \Exception(
  249. $l10n->t('Could not find category "%s"', $tag)
  250. );
  251. }
  252. $ids = array();
  253. $sql = 'SELECT `objid` FROM `' . self::RELATION_TABLE
  254. . '` WHERE `categoryid` = ?';
  255. try {
  256. $stmt = \OCP\DB::prepare($sql);
  257. $result = $stmt->execute(array($tagId));
  258. if (\OCP\DB::isError($result)) {
  259. \OCP\Util::writeLog('core', __METHOD__. 'DB error: ' . \OCP\DB::getErrorMessage(), \OCP\Util::ERROR);
  260. return false;
  261. }
  262. } catch(\Exception $e) {
  263. \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(),
  264. \OCP\Util::ERROR);
  265. return false;
  266. }
  267. if(!is_null($result)) {
  268. while( $row = $result->fetchRow()) {
  269. $id = (int)$row['objid'];
  270. if ($this->includeShared) {
  271. // We have to check if we are really allowed to access the
  272. // items that are tagged with $tag. To that end, we ask the
  273. // corresponding sharing backend if the item identified by $id
  274. // is owned by any of $this->owners.
  275. foreach ($this->owners as $owner) {
  276. if ($this->backend->isValidSource($id, $owner)) {
  277. $ids[] = $id;
  278. break;
  279. }
  280. }
  281. } else {
  282. $ids[] = $id;
  283. }
  284. }
  285. }
  286. return $ids;
  287. }
  288. /**
  289. * Checks whether a tag is saved for the given user,
  290. * disregarding the ones shared with him or her.
  291. *
  292. * @param string $name The tag name to check for.
  293. * @param string $user The user whose tags are to be checked.
  294. * @return bool
  295. */
  296. public function userHasTag($name, $user) {
  297. $key = $this->array_searchi($name, $this->getTagsForUser($user));
  298. return ($key !== false) ? $this->tags[$key]->getId() : false;
  299. }
  300. /**
  301. * Checks whether a tag is saved for or shared with the current user.
  302. *
  303. * @param string $name The tag name to check for.
  304. * @return bool
  305. */
  306. public function hasTag($name) {
  307. return $this->getTagId($name) !== false;
  308. }
  309. /**
  310. * Add a new tag.
  311. *
  312. * @param string $name A string with a name of the tag
  313. * @return false|int the id of the added tag or false on error.
  314. */
  315. public function add($name) {
  316. $name = trim($name);
  317. if($name === '') {
  318. \OCP\Util::writeLog('core', __METHOD__.', Cannot add an empty tag', \OCP\Util::DEBUG);
  319. return false;
  320. }
  321. if($this->userHasTag($name, $this->user)) {
  322. \OCP\Util::writeLog('core', __METHOD__.', name: ' . $name. ' exists already', \OCP\Util::DEBUG);
  323. return false;
  324. }
  325. try {
  326. $tag = new Tag($this->user, $this->type, $name);
  327. $tag = $this->mapper->insert($tag);
  328. $this->tags[] = $tag;
  329. } catch(\Exception $e) {
  330. \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(),
  331. \OCP\Util::ERROR);
  332. return false;
  333. }
  334. \OCP\Util::writeLog('core', __METHOD__.', id: ' . $tag->getId(), \OCP\Util::DEBUG);
  335. return $tag->getId();
  336. }
  337. /**
  338. * Rename tag.
  339. *
  340. * @param string|integer $from The name or ID of the existing tag
  341. * @param string $to The new name of the tag.
  342. * @return bool
  343. */
  344. public function rename($from, $to) {
  345. $from = trim($from);
  346. $to = trim($to);
  347. if($to === '' || $from === '') {
  348. \OCP\Util::writeLog('core', __METHOD__.', Cannot use empty tag names', \OCP\Util::DEBUG);
  349. return false;
  350. }
  351. if (is_numeric($from)) {
  352. $key = $this->getTagById($from);
  353. } else {
  354. $key = $this->getTagByName($from);
  355. }
  356. if($key === false) {
  357. \OCP\Util::writeLog('core', __METHOD__.', tag: ' . $from. ' does not exist', \OCP\Util::DEBUG);
  358. return false;
  359. }
  360. $tag = $this->tags[$key];
  361. if($this->userHasTag($to, $tag->getOwner())) {
  362. \OCP\Util::writeLog('core', __METHOD__.', A tag named ' . $to. ' already exists for user ' . $tag->getOwner() . '.', \OCP\Util::DEBUG);
  363. return false;
  364. }
  365. try {
  366. $tag->setName($to);
  367. $this->tags[$key] = $this->mapper->update($tag);
  368. } catch(\Exception $e) {
  369. \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(),
  370. \OCP\Util::ERROR);
  371. return false;
  372. }
  373. return true;
  374. }
  375. /**
  376. * Add a list of new tags.
  377. *
  378. * @param string[] $names A string with a name or an array of strings containing
  379. * the name(s) of the tag(s) to add.
  380. * @param bool $sync When true, save the tags
  381. * @param int|null $id int Optional object id to add to this|these tag(s)
  382. * @return bool Returns false on error.
  383. */
  384. public function addMultiple($names, $sync=false, $id = null) {
  385. if(!is_array($names)) {
  386. $names = array($names);
  387. }
  388. $names = array_map('trim', $names);
  389. array_filter($names);
  390. $newones = array();
  391. foreach($names as $name) {
  392. if(!$this->hasTag($name) && $name !== '') {
  393. $newones[] = new Tag($this->user, $this->type, $name);
  394. }
  395. if(!is_null($id) ) {
  396. // Insert $objectid, $categoryid pairs if not exist.
  397. self::$relations[] = array('objid' => $id, 'tag' => $name);
  398. }
  399. }
  400. $this->tags = array_merge($this->tags, $newones);
  401. if($sync === true) {
  402. $this->save();
  403. }
  404. return true;
  405. }
  406. /**
  407. * Save the list of tags and their object relations
  408. */
  409. protected function save() {
  410. if(is_array($this->tags)) {
  411. foreach($this->tags as $tag) {
  412. try {
  413. if (!$this->mapper->tagExists($tag)) {
  414. $this->mapper->insert($tag);
  415. }
  416. } catch(\Exception $e) {
  417. \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(),
  418. \OCP\Util::ERROR);
  419. }
  420. }
  421. // reload tags to get the proper ids.
  422. $this->tags = $this->mapper->loadTags($this->owners, $this->type);
  423. \OCP\Util::writeLog('core', __METHOD__.', tags: ' . print_r($this->tags, true),
  424. \OCP\Util::DEBUG);
  425. // Loop through temporarily cached objectid/tagname pairs
  426. // and save relations.
  427. $tags = $this->tags;
  428. // For some reason this is needed or array_search(i) will return 0..?
  429. ksort($tags);
  430. foreach(self::$relations as $relation) {
  431. $tagId = $this->getTagId($relation['tag']);
  432. \OCP\Util::writeLog('core', __METHOD__ . 'catid, ' . $relation['tag'] . ' ' . $tagId, \OCP\Util::DEBUG);
  433. if($tagId) {
  434. try {
  435. \OCP\DB::insertIfNotExist(self::RELATION_TABLE,
  436. array(
  437. 'objid' => $relation['objid'],
  438. 'categoryid' => $tagId,
  439. 'type' => $this->type,
  440. ));
  441. } catch(\Exception $e) {
  442. \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(),
  443. \OCP\Util::ERROR);
  444. }
  445. }
  446. }
  447. self::$relations = array(); // reset
  448. } else {
  449. \OCP\Util::writeLog('core', __METHOD__.', $this->tags is not an array! '
  450. . print_r($this->tags, true), \OCP\Util::ERROR);
  451. }
  452. }
  453. /**
  454. * Delete tags and tag/object relations for a user.
  455. *
  456. * For hooking up on post_deleteUser
  457. *
  458. * @param array $arguments
  459. */
  460. public static function post_deleteUser($arguments) {
  461. // Find all objectid/tagId pairs.
  462. $result = null;
  463. try {
  464. $stmt = \OCP\DB::prepare('SELECT `id` FROM `' . self::TAG_TABLE . '` '
  465. . 'WHERE `uid` = ?');
  466. $result = $stmt->execute(array($arguments['uid']));
  467. if (\OCP\DB::isError($result)) {
  468. \OCP\Util::writeLog('core', __METHOD__. 'DB error: ' . \OCP\DB::getErrorMessage(), \OCP\Util::ERROR);
  469. }
  470. } catch(\Exception $e) {
  471. \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(),
  472. \OCP\Util::ERROR);
  473. }
  474. if(!is_null($result)) {
  475. try {
  476. $stmt = \OCP\DB::prepare('DELETE FROM `' . self::RELATION_TABLE . '` '
  477. . 'WHERE `categoryid` = ?');
  478. while( $row = $result->fetchRow()) {
  479. try {
  480. $stmt->execute(array($row['id']));
  481. } catch(\Exception $e) {
  482. \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(),
  483. \OCP\Util::ERROR);
  484. }
  485. }
  486. } catch(\Exception $e) {
  487. \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(),
  488. \OCP\Util::ERROR);
  489. }
  490. }
  491. try {
  492. $stmt = \OCP\DB::prepare('DELETE FROM `' . self::TAG_TABLE . '` '
  493. . 'WHERE `uid` = ?');
  494. $result = $stmt->execute(array($arguments['uid']));
  495. if (\OCP\DB::isError($result)) {
  496. \OCP\Util::writeLog('core', __METHOD__. ', DB error: ' . \OCP\DB::getErrorMessage(), \OCP\Util::ERROR);
  497. }
  498. } catch(\Exception $e) {
  499. \OCP\Util::writeLog('core', __METHOD__ . ', exception: '
  500. . $e->getMessage(), \OCP\Util::ERROR);
  501. }
  502. }
  503. /**
  504. * Delete tag/object relations from the db
  505. *
  506. * @param array $ids The ids of the objects
  507. * @return boolean Returns false on error.
  508. */
  509. public function purgeObjects(array $ids) {
  510. if(count($ids) === 0) {
  511. // job done ;)
  512. return true;
  513. }
  514. $updates = $ids;
  515. try {
  516. $query = 'DELETE FROM `' . self::RELATION_TABLE . '` ';
  517. $query .= 'WHERE `objid` IN (' . str_repeat('?,', count($ids)-1) . '?) ';
  518. $query .= 'AND `type`= ?';
  519. $updates[] = $this->type;
  520. $stmt = \OCP\DB::prepare($query);
  521. $result = $stmt->execute($updates);
  522. if (\OCP\DB::isError($result)) {
  523. \OCP\Util::writeLog('core', __METHOD__. 'DB error: ' . \OCP\DB::getErrorMessage(), \OCP\Util::ERROR);
  524. return false;
  525. }
  526. } catch(\Exception $e) {
  527. \OCP\Util::writeLog('core', __METHOD__.', exception: ' . $e->getMessage(),
  528. \OCP\Util::ERROR);
  529. return false;
  530. }
  531. return true;
  532. }
  533. /**
  534. * Get favorites for an object type
  535. *
  536. * @return array|false An array of object ids.
  537. */
  538. public function getFavorites() {
  539. try {
  540. return $this->getIdsForTag(self::TAG_FAVORITE);
  541. } catch(\Exception $e) {
  542. \OCP\Util::writeLog('core', __METHOD__.', exception: ' . $e->getMessage(),
  543. \OCP\Util::DEBUG);
  544. return array();
  545. }
  546. }
  547. /**
  548. * Add an object to favorites
  549. *
  550. * @param int $objid The id of the object
  551. * @return boolean
  552. */
  553. public function addToFavorites($objid) {
  554. if(!$this->userHasTag(self::TAG_FAVORITE, $this->user)) {
  555. $this->add(self::TAG_FAVORITE);
  556. }
  557. return $this->tagAs($objid, self::TAG_FAVORITE);
  558. }
  559. /**
  560. * Remove an object from favorites
  561. *
  562. * @param int $objid The id of the object
  563. * @return boolean
  564. */
  565. public function removeFromFavorites($objid) {
  566. return $this->unTag($objid, self::TAG_FAVORITE);
  567. }
  568. /**
  569. * Creates a tag/object relation.
  570. *
  571. * @param int $objid The id of the object
  572. * @param string $tag The id or name of the tag
  573. * @return boolean Returns false on error.
  574. */
  575. public function tagAs($objid, $tag) {
  576. if(is_string($tag) && !is_numeric($tag)) {
  577. $tag = trim($tag);
  578. if($tag === '') {
  579. \OCP\Util::writeLog('core', __METHOD__.', Cannot add an empty tag', \OCP\Util::DEBUG);
  580. return false;
  581. }
  582. if(!$this->hasTag($tag)) {
  583. $this->add($tag);
  584. }
  585. $tagId = $this->getTagId($tag);
  586. } else {
  587. $tagId = $tag;
  588. }
  589. try {
  590. \OCP\DB::insertIfNotExist(self::RELATION_TABLE,
  591. array(
  592. 'objid' => $objid,
  593. 'categoryid' => $tagId,
  594. 'type' => $this->type,
  595. ));
  596. } catch(\Exception $e) {
  597. \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(),
  598. \OCP\Util::ERROR);
  599. return false;
  600. }
  601. return true;
  602. }
  603. /**
  604. * Delete single tag/object relation from the db
  605. *
  606. * @param int $objid The id of the object
  607. * @param string $tag The id or name of the tag
  608. * @return boolean
  609. */
  610. public function unTag($objid, $tag) {
  611. if(is_string($tag) && !is_numeric($tag)) {
  612. $tag = trim($tag);
  613. if($tag === '') {
  614. \OCP\Util::writeLog('core', __METHOD__.', Tag name is empty', \OCP\Util::DEBUG);
  615. return false;
  616. }
  617. $tagId = $this->getTagId($tag);
  618. } else {
  619. $tagId = $tag;
  620. }
  621. try {
  622. $sql = 'DELETE FROM `' . self::RELATION_TABLE . '` '
  623. . 'WHERE `objid` = ? AND `categoryid` = ? AND `type` = ?';
  624. $stmt = \OCP\DB::prepare($sql);
  625. $stmt->execute(array($objid, $tagId, $this->type));
  626. } catch(\Exception $e) {
  627. \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(),
  628. \OCP\Util::ERROR);
  629. return false;
  630. }
  631. return true;
  632. }
  633. /**
  634. * Delete tags from the database.
  635. *
  636. * @param string[]|integer[] $names An array of tags (names or IDs) to delete
  637. * @return bool Returns false on error
  638. */
  639. public function delete($names) {
  640. if(!is_array($names)) {
  641. $names = array($names);
  642. }
  643. $names = array_map('trim', $names);
  644. array_filter($names);
  645. \OCP\Util::writeLog('core', __METHOD__ . ', before: '
  646. . print_r($this->tags, true), \OCP\Util::DEBUG);
  647. foreach($names as $name) {
  648. $id = null;
  649. if (is_numeric($name)) {
  650. $key = $this->getTagById($name);
  651. } else {
  652. $key = $this->getTagByName($name);
  653. }
  654. if ($key !== false) {
  655. $tag = $this->tags[$key];
  656. $id = $tag->getId();
  657. unset($this->tags[$key]);
  658. $this->mapper->delete($tag);
  659. } else {
  660. \OCP\Util::writeLog('core', __METHOD__ . 'Cannot delete tag ' . $name
  661. . ': not found.', \OCP\Util::ERROR);
  662. }
  663. if(!is_null($id) && $id !== false) {
  664. try {
  665. $sql = 'DELETE FROM `' . self::RELATION_TABLE . '` '
  666. . 'WHERE `categoryid` = ?';
  667. $stmt = \OCP\DB::prepare($sql);
  668. $result = $stmt->execute(array($id));
  669. if (\OCP\DB::isError($result)) {
  670. \OCP\Util::writeLog('core',
  671. __METHOD__. 'DB error: ' . \OCP\DB::getErrorMessage(),
  672. \OCP\Util::ERROR);
  673. return false;
  674. }
  675. } catch(\Exception $e) {
  676. \OCP\Util::writeLog('core', __METHOD__.', exception: '.$e->getMessage(),
  677. \OCP\Util::ERROR);
  678. return false;
  679. }
  680. }
  681. }
  682. return true;
  683. }
  684. // case-insensitive array_search
  685. protected function array_searchi($needle, $haystack, $mem='getName') {
  686. if(!is_array($haystack)) {
  687. return false;
  688. }
  689. return array_search(strtolower($needle), array_map(
  690. function($tag) use($mem) {
  691. return strtolower(call_user_func(array($tag, $mem)));
  692. }, $haystack)
  693. );
  694. }
  695. /**
  696. * Get a tag's ID.
  697. *
  698. * @param string $name The tag name to look for.
  699. * @return string|bool The tag's id or false if no matching tag is found.
  700. */
  701. private function getTagId($name) {
  702. $key = $this->array_searchi($name, $this->tags);
  703. if ($key !== false) {
  704. return $this->tags[$key]->getId();
  705. }
  706. return false;
  707. }
  708. /**
  709. * Get a tag by its name.
  710. *
  711. * @param string $name The tag name.
  712. * @return integer|bool The tag object's offset within the $this->tags
  713. * array or false if it doesn't exist.
  714. */
  715. private function getTagByName($name) {
  716. return $this->array_searchi($name, $this->tags, 'getName');
  717. }
  718. /**
  719. * Get a tag by its ID.
  720. *
  721. * @param string $id The tag ID to look for.
  722. * @return integer|bool The tag object's offset within the $this->tags
  723. * array or false if it doesn't exist.
  724. */
  725. private function getTagById($id) {
  726. return $this->array_searchi($id, $this->tags, 'getId');
  727. }
  728. /**
  729. * Returns an array mapping a given tag's properties to its values:
  730. * ['id' => 0, 'name' = 'Tag', 'owner' = 'User', 'type' => 'tagtype']
  731. *
  732. * @param Tag $tag The tag that is going to be mapped
  733. * @return array
  734. */
  735. private function tagMap(Tag $tag) {
  736. return array(
  737. 'id' => $tag->getId(),
  738. 'name' => $tag->getName(),
  739. 'owner' => $tag->getOwner(),
  740. 'type' => $tag->getType()
  741. );
  742. }
  743. }