Completed
Pull Request — master (#9129)
by Joas
36:10 queued 16:59
created
lib/private/Comments/Manager.php 1 patch
Indentation   +933 added lines, -933 removed lines patch added patch discarded remove patch
@@ -41,937 +41,937 @@
 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
-		$objectType,
389
-		$objectId,
390
-		$lastKnownCommentId,
391
-		$sortDirection = 'asc',
392
-		$limit = 30
393
-	) {
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
-			$query->andWhere(
415
-				$query->expr()->lte(
416
-					'creation_timestamp',
417
-					$query->createNamedParameter($lastKnownComment->getCreationDateTime()->getTimestamp()
418
-				)),
419
-				$query->expr()->lte(
420
-					'id',
421
-					$query->createNamedParameter($lastKnownComment->getId()
422
-				))
423
-			);
424
-		}
425
-
426
-		$resultStatement = $query->execute();
427
-		while ($data = $resultStatement->fetch()) {
428
-			$comment = new Comment($this->normalizeDatabaseData($data));
429
-			$this->cache($comment);
430
-			$comments[] = $comment;
431
-		}
432
-		$resultStatement->closeCursor();
433
-
434
-		return $comments;
435
-	}
436
-
437
-	/**
438
-	 * @param string $objectType
439
-	 * @param string $objectId
440
-	 * @param int $id
441
-	 * @return Comment|null
442
-	 */
443
-	protected function getLastKnownComment($objectType,
444
-													  $objectId,
445
-													  $id) {
446
-		$query = $this->dbConn->getQueryBuilder();
447
-		$query->select('*')
448
-			->from('comments')
449
-			->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
450
-			->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
451
-			->andWhere($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
452
-
453
-		$result = $query->execute();
454
-		$row = $result->fetch();
455
-		$result->closeCursor();
456
-
457
-		if ($row) {
458
-			$comment = new Comment($this->normalizeDatabaseData($row));
459
-			$this->cache($comment);
460
-			return $comment;
461
-		}
462
-
463
-		return null;
464
-	}
465
-
466
-	/**
467
-	 * @param $objectType string the object type, e.g. 'files'
468
-	 * @param $objectId string the id of the object
469
-	 * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
470
-	 * that may be returned
471
-	 * @return Int
472
-	 * @since 9.0.0
473
-	 */
474
-	public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null) {
475
-		$qb = $this->dbConn->getQueryBuilder();
476
-		$query = $qb->select($qb->createFunction('COUNT(`id`)'))
477
-			->from('comments')
478
-			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
479
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
480
-			->setParameter('type', $objectType)
481
-			->setParameter('id', $objectId);
482
-
483
-		if (!is_null($notOlderThan)) {
484
-			$query
485
-				->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
486
-				->setParameter('notOlderThan', $notOlderThan, 'datetime');
487
-		}
488
-
489
-		$resultStatement = $query->execute();
490
-		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
491
-		$resultStatement->closeCursor();
492
-		return (int)$data[0];
493
-	}
494
-
495
-	/**
496
-	 * Get the number of unread comments for all files in a folder
497
-	 *
498
-	 * @param int $folderId
499
-	 * @param IUser $user
500
-	 * @return array [$fileId => $unreadCount]
501
-	 */
502
-	public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user) {
503
-		$qb = $this->dbConn->getQueryBuilder();
504
-		$query = $qb->select('f.fileid')
505
-			->selectAlias(
506
-				$qb->createFunction('COUNT(' . $qb->getColumnName('c.id') . ')'),
507
-				'num_ids'
508
-			)
509
-			->from('comments', 'c')
510
-			->innerJoin('c', 'filecache', 'f', $qb->expr()->andX(
511
-				$qb->expr()->eq('c.object_type', $qb->createNamedParameter('files')),
512
-				$qb->expr()->eq('f.fileid', $qb->expr()->castColumn('c.object_id', IQueryBuilder::PARAM_INT))
513
-			))
514
-			->leftJoin('c', 'comments_read_markers', 'm', $qb->expr()->andX(
515
-				$qb->expr()->eq('m.object_type', $qb->createNamedParameter('files')),
516
-				$qb->expr()->eq('m.object_id', 'c.object_id'),
517
-				$qb->expr()->eq('m.user_id', $qb->createNamedParameter($user->getUID()))
518
-			))
519
-			->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($folderId)))
520
-			->andWhere($qb->expr()->orX(
521
-				$qb->expr()->gt('c.creation_timestamp', 'marker_datetime'),
522
-				$qb->expr()->isNull('marker_datetime')
523
-			))
524
-			->groupBy('f.fileid');
525
-
526
-		$resultStatement = $query->execute();
527
-
528
-		$results = [];
529
-		while ($row = $resultStatement->fetch()) {
530
-			$results[$row['fileid']] = (int) $row['num_ids'];
531
-		}
532
-		$resultStatement->closeCursor();
533
-		return $results;
534
-	}
535
-
536
-	/**
537
-	 * creates a new comment and returns it. At this point of time, it is not
538
-	 * saved in the used data storage. Use save() after setting other fields
539
-	 * of the comment (e.g. message or verb).
540
-	 *
541
-	 * @param string $actorType the actor type (e.g. 'users')
542
-	 * @param string $actorId a user id
543
-	 * @param string $objectType the object type the comment is attached to
544
-	 * @param string $objectId the object id the comment is attached to
545
-	 * @return IComment
546
-	 * @since 9.0.0
547
-	 */
548
-	public function create($actorType, $actorId, $objectType, $objectId) {
549
-		$comment = new Comment();
550
-		$comment
551
-			->setActor($actorType, $actorId)
552
-			->setObject($objectType, $objectId);
553
-		return $comment;
554
-	}
555
-
556
-	/**
557
-	 * permanently deletes the comment specified by the ID
558
-	 *
559
-	 * When the comment has child comments, their parent ID will be changed to
560
-	 * the parent ID of the item that is to be deleted.
561
-	 *
562
-	 * @param string $id
563
-	 * @return bool
564
-	 * @throws \InvalidArgumentException
565
-	 * @since 9.0.0
566
-	 */
567
-	public function delete($id) {
568
-		if (!is_string($id)) {
569
-			throw new \InvalidArgumentException('Parameter must be string');
570
-		}
571
-
572
-		try {
573
-			$comment = $this->get($id);
574
-		} catch (\Exception $e) {
575
-			// Ignore exceptions, we just don't fire a hook then
576
-			$comment = null;
577
-		}
578
-
579
-		$qb = $this->dbConn->getQueryBuilder();
580
-		$query = $qb->delete('comments')
581
-			->where($qb->expr()->eq('id', $qb->createParameter('id')))
582
-			->setParameter('id', $id);
583
-
584
-		try {
585
-			$affectedRows = $query->execute();
586
-			$this->uncache($id);
587
-		} catch (DriverException $e) {
588
-			$this->logger->logException($e, ['app' => 'core_comments']);
589
-			return false;
590
-		}
591
-
592
-		if ($affectedRows > 0 && $comment instanceof IComment) {
593
-			$this->sendEvent(CommentsEvent::EVENT_DELETE, $comment);
594
-		}
595
-
596
-		return ($affectedRows > 0);
597
-	}
598
-
599
-	/**
600
-	 * saves the comment permanently
601
-	 *
602
-	 * if the supplied comment has an empty ID, a new entry comment will be
603
-	 * saved and the instance updated with the new ID.
604
-	 *
605
-	 * Otherwise, an existing comment will be updated.
606
-	 *
607
-	 * Throws NotFoundException when a comment that is to be updated does not
608
-	 * exist anymore at this point of time.
609
-	 *
610
-	 * @param IComment $comment
611
-	 * @return bool
612
-	 * @throws NotFoundException
613
-	 * @since 9.0.0
614
-	 */
615
-	public function save(IComment $comment) {
616
-		if ($this->prepareCommentForDatabaseWrite($comment)->getId() === '') {
617
-			$result = $this->insert($comment);
618
-		} else {
619
-			$result = $this->update($comment);
620
-		}
621
-
622
-		if ($result && !!$comment->getParentId()) {
623
-			$this->updateChildrenInformation(
624
-				$comment->getParentId(),
625
-				$comment->getCreationDateTime()
626
-			);
627
-			$this->cache($comment);
628
-		}
629
-
630
-		return $result;
631
-	}
632
-
633
-	/**
634
-	 * inserts the provided comment in the database
635
-	 *
636
-	 * @param IComment $comment
637
-	 * @return bool
638
-	 */
639
-	protected function insert(IComment &$comment) {
640
-		$qb = $this->dbConn->getQueryBuilder();
641
-		$affectedRows = $qb
642
-			->insert('comments')
643
-			->values([
644
-				'parent_id' => $qb->createNamedParameter($comment->getParentId()),
645
-				'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()),
646
-				'children_count' => $qb->createNamedParameter($comment->getChildrenCount()),
647
-				'actor_type' => $qb->createNamedParameter($comment->getActorType()),
648
-				'actor_id' => $qb->createNamedParameter($comment->getActorId()),
649
-				'message' => $qb->createNamedParameter($comment->getMessage()),
650
-				'verb' => $qb->createNamedParameter($comment->getVerb()),
651
-				'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'),
652
-				'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'),
653
-				'object_type' => $qb->createNamedParameter($comment->getObjectType()),
654
-				'object_id' => $qb->createNamedParameter($comment->getObjectId()),
655
-			])
656
-			->execute();
657
-
658
-		if ($affectedRows > 0) {
659
-			$comment->setId((string)$qb->getLastInsertId());
660
-			$this->sendEvent(CommentsEvent::EVENT_ADD, $comment);
661
-		}
662
-
663
-		return $affectedRows > 0;
664
-	}
665
-
666
-	/**
667
-	 * updates a Comment data row
668
-	 *
669
-	 * @param IComment $comment
670
-	 * @return bool
671
-	 * @throws NotFoundException
672
-	 */
673
-	protected function update(IComment $comment) {
674
-		// for properly working preUpdate Events we need the old comments as is
675
-		// in the DB and overcome caching. Also avoid that outdated information stays.
676
-		$this->uncache($comment->getId());
677
-		$this->sendEvent(CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId()));
678
-		$this->uncache($comment->getId());
679
-
680
-		$qb = $this->dbConn->getQueryBuilder();
681
-		$affectedRows = $qb
682
-			->update('comments')
683
-			->set('parent_id', $qb->createNamedParameter($comment->getParentId()))
684
-			->set('topmost_parent_id', $qb->createNamedParameter($comment->getTopmostParentId()))
685
-			->set('children_count', $qb->createNamedParameter($comment->getChildrenCount()))
686
-			->set('actor_type', $qb->createNamedParameter($comment->getActorType()))
687
-			->set('actor_id', $qb->createNamedParameter($comment->getActorId()))
688
-			->set('message', $qb->createNamedParameter($comment->getMessage()))
689
-			->set('verb', $qb->createNamedParameter($comment->getVerb()))
690
-			->set('creation_timestamp', $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'))
691
-			->set('latest_child_timestamp', $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'))
692
-			->set('object_type', $qb->createNamedParameter($comment->getObjectType()))
693
-			->set('object_id', $qb->createNamedParameter($comment->getObjectId()))
694
-			->where($qb->expr()->eq('id', $qb->createParameter('id')))
695
-			->setParameter('id', $comment->getId())
696
-			->execute();
697
-
698
-		if ($affectedRows === 0) {
699
-			throw new NotFoundException('Comment to update does ceased to exist');
700
-		}
701
-
702
-		$this->sendEvent(CommentsEvent::EVENT_UPDATE, $comment);
703
-
704
-		return $affectedRows > 0;
705
-	}
706
-
707
-	/**
708
-	 * removes references to specific actor (e.g. on user delete) of a comment.
709
-	 * The comment itself must not get lost/deleted.
710
-	 *
711
-	 * @param string $actorType the actor type (e.g. 'users')
712
-	 * @param string $actorId a user id
713
-	 * @return boolean
714
-	 * @since 9.0.0
715
-	 */
716
-	public function deleteReferencesOfActor($actorType, $actorId) {
717
-		$this->checkRoleParameters('Actor', $actorType, $actorId);
718
-
719
-		$qb = $this->dbConn->getQueryBuilder();
720
-		$affectedRows = $qb
721
-			->update('comments')
722
-			->set('actor_type', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
723
-			->set('actor_id', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
724
-			->where($qb->expr()->eq('actor_type', $qb->createParameter('type')))
725
-			->andWhere($qb->expr()->eq('actor_id', $qb->createParameter('id')))
726
-			->setParameter('type', $actorType)
727
-			->setParameter('id', $actorId)
728
-			->execute();
729
-
730
-		$this->commentsCache = [];
731
-
732
-		return is_int($affectedRows);
733
-	}
734
-
735
-	/**
736
-	 * deletes all comments made of a specific object (e.g. on file delete)
737
-	 *
738
-	 * @param string $objectType the object type (e.g. 'files')
739
-	 * @param string $objectId e.g. the file id
740
-	 * @return boolean
741
-	 * @since 9.0.0
742
-	 */
743
-	public function deleteCommentsAtObject($objectType, $objectId) {
744
-		$this->checkRoleParameters('Object', $objectType, $objectId);
745
-
746
-		$qb = $this->dbConn->getQueryBuilder();
747
-		$affectedRows = $qb
748
-			->delete('comments')
749
-			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
750
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
751
-			->setParameter('type', $objectType)
752
-			->setParameter('id', $objectId)
753
-			->execute();
754
-
755
-		$this->commentsCache = [];
756
-
757
-		return is_int($affectedRows);
758
-	}
759
-
760
-	/**
761
-	 * deletes the read markers for the specified user
762
-	 *
763
-	 * @param \OCP\IUser $user
764
-	 * @return bool
765
-	 * @since 9.0.0
766
-	 */
767
-	public function deleteReadMarksFromUser(IUser $user) {
768
-		$qb = $this->dbConn->getQueryBuilder();
769
-		$query = $qb->delete('comments_read_markers')
770
-			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
771
-			->setParameter('user_id', $user->getUID());
772
-
773
-		try {
774
-			$affectedRows = $query->execute();
775
-		} catch (DriverException $e) {
776
-			$this->logger->logException($e, ['app' => 'core_comments']);
777
-			return false;
778
-		}
779
-		return ($affectedRows > 0);
780
-	}
781
-
782
-	/**
783
-	 * sets the read marker for a given file to the specified date for the
784
-	 * provided user
785
-	 *
786
-	 * @param string $objectType
787
-	 * @param string $objectId
788
-	 * @param \DateTime $dateTime
789
-	 * @param IUser $user
790
-	 * @since 9.0.0
791
-	 * @suppress SqlInjectionChecker
792
-	 */
793
-	public function setReadMark($objectType, $objectId, \DateTime $dateTime, IUser $user) {
794
-		$this->checkRoleParameters('Object', $objectType, $objectId);
795
-
796
-		$qb = $this->dbConn->getQueryBuilder();
797
-		$values = [
798
-			'user_id' => $qb->createNamedParameter($user->getUID()),
799
-			'marker_datetime' => $qb->createNamedParameter($dateTime, 'datetime'),
800
-			'object_type' => $qb->createNamedParameter($objectType),
801
-			'object_id' => $qb->createNamedParameter($objectId),
802
-		];
803
-
804
-		// Strategy: try to update, if this does not return affected rows, do an insert.
805
-		$affectedRows = $qb
806
-			->update('comments_read_markers')
807
-			->set('user_id', $values['user_id'])
808
-			->set('marker_datetime', $values['marker_datetime'])
809
-			->set('object_type', $values['object_type'])
810
-			->set('object_id', $values['object_id'])
811
-			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
812
-			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
813
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
814
-			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
815
-			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
816
-			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
817
-			->execute();
818
-
819
-		if ($affectedRows > 0) {
820
-			return;
821
-		}
822
-
823
-		$qb->insert('comments_read_markers')
824
-			->values($values)
825
-			->execute();
826
-	}
827
-
828
-	/**
829
-	 * returns the read marker for a given file to the specified date for the
830
-	 * provided user. It returns null, when the marker is not present, i.e.
831
-	 * no comments were marked as read.
832
-	 *
833
-	 * @param string $objectType
834
-	 * @param string $objectId
835
-	 * @param IUser $user
836
-	 * @return \DateTime|null
837
-	 * @since 9.0.0
838
-	 */
839
-	public function getReadMark($objectType, $objectId, IUser $user) {
840
-		$qb = $this->dbConn->getQueryBuilder();
841
-		$resultStatement = $qb->select('marker_datetime')
842
-			->from('comments_read_markers')
843
-			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
844
-			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
845
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
846
-			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
847
-			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
848
-			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
849
-			->execute();
850
-
851
-		$data = $resultStatement->fetch();
852
-		$resultStatement->closeCursor();
853
-		if (!$data || is_null($data['marker_datetime'])) {
854
-			return null;
855
-		}
856
-
857
-		return new \DateTime($data['marker_datetime']);
858
-	}
859
-
860
-	/**
861
-	 * deletes the read markers on the specified object
862
-	 *
863
-	 * @param string $objectType
864
-	 * @param string $objectId
865
-	 * @return bool
866
-	 * @since 9.0.0
867
-	 */
868
-	public function deleteReadMarksOnObject($objectType, $objectId) {
869
-		$this->checkRoleParameters('Object', $objectType, $objectId);
870
-
871
-		$qb = $this->dbConn->getQueryBuilder();
872
-		$query = $qb->delete('comments_read_markers')
873
-			->where($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
874
-			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
875
-			->setParameter('object_type', $objectType)
876
-			->setParameter('object_id', $objectId);
877
-
878
-		try {
879
-			$affectedRows = $query->execute();
880
-		} catch (DriverException $e) {
881
-			$this->logger->logException($e, ['app' => 'core_comments']);
882
-			return false;
883
-		}
884
-		return ($affectedRows > 0);
885
-	}
886
-
887
-	/**
888
-	 * registers an Entity to the manager, so event notifications can be send
889
-	 * to consumers of the comments infrastructure
890
-	 *
891
-	 * @param \Closure $closure
892
-	 */
893
-	public function registerEventHandler(\Closure $closure) {
894
-		$this->eventHandlerClosures[] = $closure;
895
-		$this->eventHandlers = [];
896
-	}
897
-
898
-	/**
899
-	 * registers a method that resolves an ID to a display name for a given type
900
-	 *
901
-	 * @param string $type
902
-	 * @param \Closure $closure
903
-	 * @throws \OutOfBoundsException
904
-	 * @since 11.0.0
905
-	 *
906
-	 * Only one resolver shall be registered per type. Otherwise a
907
-	 * \OutOfBoundsException has to thrown.
908
-	 */
909
-	public function registerDisplayNameResolver($type, \Closure $closure) {
910
-		if (!is_string($type)) {
911
-			throw new \InvalidArgumentException('String expected.');
912
-		}
913
-		if (isset($this->displayNameResolvers[$type])) {
914
-			throw new \OutOfBoundsException('Displayname resolver for this type already registered');
915
-		}
916
-		$this->displayNameResolvers[$type] = $closure;
917
-	}
918
-
919
-	/**
920
-	 * resolves a given ID of a given Type to a display name.
921
-	 *
922
-	 * @param string $type
923
-	 * @param string $id
924
-	 * @return string
925
-	 * @throws \OutOfBoundsException
926
-	 * @since 11.0.0
927
-	 *
928
-	 * If a provided type was not registered, an \OutOfBoundsException shall
929
-	 * be thrown. It is upon the resolver discretion what to return of the
930
-	 * provided ID is unknown. It must be ensured that a string is returned.
931
-	 */
932
-	public function resolveDisplayName($type, $id) {
933
-		if (!is_string($type)) {
934
-			throw new \InvalidArgumentException('String expected.');
935
-		}
936
-		if (!isset($this->displayNameResolvers[$type])) {
937
-			throw new \OutOfBoundsException('No Displayname resolver for this type registered');
938
-		}
939
-		return (string)$this->displayNameResolvers[$type]($id);
940
-	}
941
-
942
-	/**
943
-	 * returns valid, registered entities
944
-	 *
945
-	 * @return \OCP\Comments\ICommentsEventHandler[]
946
-	 */
947
-	private function getEventHandlers() {
948
-		if (!empty($this->eventHandlers)) {
949
-			return $this->eventHandlers;
950
-		}
951
-
952
-		$this->eventHandlers = [];
953
-		foreach ($this->eventHandlerClosures as $name => $closure) {
954
-			$entity = $closure();
955
-			if (!($entity instanceof ICommentsEventHandler)) {
956
-				throw new \InvalidArgumentException('The given entity does not implement the ICommentsEntity interface');
957
-			}
958
-			$this->eventHandlers[$name] = $entity;
959
-		}
960
-
961
-		return $this->eventHandlers;
962
-	}
963
-
964
-	/**
965
-	 * sends notifications to the registered entities
966
-	 *
967
-	 * @param $eventType
968
-	 * @param IComment $comment
969
-	 */
970
-	private function sendEvent($eventType, IComment $comment) {
971
-		$entities = $this->getEventHandlers();
972
-		$event = new CommentsEvent($eventType, $comment);
973
-		foreach ($entities as $entity) {
974
-			$entity->handle($event);
975
-		}
976
-	}
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
+        $objectType,
389
+        $objectId,
390
+        $lastKnownCommentId,
391
+        $sortDirection = 'asc',
392
+        $limit = 30
393
+    ) {
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
+            $query->andWhere(
415
+                $query->expr()->lte(
416
+                    'creation_timestamp',
417
+                    $query->createNamedParameter($lastKnownComment->getCreationDateTime()->getTimestamp()
418
+                )),
419
+                $query->expr()->lte(
420
+                    'id',
421
+                    $query->createNamedParameter($lastKnownComment->getId()
422
+                ))
423
+            );
424
+        }
425
+
426
+        $resultStatement = $query->execute();
427
+        while ($data = $resultStatement->fetch()) {
428
+            $comment = new Comment($this->normalizeDatabaseData($data));
429
+            $this->cache($comment);
430
+            $comments[] = $comment;
431
+        }
432
+        $resultStatement->closeCursor();
433
+
434
+        return $comments;
435
+    }
436
+
437
+    /**
438
+     * @param string $objectType
439
+     * @param string $objectId
440
+     * @param int $id
441
+     * @return Comment|null
442
+     */
443
+    protected function getLastKnownComment($objectType,
444
+                                                        $objectId,
445
+                                                        $id) {
446
+        $query = $this->dbConn->getQueryBuilder();
447
+        $query->select('*')
448
+            ->from('comments')
449
+            ->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
450
+            ->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
451
+            ->andWhere($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
452
+
453
+        $result = $query->execute();
454
+        $row = $result->fetch();
455
+        $result->closeCursor();
456
+
457
+        if ($row) {
458
+            $comment = new Comment($this->normalizeDatabaseData($row));
459
+            $this->cache($comment);
460
+            return $comment;
461
+        }
462
+
463
+        return null;
464
+    }
465
+
466
+    /**
467
+     * @param $objectType string the object type, e.g. 'files'
468
+     * @param $objectId string the id of the object
469
+     * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
470
+     * that may be returned
471
+     * @return Int
472
+     * @since 9.0.0
473
+     */
474
+    public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null) {
475
+        $qb = $this->dbConn->getQueryBuilder();
476
+        $query = $qb->select($qb->createFunction('COUNT(`id`)'))
477
+            ->from('comments')
478
+            ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
479
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
480
+            ->setParameter('type', $objectType)
481
+            ->setParameter('id', $objectId);
482
+
483
+        if (!is_null($notOlderThan)) {
484
+            $query
485
+                ->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
486
+                ->setParameter('notOlderThan', $notOlderThan, 'datetime');
487
+        }
488
+
489
+        $resultStatement = $query->execute();
490
+        $data = $resultStatement->fetch(\PDO::FETCH_NUM);
491
+        $resultStatement->closeCursor();
492
+        return (int)$data[0];
493
+    }
494
+
495
+    /**
496
+     * Get the number of unread comments for all files in a folder
497
+     *
498
+     * @param int $folderId
499
+     * @param IUser $user
500
+     * @return array [$fileId => $unreadCount]
501
+     */
502
+    public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user) {
503
+        $qb = $this->dbConn->getQueryBuilder();
504
+        $query = $qb->select('f.fileid')
505
+            ->selectAlias(
506
+                $qb->createFunction('COUNT(' . $qb->getColumnName('c.id') . ')'),
507
+                'num_ids'
508
+            )
509
+            ->from('comments', 'c')
510
+            ->innerJoin('c', 'filecache', 'f', $qb->expr()->andX(
511
+                $qb->expr()->eq('c.object_type', $qb->createNamedParameter('files')),
512
+                $qb->expr()->eq('f.fileid', $qb->expr()->castColumn('c.object_id', IQueryBuilder::PARAM_INT))
513
+            ))
514
+            ->leftJoin('c', 'comments_read_markers', 'm', $qb->expr()->andX(
515
+                $qb->expr()->eq('m.object_type', $qb->createNamedParameter('files')),
516
+                $qb->expr()->eq('m.object_id', 'c.object_id'),
517
+                $qb->expr()->eq('m.user_id', $qb->createNamedParameter($user->getUID()))
518
+            ))
519
+            ->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($folderId)))
520
+            ->andWhere($qb->expr()->orX(
521
+                $qb->expr()->gt('c.creation_timestamp', 'marker_datetime'),
522
+                $qb->expr()->isNull('marker_datetime')
523
+            ))
524
+            ->groupBy('f.fileid');
525
+
526
+        $resultStatement = $query->execute();
527
+
528
+        $results = [];
529
+        while ($row = $resultStatement->fetch()) {
530
+            $results[$row['fileid']] = (int) $row['num_ids'];
531
+        }
532
+        $resultStatement->closeCursor();
533
+        return $results;
534
+    }
535
+
536
+    /**
537
+     * creates a new comment and returns it. At this point of time, it is not
538
+     * saved in the used data storage. Use save() after setting other fields
539
+     * of the comment (e.g. message or verb).
540
+     *
541
+     * @param string $actorType the actor type (e.g. 'users')
542
+     * @param string $actorId a user id
543
+     * @param string $objectType the object type the comment is attached to
544
+     * @param string $objectId the object id the comment is attached to
545
+     * @return IComment
546
+     * @since 9.0.0
547
+     */
548
+    public function create($actorType, $actorId, $objectType, $objectId) {
549
+        $comment = new Comment();
550
+        $comment
551
+            ->setActor($actorType, $actorId)
552
+            ->setObject($objectType, $objectId);
553
+        return $comment;
554
+    }
555
+
556
+    /**
557
+     * permanently deletes the comment specified by the ID
558
+     *
559
+     * When the comment has child comments, their parent ID will be changed to
560
+     * the parent ID of the item that is to be deleted.
561
+     *
562
+     * @param string $id
563
+     * @return bool
564
+     * @throws \InvalidArgumentException
565
+     * @since 9.0.0
566
+     */
567
+    public function delete($id) {
568
+        if (!is_string($id)) {
569
+            throw new \InvalidArgumentException('Parameter must be string');
570
+        }
571
+
572
+        try {
573
+            $comment = $this->get($id);
574
+        } catch (\Exception $e) {
575
+            // Ignore exceptions, we just don't fire a hook then
576
+            $comment = null;
577
+        }
578
+
579
+        $qb = $this->dbConn->getQueryBuilder();
580
+        $query = $qb->delete('comments')
581
+            ->where($qb->expr()->eq('id', $qb->createParameter('id')))
582
+            ->setParameter('id', $id);
583
+
584
+        try {
585
+            $affectedRows = $query->execute();
586
+            $this->uncache($id);
587
+        } catch (DriverException $e) {
588
+            $this->logger->logException($e, ['app' => 'core_comments']);
589
+            return false;
590
+        }
591
+
592
+        if ($affectedRows > 0 && $comment instanceof IComment) {
593
+            $this->sendEvent(CommentsEvent::EVENT_DELETE, $comment);
594
+        }
595
+
596
+        return ($affectedRows > 0);
597
+    }
598
+
599
+    /**
600
+     * saves the comment permanently
601
+     *
602
+     * if the supplied comment has an empty ID, a new entry comment will be
603
+     * saved and the instance updated with the new ID.
604
+     *
605
+     * Otherwise, an existing comment will be updated.
606
+     *
607
+     * Throws NotFoundException when a comment that is to be updated does not
608
+     * exist anymore at this point of time.
609
+     *
610
+     * @param IComment $comment
611
+     * @return bool
612
+     * @throws NotFoundException
613
+     * @since 9.0.0
614
+     */
615
+    public function save(IComment $comment) {
616
+        if ($this->prepareCommentForDatabaseWrite($comment)->getId() === '') {
617
+            $result = $this->insert($comment);
618
+        } else {
619
+            $result = $this->update($comment);
620
+        }
621
+
622
+        if ($result && !!$comment->getParentId()) {
623
+            $this->updateChildrenInformation(
624
+                $comment->getParentId(),
625
+                $comment->getCreationDateTime()
626
+            );
627
+            $this->cache($comment);
628
+        }
629
+
630
+        return $result;
631
+    }
632
+
633
+    /**
634
+     * inserts the provided comment in the database
635
+     *
636
+     * @param IComment $comment
637
+     * @return bool
638
+     */
639
+    protected function insert(IComment &$comment) {
640
+        $qb = $this->dbConn->getQueryBuilder();
641
+        $affectedRows = $qb
642
+            ->insert('comments')
643
+            ->values([
644
+                'parent_id' => $qb->createNamedParameter($comment->getParentId()),
645
+                'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()),
646
+                'children_count' => $qb->createNamedParameter($comment->getChildrenCount()),
647
+                'actor_type' => $qb->createNamedParameter($comment->getActorType()),
648
+                'actor_id' => $qb->createNamedParameter($comment->getActorId()),
649
+                'message' => $qb->createNamedParameter($comment->getMessage()),
650
+                'verb' => $qb->createNamedParameter($comment->getVerb()),
651
+                'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'),
652
+                'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'),
653
+                'object_type' => $qb->createNamedParameter($comment->getObjectType()),
654
+                'object_id' => $qb->createNamedParameter($comment->getObjectId()),
655
+            ])
656
+            ->execute();
657
+
658
+        if ($affectedRows > 0) {
659
+            $comment->setId((string)$qb->getLastInsertId());
660
+            $this->sendEvent(CommentsEvent::EVENT_ADD, $comment);
661
+        }
662
+
663
+        return $affectedRows > 0;
664
+    }
665
+
666
+    /**
667
+     * updates a Comment data row
668
+     *
669
+     * @param IComment $comment
670
+     * @return bool
671
+     * @throws NotFoundException
672
+     */
673
+    protected function update(IComment $comment) {
674
+        // for properly working preUpdate Events we need the old comments as is
675
+        // in the DB and overcome caching. Also avoid that outdated information stays.
676
+        $this->uncache($comment->getId());
677
+        $this->sendEvent(CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId()));
678
+        $this->uncache($comment->getId());
679
+
680
+        $qb = $this->dbConn->getQueryBuilder();
681
+        $affectedRows = $qb
682
+            ->update('comments')
683
+            ->set('parent_id', $qb->createNamedParameter($comment->getParentId()))
684
+            ->set('topmost_parent_id', $qb->createNamedParameter($comment->getTopmostParentId()))
685
+            ->set('children_count', $qb->createNamedParameter($comment->getChildrenCount()))
686
+            ->set('actor_type', $qb->createNamedParameter($comment->getActorType()))
687
+            ->set('actor_id', $qb->createNamedParameter($comment->getActorId()))
688
+            ->set('message', $qb->createNamedParameter($comment->getMessage()))
689
+            ->set('verb', $qb->createNamedParameter($comment->getVerb()))
690
+            ->set('creation_timestamp', $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'))
691
+            ->set('latest_child_timestamp', $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'))
692
+            ->set('object_type', $qb->createNamedParameter($comment->getObjectType()))
693
+            ->set('object_id', $qb->createNamedParameter($comment->getObjectId()))
694
+            ->where($qb->expr()->eq('id', $qb->createParameter('id')))
695
+            ->setParameter('id', $comment->getId())
696
+            ->execute();
697
+
698
+        if ($affectedRows === 0) {
699
+            throw new NotFoundException('Comment to update does ceased to exist');
700
+        }
701
+
702
+        $this->sendEvent(CommentsEvent::EVENT_UPDATE, $comment);
703
+
704
+        return $affectedRows > 0;
705
+    }
706
+
707
+    /**
708
+     * removes references to specific actor (e.g. on user delete) of a comment.
709
+     * The comment itself must not get lost/deleted.
710
+     *
711
+     * @param string $actorType the actor type (e.g. 'users')
712
+     * @param string $actorId a user id
713
+     * @return boolean
714
+     * @since 9.0.0
715
+     */
716
+    public function deleteReferencesOfActor($actorType, $actorId) {
717
+        $this->checkRoleParameters('Actor', $actorType, $actorId);
718
+
719
+        $qb = $this->dbConn->getQueryBuilder();
720
+        $affectedRows = $qb
721
+            ->update('comments')
722
+            ->set('actor_type', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
723
+            ->set('actor_id', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
724
+            ->where($qb->expr()->eq('actor_type', $qb->createParameter('type')))
725
+            ->andWhere($qb->expr()->eq('actor_id', $qb->createParameter('id')))
726
+            ->setParameter('type', $actorType)
727
+            ->setParameter('id', $actorId)
728
+            ->execute();
729
+
730
+        $this->commentsCache = [];
731
+
732
+        return is_int($affectedRows);
733
+    }
734
+
735
+    /**
736
+     * deletes all comments made of a specific object (e.g. on file delete)
737
+     *
738
+     * @param string $objectType the object type (e.g. 'files')
739
+     * @param string $objectId e.g. the file id
740
+     * @return boolean
741
+     * @since 9.0.0
742
+     */
743
+    public function deleteCommentsAtObject($objectType, $objectId) {
744
+        $this->checkRoleParameters('Object', $objectType, $objectId);
745
+
746
+        $qb = $this->dbConn->getQueryBuilder();
747
+        $affectedRows = $qb
748
+            ->delete('comments')
749
+            ->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
750
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
751
+            ->setParameter('type', $objectType)
752
+            ->setParameter('id', $objectId)
753
+            ->execute();
754
+
755
+        $this->commentsCache = [];
756
+
757
+        return is_int($affectedRows);
758
+    }
759
+
760
+    /**
761
+     * deletes the read markers for the specified user
762
+     *
763
+     * @param \OCP\IUser $user
764
+     * @return bool
765
+     * @since 9.0.0
766
+     */
767
+    public function deleteReadMarksFromUser(IUser $user) {
768
+        $qb = $this->dbConn->getQueryBuilder();
769
+        $query = $qb->delete('comments_read_markers')
770
+            ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
771
+            ->setParameter('user_id', $user->getUID());
772
+
773
+        try {
774
+            $affectedRows = $query->execute();
775
+        } catch (DriverException $e) {
776
+            $this->logger->logException($e, ['app' => 'core_comments']);
777
+            return false;
778
+        }
779
+        return ($affectedRows > 0);
780
+    }
781
+
782
+    /**
783
+     * sets the read marker for a given file to the specified date for the
784
+     * provided user
785
+     *
786
+     * @param string $objectType
787
+     * @param string $objectId
788
+     * @param \DateTime $dateTime
789
+     * @param IUser $user
790
+     * @since 9.0.0
791
+     * @suppress SqlInjectionChecker
792
+     */
793
+    public function setReadMark($objectType, $objectId, \DateTime $dateTime, IUser $user) {
794
+        $this->checkRoleParameters('Object', $objectType, $objectId);
795
+
796
+        $qb = $this->dbConn->getQueryBuilder();
797
+        $values = [
798
+            'user_id' => $qb->createNamedParameter($user->getUID()),
799
+            'marker_datetime' => $qb->createNamedParameter($dateTime, 'datetime'),
800
+            'object_type' => $qb->createNamedParameter($objectType),
801
+            'object_id' => $qb->createNamedParameter($objectId),
802
+        ];
803
+
804
+        // Strategy: try to update, if this does not return affected rows, do an insert.
805
+        $affectedRows = $qb
806
+            ->update('comments_read_markers')
807
+            ->set('user_id', $values['user_id'])
808
+            ->set('marker_datetime', $values['marker_datetime'])
809
+            ->set('object_type', $values['object_type'])
810
+            ->set('object_id', $values['object_id'])
811
+            ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
812
+            ->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
813
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
814
+            ->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
815
+            ->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
816
+            ->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
817
+            ->execute();
818
+
819
+        if ($affectedRows > 0) {
820
+            return;
821
+        }
822
+
823
+        $qb->insert('comments_read_markers')
824
+            ->values($values)
825
+            ->execute();
826
+    }
827
+
828
+    /**
829
+     * returns the read marker for a given file to the specified date for the
830
+     * provided user. It returns null, when the marker is not present, i.e.
831
+     * no comments were marked as read.
832
+     *
833
+     * @param string $objectType
834
+     * @param string $objectId
835
+     * @param IUser $user
836
+     * @return \DateTime|null
837
+     * @since 9.0.0
838
+     */
839
+    public function getReadMark($objectType, $objectId, IUser $user) {
840
+        $qb = $this->dbConn->getQueryBuilder();
841
+        $resultStatement = $qb->select('marker_datetime')
842
+            ->from('comments_read_markers')
843
+            ->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
844
+            ->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
845
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
846
+            ->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
847
+            ->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
848
+            ->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
849
+            ->execute();
850
+
851
+        $data = $resultStatement->fetch();
852
+        $resultStatement->closeCursor();
853
+        if (!$data || is_null($data['marker_datetime'])) {
854
+            return null;
855
+        }
856
+
857
+        return new \DateTime($data['marker_datetime']);
858
+    }
859
+
860
+    /**
861
+     * deletes the read markers on the specified object
862
+     *
863
+     * @param string $objectType
864
+     * @param string $objectId
865
+     * @return bool
866
+     * @since 9.0.0
867
+     */
868
+    public function deleteReadMarksOnObject($objectType, $objectId) {
869
+        $this->checkRoleParameters('Object', $objectType, $objectId);
870
+
871
+        $qb = $this->dbConn->getQueryBuilder();
872
+        $query = $qb->delete('comments_read_markers')
873
+            ->where($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
874
+            ->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
875
+            ->setParameter('object_type', $objectType)
876
+            ->setParameter('object_id', $objectId);
877
+
878
+        try {
879
+            $affectedRows = $query->execute();
880
+        } catch (DriverException $e) {
881
+            $this->logger->logException($e, ['app' => 'core_comments']);
882
+            return false;
883
+        }
884
+        return ($affectedRows > 0);
885
+    }
886
+
887
+    /**
888
+     * registers an Entity to the manager, so event notifications can be send
889
+     * to consumers of the comments infrastructure
890
+     *
891
+     * @param \Closure $closure
892
+     */
893
+    public function registerEventHandler(\Closure $closure) {
894
+        $this->eventHandlerClosures[] = $closure;
895
+        $this->eventHandlers = [];
896
+    }
897
+
898
+    /**
899
+     * registers a method that resolves an ID to a display name for a given type
900
+     *
901
+     * @param string $type
902
+     * @param \Closure $closure
903
+     * @throws \OutOfBoundsException
904
+     * @since 11.0.0
905
+     *
906
+     * Only one resolver shall be registered per type. Otherwise a
907
+     * \OutOfBoundsException has to thrown.
908
+     */
909
+    public function registerDisplayNameResolver($type, \Closure $closure) {
910
+        if (!is_string($type)) {
911
+            throw new \InvalidArgumentException('String expected.');
912
+        }
913
+        if (isset($this->displayNameResolvers[$type])) {
914
+            throw new \OutOfBoundsException('Displayname resolver for this type already registered');
915
+        }
916
+        $this->displayNameResolvers[$type] = $closure;
917
+    }
918
+
919
+    /**
920
+     * resolves a given ID of a given Type to a display name.
921
+     *
922
+     * @param string $type
923
+     * @param string $id
924
+     * @return string
925
+     * @throws \OutOfBoundsException
926
+     * @since 11.0.0
927
+     *
928
+     * If a provided type was not registered, an \OutOfBoundsException shall
929
+     * be thrown. It is upon the resolver discretion what to return of the
930
+     * provided ID is unknown. It must be ensured that a string is returned.
931
+     */
932
+    public function resolveDisplayName($type, $id) {
933
+        if (!is_string($type)) {
934
+            throw new \InvalidArgumentException('String expected.');
935
+        }
936
+        if (!isset($this->displayNameResolvers[$type])) {
937
+            throw new \OutOfBoundsException('No Displayname resolver for this type registered');
938
+        }
939
+        return (string)$this->displayNameResolvers[$type]($id);
940
+    }
941
+
942
+    /**
943
+     * returns valid, registered entities
944
+     *
945
+     * @return \OCP\Comments\ICommentsEventHandler[]
946
+     */
947
+    private function getEventHandlers() {
948
+        if (!empty($this->eventHandlers)) {
949
+            return $this->eventHandlers;
950
+        }
951
+
952
+        $this->eventHandlers = [];
953
+        foreach ($this->eventHandlerClosures as $name => $closure) {
954
+            $entity = $closure();
955
+            if (!($entity instanceof ICommentsEventHandler)) {
956
+                throw new \InvalidArgumentException('The given entity does not implement the ICommentsEntity interface');
957
+            }
958
+            $this->eventHandlers[$name] = $entity;
959
+        }
960
+
961
+        return $this->eventHandlers;
962
+    }
963
+
964
+    /**
965
+     * sends notifications to the registered entities
966
+     *
967
+     * @param $eventType
968
+     * @param IComment $comment
969
+     */
970
+    private function sendEvent($eventType, IComment $comment) {
971
+        $entities = $this->getEventHandlers();
972
+        $event = new CommentsEvent($eventType, $comment);
973
+        foreach ($entities as $entity) {
974
+            $entity->handle($event);
975
+        }
976
+    }
977 977
 }
Please login to merge, or discard this patch.