Passed
Push — master ( c2f21b...e8c66d )
by Joas
14:14 queued 12s
created

getCommentsWithVerbForObjectSinceComment()   C

Complexity

Conditions 11
Paths 80

Size

Total Lines 92
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 60
nc 80
nop 7
dl 0
loc 92
rs 6.726
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Joas Schilling <[email protected]>
7
 * @author John Molakvoæ <[email protected]>
8
 * @author Morris Jobke <[email protected]>
9
 * @author Robin Appelman <[email protected]>
10
 * @author Roeland Jago Douma <[email protected]>
11
 * @author Simounet <[email protected]>
12
 * @author Thomas Müller <[email protected]>
13
 *
14
 * @license AGPL-3.0
15
 *
16
 * This code is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License, version 3,
18
 * as published by the Free Software Foundation.
19
 *
20
 * This program is distributed in the hope that it will be useful,
21
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
 * GNU Affero General Public License for more details.
24
 *
25
 * You should have received a copy of the GNU Affero General Public License, version 3,
26
 * along with this program. If not, see <http://www.gnu.org/licenses/>
27
 *
28
 */
29
namespace OC\Comments;
30
31
use Doctrine\DBAL\Exception\DriverException;
32
use Doctrine\DBAL\Exception\InvalidFieldNameException;
33
use OCP\AppFramework\Utility\ITimeFactory;
34
use OCP\Comments\CommentsEvent;
35
use OCP\Comments\IComment;
36
use OCP\Comments\ICommentsEventHandler;
37
use OCP\Comments\ICommentsManager;
38
use OCP\Comments\NotFoundException;
39
use OCP\DB\QueryBuilder\IQueryBuilder;
40
use OCP\IConfig;
41
use OCP\IDBConnection;
42
use OCP\IUser;
43
use OCP\IInitialStateService;
44
use OCP\PreConditionNotMetException;
45
use OCP\Util;
46
use Psr\Log\LoggerInterface;
47
48
class Manager implements ICommentsManager {
49
50
	/** @var  IDBConnection */
51
	protected $dbConn;
52
53
	/** @var  LoggerInterface */
54
	protected $logger;
55
56
	/** @var IConfig */
57
	protected $config;
58
59
	/** @var ITimeFactory */
60
	protected $timeFactory;
61
62
	/** @var IInitialStateService */
63
	protected $initialStateService;
64
65
	/** @var IComment[] */
66
	protected $commentsCache = [];
67
68
	/** @var  \Closure[] */
69
	protected $eventHandlerClosures = [];
70
71
	/** @var  ICommentsEventHandler[] */
72
	protected $eventHandlers = [];
73
74
	/** @var \Closure[] */
75
	protected $displayNameResolvers = [];
76
77
	public function __construct(IDBConnection $dbConn,
78
								LoggerInterface $logger,
79
								IConfig $config,
80
								ITimeFactory $timeFactory,
81
								IInitialStateService $initialStateService) {
82
		$this->dbConn = $dbConn;
83
		$this->logger = $logger;
84
		$this->config = $config;
85
		$this->timeFactory = $timeFactory;
86
		$this->initialStateService = $initialStateService;
87
	}
88
89
	/**
90
	 * converts data base data into PHP native, proper types as defined by
91
	 * IComment interface.
92
	 *
93
	 * @param array $data
94
	 * @return array
95
	 */
96
	protected function normalizeDatabaseData(array $data) {
97
		$data['id'] = (string)$data['id'];
98
		$data['parent_id'] = (string)$data['parent_id'];
99
		$data['topmost_parent_id'] = (string)$data['topmost_parent_id'];
100
		$data['creation_timestamp'] = new \DateTime($data['creation_timestamp']);
101
		if (!is_null($data['latest_child_timestamp'])) {
102
			$data['latest_child_timestamp'] = new \DateTime($data['latest_child_timestamp']);
103
		}
104
		$data['children_count'] = (int)$data['children_count'];
105
		$data['reference_id'] = $data['reference_id'] ?? null;
106
		if ($this->supportReactions()) {
107
			$list = json_decode($data['reactions'], true);
108
			// Ordering does not work on the database with group concat and Oracle,
109
			// So we simply sort on the output.
110
			if (is_array($list)) {
111
				uasort($list, static function ($a, $b) {
112
					if ($a === $b) {
113
						return 0;
114
					}
115
					return ($a > $b) ? -1 : 1;
116
				});
117
			}
118
			$data['reactions'] = $list;
119
		}
120
		return $data;
121
	}
122
123
124
	/**
125
	 * @param array $data
126
	 * @return IComment
127
	 */
128
	public function getCommentFromData(array $data): IComment {
129
		return new Comment($this->normalizeDatabaseData($data));
130
	}
131
132
	/**
133
	 * prepares a comment for an insert or update operation after making sure
134
	 * all necessary fields have a value assigned.
135
	 *
136
	 * @param IComment $comment
137
	 * @return IComment returns the same updated IComment instance as provided
138
	 *                  by parameter for convenience
139
	 * @throws \UnexpectedValueException
140
	 */
141
	protected function prepareCommentForDatabaseWrite(IComment $comment) {
142
		if (!$comment->getActorType()
143
			|| $comment->getActorId() === ''
144
			|| !$comment->getObjectType()
145
			|| $comment->getObjectId() === ''
146
			|| !$comment->getVerb()
147
		) {
148
			throw new \UnexpectedValueException('Actor, Object and Verb information must be provided for saving');
149
		}
150
151
		if ($comment->getVerb() === 'reaction' && mb_strlen($comment->getMessage()) > 2) {
152
			throw new \UnexpectedValueException('Reactions cannot be longer than 2 chars (emoji with skin tone have two chars)');
153
		}
154
155
		if ($comment->getId() === '') {
156
			$comment->setChildrenCount(0);
157
			$comment->setLatestChildDateTime(new \DateTime('0000-00-00 00:00:00', new \DateTimeZone('UTC')));
158
			$comment->setLatestChildDateTime(null);
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type DateTime expected by parameter $dateTime of OCP\Comments\IComment::setLatestChildDateTime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

158
			$comment->setLatestChildDateTime(/** @scrutinizer ignore-type */ null);
Loading history...
159
		}
160
161
		if (is_null($comment->getCreationDateTime())) {
162
			$comment->setCreationDateTime(new \DateTime());
163
		}
164
165
		if ($comment->getParentId() !== '0') {
166
			$comment->setTopmostParentId($this->determineTopmostParentId($comment->getParentId()));
167
		} else {
168
			$comment->setTopmostParentId('0');
169
		}
170
171
		$this->cache($comment);
172
173
		return $comment;
174
	}
175
176
	/**
177
	 * returns the topmost parent id of a given comment identified by ID
178
	 *
179
	 * @param string $id
180
	 * @return string
181
	 * @throws NotFoundException
182
	 */
183
	protected function determineTopmostParentId($id) {
184
		$comment = $this->get($id);
185
		if ($comment->getParentId() === '0') {
186
			return $comment->getId();
187
		}
188
189
		return $this->determineTopmostParentId($comment->getParentId());
190
	}
191
192
	/**
193
	 * updates child information of a comment
194
	 *
195
	 * @param string $id
196
	 * @param \DateTime $cDateTime the date time of the most recent child
197
	 * @throws NotFoundException
198
	 */
199
	protected function updateChildrenInformation($id, \DateTime $cDateTime) {
200
		$qb = $this->dbConn->getQueryBuilder();
201
		$query = $qb->select($qb->func()->count('id'))
202
			->from('comments')
203
			->where($qb->expr()->eq('parent_id', $qb->createParameter('id')))
204
			->setParameter('id', $id);
205
206
		$resultStatement = $query->execute();
207
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
208
		$resultStatement->closeCursor();
209
		$children = (int)$data[0];
210
211
		$comment = $this->get($id);
212
		$comment->setChildrenCount($children);
213
		$comment->setLatestChildDateTime($cDateTime);
214
		$this->save($comment);
215
	}
216
217
	/**
218
	 * Tests whether actor or object type and id parameters are acceptable.
219
	 * Throws exception if not.
220
	 *
221
	 * @param string $role
222
	 * @param string $type
223
	 * @param string $id
224
	 * @throws \InvalidArgumentException
225
	 */
226
	protected function checkRoleParameters($role, $type, $id) {
227
		if (
228
			!is_string($type) || empty($type)
0 ignored issues
show
introduced by
The condition is_string($type) is always true.
Loading history...
229
			|| !is_string($id) || empty($id)
230
		) {
231
			throw new \InvalidArgumentException($role . ' parameters must be string and not empty');
232
		}
233
	}
234
235
	/**
236
	 * run-time caches a comment
237
	 *
238
	 * @param IComment $comment
239
	 */
240
	protected function cache(IComment $comment) {
241
		$id = $comment->getId();
242
		if (empty($id)) {
243
			return;
244
		}
245
		$this->commentsCache[(string)$id] = $comment;
246
	}
247
248
	/**
249
	 * removes an entry from the comments run time cache
250
	 *
251
	 * @param mixed $id the comment's id
252
	 */
253
	protected function uncache($id) {
254
		$id = (string)$id;
255
		if (isset($this->commentsCache[$id])) {
256
			unset($this->commentsCache[$id]);
257
		}
258
	}
259
260
	/**
261
	 * returns a comment instance
262
	 *
263
	 * @param string $id the ID of the comment
264
	 * @return IComment
265
	 * @throws NotFoundException
266
	 * @throws \InvalidArgumentException
267
	 * @since 9.0.0
268
	 */
269
	public function get($id) {
270
		if ((int)$id === 0) {
271
			throw new \InvalidArgumentException('IDs must be translatable to a number in this implementation.');
272
		}
273
274
		if (isset($this->commentsCache[$id])) {
275
			return $this->commentsCache[$id];
276
		}
277
278
		$qb = $this->dbConn->getQueryBuilder();
279
		$resultStatement = $qb->select('*')
280
			->from('comments')
281
			->where($qb->expr()->eq('id', $qb->createParameter('id')))
282
			->setParameter('id', $id, IQueryBuilder::PARAM_INT)
283
			->execute();
284
285
		$data = $resultStatement->fetch();
286
		$resultStatement->closeCursor();
287
		if (!$data) {
288
			throw new NotFoundException();
289
		}
290
291
292
		$comment = $this->getCommentFromData($data);
293
		$this->cache($comment);
294
		return $comment;
295
	}
296
297
	/**
298
	 * returns the comment specified by the id and all it's child comments.
299
	 * At this point of time, we do only support one level depth.
300
	 *
301
	 * @param string $id
302
	 * @param int $limit max number of entries to return, 0 returns all
303
	 * @param int $offset the start entry
304
	 * @return array
305
	 * @since 9.0.0
306
	 *
307
	 * The return array looks like this
308
	 * [
309
	 *   'comment' => IComment, // root comment
310
	 *   'replies' =>
311
	 *   [
312
	 *     0 =>
313
	 *     [
314
	 *       'comment' => IComment,
315
	 *       'replies' => []
316
	 *     ]
317
	 *     1 =>
318
	 *     [
319
	 *       'comment' => IComment,
320
	 *       'replies'=> []
321
	 *     ],
322
	 *     …
323
	 *   ]
324
	 * ]
325
	 */
326
	public function getTree($id, $limit = 0, $offset = 0) {
327
		$tree = [];
328
		$tree['comment'] = $this->get($id);
329
		$tree['replies'] = [];
330
331
		$qb = $this->dbConn->getQueryBuilder();
332
		$query = $qb->select('*')
333
			->from('comments')
334
			->where($qb->expr()->eq('topmost_parent_id', $qb->createParameter('id')))
335
			->orderBy('creation_timestamp', 'DESC')
336
			->setParameter('id', $id);
337
338
		if ($limit > 0) {
339
			$query->setMaxResults($limit);
340
		}
341
		if ($offset > 0) {
342
			$query->setFirstResult($offset);
343
		}
344
345
		$resultStatement = $query->execute();
346
		while ($data = $resultStatement->fetch()) {
347
			$comment = $this->getCommentFromData($data);
348
			$this->cache($comment);
349
			$tree['replies'][] = [
350
				'comment' => $comment,
351
				'replies' => []
352
			];
353
		}
354
		$resultStatement->closeCursor();
355
356
		return $tree;
357
	}
358
359
	/**
360
	 * returns comments for a specific object (e.g. a file).
361
	 *
362
	 * The sort order is always newest to oldest.
363
	 *
364
	 * @param string $objectType the object type, e.g. 'files'
365
	 * @param string $objectId the id of the object
366
	 * @param int $limit optional, number of maximum comments to be returned. if
367
	 * not specified, all comments are returned.
368
	 * @param int $offset optional, starting point
369
	 * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
370
	 * that may be returned
371
	 * @return IComment[]
372
	 * @since 9.0.0
373
	 */
374
	public function getForObject(
375
		$objectType,
376
		$objectId,
377
		$limit = 0,
378
		$offset = 0,
379
		\DateTime $notOlderThan = null
380
	) {
381
		$comments = [];
382
383
		$qb = $this->dbConn->getQueryBuilder();
384
		$query = $qb->select('*')
385
			->from('comments')
386
			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
387
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
388
			->orderBy('creation_timestamp', 'DESC')
389
			->setParameter('type', $objectType)
390
			->setParameter('id', $objectId);
391
392
		if ($limit > 0) {
393
			$query->setMaxResults($limit);
394
		}
395
		if ($offset > 0) {
396
			$query->setFirstResult($offset);
397
		}
398
		if (!is_null($notOlderThan)) {
399
			$query
400
				->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
401
				->setParameter('notOlderThan', $notOlderThan, 'datetime');
402
		}
403
404
		$resultStatement = $query->execute();
405
		while ($data = $resultStatement->fetch()) {
406
			$comment = $this->getCommentFromData($data);
407
			$this->cache($comment);
408
			$comments[] = $comment;
409
		}
410
		$resultStatement->closeCursor();
411
412
		return $comments;
413
	}
414
415
	/**
416
	 * @param string $objectType the object type, e.g. 'files'
417
	 * @param string $objectId the id of the object
418
	 * @param int $lastKnownCommentId the last known comment (will be used as offset)
419
	 * @param string $sortDirection direction of the comments (`asc` or `desc`)
420
	 * @param int $limit optional, number of maximum comments to be returned. if
421
	 * set to 0, all comments are returned.
422
	 * @param bool $includeLastKnown
423
	 * @return IComment[]
424
	 * @return array
425
	 */
426
	public function getForObjectSince(
427
		string $objectType,
428
		string $objectId,
429
		int $lastKnownCommentId,
430
		string $sortDirection = 'asc',
431
		int $limit = 30,
432
		bool $includeLastKnown = false
433
	): array {
434
		return $this->getCommentsWithVerbForObjectSinceComment(
435
			$objectType,
436
			$objectId,
437
			[],
438
			$lastKnownCommentId,
439
			$sortDirection,
440
			$limit,
441
			$includeLastKnown
442
		);
443
	}
444
445
	/**
446
	 * @param string $objectType the object type, e.g. 'files'
447
	 * @param string $objectId the id of the object
448
	 * @param string[] $verbs List of verbs to filter by
449
	 * @param int $lastKnownCommentId the last known comment (will be used as offset)
450
	 * @param string $sortDirection direction of the comments (`asc` or `desc`)
451
	 * @param int $limit optional, number of maximum comments to be returned. if
452
	 * set to 0, all comments are returned.
453
	 * @param bool $includeLastKnown
454
	 * @return IComment[]
455
	 */
456
	public function getCommentsWithVerbForObjectSinceComment(
457
		string $objectType,
458
		string $objectId,
459
		array $verbs,
460
		int $lastKnownCommentId,
461
		string $sortDirection = 'asc',
462
		int $limit = 30,
463
		bool $includeLastKnown = false
464
	): array {
465
		$comments = [];
466
467
		$query = $this->dbConn->getQueryBuilder();
468
		$query->select('*')
469
			->from('comments')
470
			->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
471
			->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
472
			->orderBy('creation_timestamp', $sortDirection === 'desc' ? 'DESC' : 'ASC')
473
			->addOrderBy('id', $sortDirection === 'desc' ? 'DESC' : 'ASC');
474
475
		if ($limit > 0) {
476
			$query->setMaxResults($limit);
477
		}
478
479
		if (!empty($verbs)) {
480
			$query->andWhere($query->expr()->in('verb', $query->createNamedParameter($verbs, IQueryBuilder::PARAM_STR_ARRAY)));
481
		}
482
483
		$lastKnownComment = $lastKnownCommentId > 0 ? $this->getLastKnownComment(
484
			$objectType,
485
			$objectId,
486
			$lastKnownCommentId
487
		) : null;
488
		if ($lastKnownComment instanceof IComment) {
489
			$lastKnownCommentDateTime = $lastKnownComment->getCreationDateTime();
490
			if ($sortDirection === 'desc') {
491
				if ($includeLastKnown) {
492
					$idComparison = $query->expr()->lte('id', $query->createNamedParameter($lastKnownCommentId));
493
				} else {
494
					$idComparison = $query->expr()->lt('id', $query->createNamedParameter($lastKnownCommentId));
495
				}
496
				$query->andWhere(
497
					$query->expr()->orX(
498
						$query->expr()->lt(
499
							'creation_timestamp',
500
							$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
501
							IQueryBuilder::PARAM_DATE
502
						),
503
						$query->expr()->andX(
504
							$query->expr()->eq(
505
								'creation_timestamp',
506
								$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
507
								IQueryBuilder::PARAM_DATE
508
							),
509
							$idComparison
510
						)
511
					)
512
				);
513
			} else {
514
				if ($includeLastKnown) {
515
					$idComparison = $query->expr()->gte('id', $query->createNamedParameter($lastKnownCommentId));
516
				} else {
517
					$idComparison = $query->expr()->gt('id', $query->createNamedParameter($lastKnownCommentId));
518
				}
519
				$query->andWhere(
520
					$query->expr()->orX(
521
						$query->expr()->gt(
522
							'creation_timestamp',
523
							$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
524
							IQueryBuilder::PARAM_DATE
525
						),
526
						$query->expr()->andX(
527
							$query->expr()->eq(
528
								'creation_timestamp',
529
								$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
530
								IQueryBuilder::PARAM_DATE
531
							),
532
							$idComparison
533
						)
534
					)
535
				);
536
			}
537
		}
538
539
		$resultStatement = $query->execute();
540
		while ($data = $resultStatement->fetch()) {
541
			$comment = $this->getCommentFromData($data);
542
			$this->cache($comment);
543
			$comments[] = $comment;
544
		}
545
		$resultStatement->closeCursor();
546
547
		return $comments;
548
	}
549
550
	/**
551
	 * @param string $objectType the object type, e.g. 'files'
552
	 * @param string $objectId the id of the object
553
	 * @param int $id the comment to look for
554
	 * @return Comment|null
555
	 */
556
	protected function getLastKnownComment(string $objectType,
557
										   string $objectId,
558
										   int $id) {
559
		$query = $this->dbConn->getQueryBuilder();
560
		$query->select('*')
561
			->from('comments')
562
			->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
563
			->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
564
			->andWhere($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
565
566
		$result = $query->execute();
567
		$row = $result->fetch();
568
		$result->closeCursor();
569
570
		if ($row) {
571
			$comment = $this->getCommentFromData($row);
572
			$this->cache($comment);
573
			return $comment;
574
		}
575
576
		return null;
577
	}
578
579
	/**
580
	 * Search for comments with a given content
581
	 *
582
	 * @param string $search content to search for
583
	 * @param string $objectType Limit the search by object type
584
	 * @param string $objectId Limit the search by object id
585
	 * @param string $verb Limit the verb of the comment
586
	 * @param int $offset
587
	 * @param int $limit
588
	 * @return IComment[]
589
	 */
590
	public function search(string $search, string $objectType, string $objectId, string $verb, int $offset, int $limit = 50): array {
591
		$objectIds = [];
592
		if ($objectId) {
593
			$objectIds[] = $objectIds;
594
		}
595
		return $this->searchForObjects($search, $objectType, $objectIds, $verb, $offset, $limit);
596
	}
597
598
	/**
599
	 * Search for comments on one or more objects with a given content
600
	 *
601
	 * @param string $search content to search for
602
	 * @param string $objectType Limit the search by object type
603
	 * @param array $objectIds Limit the search by object ids
604
	 * @param string $verb Limit the verb of the comment
605
	 * @param int $offset
606
	 * @param int $limit
607
	 * @return IComment[]
608
	 */
609
	public function searchForObjects(string $search, string $objectType, array $objectIds, string $verb, int $offset, int $limit = 50): array {
610
		$query = $this->dbConn->getQueryBuilder();
611
612
		$query->select('*')
613
			->from('comments')
614
			->where($query->expr()->iLike('message', $query->createNamedParameter(
615
				'%' . $this->dbConn->escapeLikeParameter($search). '%'
616
			)))
617
			->orderBy('creation_timestamp', 'DESC')
618
			->addOrderBy('id', 'DESC')
619
			->setMaxResults($limit);
620
621
		if ($objectType !== '') {
622
			$query->andWhere($query->expr()->eq('object_type', $query->createNamedParameter($objectType)));
623
		}
624
		if (!empty($objectIds)) {
625
			$query->andWhere($query->expr()->in('object_id', $query->createNamedParameter($objectIds, IQueryBuilder::PARAM_STR_ARRAY)));
626
		}
627
		if ($verb !== '') {
628
			$query->andWhere($query->expr()->eq('verb', $query->createNamedParameter($verb)));
629
		}
630
		if ($offset !== 0) {
631
			$query->setFirstResult($offset);
632
		}
633
634
		$comments = [];
635
		$result = $query->execute();
636
		while ($data = $result->fetch()) {
637
			$comment = $this->getCommentFromData($data);
638
			$this->cache($comment);
639
			$comments[] = $comment;
640
		}
641
		$result->closeCursor();
642
643
		return $comments;
644
	}
645
646
	/**
647
	 * @param $objectType string the object type, e.g. 'files'
648
	 * @param $objectId string the id of the object
649
	 * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
650
	 * that may be returned
651
	 * @param string $verb Limit the verb of the comment - Added in 14.0.0
652
	 * @return Int
653
	 * @since 9.0.0
654
	 */
655
	public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null, $verb = '') {
656
		$qb = $this->dbConn->getQueryBuilder();
657
		$query = $qb->select($qb->func()->count('id'))
658
			->from('comments')
659
			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
660
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
661
			->setParameter('type', $objectType)
662
			->setParameter('id', $objectId);
663
664
		if (!is_null($notOlderThan)) {
665
			$query
666
				->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
667
				->setParameter('notOlderThan', $notOlderThan, 'datetime');
668
		}
669
670
		if ($verb !== '') {
671
			$query->andWhere($qb->expr()->eq('verb', $qb->createNamedParameter($verb)));
672
		}
673
674
		$resultStatement = $query->execute();
675
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
676
		$resultStatement->closeCursor();
677
		return (int)$data[0];
678
	}
679
680
	/**
681
	 * @param string $objectType the object type, e.g. 'files'
682
	 * @param string[] $objectIds the id of the object
683
	 * @param IUser $user
684
	 * @param string $verb Limit the verb of the comment - Added in 14.0.0
685
	 * @return array Map with object id => # of unread comments
686
	 * @psalm-return array<string, int>
687
	 * @since 21.0.0
688
	 */
689
	public function getNumberOfUnreadCommentsForObjects(string $objectType, array $objectIds, IUser $user, $verb = ''): array {
690
		$unreadComments = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $unreadComments is dead and can be removed.
Loading history...
691
		$query = $this->dbConn->getQueryBuilder();
692
		$query->select('c.object_id', $query->func()->count('c.id', 'num_comments'))
693
			->from('comments', 'c')
694
			->leftJoin('c', 'comments_read_markers', 'm', $query->expr()->andX(
695
				$query->expr()->eq('m.user_id', $query->createNamedParameter($user->getUID())),
696
				$query->expr()->eq('c.object_type', 'm.object_type'),
697
				$query->expr()->eq('c.object_id', 'm.object_id')
698
			))
699
			->where($query->expr()->eq('c.object_type', $query->createNamedParameter($objectType)))
700
			->andWhere($query->expr()->in('c.object_id', $query->createParameter('ids')))
701
			->andWhere($query->expr()->orX(
702
				$query->expr()->gt('c.creation_timestamp', 'm.marker_datetime'),
703
				$query->expr()->isNull('m.marker_datetime')
704
			))
705
			->groupBy('c.object_id');
706
707
		if ($verb !== '') {
708
			$query->andWhere($query->expr()->eq('c.verb', $query->createNamedParameter($verb)));
709
		}
710
711
		$unreadComments = array_fill_keys($objectIds, 0);
712
		foreach (array_chunk($objectIds, 1000) as $chunk) {
713
			$query->setParameter('ids', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
714
715
			$result = $query->executeQuery();
716
			while ($row = $result->fetch()) {
717
				$unreadComments[$row['object_id']] = (int) $row['num_comments'];
718
			}
719
			$result->closeCursor();
720
		}
721
722
		return $unreadComments;
723
	}
724
725
	/**
726
	 * @param string $objectType
727
	 * @param string $objectId
728
	 * @param int $lastRead
729
	 * @param string $verb
730
	 * @return int
731
	 * @since 21.0.0
732
	 */
733
	public function getNumberOfCommentsForObjectSinceComment(string $objectType, string $objectId, int $lastRead, string $verb = ''): int {
734
		if ($verb !== '') {
735
			return $this->getNumberOfCommentsWithVerbsForObjectSinceComment($objectType, $objectId, $lastRead, [$verb]);
736
		}
737
738
		return $this->getNumberOfCommentsWithVerbsForObjectSinceComment($objectType, $objectId, $lastRead, []);
739
	}
740
741
	/**
742
	 * @param string $objectType
743
	 * @param string $objectId
744
	 * @param int $lastRead
745
	 * @param string[] $verbs
746
	 * @return int
747
	 * @since 24.0.0
748
	 */
749
	public function getNumberOfCommentsWithVerbsForObjectSinceComment(string $objectType, string $objectId, int $lastRead, array $verbs): int {
750
		$query = $this->dbConn->getQueryBuilder();
751
		$query->select($query->func()->count('id', 'num_messages'))
752
			->from('comments')
753
			->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
754
			->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
755
			->andWhere($query->expr()->gt('id', $query->createNamedParameter($lastRead)));
756
757
		if (!empty($verbs)) {
758
			$query->andWhere($query->expr()->in('verb', $query->createNamedParameter($verbs, IQueryBuilder::PARAM_STR_ARRAY)));
759
		}
760
761
		$result = $query->executeQuery();
762
		$data = $result->fetch();
763
		$result->closeCursor();
764
765
		return (int) ($data['num_messages'] ?? 0);
766
	}
767
768
	/**
769
	 * @param string $objectType
770
	 * @param string $objectId
771
	 * @param \DateTime $beforeDate
772
	 * @param string $verb
773
	 * @return int
774
	 * @since 21.0.0
775
	 */
776
	public function getLastCommentBeforeDate(string $objectType, string $objectId, \DateTime $beforeDate, string $verb = ''): int {
777
		$query = $this->dbConn->getQueryBuilder();
778
		$query->select('id')
779
			->from('comments')
780
			->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
781
			->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
782
			->andWhere($query->expr()->lt('creation_timestamp', $query->createNamedParameter($beforeDate, IQueryBuilder::PARAM_DATE)))
783
			->orderBy('creation_timestamp', 'desc');
784
785
		if ($verb !== '') {
786
			$query->andWhere($query->expr()->eq('verb', $query->createNamedParameter($verb)));
787
		}
788
789
		$result = $query->execute();
790
		$data = $result->fetch();
791
		$result->closeCursor();
792
793
		return (int) ($data['id'] ?? 0);
794
	}
795
796
	/**
797
	 * @param string $objectType
798
	 * @param string $objectId
799
	 * @param string $verb
800
	 * @param string $actorType
801
	 * @param string[] $actors
802
	 * @return \DateTime[] Map of "string actor" => "\DateTime most recent comment date"
803
	 * @psalm-return array<string, \DateTime>
804
	 * @since 21.0.0
805
	 */
806
	public function getLastCommentDateByActor(
807
		string $objectType,
808
		string $objectId,
809
		string $verb,
810
		string $actorType,
811
		array $actors
812
	): array {
813
		$lastComments = [];
814
815
		$query = $this->dbConn->getQueryBuilder();
816
		$query->select('actor_id')
817
			->selectAlias($query->createFunction('MAX(' . $query->getColumnName('creation_timestamp') . ')'), 'last_comment')
818
			->from('comments')
819
			->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
820
			->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
821
			->andWhere($query->expr()->eq('verb', $query->createNamedParameter($verb)))
822
			->andWhere($query->expr()->eq('actor_type', $query->createNamedParameter($actorType)))
823
			->andWhere($query->expr()->in('actor_id', $query->createNamedParameter($actors, IQueryBuilder::PARAM_STR_ARRAY)))
824
			->groupBy('actor_id');
825
826
		$result = $query->execute();
827
		while ($row = $result->fetch()) {
828
			$lastComments[$row['actor_id']] = $this->timeFactory->getDateTime($row['last_comment']);
829
		}
830
		$result->closeCursor();
831
832
		return $lastComments;
833
	}
834
835
	/**
836
	 * Get the number of unread comments for all files in a folder
837
	 *
838
	 * @param int $folderId
839
	 * @param IUser $user
840
	 * @return array [$fileId => $unreadCount]
841
	 */
842
	public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user) {
843
		$qb = $this->dbConn->getQueryBuilder();
844
845
		$query = $qb->select('f.fileid')
846
			->addSelect($qb->func()->count('c.id', 'num_ids'))
847
			->from('filecache', 'f')
848
			->leftJoin('f', 'comments', 'c', $qb->expr()->andX(
849
				$qb->expr()->eq('f.fileid', $qb->expr()->castColumn('c.object_id', IQueryBuilder::PARAM_INT)),
850
				$qb->expr()->eq('c.object_type', $qb->createNamedParameter('files'))
851
			))
852
			->leftJoin('c', 'comments_read_markers', 'm', $qb->expr()->andX(
853
				$qb->expr()->eq('c.object_id', 'm.object_id'),
854
				$qb->expr()->eq('m.object_type', $qb->createNamedParameter('files'))
855
			))
856
			->where(
857
				$qb->expr()->andX(
858
					$qb->expr()->eq('f.parent', $qb->createNamedParameter($folderId)),
859
					$qb->expr()->orX(
860
						$qb->expr()->eq('c.object_type', $qb->createNamedParameter('files')),
861
						$qb->expr()->isNull('c.object_type')
862
					),
863
					$qb->expr()->orX(
864
						$qb->expr()->eq('m.object_type', $qb->createNamedParameter('files')),
865
						$qb->expr()->isNull('m.object_type')
866
					),
867
					$qb->expr()->orX(
868
						$qb->expr()->eq('m.user_id', $qb->createNamedParameter($user->getUID())),
869
						$qb->expr()->isNull('m.user_id')
870
					),
871
					$qb->expr()->orX(
872
						$qb->expr()->gt('c.creation_timestamp', 'm.marker_datetime'),
873
						$qb->expr()->isNull('m.marker_datetime')
874
					)
875
				)
876
			)->groupBy('f.fileid');
877
878
		$resultStatement = $query->execute();
879
880
		$results = [];
881
		while ($row = $resultStatement->fetch()) {
882
			$results[$row['fileid']] = (int) $row['num_ids'];
883
		}
884
		$resultStatement->closeCursor();
885
		return $results;
886
	}
887
888
	/**
889
	 * creates a new comment and returns it. At this point of time, it is not
890
	 * saved in the used data storage. Use save() after setting other fields
891
	 * of the comment (e.g. message or verb).
892
	 *
893
	 * @param string $actorType the actor type (e.g. 'users')
894
	 * @param string $actorId a user id
895
	 * @param string $objectType the object type the comment is attached to
896
	 * @param string $objectId the object id the comment is attached to
897
	 * @return IComment
898
	 * @since 9.0.0
899
	 */
900
	public function create($actorType, $actorId, $objectType, $objectId) {
901
		$comment = new Comment();
902
		$comment
903
			->setActor($actorType, $actorId)
904
			->setObject($objectType, $objectId);
905
		return $comment;
906
	}
907
908
	/**
909
	 * permanently deletes the comment specified by the ID
910
	 *
911
	 * When the comment has child comments, their parent ID will be changed to
912
	 * the parent ID of the item that is to be deleted.
913
	 *
914
	 * @param string $id
915
	 * @return bool
916
	 * @throws \InvalidArgumentException
917
	 * @since 9.0.0
918
	 */
919
	public function delete($id) {
920
		if (!is_string($id)) {
0 ignored issues
show
introduced by
The condition is_string($id) is always true.
Loading history...
921
			throw new \InvalidArgumentException('Parameter must be string');
922
		}
923
924
		try {
925
			$comment = $this->get($id);
926
		} catch (\Exception $e) {
927
			// Ignore exceptions, we just don't fire a hook then
928
			$comment = null;
929
		}
930
931
		$qb = $this->dbConn->getQueryBuilder();
932
		$query = $qb->delete('comments')
933
			->where($qb->expr()->eq('id', $qb->createParameter('id')))
934
			->setParameter('id', $id);
935
936
		try {
937
			$affectedRows = $query->execute();
938
			$this->uncache($id);
939
		} catch (DriverException $e) {
940
			$this->logger->error($e->getMessage(), [
941
				'exception' => $e,
942
				'app' => 'core_comments',
943
			]);
944
			return false;
945
		}
946
947
		if ($affectedRows > 0 && $comment instanceof IComment) {
948
			if ($comment->getVerb() === 'reaction_deleted') {
949
				$this->deleteReaction($comment);
950
			}
951
			$this->sendEvent(CommentsEvent::EVENT_DELETE, $comment);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\Comments\CommentsEvent::EVENT_DELETE has been deprecated: 22.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

951
			$this->sendEvent(/** @scrutinizer ignore-deprecated */ CommentsEvent::EVENT_DELETE, $comment);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
952
		}
953
954
		return ($affectedRows > 0);
955
	}
956
957
	private function deleteReaction(IComment $reaction): void {
958
		$qb = $this->dbConn->getQueryBuilder();
959
		$qb->delete('reactions')
960
			->where($qb->expr()->eq('parent_id', $qb->createNamedParameter($reaction->getParentId())))
961
			->andWhere($qb->expr()->eq('message_id', $qb->createNamedParameter($reaction->getId())))
962
			->executeStatement();
963
		$this->sumReactions($reaction->getParentId());
964
	}
965
966
	/**
967
	 * Get comment related with user reaction
968
	 *
969
	 * Throws PreConditionNotMetException when the system haven't the minimum requirements to
970
	 * use reactions
971
	 *
972
	 * @param integer $parentId
973
	 * @param string $actorType
974
	 * @param string $actorId
975
	 * @param string $reaction
976
	 * @return IComment
977
	 * @throws NotFoundException
978
	 * @throws PreConditionNotMetException
979
	 * @since 24.0.0
980
	 */
981
	public function getReactionComment(int $parentId, string $actorType, string $actorId, string $reaction): IComment {
982
		$this->throwIfNotSupportReactions();
983
		$qb = $this->dbConn->getQueryBuilder();
984
		$messageId = $qb
985
			->select('message_id')
986
			->from('reactions')
987
			->where($qb->expr()->eq('parent_id', $qb->createNamedParameter($parentId)))
988
			->andWhere($qb->expr()->eq('actor_type', $qb->createNamedParameter($actorType)))
989
			->andWhere($qb->expr()->eq('actor_id', $qb->createNamedParameter($actorId)))
990
			->andWhere($qb->expr()->eq('reaction', $qb->createNamedParameter($reaction)))
991
			->executeQuery()
992
			->fetchOne();
993
		if (!$messageId) {
994
			throw new NotFoundException('Comment related with reaction not found');
995
		}
996
		return $this->get($messageId);
997
	}
998
999
	/**
1000
	 * Retrieve all reactions with specific reaction of a message
1001
	 *
1002
	 * @param integer $parentId
1003
	 * @param string $reaction
1004
	 * @return IComment[]
1005
	 * @since 24.0.0
1006
	 */
1007
	public function retrieveAllReactionsWithSpecificReaction(int $parentId, string $reaction): ?array {
1008
		$this->throwIfNotSupportReactions();
1009
		$qb = $this->dbConn->getQueryBuilder();
1010
		$result = $qb
1011
			->select('message_id')
1012
			->from('reactions')
1013
			->where($qb->expr()->eq('parent_id', $qb->createNamedParameter($parentId)))
1014
			->andWhere($qb->expr()->eq('reaction', $qb->createNamedParameter($reaction)))
1015
			->executeQuery();
1016
1017
		$commentIds = [];
1018
		while ($data = $result->fetch()) {
1019
			$commentIds[] = $data['message_id'];
1020
		}
1021
		$comments = [];
1022
		if ($commentIds) {
1023
			$comments = $this->getCommentsById($commentIds);
1024
		}
1025
1026
		return $comments;
1027
	}
1028
1029
	/**
1030
	 * Support reactions
1031
	 *
1032
	 * @return boolean
1033
	 * @since 24.0.0
1034
	 */
1035
	public function supportReactions(): bool {
1036
		return $this->dbConn->supports4ByteText();
1037
	}
1038
1039
	/**
1040
	 * @throws PreConditionNotMetException
1041
	 * @since 24.0.0
1042
	 */
1043
	private function throwIfNotSupportReactions() {
1044
		if (!$this->supportReactions()) {
1045
			throw new PreConditionNotMetException('The database does not support reactions');
1046
		}
1047
	}
1048
1049
	/**
1050
	 * Retrieve all reactions of a message
1051
	 *
1052
	 * Throws PreConditionNotMetException when the system haven't the minimum requirements to
1053
	 * use reactions
1054
	 *
1055
	 * @param integer $parentId
1056
	 * @param string $reaction
1057
	 * @throws PreConditionNotMetException
1058
	 * @return IComment[]
1059
	 * @since 24.0.0
1060
	 */
1061
	public function retrieveAllReactions(int $parentId): array {
1062
		$this->throwIfNotSupportReactions();
1063
		$qb = $this->dbConn->getQueryBuilder();
1064
		$result = $qb
1065
			->select('message_id')
1066
			->from('reactions')
1067
			->where($qb->expr()->eq('parent_id', $qb->createNamedParameter($parentId)))
1068
			->executeQuery();
1069
1070
		$commentIds = [];
1071
		while ($data = $result->fetch()) {
1072
			$commentIds[] = $data['message_id'];
1073
		}
1074
1075
		return $this->getCommentsById($commentIds);
1076
	}
1077
1078
	/**
1079
	 * Get all comments on list
1080
	 *
1081
	 * @param integer[] $commentIds
1082
	 * @return IComment[]
1083
	 * @since 24.0.0
1084
	 */
1085
	private function getCommentsById(array $commentIds): array {
1086
		if (!$commentIds) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $commentIds of type integer[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1087
			return [];
1088
		}
1089
		$query = $this->dbConn->getQueryBuilder();
1090
1091
		$query->select('*')
1092
			->from('comments')
1093
			->where($query->expr()->in('id', $query->createNamedParameter($commentIds, IQueryBuilder::PARAM_STR_ARRAY)))
1094
			->orderBy('creation_timestamp', 'DESC')
1095
			->addOrderBy('id', 'DESC');
1096
1097
		$comments = [];
1098
		$result = $query->executeQuery();
1099
		while ($data = $result->fetch()) {
1100
			$comment = $this->getCommentFromData($data);
1101
			$this->cache($comment);
1102
			$comments[] = $comment;
1103
		}
1104
		$result->closeCursor();
1105
		return $comments;
1106
	}
1107
1108
	/**
1109
	 * saves the comment permanently
1110
	 *
1111
	 * if the supplied comment has an empty ID, a new entry comment will be
1112
	 * saved and the instance updated with the new ID.
1113
	 *
1114
	 * Otherwise, an existing comment will be updated.
1115
	 *
1116
	 * Throws NotFoundException when a comment that is to be updated does not
1117
	 * exist anymore at this point of time.
1118
	 *
1119
	 * Throws PreConditionNotMetException when the system haven't the minimum requirements to
1120
	 * use reactions
1121
	 *
1122
	 * @param IComment $comment
1123
	 * @return bool
1124
	 * @throws NotFoundException
1125
	 * @throws PreConditionNotMetException
1126
	 * @since 9.0.0
1127
	 */
1128
	public function save(IComment $comment) {
1129
		if ($comment->getVerb() === 'reaction') {
1130
			$this->throwIfNotSupportReactions();
1131
		}
1132
1133
		if ($this->prepareCommentForDatabaseWrite($comment)->getId() === '') {
1134
			$result = $this->insert($comment);
1135
		} else {
1136
			$result = $this->update($comment);
1137
		}
1138
1139
		if ($result && !!$comment->getParentId()) {
1140
			$this->updateChildrenInformation(
1141
				$comment->getParentId(),
1142
				$comment->getCreationDateTime()
1143
			);
1144
			$this->cache($comment);
1145
		}
1146
1147
		return $result;
1148
	}
1149
1150
	/**
1151
	 * inserts the provided comment in the database
1152
	 *
1153
	 * @param IComment $comment
1154
	 * @return bool
1155
	 */
1156
	protected function insert(IComment $comment): bool {
1157
		try {
1158
			$result = $this->insertQuery($comment, true);
1159
		} catch (InvalidFieldNameException $e) {
1160
			// The reference id field was only added in Nextcloud 19.
1161
			// In order to not cause too long waiting times on the update,
1162
			// it was decided to only add it lazy, as it is also not a critical
1163
			// feature, but only helps to have a better experience while commenting.
1164
			// So in case the reference_id field is missing,
1165
			// we simply save the comment without that field.
1166
			$result = $this->insertQuery($comment, false);
1167
		}
1168
1169
		return $result;
1170
	}
1171
1172
	protected function insertQuery(IComment $comment, bool $tryWritingReferenceId): bool {
1173
		$qb = $this->dbConn->getQueryBuilder();
1174
1175
		$values = [
1176
			'parent_id' => $qb->createNamedParameter($comment->getParentId()),
1177
			'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()),
1178
			'children_count' => $qb->createNamedParameter($comment->getChildrenCount()),
1179
			'actor_type' => $qb->createNamedParameter($comment->getActorType()),
1180
			'actor_id' => $qb->createNamedParameter($comment->getActorId()),
1181
			'message' => $qb->createNamedParameter($comment->getMessage()),
1182
			'verb' => $qb->createNamedParameter($comment->getVerb()),
1183
			'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'),
1184
			'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'),
1185
			'object_type' => $qb->createNamedParameter($comment->getObjectType()),
1186
			'object_id' => $qb->createNamedParameter($comment->getObjectId()),
1187
		];
