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