Manager.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  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\ICommentsManager;
  29. use OCP\Comments\NotFoundException;
  30. use OCP\DB\QueryBuilder\IQueryBuilder;
  31. use OCP\IDBConnection;
  32. use OCP\IConfig;
  33. use OCP\ILogger;
  34. use OCP\IUser;
  35. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  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 EventDispatcherInterface */
  44. protected $dispatcher;
  45. /** @var IComment[] */
  46. protected $commentsCache = [];
  47. /**
  48. * Manager constructor.
  49. *
  50. * @param IDBConnection $dbConn
  51. * @param ILogger $logger
  52. * @param IConfig $config
  53. * @param EventDispatcherInterface $dispatcher
  54. */
  55. public function __construct(
  56. IDBConnection $dbConn,
  57. ILogger $logger,
  58. IConfig $config,
  59. EventDispatcherInterface $dispatcher
  60. ) {
  61. $this->dbConn = $dbConn;
  62. $this->logger = $logger;
  63. $this->config = $config;
  64. $this->dispatcher = $dispatcher;
  65. }
  66. /**
  67. * converts data base data into PHP native, proper types as defined by
  68. * IComment interface.
  69. *
  70. * @param array $data
  71. * @return array
  72. */
  73. protected function normalizeDatabaseData(array $data) {
  74. $data['id'] = strval($data['id']);
  75. $data['parent_id'] = strval($data['parent_id']);
  76. $data['topmost_parent_id'] = strval($data['topmost_parent_id']);
  77. $data['creation_timestamp'] = new \DateTime($data['creation_timestamp']);
  78. if (!is_null($data['latest_child_timestamp'])) {
  79. $data['latest_child_timestamp'] = new \DateTime($data['latest_child_timestamp']);
  80. }
  81. $data['children_count'] = intval($data['children_count']);
  82. return $data;
  83. }
  84. /**
  85. * prepares a comment for an insert or update operation after making sure
  86. * all necessary fields have a value assigned.
  87. *
  88. * @param IComment $comment
  89. * @return IComment returns the same updated IComment instance as provided
  90. * by parameter for convenience
  91. * @throws \UnexpectedValueException
  92. */
  93. protected function prepareCommentForDatabaseWrite(IComment $comment) {
  94. if( !$comment->getActorType()
  95. || !$comment->getActorId()
  96. || !$comment->getObjectType()
  97. || !$comment->getObjectId()
  98. || !$comment->getVerb()
  99. ) {
  100. throw new \UnexpectedValueException('Actor, Object and Verb information must be provided for saving');
  101. }
  102. if($comment->getId() === '') {
  103. $comment->setChildrenCount(0);
  104. $comment->setLatestChildDateTime(new \DateTime('0000-00-00 00:00:00', new \DateTimeZone('UTC')));
  105. $comment->setLatestChildDateTime(null);
  106. }
  107. if(is_null($comment->getCreationDateTime())) {
  108. $comment->setCreationDateTime(new \DateTime());
  109. }
  110. if($comment->getParentId() !== '0') {
  111. $comment->setTopmostParentId($this->determineTopmostParentId($comment->getParentId()));
  112. } else {
  113. $comment->setTopmostParentId('0');
  114. }
  115. $this->cache($comment);
  116. return $comment;
  117. }
  118. /**
  119. * returns the topmost parent id of a given comment identified by ID
  120. *
  121. * @param string $id
  122. * @return string
  123. * @throws NotFoundException
  124. */
  125. protected function determineTopmostParentId($id) {
  126. $comment = $this->get($id);
  127. if($comment->getParentId() === '0') {
  128. return $comment->getId();
  129. } else {
  130. return $this->determineTopmostParentId($comment->getId());
  131. }
  132. }
  133. /**
  134. * updates child information of a comment
  135. *
  136. * @param string $id
  137. * @param \DateTime $cDateTime the date time of the most recent child
  138. * @throws NotFoundException
  139. */
  140. protected function updateChildrenInformation($id, \DateTime $cDateTime) {
  141. $qb = $this->dbConn->getQueryBuilder();
  142. $query = $qb->select($qb->createFunction('COUNT(`id`)'))
  143. ->from('comments')
  144. ->where($qb->expr()->eq('parent_id', $qb->createParameter('id')))
  145. ->setParameter('id', $id);
  146. $resultStatement = $query->execute();
  147. $data = $resultStatement->fetch(\PDO::FETCH_NUM);
  148. $resultStatement->closeCursor();
  149. $children = intval($data[0]);
  150. $comment = $this->get($id);
  151. $comment->setChildrenCount($children);
  152. $comment->setLatestChildDateTime($cDateTime);
  153. $this->save($comment);
  154. }
  155. /**
  156. * Tests whether actor or object type and id parameters are acceptable.
  157. * Throws exception if not.
  158. *
  159. * @param string $role
  160. * @param string $type
  161. * @param string $id
  162. * @throws \InvalidArgumentException
  163. */
  164. protected function checkRoleParameters($role, $type, $id) {
  165. if(
  166. !is_string($type) || empty($type)
  167. || !is_string($id) || empty($id)
  168. ) {
  169. throw new \InvalidArgumentException($role . ' parameters must be string and not empty');
  170. }
  171. }
  172. /**
  173. * run-time caches a comment
  174. *
  175. * @param IComment $comment
  176. */
  177. protected function cache(IComment $comment) {
  178. $id = $comment->getId();
  179. if(empty($id)) {
  180. return;
  181. }
  182. $this->commentsCache[strval($id)] = $comment;
  183. }
  184. /**
  185. * removes an entry from the comments run time cache
  186. *
  187. * @param mixed $id the comment's id
  188. */
  189. protected function uncache($id) {
  190. $id = strval($id);
  191. if (isset($this->commentsCache[$id])) {
  192. unset($this->commentsCache[$id]);
  193. }
  194. }
  195. /**
  196. * returns a comment instance
  197. *
  198. * @param string $id the ID of the comment
  199. * @return IComment
  200. * @throws NotFoundException
  201. * @throws \InvalidArgumentException
  202. * @since 9.0.0
  203. */
  204. public function get($id) {
  205. if(intval($id) === 0) {
  206. throw new \InvalidArgumentException('IDs must be translatable to a number in this implementation.');
  207. }
  208. if(isset($this->commentsCache[$id])) {
  209. return $this->commentsCache[$id];
  210. }
  211. $qb = $this->dbConn->getQueryBuilder();
  212. $resultStatement = $qb->select('*')
  213. ->from('comments')
  214. ->where($qb->expr()->eq('id', $qb->createParameter('id')))
  215. ->setParameter('id', $id, IQueryBuilder::PARAM_INT)
  216. ->execute();
  217. $data = $resultStatement->fetch();
  218. $resultStatement->closeCursor();
  219. if(!$data) {
  220. throw new NotFoundException();
  221. }
  222. $comment = new Comment($this->normalizeDatabaseData($data));
  223. $this->cache($comment);
  224. return $comment;
  225. }
  226. /**
  227. * returns the comment specified by the id and all it's child comments.
  228. * At this point of time, we do only support one level depth.
  229. *
  230. * @param string $id
  231. * @param int $limit max number of entries to return, 0 returns all
  232. * @param int $offset the start entry
  233. * @return array
  234. * @since 9.0.0
  235. *
  236. * The return array looks like this
  237. * [
  238. * 'comment' => IComment, // root comment
  239. * 'replies' =>
  240. * [
  241. * 0 =>
  242. * [
  243. * 'comment' => IComment,
  244. * 'replies' => []
  245. * ]
  246. * 1 =>
  247. * [
  248. * 'comment' => IComment,
  249. * 'replies'=> []
  250. * ],
  251. * …
  252. * ]
  253. * ]
  254. */
  255. public function getTree($id, $limit = 0, $offset = 0) {
  256. $tree = [];
  257. $tree['comment'] = $this->get($id);
  258. $tree['replies'] = [];
  259. $qb = $this->dbConn->getQueryBuilder();
  260. $query = $qb->select('*')
  261. ->from('comments')
  262. ->where($qb->expr()->eq('topmost_parent_id', $qb->createParameter('id')))
  263. ->orderBy('creation_timestamp', 'DESC')
  264. ->setParameter('id', $id);
  265. if($limit > 0) {
  266. $query->setMaxResults($limit);
  267. }
  268. if($offset > 0) {
  269. $query->setFirstResult($offset);
  270. }
  271. $resultStatement = $query->execute();
  272. while($data = $resultStatement->fetch()) {
  273. $comment = new Comment($this->normalizeDatabaseData($data));
  274. $this->cache($comment);
  275. $tree['replies'][] = [
  276. 'comment' => $comment,
  277. 'replies' => []
  278. ];
  279. }
  280. $resultStatement->closeCursor();
  281. return $tree;
  282. }
  283. /**
  284. * returns comments for a specific object (e.g. a file).
  285. *
  286. * The sort order is always newest to oldest.
  287. *
  288. * @param string $objectType the object type, e.g. 'files'
  289. * @param string $objectId the id of the object
  290. * @param int $limit optional, number of maximum comments to be returned. if
  291. * not specified, all comments are returned.
  292. * @param int $offset optional, starting point
  293. * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
  294. * that may be returned
  295. * @return IComment[]
  296. * @since 9.0.0
  297. */
  298. public function getForObject(
  299. $objectType,
  300. $objectId,
  301. $limit = 0,
  302. $offset = 0,
  303. \DateTime $notOlderThan = null
  304. ) {
  305. $comments = [];
  306. $qb = $this->dbConn->getQueryBuilder();
  307. $query = $qb->select('*')
  308. ->from('comments')
  309. ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
  310. ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
  311. ->orderBy('creation_timestamp', 'DESC')
  312. ->setParameter('type', $objectType)
  313. ->setParameter('id', $objectId);
  314. if($limit > 0) {
  315. $query->setMaxResults($limit);
  316. }
  317. if($offset > 0) {
  318. $query->setFirstResult($offset);
  319. }
  320. if(!is_null($notOlderThan)) {
  321. $query
  322. ->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
  323. ->setParameter('notOlderThan', $notOlderThan, 'datetime');
  324. }
  325. $resultStatement = $query->execute();
  326. while($data = $resultStatement->fetch()) {
  327. $comment = new Comment($this->normalizeDatabaseData($data));
  328. $this->cache($comment);
  329. $comments[] = $comment;
  330. }
  331. $resultStatement->closeCursor();
  332. return $comments;
  333. }
  334. /**
  335. * @param $objectType string the object type, e.g. 'files'
  336. * @param $objectId string the id of the object
  337. * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
  338. * that may be returned
  339. * @return Int
  340. * @since 9.0.0
  341. */
  342. public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null) {
  343. $qb = $this->dbConn->getQueryBuilder();
  344. $query = $qb->select($qb->createFunction('COUNT(`id`)'))
  345. ->from('comments')
  346. ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
  347. ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
  348. ->setParameter('type', $objectType)
  349. ->setParameter('id', $objectId);
  350. if(!is_null($notOlderThan)) {
  351. $query
  352. ->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
  353. ->setParameter('notOlderThan', $notOlderThan, 'datetime');
  354. }
  355. $resultStatement = $query->execute();
  356. $data = $resultStatement->fetch(\PDO::FETCH_NUM);
  357. $resultStatement->closeCursor();
  358. return intval($data[0]);
  359. }
  360. /**
  361. * creates a new comment and returns it. At this point of time, it is not
  362. * saved in the used data storage. Use save() after setting other fields
  363. * of the comment (e.g. message or verb).
  364. *
  365. * @param string $actorType the actor type (e.g. 'users')
  366. * @param string $actorId a user id
  367. * @param string $objectType the object type the comment is attached to
  368. * @param string $objectId the object id the comment is attached to
  369. * @return IComment
  370. * @since 9.0.0
  371. */
  372. public function create($actorType, $actorId, $objectType, $objectId) {
  373. $comment = new Comment();
  374. $comment
  375. ->setActor($actorType, $actorId)
  376. ->setObject($objectType, $objectId);
  377. return $comment;
  378. }
  379. /**
  380. * permanently deletes the comment specified by the ID
  381. *
  382. * When the comment has child comments, their parent ID will be changed to
  383. * the parent ID of the item that is to be deleted.
  384. *
  385. * @param string $id
  386. * @return bool
  387. * @throws \InvalidArgumentException
  388. * @since 9.0.0
  389. */
  390. public function delete($id) {
  391. if(!is_string($id)) {
  392. throw new \InvalidArgumentException('Parameter must be string');
  393. }
  394. try {
  395. $comment = $this->get($id);
  396. } catch (\Exception $e) {
  397. // Ignore exceptions, we just don't fire a hook then
  398. $comment = null;
  399. }
  400. $qb = $this->dbConn->getQueryBuilder();
  401. $query = $qb->delete('comments')
  402. ->where($qb->expr()->eq('id', $qb->createParameter('id')))
  403. ->setParameter('id', $id);
  404. try {
  405. $affectedRows = $query->execute();
  406. $this->uncache($id);
  407. } catch (DriverException $e) {
  408. $this->logger->logException($e, ['app' => 'core_comments']);
  409. return false;
  410. }
  411. if ($affectedRows > 0 && $comment instanceof IComment) {
  412. $this->dispatcher->dispatch(CommentsEvent::EVENT_DELETE, new CommentsEvent(
  413. CommentsEvent::EVENT_DELETE,
  414. $comment
  415. ));
  416. }
  417. return ($affectedRows > 0);
  418. }
  419. /**
  420. * saves the comment permanently
  421. *
  422. * if the supplied comment has an empty ID, a new entry comment will be
  423. * saved and the instance updated with the new ID.
  424. *
  425. * Otherwise, an existing comment will be updated.
  426. *
  427. * Throws NotFoundException when a comment that is to be updated does not
  428. * exist anymore at this point of time.
  429. *
  430. * @param IComment $comment
  431. * @return bool
  432. * @throws NotFoundException
  433. * @since 9.0.0
  434. */
  435. public function save(IComment $comment) {
  436. if($this->prepareCommentForDatabaseWrite($comment)->getId() === '') {
  437. $result = $this->insert($comment);
  438. } else {
  439. $result = $this->update($comment);
  440. }
  441. if($result && !!$comment->getParentId()) {
  442. $this->updateChildrenInformation(
  443. $comment->getParentId(),
  444. $comment->getCreationDateTime()
  445. );
  446. $this->cache($comment);
  447. }
  448. return $result;
  449. }
  450. /**
  451. * inserts the provided comment in the database
  452. *
  453. * @param IComment $comment
  454. * @return bool
  455. */
  456. protected function insert(IComment &$comment) {
  457. $qb = $this->dbConn->getQueryBuilder();
  458. $affectedRows = $qb
  459. ->insert('comments')
  460. ->values([
  461. 'parent_id' => $qb->createNamedParameter($comment->getParentId()),
  462. 'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()),
  463. 'children_count' => $qb->createNamedParameter($comment->getChildrenCount()),
  464. 'actor_type' => $qb->createNamedParameter($comment->getActorType()),
  465. 'actor_id' => $qb->createNamedParameter($comment->getActorId()),
  466. 'message' => $qb->createNamedParameter($comment->getMessage()),
  467. 'verb' => $qb->createNamedParameter($comment->getVerb()),
  468. 'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'),
  469. 'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'),
  470. 'object_type' => $qb->createNamedParameter($comment->getObjectType()),
  471. 'object_id' => $qb->createNamedParameter($comment->getObjectId()),
  472. ])
  473. ->execute();
  474. if ($affectedRows > 0) {
  475. $comment->setId(strval($qb->getLastInsertId()));
  476. }
  477. $this->dispatcher->dispatch(CommentsEvent::EVENT_ADD, new CommentsEvent(
  478. CommentsEvent::EVENT_ADD,
  479. $comment
  480. ));
  481. return $affectedRows > 0;
  482. }
  483. /**
  484. * updates a Comment data row
  485. *
  486. * @param IComment $comment
  487. * @return bool
  488. * @throws NotFoundException
  489. */
  490. protected function update(IComment $comment) {
  491. $qb = $this->dbConn->getQueryBuilder();
  492. $affectedRows = $qb
  493. ->update('comments')
  494. ->set('parent_id', $qb->createNamedParameter($comment->getParentId()))
  495. ->set('topmost_parent_id', $qb->createNamedParameter($comment->getTopmostParentId()))
  496. ->set('children_count', $qb->createNamedParameter($comment->getChildrenCount()))
  497. ->set('actor_type', $qb->createNamedParameter($comment->getActorType()))
  498. ->set('actor_id', $qb->createNamedParameter($comment->getActorId()))
  499. ->set('message', $qb->createNamedParameter($comment->getMessage()))
  500. ->set('verb', $qb->createNamedParameter($comment->getVerb()))
  501. ->set('creation_timestamp', $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'))
  502. ->set('latest_child_timestamp', $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'))
  503. ->set('object_type', $qb->createNamedParameter($comment->getObjectType()))
  504. ->set('object_id', $qb->createNamedParameter($comment->getObjectId()))
  505. ->where($qb->expr()->eq('id', $qb->createParameter('id')))
  506. ->setParameter('id', $comment->getId())
  507. ->execute();
  508. if($affectedRows === 0) {
  509. throw new NotFoundException('Comment to update does ceased to exist');
  510. }
  511. $this->dispatcher->dispatch(CommentsEvent::EVENT_UPDATE, new CommentsEvent(
  512. CommentsEvent::EVENT_UPDATE,
  513. $comment
  514. ));
  515. return $affectedRows > 0;
  516. }
  517. /**
  518. * removes references to specific actor (e.g. on user delete) of a comment.
  519. * The comment itself must not get lost/deleted.
  520. *
  521. * @param string $actorType the actor type (e.g. 'users')
  522. * @param string $actorId a user id
  523. * @return boolean
  524. * @since 9.0.0
  525. */
  526. public function deleteReferencesOfActor($actorType, $actorId) {
  527. $this->checkRoleParameters('Actor', $actorType, $actorId);
  528. $qb = $this->dbConn->getQueryBuilder();
  529. $affectedRows = $qb
  530. ->update('comments')
  531. ->set('actor_type', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
  532. ->set('actor_id', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
  533. ->where($qb->expr()->eq('actor_type', $qb->createParameter('type')))
  534. ->andWhere($qb->expr()->eq('actor_id', $qb->createParameter('id')))
  535. ->setParameter('type', $actorType)
  536. ->setParameter('id', $actorId)
  537. ->execute();
  538. $this->commentsCache = [];
  539. return is_int($affectedRows);
  540. }
  541. /**
  542. * deletes all comments made of a specific object (e.g. on file delete)
  543. *
  544. * @param string $objectType the object type (e.g. 'files')
  545. * @param string $objectId e.g. the file id
  546. * @return boolean
  547. * @since 9.0.0
  548. */
  549. public function deleteCommentsAtObject($objectType, $objectId) {
  550. $this->checkRoleParameters('Object', $objectType, $objectId);
  551. $qb = $this->dbConn->getQueryBuilder();
  552. $affectedRows = $qb
  553. ->delete('comments')
  554. ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
  555. ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
  556. ->setParameter('type', $objectType)
  557. ->setParameter('id', $objectId)
  558. ->execute();
  559. $this->commentsCache = [];
  560. return is_int($affectedRows);
  561. }
  562. /**
  563. * deletes the read markers for the specified user
  564. *
  565. * @param \OCP\IUser $user
  566. * @return bool
  567. * @since 9.0.0
  568. */
  569. public function deleteReadMarksFromUser(IUser $user) {
  570. $qb = $this->dbConn->getQueryBuilder();
  571. $query = $qb->delete('comments_read_markers')
  572. ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
  573. ->setParameter('user_id', $user->getUID());
  574. try {
  575. $affectedRows = $query->execute();
  576. } catch (DriverException $e) {
  577. $this->logger->logException($e, ['app' => 'core_comments']);
  578. return false;
  579. }
  580. return ($affectedRows > 0);
  581. }
  582. /**
  583. * sets the read marker for a given file to the specified date for the
  584. * provided user
  585. *
  586. * @param string $objectType
  587. * @param string $objectId
  588. * @param \DateTime $dateTime
  589. * @param IUser $user
  590. * @since 9.0.0
  591. */
  592. public function setReadMark($objectType, $objectId, \DateTime $dateTime, IUser $user) {
  593. $this->checkRoleParameters('Object', $objectType, $objectId);
  594. $qb = $this->dbConn->getQueryBuilder();
  595. $values = [
  596. 'user_id' => $qb->createNamedParameter($user->getUID()),
  597. 'marker_datetime' => $qb->createNamedParameter($dateTime, 'datetime'),
  598. 'object_type' => $qb->createNamedParameter($objectType),
  599. 'object_id' => $qb->createNamedParameter($objectId),
  600. ];
  601. // Strategy: try to update, if this does not return affected rows, do an insert.
  602. $affectedRows = $qb
  603. ->update('comments_read_markers')
  604. ->set('user_id', $values['user_id'])
  605. ->set('marker_datetime', $values['marker_datetime'])
  606. ->set('object_type', $values['object_type'])
  607. ->set('object_id', $values['object_id'])
  608. ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
  609. ->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
  610. ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
  611. ->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
  612. ->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
  613. ->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
  614. ->execute();
  615. if ($affectedRows > 0) {
  616. return;
  617. }
  618. $qb->insert('comments_read_markers')
  619. ->values($values)
  620. ->execute();
  621. }
  622. /**
  623. * returns the read marker for a given file to the specified date for the
  624. * provided user. It returns null, when the marker is not present, i.e.
  625. * no comments were marked as read.
  626. *
  627. * @param string $objectType
  628. * @param string $objectId
  629. * @param IUser $user
  630. * @return \DateTime|null
  631. * @since 9.0.0
  632. */
  633. public function getReadMark($objectType, $objectId, IUser $user) {
  634. $qb = $this->dbConn->getQueryBuilder();
  635. $resultStatement = $qb->select('marker_datetime')
  636. ->from('comments_read_markers')
  637. ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
  638. ->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
  639. ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
  640. ->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
  641. ->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
  642. ->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
  643. ->execute();
  644. $data = $resultStatement->fetch();
  645. $resultStatement->closeCursor();
  646. if(!$data || is_null($data['marker_datetime'])) {
  647. return null;
  648. }
  649. return new \DateTime($data['marker_datetime']);
  650. }
  651. /**
  652. * deletes the read markers on the specified object
  653. *
  654. * @param string $objectType
  655. * @param string $objectId
  656. * @return bool
  657. * @since 9.0.0
  658. */
  659. public function deleteReadMarksOnObject($objectType, $objectId) {
  660. $this->checkRoleParameters('Object', $objectType, $objectId);
  661. $qb = $this->dbConn->getQueryBuilder();
  662. $query = $qb->delete('comments_read_markers')
  663. ->where($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
  664. ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
  665. ->setParameter('object_type', $objectType)
  666. ->setParameter('object_id', $objectId);
  667. try {
  668. $affectedRows = $query->execute();
  669. } catch (DriverException $e) {
  670. $this->logger->logException($e, ['app' => 'core_comments']);
  671. return false;
  672. }
  673. return ($affectedRows > 0);
  674. }
  675. }