1188
1189
		if ($tryWritingReferenceId) {
1190
			$values['reference_id'] = $qb->createNamedParameter($comment->getReferenceId());
1191
		}
1192
1193
		$affectedRows = $qb->insert('comments')
1194
			->values($values)
1195
			->execute();
1196
1197
		if ($affectedRows > 0) {
1198
			$comment->setId((string)$qb->getLastInsertId());
1199
			if ($comment->getVerb() === 'reaction') {
1200
				$this->addReaction($comment);
1201
			}
1202
			$this->sendEvent(CommentsEvent::EVENT_ADD, $comment);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\Comments\CommentsEvent::EVENT_ADD has been deprecated: 22.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1202
			$this->sendEvent(/** @scrutinizer ignore-deprecated */ CommentsEvent::EVENT_ADD, $comment);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1203
		}
1204
1205
		return $affectedRows > 0;
1206
	}
1207
1208
	private function addReaction(IComment $reaction): void {
1209
		// Prevent violate constraint
1210
		$qb = $this->dbConn->getQueryBuilder();
1211
		$qb->select($qb->func()->count('*'))
1212
			->from('reactions')
1213
			->where($qb->expr()->eq('parent_id', $qb->createNamedParameter($reaction->getParentId())))
1214
			->andWhere($qb->expr()->eq('actor_type', $qb->createNamedParameter($reaction->getActorType())))
1215
			->andWhere($qb->expr()->eq('actor_id', $qb->createNamedParameter($reaction->getActorId())))
1216
			->andWhere($qb->expr()->eq('reaction', $qb->createNamedParameter($reaction->getMessage())));
1217
		$result = $qb->executeQuery();
1218
		$exists = (int) $result->fetchOne();
1219
		if (!$exists) {
1220
			$qb = $this->dbConn->getQueryBuilder();
1221
			try {
1222
				$qb->insert('reactions')
1223
					->values([
1224
						'parent_id' => $qb->createNamedParameter($reaction->getParentId()),
1225
						'message_id' => $qb->createNamedParameter($reaction->getId()),
1226
						'actor_type' => $qb->createNamedParameter($reaction->getActorType()),
1227
						'actor_id' => $qb->createNamedParameter($reaction->getActorId()),
1228
						'reaction' => $qb->createNamedParameter($reaction->getMessage()),
1229
					])
1230
					->executeStatement();
1231
			} catch (\Exception $e) {
1232
				$this->logger->error($e->getMessage(), [
1233
					'exception' => $e,
1234
					'app' => 'core_comments',
1235
				]);
1236
			}
1237
		}
1238
		$this->sumReactions($reaction->getParentId());
1239
	}
