Passed
Push — master ( c0a48c...452f1e )
by Joas
15:12
created

Manager::getCommentFromData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 2
rs 10
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 Lukas Reschke <[email protected]>
8
 * @author Robin Appelman <[email protected]>
9
 * @author Roeland Jago Douma <[email protected]>
10
 * @author Thomas Müller <[email protected]>
11
 *
12
 * @license AGPL-3.0
13
 *
14
 * This code is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License, version 3,
16
 * as published by the Free Software Foundation.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License, version 3,
24
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
25
 *
26
 */
27
28
namespace OC\Comments;
29
30
use Doctrine\DBAL\Exception\DriverException;
31
use OCP\Comments\CommentsEvent;
32
use OCP\Comments\IComment;
33
use OCP\Comments\ICommentsEventHandler;
34
use OCP\Comments\ICommentsManager;
35
use OCP\Comments\NotFoundException;
36
use OCP\DB\QueryBuilder\IQueryBuilder;
37
use OCP\IDBConnection;
38
use OCP\IConfig;
39
use OCP\ILogger;
40
use OCP\IUser;
41
42
class Manager implements ICommentsManager {
43
44
	/** @var  IDBConnection */
45
	protected $dbConn;
46
47
	/** @var  ILogger */
48
	protected $logger;
49
50
	/** @var IConfig */
51
	protected $config;
52
53
	/** @var IComment[] */
54
	protected $commentsCache = [];
55
56
	/** @var  \Closure[] */
57
	protected $eventHandlerClosures = [];
58
59
	/** @var  ICommentsEventHandler[] */
60
	protected $eventHandlers = [];
61
62
	/** @var \Closure[] */
63
	protected $displayNameResolvers = [];
64
65
	/**
66
	 * Manager constructor.
67
	 *
68
	 * @param IDBConnection $dbConn
69
	 * @param ILogger $logger
70
	 * @param IConfig $config
71
	 */
72
	public function __construct(
73
		IDBConnection $dbConn,
74
		ILogger $logger,
75
		IConfig $config
76
	) {
77
		$this->dbConn = $dbConn;
78
		$this->logger = $logger;
79
		$this->config = $config;
80
	}
81
82
	/**
83
	 * converts data base data into PHP native, proper types as defined by
84
	 * IComment interface.
85
	 *
86
	 * @param array $data
87
	 * @return array
88
	 */
89
	protected function normalizeDatabaseData(array $data) {
90
		$data['id'] = (string)$data['id'];
91
		$data['parent_id'] = (string)$data['parent_id'];
92
		$data['topmost_parent_id'] = (string)$data['topmost_parent_id'];
93
		$data['creation_timestamp'] = new \DateTime($data['creation_timestamp']);
94
		if (!is_null($data['latest_child_timestamp'])) {
95
			$data['latest_child_timestamp'] = new \DateTime($data['latest_child_timestamp']);
96
		}
97
		$data['children_count'] = (int)$data['children_count'];
98
		return $data;
99
	}
100
101
102
	/**
103
	 * @param array $data
104
	 * @return IComment
105
	 */
106
	public function getCommentFromData(array $data): IComment {
107
		return new Comment($this->normalizeDatabaseData($data));
108
	}
109
110
	/**
111
	 * prepares a comment for an insert or update operation after making sure
112
	 * all necessary fields have a value assigned.
113
	 *
114
	 * @param IComment $comment
115
	 * @return IComment returns the same updated IComment instance as provided
116
	 *                  by parameter for convenience
117
	 * @throws \UnexpectedValueException
118
	 */
119
	protected function prepareCommentForDatabaseWrite(IComment $comment) {
120
		if (!$comment->getActorType()
121
			|| !$comment->getActorId()
122
			|| !$comment->getObjectType()
123
			|| !$comment->getObjectId()
124
			|| !$comment->getVerb()
125
		) {
126
			throw new \UnexpectedValueException('Actor, Object and Verb information must be provided for saving');
127
		}
128
129
		if ($comment->getId() === '') {
130
			$comment->setChildrenCount(0);
131
			$comment->setLatestChildDateTime(new \DateTime('0000-00-00 00:00:00', new \DateTimeZone('UTC')));
132
			$comment->setLatestChildDateTime(null);
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

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