Manager.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  6. * @author Joas Schilling <coding@schilljs.com>
  7. * @author Thomas Müller <thomas.mueller@tmit.eu>
  8. *
  9. * @license AGPL-3.0
  10. *
  11. * This code is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU Affero General Public License, version 3,
  13. * as published by the Free Software Foundation.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU Affero General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public License, version 3,
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>
  22. *
  23. */
  24. namespace OC\Comments;
  25. use Doctrine\DBAL\Exception\DriverException;
  26. use OCP\Comments\CommentsEvent;
  27. use OCP\Comments\IComment;
  28. use OCP\Comments\ICommentsEventHandler;
  29. use OCP\Comments\ICommentsManager;
  30. use OCP\Comments\NotFoundException;
  31. use OCP\DB\QueryBuilder\IQueryBuilder;
  32. use OCP\IDBConnection;
  33. use OCP\IConfig;
  34. use OCP\ILogger;
  35. use OCP\IUser;
  36. class Manager implements ICommentsManager {
  37. /** @var IDBConnection */
  38. protected $dbConn;
  39. /** @var ILogger */
  40. protected $logger;
  41. /** @var IConfig */
  42. protected $config;
  43. /** @var IComment[] */
  44. protected $commentsCache = [];
  45. /** @var \Closure[] */
  46. protected $eventHandlerClosures = [];
  47. /** @var ICommentsEventHandler[] */
  48. protected $eventHandlers = [];
  49. /** @var \Closure[] */
  50. protected $displayNameResolvers = [];
  51. /**
  52. * Manager constructor.
  53. *
  54. * @param IDBConnection $dbConn
  55. * @param ILogger $logger
  56. * @param IConfig $config
  57. */
  58. public function __construct(
  59. IDBConnection $dbConn,
  60. ILogger $logger,
  61. IConfig $config
  62. ) {
  63. $this->dbConn = $dbConn;
  64. $this->logger = $logger;
  65. $this->config = $config;
  66. }
  67. /**
  68. * converts data base data into PHP native, proper types as defined by
  69. * IComment interface.
  70. *
  71. * @param array $data
  72. * @return array
  73. */
  74. protected function normalizeDatabaseData(array $data) {
  75. $data['id'] = strval($data['id']);
  76. $data['parent_id'] = strval($data['parent_id']);
  77. $data['topmost_parent_id'] = strval($data['topmost_parent_id']);
  78. $data['creation_timestamp'] = new \DateTime($data['creation_timestamp']);
  79. if (!is_null($data['latest_child_timestamp'])) {
  80. $data['latest_child_timestamp'] = new \DateTime($data['latest_child_timestamp']);
  81. }
  82. $data['children_count'] = intval($data['children_count']);
  83. return $data;
  84. }
  85. /**
  86. * prepares a comment for an insert or update operation after making sure
  87. * all necessary fields have a value assigned.
  88. *
  89. * @param IComment $comment
  90. * @return IComment returns the same updated IComment instance as provided
  91. * by parameter for convenience
  92. * @throws \UnexpectedValueException
  93. */
  94. protected function prepareCommentForDatabaseWrite(IComment $comment) {
  95. if (!$comment->getActorType()
  96. || !$comment->getActorId()
  97. || !$comment->getObjectType()
  98. || !$comment->getObjectId()
  99. || !$comment->getVerb()
  100. ) {
  101. throw new \UnexpectedValueException('Actor, Object and Verb information must be provided for saving');
  102. }
  103. if ($comment->getId() === '') {
  104. $comment->setChildrenCount(0);
  105. $comment->setLatestChildDateTime(new \DateTime('0000-00-00 00:00:00', new \DateTimeZone('UTC')));
  106. $comment->setLatestChildDateTime(null);
  107. }
  108. if (is_null($comment->getCreationDateTime())) {
  109. $comment->setCreationDateTime(new \DateTime());
  110. }
  111. if ($comment->getParentId() !== '0') {
  112. $comment->setTopmostParentId($this->determineTopmostParentId($comment->getParentId()));
  113. } else {
  114. $comment->setTopmostParentId('0');
  115. }
  116. $this->cache($comment);
  117. return $comment;
  118. }
  119. /**
  120. * returns the topmost parent id of a given comment identified by ID
  121. *
  122. * @param string $id
  123. * @return string
  124. * @throws NotFoundException
  125. */
  126. protected function determineTopmostParentId($id) {
  127. $comment = $this->get($id);
  128. if ($comment->getParentId() === '0') {
  129. return $comment->getId();
  130. } else {
  131. return $this->determineTopmostParentId($comment->getId());
  132. }
  133. }
  134. /**
  135. * updates child information of a comment
  136. *
  137. * @param string $id
  138. * @param \DateTime $cDateTime the date time of the most recent child
  139. * @throws NotFoundException
  140. */
  141. protected function updateChildrenInformation($id, \DateTime $cDateTime) {
  142. $qb = $this->dbConn->getQueryBuilder();
  143. $query = $qb->select($qb->createFunction('COUNT(`id`)'))
  144. ->from('comments')
  145. ->where($qb->expr()->eq('parent_id', $qb->createParameter('id')))
  146. ->setParameter('id', $id);
  147. $resultStatement = $query->execute();
  148. $data = $resultStatement->fetch(\PDO::FETCH_NUM);
  149. $resultStatement->closeCursor();
  150. $children = intval($data[0]);
  151. $comment = $this->get($id);
  152. $comment->setChildrenCount($children);
  153. $comment->setLatestChildDateTime($cDateTime);
  154. $this->save($comment);
  155. }
  156. /**
  157. * Tests whether actor or object type and id parameters are acceptable.
  158. * Throws exception if not.
  159. *
  160. * @param string $role
  161. * @param string $type
  162. * @param string $id
  163. * @throws \InvalidArgumentException
  164. */
  165. protected function checkRoleParameters($role, $type, $id) {
  166. if (
  167. !is_string($type) || empty($type)
  168. || !is_string($id) || empty($id)
  169. ) {
  170. throw new \InvalidArgumentException($role . ' parameters must be string and not empty');
  171. }
  172. }
  173. /**
  174. * run-time caches a comment
  175. *
  176. * @param IComment $comment
  177. */
  178. protected function cache(IComment $comment) {
  179. $id = $comment->getId();
  180. if (empty($id)) {
  181. return;
  182. }
  183. $this->commentsCache[strval($id)] = $comment;
  184. }
  185. /**
  186. * removes an entry from the comments run time cache
  187. *
  188. * @param mixed $id the comment's id
  189. */
  190. protected function uncache($id) {
  191. $id = strval($id);
  192. if (isset($this->commentsCache[$id])) {
  193. unset($this->commentsCache[$id]);
  194. }
  195. }
  196. /**
  197. * returns a comment instance
  198. *
  199. * @param string $id the ID of the comment
  200. * @return IComment
  201. * @throws NotFoundException
  202. * @throws \InvalidArgumentException
  203. * @since 9.0.0
  204. */
  205. public function get($id) {
  206. if (intval($id) === 0) {
  207. throw new \InvalidArgumentException('IDs must be translatable to a number in this implementation.');
  208. }
  209. if (isset($this->commentsCache[$id])) {
  210. return $this->commentsCache[$id];
  211. }
  212. $qb = $this->dbConn->getQueryBuilder();
  213. $resultStatement = $qb->select('*')
  214. ->from('comments')
  215. ->where($qb->expr()->eq('id', $qb->createParameter('id')))
  216. ->setParameter('id', $id, IQueryBuilder::PARAM_INT)
  217. ->execute();
  218. $data = $resultStatement->fetch();
  219. $resultStatement->closeCursor();
  220. if (!$data) {
  221. throw new NotFoundException();
  222. }
  223. $comment = new Comment($this->normalizeDatabaseData($data));
  224. $this->cache($comment);
  225. return $comment;
  226. }
  227. /**
  228. * returns the comment specified by the id and all it's child comments.
  229. * At this point of time, we do only support one level depth.
  230. *
  231. * @param string $id
  232. * @param int $limit max number of entries to return, 0 returns all
  233. * @param int $offset the start entry
  234. * @return array
  235. * @since 9.0.0
  236. *
  237. * The return array looks like this
  238. * [
  239. * 'comment' => IComment, // root comment
  240. * 'replies' =>
  241. * [
  242. * 0 =>
  243. * [
  244. * 'comment' => IComment,
  245. * 'replies' => []
  246. * ]
  247. * 1 =>
  248. * [
  249. * 'comment' => IComment,
  250. * 'replies'=> []
  251. * ],
  252. * …
  253. * ]
  254. * ]
  255. */
  256. public function getTree($id, $limit = 0, $offset = 0) {
  257. $tree = [];
  258. $tree['comment'] = $this->get($id);
  259. $tree['replies'] = [];
  260. $qb = $this->dbConn->getQueryBuilder();
  261. $query = $qb->select('*')
  262. ->from('comments')
  263. ->where($qb->expr()->eq('topmost_parent_id', $qb->createParameter('id')))
  264. ->orderBy('creation_timestamp', 'DESC')
  265. ->setParameter('id', $id);
  266. if ($limit > 0) {
  267. $query->setMaxResults($limit);
  268. }
  269. if ($offset > 0) {
  270. $query->setFirstResult($offset);
  271. }
  272. $resultStatement = $query->execute();
  273. while ($data = $resultStatement->fetch()) {
  274. $comment = new Comment($this->normalizeDatabaseData($data));
  275. $this->cache($comment);
  276. $tree['replies'][] = [
  277. 'comment' => $comment,
  278. 'replies' => []
  279. ];
  280. }
  281. $resultStatement->closeCursor();
  282. return $tree;
  283. }
  284. /**
  285. * returns comments for a specific object (e.g. a file).
  286. *
  287. * The sort order is always newest to oldest.
  288. *
  289. * @param string $objectType the object type, e.g. 'files'
  290. * @param string $objectId the id of the object
  291. * @param int $limit optional, number of maximum comments to be returned. if
  292. * not specified, all comments are returned.
  293. * @param int $offset optional, starting point
  294. * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
  295. * that may be returned
  296. * @return IComment[]
  297. * @since 9.0.0
  298. */
  299. public function getForObject(
  300. $objectType,
  301. $objectId,
  302. $limit = 0,
  303. $offset = 0,
  304. \DateTime $notOlderThan = null
  305. ) {
  306. $comments = [];
  307. $qb = $this->dbConn->getQueryBuilder();
  308. $query = $qb->select('*')
  309. ->from('comments')
  310. ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
  311. ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
  312. ->orderBy('creation_timestamp', 'DESC')
  313. ->setParameter('type', $objectType)
  314. ->setParameter('id', $objectId);
  315. if ($limit > 0) {
  316. $query->setMaxResults($limit);
  317. }
  318. if ($offset > 0) {
  319. $query->setFirstResult($offset);
  320. }
  321. if (!is_null($notOlderThan)) {
  322. $query
  323. ->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
  324. ->setParameter('notOlderThan', $notOlderThan, 'datetime');
  325. }
  326. $resultStatement = $query->execute();
  327. while ($data = $resultStatement->fetch()) {
  328. $comment = new Comment($this->normalizeDatabaseData($data));
  329. $this->cache($comment);
  330. $comments[] = $comment;
  331. }
  332. $resultStatement->closeCursor();
  333. return $comments;
  334. }
  335. /**
  336. * @param $objectType string the object type, e.g. 'files'
  337. * @param $objectId string the id of the object
  338. * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
  339. * that may be returned
  340. * @return Int
  341. * @since 9.0.0
  342. */
  343. public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null) {
  344. $qb = $this->dbConn->getQueryBuilder();
  345. $query = $qb->select($qb->createFunction('COUNT(`id`)'))
  346. ->from('comments')
  347. ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
  348. ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
  349. ->setParameter('type', $objectType)
  350. ->setParameter('id', $objectId);
  351. if (!is_null($notOlderThan)) {
  352. $query
  353. ->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
  354. ->setParameter('notOlderThan', $notOlderThan, 'datetime');
  355. }
  356. $resultStatement = $query->execute();
  357. $data = $resultStatement->fetch(\PDO::FETCH_NUM);
  358. $resultStatement->closeCursor();
  359. return intval($data[0]);
  360. }
  361. /**
  362. * Get the number of unread comments for all files in a folder
  363. *
  364. * @param int $folderId
  365. * @param IUser $user
  366. * @return array [$fileId => $unreadCount]
  367. */
  368. public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user) {
  369. $qb = $this->dbConn->getQueryBuilder();
  370. $query = $qb->select('f.fileid')
  371. ->selectAlias(
  372. $qb->createFunction('COUNT(' . $qb->getColumnName('c.id') . ')'),
  373. 'num_ids'
  374. )
  375. ->from('comments', 'c')
  376. ->innerJoin('c', 'filecache', 'f', $qb->expr()->andX(
  377. $qb->expr()->eq('c.object_type', $qb->createNamedParameter('files')),
  378. $qb->expr()->eq('f.fileid', $qb->expr()->castColumn('c.object_id', IQueryBuilder::PARAM_INT))
  379. ))
  380. ->leftJoin('c', 'comments_read_markers', 'm', $qb->expr()->andX(
  381. $qb->expr()->eq('m.object_type', $qb->createNamedParameter('files')),
  382. $qb->expr()->eq('m.object_id', 'c.object_id'),
  383. $qb->expr()->eq('m.user_id', $qb->createNamedParameter($user->getUID()))
  384. ))
  385. ->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($folderId)))
  386. ->andWhere($qb->expr()->orX(
  387. $qb->expr()->gt('c.creation_timestamp', 'marker_datetime'),
  388. $qb->expr()->isNull('marker_datetime')
  389. ))
  390. ->groupBy('f.fileid');
  391. $resultStatement = $query->execute();
  392. $results = [];
  393. while ($row = $resultStatement->fetch()) {
  394. $results[$row['fileid']] = (int) $row['num_ids'];
  395. }
  396. $resultStatement->closeCursor();
  397. return $results;
  398. }
  399. /**
  400. * creates a new comment and returns it. At this point of time, it is not
  401. * saved in the used data storage. Use save() after setting other fields
  402. * of the comment (e.g. message or verb).
  403. *
  404. * @param string $actorType the actor type (e.g. 'users')
  405. * @param string $actorId a user id
  406. * @param string $objectType the object type the comment is attached to
  407. * @param string $objectId the object id the comment is attached to
  408. * @return IComment
  409. * @since 9.0.0
  410. */
  411. public function create($actorType, $actorId, $objectType, $objectId) {
  412. $comment = new Comment();
  413. $comment
  414. ->setActor($actorType, $actorId)
  415. ->setObject($objectType, $objectId);
  416. return $comment;
  417. }
  418. /**
  419. * permanently deletes the comment specified by the ID
  420. *
  421. * When the comment has child comments, their parent ID will be changed to
  422. * the parent ID of the item that is to be deleted.
  423. *
  424. * @param string $id
  425. * @return bool
  426. * @throws \InvalidArgumentException
  427. * @since 9.0.0
  428. */
  429. public function delete($id) {
  430. if (!is_string($id)) {
  431. throw new \InvalidArgumentException('Parameter must be string');
  432. }
  433. try {
  434. $comment = $this->get($id);
  435. } catch (\Exception $e) {
  436. // Ignore exceptions, we just don't fire a hook then
  437. $comment = null;
  438. }
  439. $qb = $this->dbConn->getQueryBuilder();
  440. $query = $qb->delete('comments')
  441. ->where($qb->expr()->eq('id', $qb->createParameter('id')))
  442. ->setParameter('id', $id);
  443. try {
  444. $affectedRows = $query->execute();
  445. $this->uncache($id);
  446. } catch (DriverException $e) {
  447. $this->logger->logException($e, ['app' => 'core_comments']);
  448. return false;
  449. }
  450. if ($affectedRows > 0 && $comment instanceof IComment) {
  451. $this->sendEvent(CommentsEvent::EVENT_DELETE, $comment);
  452. }
  453. return ($affectedRows > 0);
  454. }
  455. /**
  456. * saves the comment permanently
  457. *
  458. * if the supplied comment has an empty ID, a new entry comment will be
  459. * saved and the instance updated with the new ID.
  460. *
  461. * Otherwise, an existing comment will be updated.
  462. *
  463. * Throws NotFoundException when a comment that is to be updated does not
  464. * exist anymore at this point of time.
  465. *
  466. * @param IComment $comment
  467. * @return bool
  468. * @throws NotFoundException
  469. * @since 9.0.0
  470. */
  471. public function save(IComment $comment) {
  472. if ($this->prepareCommentForDatabaseWrite($comment)->getId() === '') {
  473. $result = $this->insert($comment);
  474. } else {
  475. $result = $this->update($comment);
  476. }
  477. if ($result && !!$comment->getParentId()) {
  478. $this->updateChildrenInformation(
  479. $comment->getParentId(),
  480. $comment->getCreationDateTime()
  481. );
  482. $this->cache($comment);
  483. }
  484. return $result;
  485. }
  486. /**
  487. * inserts the provided comment in the database
  488. *
  489. * @param IComment $comment
  490. * @return bool
  491. */
  492. protected function insert(IComment &$comment) {
  493. $qb = $this->dbConn->getQueryBuilder();
  494. $affectedRows = $qb
  495. ->insert('comments')
  496. ->values([
  497. 'parent_id' => $qb->createNamedParameter($comment->getParentId()),
  498. 'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()),
  499. 'children_count' => $qb->createNamedParameter($comment->getChildrenCount()),
  500. 'actor_type' => $qb->createNamedParameter($comment->getActorType()),
  501. 'actor_id' => $qb->createNamedParameter($comment->getActorId()),
  502. 'message' => $qb->createNamedParameter($comment->getMessage()),
  503. 'verb' => $qb->createNamedParameter($comment->getVerb()),
  504. 'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'),
  505. 'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'),
  506. 'object_type' => $qb->createNamedParameter($comment->getObjectType()),
  507. 'object_id' => $qb->createNamedParameter($comment->getObjectId()),
  508. ])
  509. ->execute();
  510. if ($affectedRows > 0) {
  511. $comment->setId(strval($qb->getLastInsertId()));
  512. $this->sendEvent(CommentsEvent::EVENT_ADD, $comment);
  513. }
  514. return $affectedRows > 0;
  515. }
  516. /**
  517. * updates a Comment data row
  518. *
  519. * @param IComment $comment
  520. * @return bool
  521. * @throws NotFoundException
  522. */
  523. protected function update(IComment $comment) {
  524. // for properly working preUpdate Events we need the old comments as is
  525. // in the DB and overcome caching. Also avoid that outdated information stays.
  526. $this->uncache($comment->getId());
  527. $this->sendEvent(CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId()));
  528. $this->uncache($comment->getId());
  529. $qb = $this->dbConn->getQueryBuilder();
  530. $affectedRows = $qb
  531. ->update('comments')
  532. ->set('parent_id', $qb->createNamedParameter($comment->getParentId()))
  533. ->set('topmost_parent_id', $qb->createNamedParameter($comment->getTopmostParentId()))
  534. ->set('children_count', $qb->createNamedParameter($comment->getChildrenCount()))
  535. ->set('actor_type', $qb->createNamedParameter($comment->getActorType()))
  536. ->set('actor_id', $qb->createNamedParameter($comment->getActorId()))
  537. ->set('message', $qb->createNamedParameter($comment->getMessage()))
  538. ->set('verb', $qb->createNamedParameter($comment->getVerb()))
  539. ->set('creation_timestamp', $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'))
  540. ->set('latest_child_timestamp', $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'))
  541. ->set('object_type', $qb->createNamedParameter($comment->getObjectType()))
  542. ->set('object_id', $qb->createNamedParameter($comment->getObjectId()))
  543. ->where($qb->expr()->eq('id', $qb->createParameter('id')))
  544. ->setParameter('id', $comment->getId())
  545. ->execute();
  546. if ($affectedRows === 0) {
  547. throw new NotFoundException('Comment to update does ceased to exist');
  548. }
  549. $this->sendEvent(CommentsEvent::EVENT_UPDATE, $comment);
  550. return $affectedRows > 0;
  551. }
  552. /**
  553. * removes references to specific actor (e.g. on user delete) of a comment.
  554. * The comment itself must not get lost/deleted.
  555. *
  556. * @param string $actorType the actor type (e.g. 'users')
  557. * @param string $actorId a user id
  558. * @return boolean
  559. * @since 9.0.0
  560. */
  561. public function deleteReferencesOfActor($actorType, $actorId) {
  562. $this->checkRoleParameters('Actor', $actorType, $actorId);
  563. $qb = $this->dbConn->getQueryBuilder();
  564. $affectedRows = $qb
  565. ->update('comments')
  566. ->set('actor_type', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
  567. ->set('actor_id', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
  568. ->where($qb->expr()->eq('actor_type', $qb->createParameter('type')))
  569. ->andWhere($qb->expr()->eq('actor_id', $qb->createParameter('id')))
  570. ->setParameter('type', $actorType)
  571. ->setParameter('id', $actorId)
  572. ->execute();
  573. $this->commentsCache = [];
  574. return is_int($affectedRows);
  575. }
  576. /**
  577. * deletes all comments made of a specific object (e.g. on file delete)
  578. *
  579. * @param string $objectType the object type (e.g. 'files')
  580. * @param string $objectId e.g. the file id
  581. * @return boolean
  582. * @since 9.0.0
  583. */
  584. public function deleteCommentsAtObject($objectType, $objectId) {
  585. $this->checkRoleParameters('Object', $objectType, $objectId);
  586. $qb = $this->dbConn->getQueryBuilder();
  587. $affectedRows = $qb
  588. ->delete('comments')
  589. ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
  590. ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
  591. ->setParameter('type', $objectType)
  592. ->setParameter('id', $objectId)
  593. ->execute();
  594. $this->commentsCache = [];
  595. return is_int($affectedRows);
  596. }
  597. /**
  598. * deletes the read markers for the specified user
  599. *
  600. * @param \OCP\IUser $user
  601. * @return bool
  602. * @since 9.0.0
  603. */
  604. public function deleteReadMarksFromUser(IUser $user) {
  605. $qb = $this->dbConn->getQueryBuilder();
  606. $query = $qb->delete('comments_read_markers')
  607. ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
  608. ->setParameter('user_id', $user->getUID());
  609. try {
  610. $affectedRows = $query->execute();
  611. } catch (DriverException $e) {
  612. $this->logger->logException($e, ['app' => 'core_comments']);
  613. return false;
  614. }
  615. return ($affectedRows > 0);
  616. }
  617. /**
  618. * sets the read marker for a given file to the specified date for the
  619. * provided user
  620. *
  621. * @param string $objectType
  622. * @param string $objectId
  623. * @param \DateTime $dateTime
  624. * @param IUser $user
  625. * @since 9.0.0
  626. */
  627. public function setReadMark($objectType, $objectId, \DateTime $dateTime, IUser $user) {
  628. $this->checkRoleParameters('Object', $objectType, $objectId);
  629. $qb = $this->dbConn->getQueryBuilder();
  630. $values = [
  631. 'user_id' => $qb->createNamedParameter($user->getUID()),
  632. 'marker_datetime' => $qb->createNamedParameter($dateTime, 'datetime'),
  633. 'object_type' => $qb->createNamedParameter($objectType),
  634. 'object_id' => $qb->createNamedParameter($objectId),
  635. ];
  636. // Strategy: try to update, if this does not return affected rows, do an insert.
  637. $affectedRows = $qb
  638. ->update('comments_read_markers')
  639. ->set('user_id', $values['user_id'])
  640. ->set('marker_datetime', $values['marker_datetime'])
  641. ->set('object_type', $values['object_type'])
  642. ->set('object_id', $values['object_id'])
  643. ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
  644. ->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
  645. ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
  646. ->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
  647. ->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
  648. ->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
  649. ->execute();
  650. if ($affectedRows > 0) {
  651. return;
  652. }
  653. $qb->insert('comments_read_markers')
  654. ->values($values)
  655. ->execute();
  656. }
  657. /**
  658. * returns the read marker for a given file to the specified date for the
  659. * provided user. It returns null, when the marker is not present, i.e.
  660. * no comments were marked as read.
  661. *
  662. * @param string $objectType
  663. * @param string $objectId
  664. * @param IUser $user
  665. * @return \DateTime|null
  666. * @since 9.0.0
  667. */
  668. public function getReadMark($objectType, $objectId, IUser $user) {
  669. $qb = $this->dbConn->getQueryBuilder();
  670. $resultStatement = $qb->select('marker_datetime')
  671. ->from('comments_read_markers')
  672. ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
  673. ->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
  674. ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
  675. ->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
  676. ->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
  677. ->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
  678. ->execute();
  679. $data = $resultStatement->fetch();
  680. $resultStatement->closeCursor();
  681. if (!$data || is_null($data['marker_datetime'])) {
  682. return null;
  683. }
  684. return new \DateTime($data['marker_datetime']);
  685. }
  686. /**
  687. * deletes the read markers on the specified object
  688. *
  689. * @param string $objectType
  690. * @param string $objectId
  691. * @return bool
  692. * @since 9.0.0
  693. */
  694. public function deleteReadMarksOnObject($objectType, $objectId) {
  695. $this->checkRoleParameters('Object', $objectType, $objectId);
  696. $qb = $this->dbConn->getQueryBuilder();
  697. $query = $qb->delete('comments_read_markers')
  698. ->where($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
  699. ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
  700. ->setParameter('object_type', $objectType)
  701. ->setParameter('object_id', $objectId);
  702. try {
  703. $affectedRows = $query->execute();
  704. } catch (DriverException $e) {
  705. $this->logger->logException($e, ['app' => 'core_comments']);
  706. return false;
  707. }
  708. return ($affectedRows > 0);
  709. }
  710. /**
  711. * registers an Entity to the manager, so event notifications can be send
  712. * to consumers of the comments infrastructure
  713. *
  714. * @param \Closure $closure
  715. */
  716. public function registerEventHandler(\Closure $closure) {
  717. $this->eventHandlerClosures[] = $closure;
  718. $this->eventHandlers = [];
  719. }
  720. /**
  721. * registers a method that resolves an ID to a display name for a given type
  722. *
  723. * @param string $type
  724. * @param \Closure $closure
  725. * @throws \OutOfBoundsException
  726. * @since 11.0.0
  727. *
  728. * Only one resolver shall be registered per type. Otherwise a
  729. * \OutOfBoundsException has to thrown.
  730. */
  731. public function registerDisplayNameResolver($type, \Closure $closure) {
  732. if (!is_string($type)) {
  733. throw new \InvalidArgumentException('String expected.');
  734. }
  735. if (isset($this->displayNameResolvers[$type])) {
  736. throw new \OutOfBoundsException('Displayname resolver for this type already registered');
  737. }
  738. $this->displayNameResolvers[$type] = $closure;
  739. }
  740. /**
  741. * resolves a given ID of a given Type to a display name.
  742. *
  743. * @param string $type
  744. * @param string $id
  745. * @return string
  746. * @throws \OutOfBoundsException
  747. * @since 11.0.0
  748. *
  749. * If a provided type was not registered, an \OutOfBoundsException shall
  750. * be thrown. It is upon the resolver discretion what to return of the
  751. * provided ID is unknown. It must be ensured that a string is returned.
  752. */
  753. public function resolveDisplayName($type, $id) {
  754. if (!is_string($type)) {
  755. throw new \InvalidArgumentException('String expected.');
  756. }
  757. if (!isset($this->displayNameResolvers[$type])) {
  758. throw new \OutOfBoundsException('No Displayname resolver for this type registered');
  759. }
  760. return (string)$this->displayNameResolvers[$type]($id);
  761. }
  762. /**
  763. * returns valid, registered entities
  764. *
  765. * @return \OCP\Comments\ICommentsEventHandler[]
  766. */
  767. private function getEventHandlers() {
  768. if (!empty($this->eventHandlers)) {
  769. return $this->eventHandlers;
  770. }
  771. $this->eventHandlers = [];
  772. foreach ($this->eventHandlerClosures as $name => $closure) {
  773. $entity = $closure();
  774. if (!($entity instanceof ICommentsEventHandler)) {
  775. throw new \InvalidArgumentException('The given entity does not implement the ICommentsEntity interface');
  776. }
  777. $this->eventHandlers[$name] = $entity;
  778. }
  779. return $this->eventHandlers;
  780. }
  781. /**
  782. * sends notifications to the registered entities
  783. *
  784. * @param $eventType
  785. * @param IComment $comment
  786. */
  787. private function sendEvent($eventType, IComment $comment) {
  788. $entities = $this->getEventHandlers();
  789. $event = new CommentsEvent($eventType, $comment);
  790. foreach ($entities as $entity) {
  791. $entity->handle($event);
  792. }
  793. }
  794. }