1240
1241
	private function sumReactions(string $parentId): void {
1242
		$qb = $this->dbConn->getQueryBuilder();
1243
1244
		$totalQuery = $this->dbConn->getQueryBuilder();
1245
		$totalQuery
1246
			->selectAlias(
1247
				$totalQuery->func()->concat(
1248
					$totalQuery->expr()->literal('"'),
1249
					'reaction',
1250
					$totalQuery->expr()->literal('":'),
1251
					$totalQuery->func()->count('id')
1252
				),
1253
				'colonseparatedvalue'
1254
			)
1255
			->selectAlias($totalQuery->func()->count('id'), 'total')
1256
			->from('reactions', 'r')
1257
			->where($totalQuery->expr()->eq('r.parent_id', $qb->createNamedParameter($parentId)))
1258
			->groupBy('r.reaction')
1259
			->orderBy('total', 'DESC')
1260
			->setMaxResults(20);
1261
1262
		$jsonQuery = $this->dbConn->getQueryBuilder();
1263
		$jsonQuery
1264
			->selectAlias(
1265
				$jsonQuery->func()->concat(
1266
					$jsonQuery->expr()->literal('{'),
1267
					$jsonQuery->func()->groupConcat('colonseparatedvalue'),
1268
					$jsonQuery->expr()->literal('}')
1269
				),
1270
				'json'
1271
			)
1272
			->from($jsonQuery->createFunction('(' . $totalQuery->getSQL() . ')'), 'json');
1273
1274
		$qb
1275
			->update('comments')
1276
			->set('reactions', $jsonQuery->createFunction('(' . $jsonQuery->getSQL() . ')'))
1277
			->where($qb->expr()->eq('id', $qb->createNamedParameter($parentId)))
1278
			->executeStatement();
1279
	}
