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