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