1280
1281
	/**
1282
	 * updates a Comment data row
1283
	 *
1284
	 * @param IComment $comment
1285
	 * @return bool
1286
	 * @throws NotFoundException
1287
	 */
1288
	protected function update(IComment $comment) {
1289
		// for properly working preUpdate Events we need the old comments as is
1290
		// in the DB and overcome caching. Also avoid that outdated information stays.
1291
		$this->uncache($comment->getId());
1292
		$this->sendEvent(CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId()));
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\Comments\CommentsEvent::EVENT_PRE_UPDATE has been deprecated: 22.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1292
		$this->sendEvent(/** @scrutinizer ignore-deprecated */ CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId()));

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1293
		$this->uncache($comment->getId());
1294
1295
		try {
1296
			$result = $this->updateQuery($comment, true);
1297
		} catch (InvalidFieldNameException $e) {
1298
			// See function insert() for explanation
1299
			$result = $this->updateQuery($comment, false);
1300
		}
1301
1302
		if ($comment->getVerb() === 'reaction_deleted') {
1303
			$this->deleteReaction($comment);
1304
		}
1305
1306
		$this->sendEvent(CommentsEvent::EVENT_UPDATE, $comment);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\Comments\CommentsEvent::EVENT_UPDATE has been deprecated: 22.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1306
		$this->sendEvent(/** @scrutinizer ignore-deprecated */ CommentsEvent::EVENT_UPDATE, $comment);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
