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