Passed
Push — master ( 689523...aad327 )
by Blizzz
11:29
created
lib/private/Comments/Manager.php 1 patch
Indentation   +1028 added lines, -1028 removed lines patch added patch discarded remove patch
@@ -41,1032 +41,1032 @@
 block discarded – undo
41 41
 
42 42
 class Manager implements ICommentsManager {
43 43
 
44
-	/** @var  IDBConnection */
45
-	protected $dbConn;
46
-
47
-	/** @var  ILogger */
48
-	protected $logger;
49
-
50
-	/** @var IConfig */
51
-	protected $config;
52
-
53
-	/** @var IComment[] */
54
-	protected $commentsCache = [];
55
-
56
-	/** @var  \Closure[] */
57
-	protected $eventHandlerClosures = [];
58
-
59
-	/** @var  ICommentsEventHandler[] */
60
-	protected $eventHandlers = [];
61
-
62
-	/** @var \Closure[] */
63
-	protected $displayNameResolvers = [];
64
-
65
-	/**
66
-	 * Manager constructor.
67
-	 *
68
-	 * @param IDBConnection $dbConn
69
-	 * @param ILogger $logger
70
-	 * @param IConfig $config
71
-	 */
72
-	public function __construct(
73
-		IDBConnection $dbConn,
74
-		ILogger $logger,
75
-		IConfig $config
76
-	) {
77
-		$this->dbConn = $dbConn;
78
-		$this->logger = $logger;
79
-		$this->config = $config;
80
-	}
81
-
82
-	/**
83
-	 * converts data base data into PHP native, proper types as defined by
84
-	 * IComment interface.
85
-	 *
86
-	 * @param array $data
87
-	 * @return array
88
-	 */
89
-	protected function normalizeDatabaseData(array $data) {
90
-		$data['id'] = (string)$data['id'];
91
-		$data['parent_id'] = (string)$data['parent_id'];
92
-		$data['topmost_parent_id'] = (string)$data['topmost_parent_id'];
93
-		$data['creation_timestamp'] = new \DateTime($data['creation_timestamp']);
94
-		if (!is_null($data['latest_child_timestamp'])) {
95
-			$data['latest_child_timestamp'] = new \DateTime($data['latest_child_timestamp']);
96
-		}
97
-		$data['children_count'] = (int)$data['children_count'];
98
-		return $data;
99
-	}
100
-
101
-	/**
102
-	 * prepares a comment for an insert or update operation after making sure
103
-	 * all necessary fields have a value assigned.
104
-	 *
105
-	 * @param IComment $comment
106
-	 * @return IComment returns the same updated IComment instance as provided
107
-	 *                  by parameter for convenience
108
-	 * @throws \UnexpectedValueException
109
-	 */
110
-	protected function prepareCommentForDatabaseWrite(IComment $comment) {
111
-		if (!$comment->getActorType()
112
-			|| !$comment->getActorId()
113
-			|| !$comment->getObjectType()
114
-			|| !$comment->getObjectId()
115
-			|| !$comment->getVerb()
116
-		) {
117
-			throw new \UnexpectedValueException('Actor, Object and Verb information must be provided for saving');
118
-		}
119
-
120
-		if ($comment->getId() === '') {
121
-			$comment->setChildrenCount(0);
122
-			$comment->setLatestChildDateTime(new \DateTime('0000-00-00 00:00:00', new \DateTimeZone('UTC')));
123
-			$comment->setLatestChildDateTime(null);
124
-		}
125
-
126
-		if (is_null($comment->getCreationDateTime())) {
127
-			$comment->setCreationDateTime(new \DateTime());
128
-		}
129
-
130
-		if ($comment->getParentId() !== '0') {
131
-			$comment->setTopmostParentId($this->determineTopmostParentId($comment->getParentId()));
132
-		} else {
133
-			$comment->setTopmostParentId('0');
134
-		}
135
-
136
-		$this->cache($comment);
137
-
138
-		return $comment;
139
-	}
140
-
141
-	/**
142
-	 * returns the topmost parent id of a given comment identified by ID
143
-	 *
144
-	 * @param string $id
145
-	 * @return string
146
-	 * @throws NotFoundException
147
-	 */
148
-	protected function determineTopmostParentId($id) {
149
-		$comment = $this->get($id);
150
-		if ($comment->getParentId() === '0') {
151
-			return $comment->getId();
152
-		} else {
153
-			return $this->determineTopmostParentId($comment->getId());
154
-		}
155
-	}
156
-
157
-	/**
158
-	 * updates child information of a comment
159
-	 *
160
-	 * @param string $id
161
-	 * @param \DateTime $cDateTime the date time of the most recent child
162
-	 * @throws NotFoundException
163
-	 */
164
-	protected function updateChildrenInformation($id, \DateTime $cDateTime) {
165
-		$qb = $this->dbConn->getQueryBuilder();
166
-		$query = $qb->select($qb->func()->count('id'))
167
-			->from('comments')
168
-			->where($qb->expr()->eq('parent_id', $qb->createParameter('id')))
169
-			->setParameter('id', $id);
170
-
171
-		$resultStatement = $query->execute();
172
-		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
173
-		$resultStatement->closeCursor();
174
-		$children = (int)$data[0];
175
-
176
-		$comment = $this->get($id);
177
-		$comment->setChildrenCount($children);
178
-		$comment->setLatestChildDateTime($cDateTime);
179
-		$this->save($comment);
180
-	}
181
-
182
-	/**
183
-	 * Tests whether actor or object type and id parameters are acceptable.
184
-	 * Throws exception if not.
185
-	 *
186
-	 * @param string $role
187
-	 * @param string $type
188
-	 * @param string $id
189
-	 * @throws \InvalidArgumentException
190
-	 */
191
-	protected function checkRoleParameters($role, $type, $id) {
192
-		if (
193
-			!is_string($type) || empty($type)
194
-			|| !is_string($id) || empty($id)
195
-		) {
196
-			throw new \InvalidArgumentException($role . ' parameters must be string and not empty');
197
-		}
198
-	}
199
-
200
-	/**
201
-	 * run-time caches a comment
202
-	 *
203
-	 * @param IComment $comment
204
-	 */
205
-	protected function cache(IComment $comment) {
206
-		$id = $comment->getId();
207
-		if (empty($id)) {
208
-			return;
209
-		}
210
-		$this->commentsCache[(string)$id] = $comment;
211
-	}
212
-
213
-	/**
214
-	 * removes an entry from the comments run time cache
215
-	 *
216
-	 * @param mixed $id the comment's id
217
-	 */
218
-	protected function uncache($id) {
219
-		$id = (string)$id;
220
-		if (isset($this->commentsCache[$id])) {
221
-			unset($this->commentsCache[$id]);
222
-		}
223
-	}
224
-
225
-	/**
226
-	 * returns a comment instance
227
-	 *
228
-	 * @param string $id the ID of the comment
229
-	 * @return IComment
230
-	 * @throws NotFoundException
231
-	 * @throws \InvalidArgumentException
232
-	 * @since 9.0.0
233
-	 */
234
-	public function get($id) {
235
-		if ((int)$id === 0) {
236
-			throw new \InvalidArgumentException('IDs must be translatable to a number in this implementation.');
237
-		}
238
-
239
-		if (isset($this->commentsCache[$id])) {
240
-			return $this->commentsCache[$id];
241
-		}
242
-
243
-		$qb = $this->dbConn->getQueryBuilder();
244
-		$resultStatement = $qb->select('*')
245
-			->from('comments')
246
-			->where($qb->expr()->eq('id', $qb->createParameter('id')))
247
-			->setParameter('id', $id, IQueryBuilder::PARAM_INT)
248
-			->execute();
249
-
250
-		$data = $resultStatement->fetch();
251
-		$resultStatement->closeCursor();
252
-		if (!$data) {
253
-			throw new NotFoundException();
254
-		}
255
-
256
-		$comment = new Comment($this->normalizeDatabaseData($data));
257
-		$this->cache($comment);
258
-		return $comment;
259
-	}
260
-
261
-	/**
262
-	 * returns the comment specified by the id and all it's child comments.
263
-	 * At this point of time, we do only support one level depth.
264
-	 *
265
-	 * @param string $id
266
-	 * @param int $limit max number of entries to return, 0 returns all
267
-	 * @param int $offset the start entry
268
-	 * @return array
269
-	 * @since 9.0.0
270
-	 *
271
-	 * The return array looks like this
272
-	 * [
273
-	 *   'comment' => IComment, // root comment
274
-	 *   'replies' =>
275
-	 *   [
276
-	 *     0 =>
277
-	 *     [
278
-	 *       'comment' => IComment,
279
-	 *       'replies' => []
280
-	 *     ]
281
-	 *     1 =>
282
-	 *     [
283
-	 *       'comment' => IComment,
284
-	 *       'replies'=> []
285
-	 *     ],
286
-	 *     …
287
-	 *   ]
288
-	 * ]
289
-	 */
290
-	public function getTree($id, $limit = 0, $offset = 0) {
291
-		$tree = [];
292
-		$tree['comment'] = $this->get($id);
293
-		$tree['replies'] = [];
294
-
295
-		$qb = $this->dbConn->getQueryBuilder();
296
-		$query = $qb->select('*')
297
-			->from('comments')
298
-			->where($qb->expr()->eq('topmost_parent_id', $qb->createParameter('id')))
299
-			->orderBy('creation_timestamp', 'DESC')
300
-			->setParameter('id', $id);
301
-
302
-		if ($limit > 0) {
303
-			$query->setMaxResults($limit);
304
-		}
305
-		if ($offset > 0) {
306
-			$query->setFirstResult($offset);
307
-		}
308
-
309
-		$resultStatement = $query->execute();
310
-		while ($data = $resultStatement->fetch()) {
311
-			$comment = new Comment($this->normalizeDatabaseData($data));
312
-			$this->cache($comment);
313
-			$tree['replies'][] = [
314
-				'comment' => $comment,
315
-				'replies' => []
316
-			];
317
-		}
318
-		$resultStatement->closeCursor();
319
-
320
-		return $tree;
321
-	}
322
-
323
-	/**
324
-	 * returns comments for a specific object (e.g. a file).
325
-	 *
326
-	 * The sort order is always newest to oldest.
327
-	 *
328
-	 * @param string $objectType the object type, e.g. 'files'
329
-	 * @param string $objectId the id of the object
330
-	 * @param int $limit optional, number of maximum comments to be returned. if
331
-	 * not specified, all comments are returned.
332
-	 * @param int $offset optional, starting point
333
-	 * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
334
-	 * that may be returned
335
-	 * @return IComment[]
336
-	 * @since 9.0.0
337
-	 */
338
-	public function getForObject(
339
-		$objectType,
340
-		$objectId,
341
-		$limit = 0,
342
-		$offset = 0,
343
-		\DateTime $notOlderThan = null
344
-	) {
345
-		$comments = [];
346
-
347
-		$qb = $this->dbConn->getQueryBuilder();
348
-		$query = $qb->select('*')
349
-			->from('comments')
350
-			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
351
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
352
-			->orderBy('creation_timestamp', 'DESC')
353
-			->setParameter('type', $objectType)
354
-			->setParameter('id', $objectId);
355
-
356
-		if ($limit > 0) {
357
-			$query->setMaxResults($limit);
358
-		}
359
-		if ($offset > 0) {
360
-			$query->setFirstResult($offset);
361
-		}
362
-		if (!is_null($notOlderThan)) {
363
-			$query
364
-				->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
365
-				->setParameter('notOlderThan', $notOlderThan, 'datetime');
366
-		}
367
-
368
-		$resultStatement = $query->execute();
369
-		while ($data = $resultStatement->fetch()) {
370
-			$comment = new Comment($this->normalizeDatabaseData($data));
371
-			$this->cache($comment);
372
-			$comments[] = $comment;
373
-		}
374
-		$resultStatement->closeCursor();
375
-
376
-		return $comments;
377
-	}
378
-
379
-	/**
380
-	 * @param string $objectType the object type, e.g. 'files'
381
-	 * @param string $objectId the id of the object
382
-	 * @param int $lastKnownCommentId the last known comment (will be used as offset)
383
-	 * @param string $sortDirection direction of the comments (`asc` or `desc`)
384
-	 * @param int $limit optional, number of maximum comments to be returned. if
385
-	 * set to 0, all comments are returned.
386
-	 * @return IComment[]
387
-	 * @return array
388
-	 */
389
-	public function getForObjectSince(
390
-		string $objectType,
391
-		string $objectId,
392
-		int $lastKnownCommentId,
393
-		string $sortDirection = 'asc',
394
-		int $limit = 30
395
-	): array {
396
-		$comments = [];
397
-
398
-		$query = $this->dbConn->getQueryBuilder();
399
-		$query->select('*')
400
-			->from('comments')
401
-			->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
402
-			->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
403
-			->orderBy('creation_timestamp', $sortDirection === 'desc' ? 'DESC' : 'ASC')
404
-			->addOrderBy('id', $sortDirection === 'desc' ? 'DESC' : 'ASC');
405
-
406
-		if ($limit > 0) {
407
-			$query->setMaxResults($limit);
408
-		}
409
-
410
-		$lastKnownComment = $lastKnownCommentId > 0 ? $this->getLastKnownComment(
411
-			$objectType,
412
-			$objectId,
413
-			$lastKnownCommentId
414
-		) : null;
415
-		if ($lastKnownComment instanceof IComment) {
416
-			$lastKnownCommentDateTime = $lastKnownComment->getCreationDateTime();
417
-			if ($sortDirection === 'desc') {
418
-				$query->andWhere(
419
-					$query->expr()->orX(
420
-						$query->expr()->lt(
421
-							'creation_timestamp',
422
-							$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
423
-							IQueryBuilder::PARAM_DATE
424
-						),
425
-						$query->expr()->andX(
426
-							$query->expr()->eq(
427
-								'creation_timestamp',
428
-								$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
429
-								IQueryBuilder::PARAM_DATE
430
-							),
431
-							$query->expr()->lt('id', $query->createNamedParameter($lastKnownCommentId))
432
-						)
433
-					)
434
-				);
435
-			} else {
436
-				$query->andWhere(
437
-					$query->expr()->orX(
438
-						$query->expr()->gt(
439
-							'creation_timestamp',
440
-							$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
441
-							IQueryBuilder::PARAM_DATE
442
-						),
443
-						$query->expr()->andX(
444
-							$query->expr()->eq(
445
-								'creation_timestamp',
446
-								$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
447
-								IQueryBuilder::PARAM_DATE
448
-							),
449
-							$query->expr()->gt('id', $query->createNamedParameter($lastKnownCommentId))
450
-						)
451
-					)
452
-				);
453
-			}
454
-		}
455
-
456
-		$resultStatement = $query->execute();
457
-		while ($data = $resultStatement->fetch()) {
458
-			$comment = new Comment($this->normalizeDatabaseData($data));
459
-			$this->cache($comment);
460
-			$comments[] = $comment;
461
-		}
462
-		$resultStatement->closeCursor();
463
-
464
-		return $comments;
465
-	}
466
-
467
-	/**
468
-	 * @param string $objectType the object type, e.g. 'files'
469
-	 * @param string $objectId the id of the object
470
-	 * @param int $id the comment to look for
471
-	 * @return Comment|null
472
-	 */
473
-	protected function getLastKnownComment(string $objectType,
474
-										   string $objectId,
475
-										   int $id) {
476
-		$query = $this->dbConn->getQueryBuilder();
477
-		$query->select('*')
478
-			->from('comments')
479
-			->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
480
-			->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
481
-			->andWhere($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
482
-
483
-		$result = $query->execute();
484
-		$row = $result->fetch();
485
-		$result->closeCursor();
486
-
487
-		if ($row) {
488
-			$comment = new Comment($this->normalizeDatabaseData($row));
489
-			$this->cache($comment);
490
-			return $comment;
491
-		}
492
-
493
-		return null;
494
-	}
495
-
496
-	/**
497
-	 * Search for comments with a given content
498
-	 *
499
-	 * @param string $search content to search for
500
-	 * @param string $objectType Limit the search by object type
501
-	 * @param string $objectId Limit the search by object id
502
-	 * @param string $verb Limit the verb of the comment
503
-	 * @param int $offset
504
-	 * @param int $limit
505
-	 * @return IComment[]
506
-	 */
507
-	public function search(string $search, string $objectType, string $objectId, string $verb, int $offset, int $limit = 50): array {
508
-		$query = $this->dbConn->getQueryBuilder();
509
-
510
-		$query->select('*')
511
-			->from('comments')
512
-			->where($query->expr()->iLike('message', $query->createNamedParameter(
513
-				'%' . $this->dbConn->escapeLikeParameter($search). '%'
514
-			)))
515
-			->orderBy('creation_timestamp', 'DESC')
516
-			->addOrderBy('id', 'DESC')
517
-			->setMaxResults($limit);
518
-
519
-		if ($objectType !== '') {
520
-			$query->andWhere($query->expr()->eq('object_type', $query->createNamedParameter($objectType)));
521
-		}
522
-		if ($objectId !== '') {
523
-			$query->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)));
524
-		}
525
-		if ($verb !== '') {
526
-			$query->andWhere($query->expr()->eq('verb', $query->createNamedParameter($verb)));
527
-		}
528
-		if ($offset !== 0) {
529
-			$query->setFirstResult($offset);
530
-		}
531
-
532
-		$comments = [];
533
-		$result = $query->execute();
534
-		while ($data = $result->fetch()) {
535
-			$comment = new Comment($this->normalizeDatabaseData($data));
536
-			$this->cache($comment);
537
-			$comments[] = $comment;
538
-		}
539
-		$result->closeCursor();
540
-
541
-		return $comments;
542
-	}
543
-
544
-	/**
545
-	 * @param $objectType string the object type, e.g. 'files'
546
-	 * @param $objectId string the id of the object
547
-	 * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
548
-	 * that may be returned
549
-	 * @param string $verb Limit the verb of the comment - Added in 14.0.0
550
-	 * @return Int
551
-	 * @since 9.0.0
552
-	 */
553
-	public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null, $verb = '') {
554
-		$qb = $this->dbConn->getQueryBuilder();
555
-		$query = $qb->select($qb->func()->count('id'))
556
-			->from('comments')
557
-			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
558
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
559
-			->setParameter('type', $objectType)
560
-			->setParameter('id', $objectId);
561
-
562
-		if (!is_null($notOlderThan)) {
563
-			$query
564
-				->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
565
-				->setParameter('notOlderThan', $notOlderThan, 'datetime');
566
-		}
567
-
568
-		if ($verb !== '') {
569
-			$query->andWhere($qb->expr()->eq('verb', $qb->createNamedParameter($verb)));
570
-		}
571
-
572
-		$resultStatement = $query->execute();
573
-		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
574
-		$resultStatement->closeCursor();
575
-		return (int)$data[0];
576
-	}
577
-
578
-	/**
579
-	 * Get the number of unread comments for all files in a folder
580
-	 *
581
-	 * @param int $folderId
582
-	 * @param IUser $user
583
-	 * @return array [$fileId => $unreadCount]
584
-	 *
585
-	 * @suppress SqlInjectionChecker
586
-	 */
587
-	public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user) {
588
-		$qb = $this->dbConn->getQueryBuilder();
589
-
590
-		$query = $qb->select('f.fileid')
591
-			->addSelect($qb->func()->count('c.id', 'num_ids'))
592
-			->from('filecache', 'f')
593
-			->leftJoin('f', 'comments', 'c', $qb->expr()->eq(
594
-				'f.fileid', $qb->expr()->castColumn('c.object_id', IQueryBuilder::PARAM_INT)
595
-			))
596
-			->leftJoin('c', 'comments_read_markers', 'm', $qb->expr()->eq(
597
-				'c.object_id', 'm.object_id'
598
-			))
599
-			->where(
600
-				$qb->expr()->andX(
601
-					$qb->expr()->eq('f.parent', $qb->createNamedParameter($folderId)),
602
-					$qb->expr()->orX(
603
-						$qb->expr()->eq('c.object_type', $qb->createNamedParameter('files')),
604
-						$qb->expr()->isNull('c.object_type')
605
-					),
606
-					$qb->expr()->orX(
607
-						$qb->expr()->eq('m.object_type', $qb->createNamedParameter('files')),
608
-						$qb->expr()->isNull('m.object_type')
609
-					),
610
-					$qb->expr()->orX(
611
-						$qb->expr()->eq('m.user_id', $qb->createNamedParameter($user->getUID())),
612
-						$qb->expr()->isNull('m.user_id')
613
-					),
614
-					$qb->expr()->orX(
615
-						$qb->expr()->gt('c.creation_timestamp', 'm.marker_datetime'),
616
-						$qb->expr()->isNull('m.marker_datetime')
617
-					)
618
-				)
619
-			)->groupBy('f.fileid');
620
-
621
-		$resultStatement = $query->execute();
622
-
623
-		$results = [];
624
-		while ($row = $resultStatement->fetch()) {
625
-			$results[$row['fileid']] = (int) $row['num_ids'];
626
-		}
627
-		$resultStatement->closeCursor();
628
-		return $results;
629
-	}
630
-
631
-	/**
632
-	 * creates a new comment and returns it. At this point of time, it is not
633
-	 * saved in the used data storage. Use save() after setting other fields
634
-	 * of the comment (e.g. message or verb).
635
-	 *
636
-	 * @param string $actorType the actor type (e.g. 'users')
637
-	 * @param string $actorId a user id
638
-	 * @param string $objectType the object type the comment is attached to
639
-	 * @param string $objectId the object id the comment is attached to
640
-	 * @return IComment
641
-	 * @since 9.0.0
642
-	 */
643
-	public function create($actorType, $actorId, $objectType, $objectId) {
644
-		$comment = new Comment();
645
-		$comment
646
-			->setActor($actorType, $actorId)
647
-			->setObject($objectType, $objectId);
648
-		return $comment;
649
-	}
650
-
651
-	/**
652
-	 * permanently deletes the comment specified by the ID
653
-	 *
654
-	 * When the comment has child comments, their parent ID will be changed to
655
-	 * the parent ID of the item that is to be deleted.
656
-	 *
657
-	 * @param string $id
658
-	 * @return bool
659
-	 * @throws \InvalidArgumentException
660
-	 * @since 9.0.0
661
-	 */
662
-	public function delete($id) {
663
-		if (!is_string($id)) {
664
-			throw new \InvalidArgumentException('Parameter must be string');
665
-		}
666
-
667
-		try {
668
-			$comment = $this->get($id);
669
-		} catch (\Exception $e) {
670
-			// Ignore exceptions, we just don't fire a hook then
671
-			$comment = null;
672
-		}
673
-
674
-		$qb = $this->dbConn->getQueryBuilder();
675
-		$query = $qb->delete('comments')
676
-			->where($qb->expr()->eq('id', $qb->createParameter('id')))
677
-			->setParameter('id', $id);
678
-
679
-		try {
680
-			$affectedRows = $query->execute();
681
-			$this->uncache($id);
682
-		} catch (DriverException $e) {
683
-			$this->logger->logException($e, ['app' => 'core_comments']);
684
-			return false;
685
-		}
686
-
687
-		if ($affectedRows > 0 && $comment instanceof IComment) {
688
-			$this->sendEvent(CommentsEvent::EVENT_DELETE, $comment);
689
-		}
690
-
691
-		return ($affectedRows > 0);
692
-	}
693
-
694
-	/**
695
-	 * saves the comment permanently
696
-	 *
697
-	 * if the supplied comment has an empty ID, a new entry comment will be
698
-	 * saved and the instance updated with the new ID.
699
-	 *
700
-	 * Otherwise, an existing comment will be updated.
701
-	 *
702
-	 * Throws NotFoundException when a comment that is to be updated does not
703
-	 * exist anymore at this point of time.
704
-	 *
705
-	 * @param IComment $comment
706
-	 * @return bool
707
-	 * @throws NotFoundException
708
-	 * @since 9.0.0
709
-	 */
710
-	public function save(IComment $comment) {
711
-		if ($this->prepareCommentForDatabaseWrite($comment)->getId() === '') {
712
-			$result = $this->insert($comment);
713
-		} else {
714
-			$result = $this->update($comment);
715
-		}
716
-
717
-		if ($result && !!$comment->getParentId()) {
718
-			$this->updateChildrenInformation(
719
-				$comment->getParentId(),
720
-				$comment->getCreationDateTime()
721
-			);
722
-			$this->cache($comment);
723
-		}
724
-
725
-		return $result;
726
-	}
727
-
728
-	/**
729
-	 * inserts the provided comment in the database
730
-	 *
731
-	 * @param IComment $comment
732
-	 * @return bool
733
-	 */
734
-	protected function insert(IComment &$comment) {
735
-		$qb = $this->dbConn->getQueryBuilder();
736
-		$affectedRows = $qb
737
-			->insert('comments')
738
-			->values([
739
-				'parent_id' => $qb->createNamedParameter($comment->getParentId()),
740
-				'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()),
741
-				'children_count' => $qb->createNamedParameter($comment->getChildrenCount()),
742
-				'actor_type' => $qb->createNamedParameter($comment->getActorType()),
743
-				'actor_id' => $qb->createNamedParameter($comment->getActorId()),
744
-				'message' => $qb->createNamedParameter($comment->getMessage()),
745
-				'verb' => $qb->createNamedParameter($comment->getVerb()),
746
-				'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'),
747
-				'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'),
748
-				'object_type' => $qb->createNamedParameter($comment->getObjectType()),
749
-				'object_id' => $qb->createNamedParameter($comment->getObjectId()),
750
-			])
751
-			->execute();
752
-
753
-		if ($affectedRows > 0) {
754
-			$comment->setId((string)$qb->getLastInsertId());
755
-			$this->sendEvent(CommentsEvent::EVENT_ADD, $comment);
756
-		}
757
-
758
-		return $affectedRows > 0;
759
-	}
760
-
761
-	/**
762
-	 * updates a Comment data row
763
-	 *
764
-	 * @param IComment $comment
765
-	 * @return bool
766
-	 * @throws NotFoundException
767
-	 */
768
-	protected function update(IComment $comment) {
769
-		// for properly working preUpdate Events we need the old comments as is
770
-		// in the DB and overcome caching. Also avoid that outdated information stays.
771
-		$this->uncache($comment->getId());
772
-		$this->sendEvent(CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId()));
773
-		$this->uncache($comment->getId());
774
-
775
-		$qb = $this->dbConn->getQueryBuilder();
776
-		$affectedRows = $qb
777
-			->update('comments')
778
-			->set('parent_id', $qb->createNamedParameter($comment->getParentId()))
779
-			->set('topmost_parent_id', $qb->createNamedParameter($comment->getTopmostParentId()))
780
-			->set('children_count', $qb->createNamedParameter($comment->getChildrenCount()))
781
-			->set('actor_type', $qb->createNamedParameter($comment->getActorType()))
782
-			->set('actor_id', $qb->createNamedParameter($comment->getActorId()))
783
-			->set('message', $qb->createNamedParameter($comment->getMessage()))
784
-			->set('verb', $qb->createNamedParameter($comment->getVerb()))
785
-			->set('creation_timestamp', $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'))
786
-			->set('latest_child_timestamp', $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'))
787
-			->set('object_type', $qb->createNamedParameter($comment->getObjectType()))
788
-			->set('object_id', $qb->createNamedParameter($comment->getObjectId()))
789
-			->where($qb->expr()->eq('id', $qb->createParameter('id')))
790
-			->setParameter('id', $comment->getId())
791
-			->execute();
792
-
793
-		if ($affectedRows === 0) {
794
-			throw new NotFoundException('Comment to update does ceased to exist');
795
-		}
796
-
797
-		$this->sendEvent(CommentsEvent::EVENT_UPDATE, $comment);
798
-
799
-		return $affectedRows > 0;
800
-	}
801
-
802
-	/**
803
-	 * removes references to specific actor (e.g. on user delete) of a comment.
804
-	 * The comment itself must not get lost/deleted.
805
-	 *
806
-	 * @param string $actorType the actor type (e.g. 'users')
807
-	 * @param string $actorId a user id
808
-	 * @return boolean
809
-	 * @since 9.0.0
810
-	 */
811
-	public function deleteReferencesOfActor($actorType, $actorId) {
812
-		$this->checkRoleParameters('Actor', $actorType, $actorId);
813
-
814
-		$qb = $this->dbConn->getQueryBuilder();
815
-		$affectedRows = $qb
816
-			->update('comments')
817
-			->set('actor_type', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
818
-			->set('actor_id', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
819
-			->where($qb->expr()->eq('actor_type', $qb->createParameter('type')))
820
-			->andWhere($qb->expr()->eq('actor_id', $qb->createParameter('id')))
821
-			->setParameter('type', $actorType)
822
-			->setParameter('id', $actorId)
823
-			->execute();
824
-
825
-		$this->commentsCache = [];
826
-
827
-		return is_int($affectedRows);
828
-	}
829
-
830
-	/**
831
-	 * deletes all comments made of a specific object (e.g. on file delete)
832
-	 *
833
-	 * @param string $objectType the object type (e.g. 'files')
834
-	 * @param string $objectId e.g. the file id
835
-	 * @return boolean
836
-	 * @since 9.0.0
837
-	 */
838
-	public function deleteCommentsAtObject($objectType, $objectId) {
839
-		$this->checkRoleParameters('Object', $objectType, $objectId);
840
-
841
-		$qb = $this->dbConn->getQueryBuilder();
842
-		$affectedRows = $qb
843
-			->delete('comments')
844
-			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
845
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
846
-			->setParameter('type', $objectType)
847
-			->setParameter('id', $objectId)
848
-			->execute();
849
-
850
-		$this->commentsCache = [];
851
-
852
-		return is_int($affectedRows);
853
-	}
854
-
855
-	/**
856
-	 * deletes the read markers for the specified user
857
-	 *
858
-	 * @param \OCP\IUser $user
859
-	 * @return bool
860
-	 * @since 9.0.0
861
-	 */
862
-	public function deleteReadMarksFromUser(IUser $user) {
863
-		$qb = $this->dbConn->getQueryBuilder();
864
-		$query = $qb->delete('comments_read_markers')
865
-			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
866
-			->setParameter('user_id', $user->getUID());
867
-
868
-		try {
869
-			$affectedRows = $query->execute();
870
-		} catch (DriverException $e) {
871
-			$this->logger->logException($e, ['app' => 'core_comments']);
872
-			return false;
873
-		}
874
-		return ($affectedRows > 0);
875
-	}
876
-
877
-	/**
878
-	 * sets the read marker for a given file to the specified date for the
879
-	 * provided user
880
-	 *
881
-	 * @param string $objectType
882
-	 * @param string $objectId
883
-	 * @param \DateTime $dateTime
884
-	 * @param IUser $user
885
-	 * @since 9.0.0
886
-	 * @suppress SqlInjectionChecker
887
-	 */
888
-	public function setReadMark($objectType, $objectId, \DateTime $dateTime, IUser $user) {
889
-		$this->checkRoleParameters('Object', $objectType, $objectId);
890
-
891
-		$qb = $this->dbConn->getQueryBuilder();
892
-		$values = [
893
-			'user_id' => $qb->createNamedParameter($user->getUID()),
894
-			'marker_datetime' => $qb->createNamedParameter($dateTime, 'datetime'),
895
-			'object_type' => $qb->createNamedParameter($objectType),
896
-			'object_id' => $qb->createNamedParameter($objectId),
897
-		];
898
-
899
-		// Strategy: try to update, if this does not return affected rows, do an insert.
900
-		$affectedRows = $qb
901
-			->update('comments_read_markers')
902
-			->set('user_id', $values['user_id'])
903
-			->set('marker_datetime', $values['marker_datetime'])
904
-			->set('object_type', $values['object_type'])
905
-			->set('object_id', $values['object_id'])
906
-			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
907
-			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
908
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
909
-			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
910
-			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
911
-			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
912
-			->execute();
913
-
914
-		if ($affectedRows > 0) {
915
-			return;
916
-		}
917
-
918
-		$qb->insert('comments_read_markers')
919
-			->values($values)
920
-			->execute();
921
-	}
922
-
923
-	/**
924
-	 * returns the read marker for a given file to the specified date for the
925
-	 * provided user. It returns null, when the marker is not present, i.e.
926
-	 * no comments were marked as read.
927
-	 *
928
-	 * @param string $objectType
929
-	 * @param string $objectId
930
-	 * @param IUser $user
931
-	 * @return \DateTime|null
932
-	 * @since 9.0.0
933
-	 */
934
-	public function getReadMark($objectType, $objectId, IUser $user) {
935
-		$qb = $this->dbConn->getQueryBuilder();
936
-		$resultStatement = $qb->select('marker_datetime')
937
-			->from('comments_read_markers')
938
-			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
939
-			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
940
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
941
-			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
942
-			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
943
-			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
944
-			->execute();
945
-
946
-		$data = $resultStatement->fetch();
947
-		$resultStatement->closeCursor();
948
-		if (!$data || is_null($data['marker_datetime'])) {
949
-			return null;
950
-		}
951
-
952
-		return new \DateTime($data['marker_datetime']);
953
-	}
954
-
955
-	/**
956
-	 * deletes the read markers on the specified object
957
-	 *
958
-	 * @param string $objectType
959
-	 * @param string $objectId
960
-	 * @return bool
961
-	 * @since 9.0.0
962
-	 */
963
-	public function deleteReadMarksOnObject($objectType, $objectId) {
964
-		$this->checkRoleParameters('Object', $objectType, $objectId);
965
-
966
-		$qb = $this->dbConn->getQueryBuilder();
967
-		$query = $qb->delete('comments_read_markers')
968
-			->where($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
969
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
970
-			->setParameter('object_type', $objectType)
971
-			->setParameter('object_id', $objectId);
972
-
973
-		try {
974
-			$affectedRows = $query->execute();
975
-		} catch (DriverException $e) {
976
-			$this->logger->logException($e, ['app' => 'core_comments']);
977
-			return false;
978
-		}
979
-		return ($affectedRows > 0);
980
-	}
981
-
982
-	/**
983
-	 * registers an Entity to the manager, so event notifications can be send
984
-	 * to consumers of the comments infrastructure
985
-	 *
986
-	 * @param \Closure $closure
987
-	 */
988
-	public function registerEventHandler(\Closure $closure) {
989
-		$this->eventHandlerClosures[] = $closure;
990
-		$this->eventHandlers = [];
991
-	}
992
-
993
-	/**
994
-	 * registers a method that resolves an ID to a display name for a given type
995
-	 *
996
-	 * @param string $type
997
-	 * @param \Closure $closure
998
-	 * @throws \OutOfBoundsException
999
-	 * @since 11.0.0
1000
-	 *
1001
-	 * Only one resolver shall be registered per type. Otherwise a
1002
-	 * \OutOfBoundsException has to thrown.
1003
-	 */
1004
-	public function registerDisplayNameResolver($type, \Closure $closure) {
1005
-		if (!is_string($type)) {
1006
-			throw new \InvalidArgumentException('String expected.');
1007
-		}
1008
-		if (isset($this->displayNameResolvers[$type])) {
1009
-			throw new \OutOfBoundsException('Displayname resolver for this type already registered');
1010
-		}
1011
-		$this->displayNameResolvers[$type] = $closure;
1012
-	}
1013
-
1014
-	/**
1015
-	 * resolves a given ID of a given Type to a display name.
1016
-	 *
1017
-	 * @param string $type
1018
-	 * @param string $id
1019
-	 * @return string
1020
-	 * @throws \OutOfBoundsException
1021
-	 * @since 11.0.0
1022
-	 *
1023
-	 * If a provided type was not registered, an \OutOfBoundsException shall
1024
-	 * be thrown. It is upon the resolver discretion what to return of the
1025
-	 * provided ID is unknown. It must be ensured that a string is returned.
1026
-	 */
1027
-	public function resolveDisplayName($type, $id) {
1028
-		if (!is_string($type)) {
1029
-			throw new \InvalidArgumentException('String expected.');
1030
-		}
1031
-		if (!isset($this->displayNameResolvers[$type])) {
1032
-			throw new \OutOfBoundsException('No Displayname resolver for this type registered');
1033
-		}
1034
-		return (string)$this->displayNameResolvers[$type]($id);
1035
-	}
1036
-
1037
-	/**
1038
-	 * returns valid, registered entities
1039
-	 *
1040
-	 * @return \OCP\Comments\ICommentsEventHandler[]
1041
-	 */
1042
-	private function getEventHandlers() {
1043
-		if (!empty($this->eventHandlers)) {
1044
-			return $this->eventHandlers;
1045
-		}
1046
-
1047
-		$this->eventHandlers = [];
1048
-		foreach ($this->eventHandlerClosures as $name => $closure) {
1049
-			$entity = $closure();
1050
-			if (!($entity instanceof ICommentsEventHandler)) {
1051
-				throw new \InvalidArgumentException('The given entity does not implement the ICommentsEntity interface');
1052
-			}
1053
-			$this->eventHandlers[$name] = $entity;
1054
-		}
1055
-
1056
-		return $this->eventHandlers;
1057
-	}
1058
-
1059
-	/**
1060
-	 * sends notifications to the registered entities
1061
-	 *
1062
-	 * @param $eventType
1063
-	 * @param IComment $comment
1064
-	 */
1065
-	private function sendEvent($eventType, IComment $comment) {
1066
-		$entities = $this->getEventHandlers();
1067
-		$event = new CommentsEvent($eventType, $comment);
1068
-		foreach ($entities as $entity) {
1069
-			$entity->handle($event);
1070
-		}
1071
-	}
44
+    /** @var  IDBConnection */
45
+    protected $dbConn;
46
+
47
+    /** @var  ILogger */
48
+    protected $logger;
49
+
50
+    /** @var IConfig */
51
+    protected $config;
52
+
53
+    /** @var IComment[] */
54
+    protected $commentsCache = [];
55
+
56
+    /** @var  \Closure[] */
57
+    protected $eventHandlerClosures = [];
58
+
59
+    /** @var  ICommentsEventHandler[] */
60
+    protected $eventHandlers = [];
61
+
62
+    /** @var \Closure[] */
63
+    protected $displayNameResolvers = [];
64
+
65
+    /**
66
+     * Manager constructor.
67
+     *
68
+     * @param IDBConnection $dbConn
69
+     * @param ILogger $logger
70
+     * @param IConfig $config
71
+     */
72
+    public function __construct(
73
+        IDBConnection $dbConn,
74
+        ILogger $logger,
75
+        IConfig $config
76
+    ) {
77
+        $this->dbConn = $dbConn;
78
+        $this->logger = $logger;
79
+        $this->config = $config;
80
+    }
81
+
82
+    /**
83
+     * converts data base data into PHP native, proper types as defined by
84
+     * IComment interface.
85
+     *
86
+     * @param array $data
87
+     * @return array
88
+     */
89
+    protected function normalizeDatabaseData(array $data) {
90
+        $data['id'] = (string)$data['id'];
91
+        $data['parent_id'] = (string)$data['parent_id'];
92
+        $data['topmost_parent_id'] = (string)$data['topmost_parent_id'];
93
+        $data['creation_timestamp'] = new \DateTime($data['creation_timestamp']);
94
+        if (!is_null($data['latest_child_timestamp'])) {
95
+            $data['latest_child_timestamp'] = new \DateTime($data['latest_child_timestamp']);
96
+        }
97
+        $data['children_count'] = (int)$data['children_count'];
98
+        return $data;
99
+    }
100
+
101
+    /**
102
+     * prepares a comment for an insert or update operation after making sure
103
+     * all necessary fields have a value assigned.
104
+     *
105
+     * @param IComment $comment
106
+     * @return IComment returns the same updated IComment instance as provided
107
+     *                  by parameter for convenience
108
+     * @throws \UnexpectedValueException
109
+     */
110
+    protected function prepareCommentForDatabaseWrite(IComment $comment) {
111
+        if (!$comment->getActorType()
112
+            || !$comment->getActorId()
113
+            || !$comment->getObjectType()
114
+            || !$comment->getObjectId()
115
+            || !$comment->getVerb()
116
+        ) {
117
+            throw new \UnexpectedValueException('Actor, Object and Verb information must be provided for saving');
118
+        }
119
+
120
+        if ($comment->getId() === '') {
121
+            $comment->setChildrenCount(0);
122
+            $comment->setLatestChildDateTime(new \DateTime('0000-00-00 00:00:00', new \DateTimeZone('UTC')));
123
+            $comment->setLatestChildDateTime(null);
124
+        }
125
+
126
+        if (is_null($comment->getCreationDateTime())) {
127
+            $comment->setCreationDateTime(new \DateTime());
128
+        }
129
+
130
+        if ($comment->getParentId() !== '0') {
131
+            $comment->setTopmostParentId($this->determineTopmostParentId($comment->getParentId()));
132
+        } else {
133
+            $comment->setTopmostParentId('0');
134
+        }
135
+
136
+        $this->cache($comment);
137
+
138
+        return $comment;
139
+    }
140
+
141
+    /**
142
+     * returns the topmost parent id of a given comment identified by ID
143
+     *
144
+     * @param string $id
145
+     * @return string
146
+     * @throws NotFoundException
147
+     */
148
+    protected function determineTopmostParentId($id) {
149
+        $comment = $this->get($id);
150
+        if ($comment->getParentId() === '0') {
151
+            return $comment->getId();
152
+        } else {
153
+            return $this->determineTopmostParentId($comment->getId());
154
+        }
155
+    }
156
+
157
+    /**
158
+     * updates child information of a comment
159
+     *
160
+     * @param string $id
161
+     * @param \DateTime $cDateTime the date time of the most recent child
162
+     * @throws NotFoundException
163
+     */
164
+    protected function updateChildrenInformation($id, \DateTime $cDateTime) {
165
+        $qb = $this->dbConn->getQueryBuilder();
166
+        $query = $qb->select($qb->func()->count('id'))
167
+            ->from('comments')
168
+            ->where($qb->expr()->eq('parent_id', $qb->createParameter('id')))
169
+            ->setParameter('id', $id);
170
+
171
+        $resultStatement = $query->execute();
172
+        $data = $resultStatement->fetch(\PDO::FETCH_NUM);
173
+        $resultStatement->closeCursor();
174
+        $children = (int)$data[0];
175
+
176
+        $comment = $this->get($id);
177
+        $comment->setChildrenCount($children);
178
+        $comment->setLatestChildDateTime($cDateTime);
179
+        $this->save($comment);
180
+    }
181
+
182
+    /**
183
+     * Tests whether actor or object type and id parameters are acceptable.
184
+     * Throws exception if not.
185
+     *
186
+     * @param string $role
187
+     * @param string $type
188
+     * @param string $id
189
+     * @throws \InvalidArgumentException
190
+     */
191
+    protected function checkRoleParameters($role, $type, $id) {
192
+        if (
193
+            !is_string($type) || empty($type)
194
+            || !is_string($id) || empty($id)
195
+        ) {
196
+            throw new \InvalidArgumentException($role . ' parameters must be string and not empty');
197
+        }
198
+    }
199
+
200
+    /**
201
+     * run-time caches a comment
202
+     *
203
+     * @param IComment $comment
204
+     */
205
+    protected function cache(IComment $comment) {
206
+        $id = $comment->getId();
207
+        if (empty($id)) {
208
+            return;
209
+        }
210
+        $this->commentsCache[(string)$id] = $comment;
211
+    }
212
+
213
+    /**
214
+     * removes an entry from the comments run time cache
215
+     *
216
+     * @param mixed $id the comment's id
217
+     */
218
+    protected function uncache($id) {
219
+        $id = (string)$id;
220
+        if (isset($this->commentsCache[$id])) {
221
+            unset($this->commentsCache[$id]);
222
+        }
223
+    }
224
+
225
+    /**
226
+     * returns a comment instance
227
+     *
228
+     * @param string $id the ID of the comment
229
+     * @return IComment
230
+     * @throws NotFoundException
231
+     * @throws \InvalidArgumentException
232
+     * @since 9.0.0
233
+     */
234
+    public function get($id) {
235
+        if ((int)$id === 0) {
236
+            throw new \InvalidArgumentException('IDs must be translatable to a number in this implementation.');
237
+        }
238
+
239
+        if (isset($this->commentsCache[$id])) {
240
+            return $this->commentsCache[$id];
241
+        }
242
+
243
+        $qb = $this->dbConn->getQueryBuilder();
244
+        $resultStatement = $qb->select('*')
245
+            ->from('comments')
246
+            ->where($qb->expr()->eq('id', $qb->createParameter('id')))
247
+            ->setParameter('id', $id, IQueryBuilder::PARAM_INT)
248
+            ->execute();
249
+
250
+        $data = $resultStatement->fetch();
251
+        $resultStatement->closeCursor();
252
+        if (!$data) {
253
+            throw new NotFoundException();
254
+        }
255
+
256
+        $comment = new Comment($this->normalizeDatabaseData($data));
257
+        $this->cache($comment);
258
+        return $comment;
259
+    }
260
+
261
+    /**
262
+     * returns the comment specified by the id and all it's child comments.
263
+     * At this point of time, we do only support one level depth.
264
+     *
265
+     * @param string $id
266
+     * @param int $limit max number of entries to return, 0 returns all
267
+     * @param int $offset the start entry
268
+     * @return array
269
+     * @since 9.0.0
270
+     *
271
+     * The return array looks like this
272
+     * [
273
+     *   'comment' => IComment, // root comment
274
+     *   'replies' =>
275
+     *   [
276
+     *     0 =>
277
+     *     [
278
+     *       'comment' => IComment,
279
+     *       'replies' => []
280
+     *     ]
281
+     *     1 =>
282
+     *     [
283
+     *       'comment' => IComment,
284
+     *       'replies'=> []
285
+     *     ],
286
+     *     …
287
+     *   ]
288
+     * ]
289
+     */
290
+    public function getTree($id, $limit = 0, $offset = 0) {
291
+        $tree = [];
292
+        $tree['comment'] = $this->get($id);
293
+        $tree['replies'] = [];
294
+
295
+        $qb = $this->dbConn->getQueryBuilder();
296
+        $query = $qb->select('*')
297
+            ->from('comments')
298
+            ->where($qb->expr()->eq('topmost_parent_id', $qb->createParameter('id')))
299
+            ->orderBy('creation_timestamp', 'DESC')
300
+            ->setParameter('id', $id);
301
+
302
+        if ($limit > 0) {
303
+            $query->setMaxResults($limit);
304
+        }
305
+        if ($offset > 0) {
306
+            $query->setFirstResult($offset);
307
+        }
308
+
309
+        $resultStatement = $query->execute();
310
+        while ($data = $resultStatement->fetch()) {
311
+            $comment = new Comment($this->normalizeDatabaseData($data));
312
+            $this->cache($comment);
313
+            $tree['replies'][] = [
314
+                'comment' => $comment,
315
+                'replies' => []
316
+            ];
317
+        }
318
+        $resultStatement->closeCursor();
319
+
320
+        return $tree;
321
+    }
322
+
323
+    /**
324
+     * returns comments for a specific object (e.g. a file).
325
+     *
326
+     * The sort order is always newest to oldest.
327
+     *
328
+     * @param string $objectType the object type, e.g. 'files'
329
+     * @param string $objectId the id of the object
330
+     * @param int $limit optional, number of maximum comments to be returned. if
331
+     * not specified, all comments are returned.
332
+     * @param int $offset optional, starting point
333
+     * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
334
+     * that may be returned
335
+     * @return IComment[]
336
+     * @since 9.0.0
337
+     */
338
+    public function getForObject(
339
+        $objectType,
340
+        $objectId,
341
+        $limit = 0,
342
+        $offset = 0,
343
+        \DateTime $notOlderThan = null
344
+    ) {
345
+        $comments = [];
346
+
347
+        $qb = $this->dbConn->getQueryBuilder();
348
+        $query = $qb->select('*')
349
+            ->from('comments')
350
+            ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
351
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
352
+            ->orderBy('creation_timestamp', 'DESC')
353
+            ->setParameter('type', $objectType)
354
+            ->setParameter('id', $objectId);
355
+
356
+        if ($limit > 0) {
357
+            $query->setMaxResults($limit);
358
+        }
359
+        if ($offset > 0) {
360
+            $query->setFirstResult($offset);
361
+        }
362
+        if (!is_null($notOlderThan)) {
363
+            $query
364
+                ->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
365
+                ->setParameter('notOlderThan', $notOlderThan, 'datetime');
366
+        }
367
+
368
+        $resultStatement = $query->execute();
369
+        while ($data = $resultStatement->fetch()) {
370
+            $comment = new Comment($this->normalizeDatabaseData($data));
371
+            $this->cache($comment);
372
+            $comments[] = $comment;
373
+        }
374
+        $resultStatement->closeCursor();
375
+
376
+        return $comments;
377
+    }
378
+
379
+    /**
380
+     * @param string $objectType the object type, e.g. 'files'
381
+     * @param string $objectId the id of the object
382
+     * @param int $lastKnownCommentId the last known comment (will be used as offset)
383
+     * @param string $sortDirection direction of the comments (`asc` or `desc`)
384
+     * @param int $limit optional, number of maximum comments to be returned. if
385
+     * set to 0, all comments are returned.
386
+     * @return IComment[]
387
+     * @return array
388
+     */
389
+    public function getForObjectSince(
390
+        string $objectType,
391
+        string $objectId,
392
+        int $lastKnownCommentId,
393
+        string $sortDirection = 'asc',
394
+        int $limit = 30
395
+    ): array {
396
+        $comments = [];
397
+
398
+        $query = $this->dbConn->getQueryBuilder();
399
+        $query->select('*')
400
+            ->from('comments')
401
+            ->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
402
+            ->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
403
+            ->orderBy('creation_timestamp', $sortDirection === 'desc' ? 'DESC' : 'ASC')
404
+            ->addOrderBy('id', $sortDirection === 'desc' ? 'DESC' : 'ASC');
405
+
406
+        if ($limit > 0) {
407
+            $query->setMaxResults($limit);
408
+        }
409
+
410
+        $lastKnownComment = $lastKnownCommentId > 0 ? $this->getLastKnownComment(
411
+            $objectType,
412
+            $objectId,
413
+            $lastKnownCommentId
414
+        ) : null;
415
+        if ($lastKnownComment instanceof IComment) {
416
+            $lastKnownCommentDateTime = $lastKnownComment->getCreationDateTime();
417
+            if ($sortDirection === 'desc') {
418
+                $query->andWhere(
419
+                    $query->expr()->orX(
420
+                        $query->expr()->lt(
421
+                            'creation_timestamp',
422
+                            $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
423
+                            IQueryBuilder::PARAM_DATE
424
+                        ),
425
+                        $query->expr()->andX(
426
+                            $query->expr()->eq(
427
+                                'creation_timestamp',
428
+                                $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
429
+                                IQueryBuilder::PARAM_DATE
430
+                            ),
431
+                            $query->expr()->lt('id', $query->createNamedParameter($lastKnownCommentId))
432
+                        )
433
+                    )
434
+                );
435
+            } else {
436
+                $query->andWhere(
437
+                    $query->expr()->orX(
438
+                        $query->expr()->gt(
439
+                            'creation_timestamp',
440
+                            $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
441
+                            IQueryBuilder::PARAM_DATE
442
+                        ),
443
+                        $query->expr()->andX(
444
+                            $query->expr()->eq(
445
+                                'creation_timestamp',
446
+                                $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
447
+                                IQueryBuilder::PARAM_DATE
448
+                            ),
449
+                            $query->expr()->gt('id', $query->createNamedParameter($lastKnownCommentId))
450
+                        )
451
+                    )
452
+                );
453
+            }
454
+        }
455
+
456
+        $resultStatement = $query->execute();
457
+        while ($data = $resultStatement->fetch()) {
458
+            $comment = new Comment($this->normalizeDatabaseData($data));
459
+            $this->cache($comment);
460
+            $comments[] = $comment;
461
+        }
462
+        $resultStatement->closeCursor();
463
+
464
+        return $comments;
465
+    }
466
+
467
+    /**
468
+     * @param string $objectType the object type, e.g. 'files'
469
+     * @param string $objectId the id of the object
470
+     * @param int $id the comment to look for
471
+     * @return Comment|null
472
+     */
473
+    protected function getLastKnownComment(string $objectType,
474
+                                            string $objectId,
475
+                                            int $id) {
476
+        $query = $this->dbConn->getQueryBuilder();
477
+        $query->select('*')
478
+            ->from('comments')
479
+            ->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
480
+            ->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
481
+            ->andWhere($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
482
+
483
+        $result = $query->execute();
484
+        $row = $result->fetch();
485
+        $result->closeCursor();
486
+
487
+        if ($row) {
488
+            $comment = new Comment($this->normalizeDatabaseData($row));
489
+            $this->cache($comment);
490
+            return $comment;
491
+        }
492
+
493
+        return null;
494
+    }
495
+
496
+    /**
497
+     * Search for comments with a given content
498
+     *
499
+     * @param string $search content to search for
500
+     * @param string $objectType Limit the search by object type
501
+     * @param string $objectId Limit the search by object id
502
+     * @param string $verb Limit the verb of the comment
503
+     * @param int $offset
504
+     * @param int $limit
505
+     * @return IComment[]
506
+     */
507
+    public function search(string $search, string $objectType, string $objectId, string $verb, int $offset, int $limit = 50): array {
508
+        $query = $this->dbConn->getQueryBuilder();
509
+
510
+        $query->select('*')
511
+            ->from('comments')
512
+            ->where($query->expr()->iLike('message', $query->createNamedParameter(
513
+                '%' . $this->dbConn->escapeLikeParameter($search). '%'
514
+            )))
515
+            ->orderBy('creation_timestamp', 'DESC')
516
+            ->addOrderBy('id', 'DESC')
517
+            ->setMaxResults($limit);
518
+
519
+        if ($objectType !== '') {
520
+            $query->andWhere($query->expr()->eq('object_type', $query->createNamedParameter($objectType)));
521
+        }
522
+        if ($objectId !== '') {
523
+            $query->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)));
524
+        }
525
+        if ($verb !== '') {
526
+            $query->andWhere($query->expr()->eq('verb', $query->createNamedParameter($verb)));
527
+        }
528
+        if ($offset !== 0) {
529
+            $query->setFirstResult($offset);
530
+        }
531
+
532
+        $comments = [];
533
+        $result = $query->execute();
534
+        while ($data = $result->fetch()) {
535
+            $comment = new Comment($this->normalizeDatabaseData($data));
536
+            $this->cache($comment);
537
+            $comments[] = $comment;
538
+        }
539
+        $result->closeCursor();
540
+
541
+        return $comments;
542
+    }
543
+
544
+    /**
545
+     * @param $objectType string the object type, e.g. 'files'
546
+     * @param $objectId string the id of the object
547
+     * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
548
+     * that may be returned
549
+     * @param string $verb Limit the verb of the comment - Added in 14.0.0
550
+     * @return Int
551
+     * @since 9.0.0
552
+     */
553
+    public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null, $verb = '') {
554
+        $qb = $this->dbConn->getQueryBuilder();
555
+        $query = $qb->select($qb->func()->count('id'))
556
+            ->from('comments')
557
+            ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
558
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
559
+            ->setParameter('type', $objectType)
560
+            ->setParameter('id', $objectId);
561
+
562
+        if (!is_null($notOlderThan)) {
563
+            $query
564
+                ->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
565
+                ->setParameter('notOlderThan', $notOlderThan, 'datetime');
566
+        }
567
+
568
+        if ($verb !== '') {
569
+            $query->andWhere($qb->expr()->eq('verb', $qb->createNamedParameter($verb)));
570
+        }
571
+
572
+        $resultStatement = $query->execute();
573
+        $data = $resultStatement->fetch(\PDO::FETCH_NUM);
574
+        $resultStatement->closeCursor();
575
+        return (int)$data[0];
576
+    }
577
+
578
+    /**
579
+     * Get the number of unread comments for all files in a folder
580
+     *
581
+     * @param int $folderId
582
+     * @param IUser $user
583
+     * @return array [$fileId => $unreadCount]
584
+     *
585
+     * @suppress SqlInjectionChecker
586
+     */
587
+    public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user) {
588
+        $qb = $this->dbConn->getQueryBuilder();
589
+
590
+        $query = $qb->select('f.fileid')
591
+            ->addSelect($qb->func()->count('c.id', 'num_ids'))
592
+            ->from('filecache', 'f')
593
+            ->leftJoin('f', 'comments', 'c', $qb->expr()->eq(
594
+                'f.fileid', $qb->expr()->castColumn('c.object_id', IQueryBuilder::PARAM_INT)
595
+            ))
596
+            ->leftJoin('c', 'comments_read_markers', 'm', $qb->expr()->eq(
597
+                'c.object_id', 'm.object_id'
598
+            ))
599
+            ->where(
600
+                $qb->expr()->andX(
601
+                    $qb->expr()->eq('f.parent', $qb->createNamedParameter($folderId)),
602
+                    $qb->expr()->orX(
603
+                        $qb->expr()->eq('c.object_type', $qb->createNamedParameter('files')),
604
+                        $qb->expr()->isNull('c.object_type')
605
+                    ),
606
+                    $qb->expr()->orX(
607
+                        $qb->expr()->eq('m.object_type', $qb->createNamedParameter('files')),
608
+                        $qb->expr()->isNull('m.object_type')
609
+                    ),
610
+                    $qb->expr()->orX(
611
+                        $qb->expr()->eq('m.user_id', $qb->createNamedParameter($user->getUID())),
612
+                        $qb->expr()->isNull('m.user_id')
613
+                    ),
614
+                    $qb->expr()->orX(
615
+                        $qb->expr()->gt('c.creation_timestamp', 'm.marker_datetime'),
616
+                        $qb->expr()->isNull('m.marker_datetime')
617
+                    )
618
+                )
619
+            )->groupBy('f.fileid');
620
+
621
+        $resultStatement = $query->execute();
622
+
623
+        $results = [];
624
+        while ($row = $resultStatement->fetch()) {
625
+            $results[$row['fileid']] = (int) $row['num_ids'];
626
+        }
627
+        $resultStatement->closeCursor();
628
+        return $results;
629
+    }
630
+
631
+    /**
632
+     * creates a new comment and returns it. At this point of time, it is not
633
+     * saved in the used data storage. Use save() after setting other fields
634
+     * of the comment (e.g. message or verb).
635
+     *
636
+     * @param string $actorType the actor type (e.g. 'users')
637
+     * @param string $actorId a user id
638
+     * @param string $objectType the object type the comment is attached to
639
+     * @param string $objectId the object id the comment is attached to
640
+     * @return IComment
641
+     * @since 9.0.0
642
+     */
643
+    public function create($actorType, $actorId, $objectType, $objectId) {
644
+        $comment = new Comment();
645
+        $comment
646
+            ->setActor($actorType, $actorId)
647
+            ->setObject($objectType, $objectId);
648
+        return $comment;
649
+    }
650
+
651
+    /**
652
+     * permanently deletes the comment specified by the ID
653
+     *
654
+     * When the comment has child comments, their parent ID will be changed to
655
+     * the parent ID of the item that is to be deleted.
656
+     *
657
+     * @param string $id
658
+     * @return bool
659
+     * @throws \InvalidArgumentException
660
+     * @since 9.0.0
661
+     */
662
+    public function delete($id) {
663
+        if (!is_string($id)) {
664
+            throw new \InvalidArgumentException('Parameter must be string');
665
+        }
666
+
667
+        try {
668
+            $comment = $this->get($id);
669
+        } catch (\Exception $e) {
670
+            // Ignore exceptions, we just don't fire a hook then
671
+            $comment = null;
672
+        }
673
+
674
+        $qb = $this->dbConn->getQueryBuilder();
675
+        $query = $qb->delete('comments')
676
+            ->where($qb->expr()->eq('id', $qb->createParameter('id')))
677
+            ->setParameter('id', $id);
678
+
679
+        try {
680
+            $affectedRows = $query->execute();
681
+            $this->uncache($id);
682
+        } catch (DriverException $e) {
683
+            $this->logger->logException($e, ['app' => 'core_comments']);
684
+            return false;
685
+        }
686
+
687
+        if ($affectedRows > 0 && $comment instanceof IComment) {
688
+            $this->sendEvent(CommentsEvent::EVENT_DELETE, $comment);
689
+        }
690
+
691
+        return ($affectedRows > 0);
692
+    }
693
+
694
+    /**
695
+     * saves the comment permanently
696
+     *
697
+     * if the supplied comment has an empty ID, a new entry comment will be
698
+     * saved and the instance updated with the new ID.
699
+     *
700
+     * Otherwise, an existing comment will be updated.
701
+     *
702
+     * Throws NotFoundException when a comment that is to be updated does not
703
+     * exist anymore at this point of time.
704
+     *
705
+     * @param IComment $comment
706
+     * @return bool
707
+     * @throws NotFoundException
708
+     * @since 9.0.0
709
+     */
710
+    public function save(IComment $comment) {
711
+        if ($this->prepareCommentForDatabaseWrite($comment)->getId() === '') {
712
+            $result = $this->insert($comment);
713
+        } else {
714
+            $result = $this->update($comment);
715
+        }
716
+
717
+        if ($result && !!$comment->getParentId()) {
718
+            $this->updateChildrenInformation(
719
+                $comment->getParentId(),
720
+                $comment->getCreationDateTime()
721
+            );
722
+            $this->cache($comment);
723
+        }
724
+
725
+        return $result;
726
+    }
727
+
728
+    /**
729
+     * inserts the provided comment in the database
730
+     *
731
+     * @param IComment $comment
732
+     * @return bool
733
+     */
734
+    protected function insert(IComment &$comment) {
735
+        $qb = $this->dbConn->getQueryBuilder();
736
+        $affectedRows = $qb
737
+            ->insert('comments')
738
+            ->values([
739
+                'parent_id' => $qb->createNamedParameter($comment->getParentId()),
740
+                'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()),
741
+                'children_count' => $qb->createNamedParameter($comment->getChildrenCount()),
742
+                'actor_type' => $qb->createNamedParameter($comment->getActorType()),
743
+                'actor_id' => $qb->createNamedParameter($comment->getActorId()),
744
+                'message' => $qb->createNamedParameter($comment->getMessage()),
745
+                'verb' => $qb->createNamedParameter($comment->getVerb()),
746
+                'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'),
747
+                'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'),
748
+                'object_type' => $qb->createNamedParameter($comment->getObjectType()),
749
+                'object_id' => $qb->createNamedParameter($comment->getObjectId()),
750
+            ])
751
+            ->execute();
752
+
753
+        if ($affectedRows > 0) {
754
+            $comment->setId((string)$qb->getLastInsertId());
755
+            $this->sendEvent(CommentsEvent::EVENT_ADD, $comment);
756
+        }
757
+
758
+        return $affectedRows > 0;
759
+    }
760
+
761
+    /**
762
+     * updates a Comment data row
763
+     *
764
+     * @param IComment $comment
765
+     * @return bool
766
+     * @throws NotFoundException
767
+     */
768
+    protected function update(IComment $comment) {
769
+        // for properly working preUpdate Events we need the old comments as is
770
+        // in the DB and overcome caching. Also avoid that outdated information stays.
771
+        $this->uncache($comment->getId());
772
+        $this->sendEvent(CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId()));
773
+        $this->uncache($comment->getId());
774
+
775
+        $qb = $this->dbConn->getQueryBuilder();
776
+        $affectedRows = $qb
777
+            ->update('comments')
778
+            ->set('parent_id', $qb->createNamedParameter($comment->getParentId()))
779
+            ->set('topmost_parent_id', $qb->createNamedParameter($comment->getTopmostParentId()))
780
+            ->set('children_count', $qb->createNamedParameter($comment->getChildrenCount()))
781
+            ->set('actor_type', $qb->createNamedParameter($comment->getActorType()))
782
+            ->set('actor_id', $qb->createNamedParameter($comment->getActorId()))
783
+            ->set('message', $qb->createNamedParameter($comment->getMessage()))
784
+            ->set('verb', $qb->createNamedParameter($comment->getVerb()))
785
+            ->set('creation_timestamp', $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'))
786
+            ->set('latest_child_timestamp', $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'))
787
+            ->set('object_type', $qb->createNamedParameter($comment->getObjectType()))
788
+            ->set('object_id', $qb->createNamedParameter($comment->getObjectId()))
789
+            ->where($qb->expr()->eq('id', $qb->createParameter('id')))
790
+            ->setParameter('id', $comment->getId())
791
+            ->execute();
792
+
793
+        if ($affectedRows === 0) {
794
+            throw new NotFoundException('Comment to update does ceased to exist');
795
+        }
796
+
797
+        $this->sendEvent(CommentsEvent::EVENT_UPDATE, $comment);
798
+
799
+        return $affectedRows > 0;
800
+    }
801
+
802
+    /**
803
+     * removes references to specific actor (e.g. on user delete) of a comment.
804
+     * The comment itself must not get lost/deleted.
805
+     *
806
+     * @param string $actorType the actor type (e.g. 'users')
807
+     * @param string $actorId a user id
808
+     * @return boolean
809
+     * @since 9.0.0
810
+     */
811
+    public function deleteReferencesOfActor($actorType, $actorId) {
812
+        $this->checkRoleParameters('Actor', $actorType, $actorId);
813
+
814
+        $qb = $this->dbConn->getQueryBuilder();
815
+        $affectedRows = $qb
816
+            ->update('comments')
817
+            ->set('actor_type', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
818
+            ->set('actor_id', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
819
+            ->where($qb->expr()->eq('actor_type', $qb->createParameter('type')))
820
+            ->andWhere($qb->expr()->eq('actor_id', $qb->createParameter('id')))
821
+            ->setParameter('type', $actorType)
822
+            ->setParameter('id', $actorId)
823
+            ->execute();
824
+
825
+        $this->commentsCache = [];
826
+
827
+        return is_int($affectedRows);
828
+    }
829
+
830
+    /**
831
+     * deletes all comments made of a specific object (e.g. on file delete)
832
+     *
833
+     * @param string $objectType the object type (e.g. 'files')
834
+     * @param string $objectId e.g. the file id
835
+     * @return boolean
836
+     * @since 9.0.0
837
+     */
838
+    public function deleteCommentsAtObject($objectType, $objectId) {
839
+        $this->checkRoleParameters('Object', $objectType, $objectId);
840
+
841
+        $qb = $this->dbConn->getQueryBuilder();
842
+        $affectedRows = $qb
843
+            ->delete('comments')
844
+            ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
845
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
846
+            ->setParameter('type', $objectType)
847
+            ->setParameter('id', $objectId)
848
+            ->execute();
849
+
850
+        $this->commentsCache = [];
851
+
852
+        return is_int($affectedRows);
853
+    }
854
+
855
+    /**
856
+     * deletes the read markers for the specified user
857
+     *
858
+     * @param \OCP\IUser $user
859
+     * @return bool
860
+     * @since 9.0.0
861
+     */
862
+    public function deleteReadMarksFromUser(IUser $user) {
863
+        $qb = $this->dbConn->getQueryBuilder();
864
+        $query = $qb->delete('comments_read_markers')
865
+            ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
866
+            ->setParameter('user_id', $user->getUID());
867
+
868
+        try {
869
+            $affectedRows = $query->execute();
870
+        } catch (DriverException $e) {
871
+            $this->logger->logException($e, ['app' => 'core_comments']);
872
+            return false;
873
+        }
874
+        return ($affectedRows > 0);
875
+    }
876
+
877
+    /**
878
+     * sets the read marker for a given file to the specified date for the
879
+     * provided user
880
+     *
881
+     * @param string $objectType
882
+     * @param string $objectId
883
+     * @param \DateTime $dateTime
884
+     * @param IUser $user
885
+     * @since 9.0.0
886
+     * @suppress SqlInjectionChecker
887
+     */
888
+    public function setReadMark($objectType, $objectId, \DateTime $dateTime, IUser $user) {
889
+        $this->checkRoleParameters('Object', $objectType, $objectId);
890
+
891
+        $qb = $this->dbConn->getQueryBuilder();
892
+        $values = [
893
+            'user_id' => $qb->createNamedParameter($user->getUID()),
894
+            'marker_datetime' => $qb->createNamedParameter($dateTime, 'datetime'),
895
+            'object_type' => $qb->createNamedParameter($objectType),
896
+            'object_id' => $qb->createNamedParameter($objectId),
897
+        ];
898
+
899
+        // Strategy: try to update, if this does not return affected rows, do an insert.
900
+        $affectedRows = $qb
901
+            ->update('comments_read_markers')
902
+            ->set('user_id', $values['user_id'])
903
+            ->set('marker_datetime', $values['marker_datetime'])
904
+            ->set('object_type', $values['object_type'])
905
+            ->set('object_id', $values['object_id'])
906
+            ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
907
+            ->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
908
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
909
+            ->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
910
+            ->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
911
+            ->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
912
+            ->execute();
913
+
914
+        if ($affectedRows > 0) {
915
+            return;
916
+        }
917
+
918
+        $qb->insert('comments_read_markers')
919
+            ->values($values)
920
+            ->execute();
921
+    }
922
+
923
+    /**
924
+     * returns the read marker for a given file to the specified date for the
925
+     * provided user. It returns null, when the marker is not present, i.e.
926
+     * no comments were marked as read.
927
+     *
928
+     * @param string $objectType
929
+     * @param string $objectId
930
+     * @param IUser $user
931
+     * @return \DateTime|null
932
+     * @since 9.0.0
933
+     */
934
+    public function getReadMark($objectType, $objectId, IUser $user) {
935
+        $qb = $this->dbConn->getQueryBuilder();
936
+        $resultStatement = $qb->select('marker_datetime')
937
+            ->from('comments_read_markers')
938
+            ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
939
+            ->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
940
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
941
+            ->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
942
+            ->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
943
+            ->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
944
+            ->execute();
945
+
946
+        $data = $resultStatement->fetch();
947
+        $resultStatement->closeCursor();
948
+        if (!$data || is_null($data['marker_datetime'])) {
949
+            return null;
950
+        }
951
+
952
+        return new \DateTime($data['marker_datetime']);
953
+    }
954
+
955
+    /**
956
+     * deletes the read markers on the specified object
957
+     *
958
+     * @param string $objectType
959
+     * @param string $objectId
960
+     * @return bool
961
+     * @since 9.0.0
962
+     */
963
+    public function deleteReadMarksOnObject($objectType, $objectId) {
964
+        $this->checkRoleParameters('Object', $objectType, $objectId);
965
+
966
+        $qb = $this->dbConn->getQueryBuilder();
967
+        $query = $qb->delete('comments_read_markers')
968
+            ->where($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
969
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
970
+            ->setParameter('object_type', $objectType)
971
+            ->setParameter('object_id', $objectId);
972
+
973
+        try {
974
+            $affectedRows = $query->execute();
975
+        } catch (DriverException $e) {
976
+            $this->logger->logException($e, ['app' => 'core_comments']);
977
+            return false;
978
+        }
979
+        return ($affectedRows > 0);
980
+    }
981
+
982
+    /**
983
+     * registers an Entity to the manager, so event notifications can be send
984
+     * to consumers of the comments infrastructure
985
+     *
986
+     * @param \Closure $closure
987
+     */
988
+    public function registerEventHandler(\Closure $closure) {
989
+        $this->eventHandlerClosures[] = $closure;
990
+        $this->eventHandlers = [];
991
+    }
992
+
993
+    /**
994
+     * registers a method that resolves an ID to a display name for a given type
995
+     *
996
+     * @param string $type
997
+     * @param \Closure $closure
998
+     * @throws \OutOfBoundsException
999
+     * @since 11.0.0
1000
+     *
1001
+     * Only one resolver shall be registered per type. Otherwise a
1002
+     * \OutOfBoundsException has to thrown.
1003
+     */
1004
+    public function registerDisplayNameResolver($type, \Closure $closure) {
1005
+        if (!is_string($type)) {
1006
+            throw new \InvalidArgumentException('String expected.');
1007
+        }
1008
+        if (isset($this->displayNameResolvers[$type])) {
1009
+            throw new \OutOfBoundsException('Displayname resolver for this type already registered');
1010
+        }
1011
+        $this->displayNameResolvers[$type] = $closure;
1012
+    }
1013
+
1014
+    /**
1015
+     * resolves a given ID of a given Type to a display name.
1016
+     *
1017
+     * @param string $type
1018
+     * @param string $id
1019
+     * @return string
1020
+     * @throws \OutOfBoundsException
1021
+     * @since 11.0.0
1022
+     *
1023
+     * If a provided type was not registered, an \OutOfBoundsException shall
1024
+     * be thrown. It is upon the resolver discretion what to return of the
1025
+     * provided ID is unknown. It must be ensured that a string is returned.
1026
+     */
1027
+    public function resolveDisplayName($type, $id) {
1028
+        if (!is_string($type)) {
1029
+            throw new \InvalidArgumentException('String expected.');
1030
+        }
1031
+        if (!isset($this->displayNameResolvers[$type])) {
1032
+            throw new \OutOfBoundsException('No Displayname resolver for this type registered');
1033
+        }
1034
+        return (string)$this->displayNameResolvers[$type]($id);
1035
+    }
1036
+
1037
+    /**
1038
+     * returns valid, registered entities
1039
+     *
1040
+     * @return \OCP\Comments\ICommentsEventHandler[]
1041
+     */
1042
+    private function getEventHandlers() {
1043
+        if (!empty($this->eventHandlers)) {
1044
+            return $this->eventHandlers;
1045
+        }
1046
+
1047
+        $this->eventHandlers = [];
1048
+        foreach ($this->eventHandlerClosures as $name => $closure) {
1049
+            $entity = $closure();
1050
+            if (!($entity instanceof ICommentsEventHandler)) {
1051
+                throw new \InvalidArgumentException('The given entity does not implement the ICommentsEntity interface');
1052
+            }
1053
+            $this->eventHandlers[$name] = $entity;
1054
+        }
1055
+
1056
+        return $this->eventHandlers;
1057
+    }
1058
+
1059
+    /**
1060
+     * sends notifications to the registered entities
1061
+     *
1062
+     * @param $eventType
1063
+     * @param IComment $comment
1064
+     */
1065
+    private function sendEvent($eventType, IComment $comment) {
1066
+        $entities = $this->getEventHandlers();
1067
+        $event = new CommentsEvent($eventType, $comment);
1068
+        foreach ($entities as $entity) {
1069
+            $entity->handle($event);
1070
+        }
1071
+    }
1072 1072
 }
Please login to merge, or discard this patch.