1307
1308
		return $result;
1309
	}
1310
1311
	protected function updateQuery(IComment $comment, bool $tryWritingReferenceId): bool {
1312
		$qb = $this->dbConn->getQueryBuilder();
1313
		$qb
1314
			->update('comments')
1315
			->set('parent_id', $qb->createNamedParameter($comment->getParentId()))
1316
			->set('topmost_parent_id', $qb->createNamedParameter($comment->getTopmostParentId()))
1317
			->set('children_count', $qb->createNamedParameter($comment->getChildrenCount()))
1318
			->set('actor_type', $qb->createNamedParameter($comment->getActorType()))
1319
			->set('actor_id', $qb->createNamedParameter($comment->getActorId()))
1320
			->set('message', $qb->createNamedParameter($comment->getMessage()))
1321
			->set('verb', $qb->createNamedParameter($comment->getVerb()))
1322
			->set('creation_timestamp', $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'))
1323
			->set('latest_child_timestamp', $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'))
1324
			->set('object_type', $qb->createNamedParameter($comment->getObjectType()))
1325
			->set('object_id', $qb->createNamedParameter($comment->getObjectId()));
1326
1327
		if ($tryWritingReferenceId) {
1328
			$qb->set('reference_id', $qb->createNamedParameter($comment->getReferenceId()));
1329
		}
1330
1331
		$affectedRows = $qb->where($qb->expr()->eq('id', $qb->createNamedParameter($comment->getId())))
1332
			->execute();
1333
1334
		if ($affectedRows === 0) {
1335
			throw new NotFoundException('Comment to update does ceased to exist');
1336
		}
1337
1338
		return $affectedRows > 0;
1339
	}
1340
1341
	/**
1342
	 * removes references to specific actor (e.g. on user delete) of a comment.
1343
	 * The comment itself must not get lost/deleted.
1344
	 *
1345
	 * @param string $actorType the actor type (e.g. 'users')
1346
	 * @param string $actorId a user id
1347
	 * @return boolean
1348
	 * @since 9.0.0
1349
	 */
1350
	public function deleteReferencesOfActor($actorType, $actorId) {
1351
		$this->checkRoleParameters('Actor', $actorType, $actorId);
1352
1353
		$qb = $this->dbConn->getQueryBuilder();
1354
		$affectedRows = $qb
1355
			->update('comments')
1356
			->set('actor_type', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
1357
			->set('actor_id', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
1358
			->where($qb->expr()->eq('actor_type', $qb->createParameter('type')))
1359
			->andWhere($qb->expr()->eq('actor_id', $qb->createParameter('id')))
1360
			->setParameter('type', $actorType)
1361
			->setParameter('id', $actorId)
1362
			->execute();
1363
1364
		$this->commentsCache = [];
1365
1366
		return is_int($affectedRows);
1367
	}
1368
1369
	/**
1370
	 * deletes all comments made of a specific object (e.g. on file delete)
1371
	 *
1372
	 * @param string $objectType the object type (e.g. 'files')
1373
	 * @param string $objectId e.g. the file id
1374
	 * @return boolean
1375
	 * @since 9.0.0
1376
	 */
1377
	public function deleteCommentsAtObject($objectType, $objectId) {
1378
		$this->checkRoleParameters('Object', $objectType, $objectId);
1379
1380
		$qb = $this->dbConn->getQueryBuilder();
1381
		$affectedRows = $qb
1382
			->delete('comments')
1383
			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
1384
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
1385
			->setParameter('type', $objectType)
1386
			->setParameter('id', $objectId)
1387
			->execute();
1388
1389
		$this->commentsCache = [];
1390
1391
		return is_int($affectedRows);
1392
	}
1393
1394
	/**
1395
	 * deletes the read markers for the specified user
1396
	 *
1397
	 * @param \OCP\IUser $user
1398
	 * @return bool
1399
	 * @since 9.0.0
1400
	 */
1401
	public function deleteReadMarksFromUser(IUser $user) {
1402
		$qb = $this->dbConn->getQueryBuilder();
1403
		$query = $qb->delete('comments_read_markers')
1404
			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
1405
			->setParameter('user_id', $user->getUID());
1406
1407
		try {
1408
			$affectedRows = $query->execute();
1409
		} catch (DriverException $e) {
1410
			$this->logger->error($e->getMessage(), [
1411
				'exception' => $e,
1412
				'app' => 'core_comments',
1413
			]);
1414
			return false;
1415
		}
1416
		return ($affectedRows > 0);
1417
	}
1418
1419
	/**
1420
	 * sets the read marker for a given file to the specified date for the
1421
	 * provided user
1422
	 *
1423
	 * @param string $objectType
1424
	 * @param string $objectId
1425
	 * @param \DateTime $dateTime
1426
	 * @param IUser $user
1427
	 * @since 9.0.0
1428
	 */
1429
	public function setReadMark($objectType, $objectId, \DateTime $dateTime, IUser $user) {
1430
		$this->checkRoleParameters('Object', $objectType, $objectId);
1431
1432
		$qb = $this->dbConn->getQueryBuilder();
1433
		$values = [
1434
			'user_id' => $qb->createNamedParameter($user->getUID()),
1435
			'marker_datetime' => $qb->createNamedParameter($dateTime, 'datetime'),
1436
			'object_type' => $qb->createNamedParameter($objectType),
1437
			'object_id' => $qb->createNamedParameter($objectId),
1438
		];
1439
1440
		// Strategy: try to update, if this does not return affected rows, do an insert.
1441
		$affectedRows = $qb
1442
			->update('comments_read_markers')
1443
			->set('user_id', $values['user_id'])
1444
			->set('marker_datetime', $values['marker_datetime'])
1445
			->set('object_type', $values['object_type'])
1446
			->set('object_id', $values['object_id'])
1447
			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
1448
			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
1449
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
1450
			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
1451
			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
1452
			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
1453
			->execute();
1454
1455
		if ($affectedRows > 0) {
1456
			return;
1457
		}
1458
1459
		$qb->insert('comments_read_markers')
1460
			->values($values)
1461
			->execute();
1462
	}
1463
1464
	/**
1465
	 * returns the read marker for a given file to the specified date for the
1466
	 * provided user. It returns null, when the marker is not present, i.e.
1467
	 * no comments were marked as read.
1468
	 *
1469
	 * @param string $objectType
1470
	 * @param string $objectId
1471
	 * @param IUser $user
1472
	 * @return \DateTime|null
1473
	 * @since 9.0.0
1474
	 */
1475
	public function getReadMark($objectType, $objectId, IUser $user) {
1476
		$qb = $this->dbConn->getQueryBuilder();
1477
		$resultStatement = $qb->select('marker_datetime')
1478
			->from('comments_read_markers')
1479
			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
1480
			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
1481
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
1482
			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
1483
			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
1484
			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
1485
			->execute();
1486
1487
		$data = $resultStatement->fetch();
1488
		$resultStatement->closeCursor();
1489
		if (!$data || is_null($data['marker_datetime'])) {
1490
			return null;
1491
		}
1492
1493
		return new \DateTime($data['marker_datetime']);
1494
	}
1495
1496
	/**
1497
	 * deletes the read markers on the specified object
1498
	 *
1499
	 * @param string $objectType
1500
	 * @param string $objectId
1501
	 * @return bool
1502
	 * @since 9.0.0
1503
	 */
1504
	public function deleteReadMarksOnObject($objectType, $objectId) {
1505
		$this->checkRoleParameters('Object', $objectType, $objectId);
1506
1507
		$qb = $this->dbConn->getQueryBuilder();
1508
		$query = $qb->delete('comments_read_markers')
1509
			->where($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
1510
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
1511
			->setParameter('object_type', $objectType)
1512
			->setParameter('object_id', $objectId);
1513
1514
		try {
1515
			$affectedRows = $query->execute();
1516
		} catch (DriverException $e) {
1517
			$this->logger->error($e->getMessage(), [
1518
				'exception' => $e,
1519
				'app' => 'core_comments',
1520
			]);
1521
			return false;
1522
		}
1523
		return ($affectedRows > 0);
1524
	}
1525
1526
	/**
1527
	 * registers an Entity to the manager, so event notifications can be send
1528
	 * to consumers of the comments infrastructure
1529
	 *
1530
	 * @param \Closure $closure
1531
	 */
1532
	public function registerEventHandler(\Closure $closure) {
1533
		$this->eventHandlerClosures[] = $closure;
1534
		$this->eventHandlers = [];
1535
	}
1536
1537
	/**
1538
	 * registers a method that resolves an ID to a display name for a given type
1539
	 *
1540
	 * @param string $type
1541
	 * @param \Closure $closure
1542
	 * @throws \OutOfBoundsException
1543
	 * @since 11.0.0
1544
	 *
1545
	 * Only one resolver shall be registered per type. Otherwise a
1546
	 * \OutOfBoundsException has to thrown.
1547
	 */
1548
	public function registerDisplayNameResolver($type, \Closure $closure) {
1549
		if (!is_string($type)) {
0 ignored issues
show
introduced by
The condition is_string($type) is always true.
Loading history...
1550
			throw new \InvalidArgumentException('String expected.');
1551
		}
1552
		if (isset($this->displayNameResolvers[$type])) {
1553
			throw new \OutOfBoundsException('Displayname resolver for this type already registered');
1554
		}
1555
		$this->displayNameResolvers[$type] = $closure;
1556
	}
1557
1558
	/**
1559
	 * resolves a given ID of a given Type to a display name.
1560
	 *
1561
	 * @param string $type
1562
	 * @param string $id
1563
	 * @return string
1564
	 * @throws \OutOfBoundsException
1565
	 * @since 11.0.0
1566
	 *
1567
	 * If a provided type was not registered, an \OutOfBoundsException shall
1568
	 * be thrown. It is upon the resolver discretion what to return of the
1569
	 * provided ID is unknown. It must be ensured that a string is returned.
1570
	 */
1571
	public function resolveDisplayName($type, $id) {
1572
		if (!is_string($type)) {
0 ignored issues
show
introduced by
The condition is_string($type) is always true.
Loading history...
1573
			throw new \InvalidArgumentException('String expected.');
1574
		}
1575
		if (!isset($this->displayNameResolvers[$type])) {
1576
			throw new \OutOfBoundsException('No Displayname resolver for this type registered');
1577
		}
1578
		return (string)$this->displayNameResolvers[$type]($id);
1579
	}
1580
1581
	/**
1582
	 * returns valid, registered entities
1583
	 *
1584
	 * @return \OCP\Comments\ICommentsEventHandler[]
1585
	 */
1586
	private function getEventHandlers() {
1587
		if (!empty($this->eventHandlers)) {
1588
			return $this->eventHandlers;
1589
		}
1590
1591
		$this->eventHandlers = [];
1592
		foreach ($this->eventHandlerClosures as $name => $closure) {
1593
			$entity = $closure();
1594
			if (!($entity instanceof ICommentsEventHandler)) {
1595
				throw new \InvalidArgumentException('The given entity does not implement the ICommentsEntity interface');
1596
			}
1597
			$this->eventHandlers[$name] = $entity;
1598
		}
1599
1600
		return $this->eventHandlers;
1601
	}
1602
1603
	/**
1604
	 * sends notifications to the registered entities
1605
	 *
1606
	 * @param $eventType
1607
	 * @param IComment $comment
1608
	 */
1609
	private function sendEvent($eventType, IComment $comment) {
1610
		$entities = $this->getEventHandlers();
1611
		$event = new CommentsEvent($eventType, $comment);
1612
		foreach ($entities as $entity) {
1613
			$entity->handle($event);
1614
		}
1615
	}
1616
1617
	/**
1618
	 * Load the Comments app into the page
1619
	 *
1620
	 * @since 21.0.0
1621
	 */
1622
	public function load(): void {
1623
		$this->initialStateService->provideInitialState('comments', 'max-message-length', IComment::MAX_MESSAGE_LENGTH);
1624
		Util::addScript('comments', 'comments-app');
1625
	}
1626
}
1627