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