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