Completed
Push — master ( 8255fa...61397e )
by Morris
16:01
created

Manager::search()   B

Complexity

Conditions 6
Paths 32

Size

Total Lines 36

Duplication

Lines 5
Ratio 13.89 %

Importance

Changes 0
Metric Value
cc 6
nc 32
nop 6
dl 5
loc 36
rs 8.7217
c 0
b 0
f 0
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 View Code Duplication
		if (!is_null($data['latest_child_timestamp'])) {
95
			$data['latest_child_timestamp'] = new \DateTime($data['latest_child_timestamp']);
96
		}
97
		$data['children_count'] = (int)$data['children_count'];
98
		return $data;
99
	}
100
101
	/**
102
	 * prepares a comment for an insert or update operation after making sure
103
	 * all necessary fields have a value assigned.
104
	 *
105
	 * @param IComment $comment
106
	 * @return IComment returns the same updated IComment instance as provided
107
	 *                  by parameter for convenience
108
	 * @throws \UnexpectedValueException
109
	 */
110
	protected function prepareCommentForDatabaseWrite(IComment $comment) {
111
		if (!$comment->getActorType()
112
			|| !$comment->getActorId()
113
			|| !$comment->getObjectType()
114
			|| !$comment->getObjectId()
115
			|| !$comment->getVerb()
116
		) {
117
			throw new \UnexpectedValueException('Actor, Object and Verb information must be provided for saving');
118
		}
119
120
		if ($comment->getId() === '') {
121
			$comment->setChildrenCount(0);
122
			$comment->setLatestChildDateTime(new \DateTime('0000-00-00 00:00:00', new \DateTimeZone('UTC')));
123
			$comment->setLatestChildDateTime(null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<DateTime>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
124
		}
125
126
		if (is_null($comment->getCreationDateTime())) {
127
			$comment->setCreationDateTime(new \DateTime());
128
		}
129
130
		if ($comment->getParentId() !== '0') {
131
			$comment->setTopmostParentId($this->determineTopmostParentId($comment->getParentId()));
132
		} else {
133
			$comment->setTopmostParentId('0');
134
		}
135
136
		$this->cache($comment);
137
138
		return $comment;
139
	}
140
141
	/**
142
	 * returns the topmost parent id of a given comment identified by ID
143
	 *
144
	 * @param string $id
145
	 * @return string
146
	 * @throws NotFoundException
147
	 */
148
	protected function determineTopmostParentId($id) {
149
		$comment = $this->get($id);
150
		if ($comment->getParentId() === '0') {
151
			return $comment->getId();
152
		} else {
153
			return $this->determineTopmostParentId($comment->getId());
154
		}
155
	}
156
157
	/**
158
	 * updates child information of a comment
159
	 *
160
	 * @param string $id
161
	 * @param \DateTime $cDateTime the date time of the most recent child
162
	 * @throws NotFoundException
163
	 */
164
	protected function updateChildrenInformation($id, \DateTime $cDateTime) {
165
		$qb = $this->dbConn->getQueryBuilder();
166
		$query = $qb->select($qb->createFunction('COUNT(`id`)'))
167
			->from('comments')
168
			->where($qb->expr()->eq('parent_id', $qb->createParameter('id')))
169
			->setParameter('id', $id);
170
171
		$resultStatement = $query->execute();
172
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
173
		$resultStatement->closeCursor();
174
		$children = (int)$data[0];
175
176
		$comment = $this->get($id);
177
		$comment->setChildrenCount($children);
178
		$comment->setLatestChildDateTime($cDateTime);
179
		$this->save($comment);
180
	}
181
182
	/**
183
	 * Tests whether actor or object type and id parameters are acceptable.
184
	 * Throws exception if not.
185
	 *
186
	 * @param string $role
187
	 * @param string $type
188
	 * @param string $id
189
	 * @throws \InvalidArgumentException
190
	 */
191
	protected function checkRoleParameters($role, $type, $id) {
192
		if (
193
			!is_string($type) || empty($type)
194
			|| !is_string($id) || empty($id)
195
		) {
196
			throw new \InvalidArgumentException($role . ' parameters must be string and not empty');
197
		}
198
	}
199
200
	/**
201
	 * run-time caches a comment
202
	 *
203
	 * @param IComment $comment
204
	 */
205
	protected function cache(IComment $comment) {
206
		$id = $comment->getId();
207
		if (empty($id)) {
208
			return;
209
		}
210
		$this->commentsCache[(string)$id] = $comment;
211
	}
212
213
	/**
214
	 * removes an entry from the comments run time cache
215
	 *
216
	 * @param mixed $id the comment's id
217
	 */
218
	protected function uncache($id) {
219
		$id = (string)$id;
220
		if (isset($this->commentsCache[$id])) {
221
			unset($this->commentsCache[$id]);
222
		}
223
	}
224
225
	/**
226
	 * returns a comment instance
227
	 *
228
	 * @param string $id the ID of the comment
229
	 * @return IComment
230
	 * @throws NotFoundException
231
	 * @throws \InvalidArgumentException
232
	 * @since 9.0.0
233
	 */
234
	public function get($id) {
235
		if ((int)$id === 0) {
236
			throw new \InvalidArgumentException('IDs must be translatable to a number in this implementation.');
237
		}
238
239
		if (isset($this->commentsCache[$id])) {
240
			return $this->commentsCache[$id];
241
		}
242
243
		$qb = $this->dbConn->getQueryBuilder();
244
		$resultStatement = $qb->select('*')
245
			->from('comments')
246
			->where($qb->expr()->eq('id', $qb->createParameter('id')))
247
			->setParameter('id', $id, IQueryBuilder::PARAM_INT)
248
			->execute();
249
250
		$data = $resultStatement->fetch();
251
		$resultStatement->closeCursor();
252
		if (!$data) {
253
			throw new NotFoundException();
254
		}
255
256
		$comment = new Comment($this->normalizeDatabaseData($data));
257
		$this->cache($comment);
258
		return $comment;
259
	}
260
261
	/**
262
	 * returns the comment specified by the id and all it's child comments.
263
	 * At this point of time, we do only support one level depth.
264
	 *
265
	 * @param string $id
266
	 * @param int $limit max number of entries to return, 0 returns all
267
	 * @param int $offset the start entry
268
	 * @return array
269
	 * @since 9.0.0
270
	 *
271
	 * The return array looks like this
272
	 * [
273
	 *   'comment' => IComment, // root comment
274
	 *   'replies' =>
275
	 *   [
276
	 *     0 =>
277
	 *     [
278
	 *       'comment' => IComment,
279
	 *       'replies' => []
280
	 *     ]
281
	 *     1 =>
282
	 *     [
283
	 *       'comment' => IComment,
284
	 *       'replies'=> []
285
	 *     ],
286
	 *     …
287
	 *   ]
288
	 * ]
289
	 */
290
	public function getTree($id, $limit = 0, $offset = 0) {
291
		$tree = [];
292
		$tree['comment'] = $this->get($id);
293
		$tree['replies'] = [];
294
295
		$qb = $this->dbConn->getQueryBuilder();
296
		$query = $qb->select('*')
297
			->from('comments')
298
			->where($qb->expr()->eq('topmost_parent_id', $qb->createParameter('id')))
299
			->orderBy('creation_timestamp', 'DESC')
300
			->setParameter('id', $id);
301
302
		if ($limit > 0) {
303
			$query->setMaxResults($limit);
304
		}
305
		if ($offset > 0) {
306
			$query->setFirstResult($offset);
307
		}
308
309
		$resultStatement = $query->execute();
310
		while ($data = $resultStatement->fetch()) {
311
			$comment = new Comment($this->normalizeDatabaseData($data));
312
			$this->cache($comment);
313
			$tree['replies'][] = [
314
				'comment' => $comment,
315
				'replies' => []
316
			];
317
		}
318
		$resultStatement->closeCursor();
319
320
		return $tree;
321
	}
322
323
	/**
324
	 * returns comments for a specific object (e.g. a file).
325
	 *
326
	 * The sort order is always newest to oldest.
327
	 *
328
	 * @param string $objectType the object type, e.g. 'files'
329
	 * @param string $objectId the id of the object
330
	 * @param int $limit optional, number of maximum comments to be returned. if
331
	 * not specified, all comments are returned.
332
	 * @param int $offset optional, starting point
333
	 * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
334
	 * that may be returned
335
	 * @return IComment[]
336
	 * @since 9.0.0
337
	 */
338
	public function getForObject(
339
		$objectType,
340
		$objectId,
341
		$limit = 0,
342
		$offset = 0,
343
		\DateTime $notOlderThan = null
344
	) {
345
		$comments = [];
346
347
		$qb = $this->dbConn->getQueryBuilder();
348
		$query = $qb->select('*')
349
			->from('comments')
350
			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
351
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
352
			->orderBy('creation_timestamp', 'DESC')
353
			->setParameter('type', $objectType)
354
			->setParameter('id', $objectId);
355
356
		if ($limit > 0) {
357
			$query->setMaxResults($limit);
358
		}
359
		if ($offset > 0) {
360
			$query->setFirstResult($offset);
361
		}
362 View Code Duplication
		if (!is_null($notOlderThan)) {
363
			$query
364
				->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
365
				->setParameter('notOlderThan', $notOlderThan, 'datetime');
366
		}
367
368
		$resultStatement = $query->execute();
369 View Code Duplication
		while ($data = $resultStatement->fetch()) {
370
			$comment = new Comment($this->normalizeDatabaseData($data));
371
			$this->cache($comment);
372
			$comments[] = $comment;
373
		}
374
		$resultStatement->closeCursor();
375
376
		return $comments;
377
	}
378
379
	/**
380
	 * @param string $objectType the object type, e.g. 'files'
381
	 * @param string $objectId the id of the object
382
	 * @param int $lastKnownCommentId the last known comment (will be used as offset)
383
	 * @param string $sortDirection direction of the comments (`asc` or `desc`)
384
	 * @param int $limit optional, number of maximum comments to be returned. if
385
	 * set to 0, all comments are returned.
386
	 * @return IComment[]
387
	 * @return array
388
	 */
389
	public function getForObjectSince(
390
		string $objectType,
391
		string $objectId,
392
		int $lastKnownCommentId,
393
		string $sortDirection = 'asc',
394
		int $limit = 30
395
	): array {
396
		$comments = [];
397
398
		$query = $this->dbConn->getQueryBuilder();
399
		$query->select('*')
400
			->from('comments')
401
			->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
402
			->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
403
			->orderBy('creation_timestamp', $sortDirection === 'desc' ? 'DESC' : 'ASC')
404
			->addOrderBy('id', $sortDirection === 'desc' ? 'DESC' : 'ASC');
405
406
		if ($limit > 0) {
407
			$query->setMaxResults($limit);
408
		}
409
410
		$lastKnownComment = $lastKnownCommentId > 0 ? $this->getLastKnownComment(
411
			$objectType,
412
			$objectId,
413
			$lastKnownCommentId
414
		) : null;
415
		if ($lastKnownComment instanceof IComment) {
416
			$lastKnownCommentDateTime = $lastKnownComment->getCreationDateTime();
417
			if ($sortDirection === 'desc') {
418
				$query->andWhere(
419
					$query->expr()->orX(
420
						$query->expr()->lt(
421
							'creation_timestamp',
422
							$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
423
							IQueryBuilder::PARAM_DATE
424
						),
425
						$query->expr()->andX(
426
							$query->expr()->eq(
427
								'creation_timestamp',
428
								$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
429
								IQueryBuilder::PARAM_DATE
430
							),
431
							$query->expr()->lt('id', $query->createNamedParameter($lastKnownCommentId))
432
						)
433
					)
434
				);
435
			} else {
436
				$query->andWhere(
437
					$query->expr()->orX(
438
						$query->expr()->gt(
439
							'creation_timestamp',
440
							$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
441
							IQueryBuilder::PARAM_DATE
442
						),
443
						$query->expr()->andX(
444
							$query->expr()->eq(
445
								'creation_timestamp',
446
								$query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
447
								IQueryBuilder::PARAM_DATE
448
							),
449
							$query->expr()->gt('id', $query->createNamedParameter($lastKnownCommentId))
450
						)
451
					)
452
				);
453
			}
454
		}
455
456
		$resultStatement = $query->execute();
457 View Code Duplication
		while ($data = $resultStatement->fetch()) {
458
			$comment = new Comment($this->normalizeDatabaseData($data));
459
			$this->cache($comment);
460
			$comments[] = $comment;
461
		}
462
		$resultStatement->closeCursor();
463
464
		return $comments;
465
	}
466
467
	/**
468
	 * @param string $objectType the object type, e.g. 'files'
469
	 * @param string $objectId the id of the object
470
	 * @param int $id the comment to look for
471
	 * @return Comment|null
472
	 */
473
	protected function getLastKnownComment(string $objectType,
474
										   string $objectId,
475
										   int $id) {
476
		$query = $this->dbConn->getQueryBuilder();
477
		$query->select('*')
478
			->from('comments')
479
			->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
480
			->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
481
			->andWhere($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
482
483
		$result = $query->execute();
484
		$row = $result->fetch();
485
		$result->closeCursor();
486
487
		if ($row) {
488
			$comment = new Comment($this->normalizeDatabaseData($row));
489
			$this->cache($comment);
490
			return $comment;
491
		}
492
493
		return null;
494
	}
495
496
	/**
497
	 * Search for comments with a given content
498
	 *
499
	 * @param string $search content to search for
500
	 * @param string $objectType Limit the search by object type
501
	 * @param string $objectId Limit the search by object id
502
	 * @param string $verb Limit the verb of the comment
503
	 * @param int $offset
504
	 * @param int $limit
505
	 * @return IComment[]
506
	 */
507
	public function search(string $search, string $objectType, string $objectId, string $verb, int $offset, int $limit = 50): array {
508
		$query = $this->dbConn->getQueryBuilder();
509
510
		$query->select('*')
511
			->from('comments')
512
			->where($query->expr()->iLike('message', $query->createNamedParameter(
513
				'%' . $this->dbConn->escapeLikeParameter($search). '%'
514
			)))
515
			->orderBy('creation_timestamp', 'DESC')
516
			->addOrderBy('id', 'DESC')
517
			->setMaxResults($limit);
518
519
		if ($objectType !== '') {
520
			$query->andWhere($query->expr()->eq('object_type', $query->createNamedParameter($objectType)));
521
		}
522
		if ($objectId !== '') {
523
			$query->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)));
524
		}
525
		if ($verb !== '') {
526
			$query->andWhere($query->expr()->eq('verb', $query->createNamedParameter($verb)));
527
		}
528
		if ($offset !== 0) {
529
			$query->setFirstResult($offset);
530
		}
531
532
		$comments = [];
533
		$result = $query->execute();
534 View Code Duplication
		while ($data = $result->fetch()) {
535
			$comment = new Comment($this->normalizeDatabaseData($data));
536
			$this->cache($comment);
537
			$comments[] = $comment;
538
		}
539
		$result->closeCursor();
540
541
		return $comments;
542
	}
543
544
	/**
545
	 * @param $objectType string the object type, e.g. 'files'
546
	 * @param $objectId string the id of the object
547
	 * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
548
	 * that may be returned
549
	 * @return Int
550
	 * @since 9.0.0
551
	 */
552
	public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null) {
553
		$qb = $this->dbConn->getQueryBuilder();
554
		$query = $qb->select($qb->createFunction('COUNT(`id`)'))
555
			->from('comments')
556
			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
557
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
558
			->setParameter('type', $objectType)
559
			->setParameter('id', $objectId);
560
561 View Code Duplication
		if (!is_null($notOlderThan)) {
562
			$query
563
				->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
564
				->setParameter('notOlderThan', $notOlderThan, 'datetime');
565
		}
566
567
		$resultStatement = $query->execute();
568
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
569
		$resultStatement->closeCursor();
570
		return (int)$data[0];
571
	}
572
573
	/**
574
	 * Get the number of unread comments for all files in a folder
575
	 *
576
	 * @param int $folderId
577
	 * @param IUser $user
578
	 * @return array [$fileId => $unreadCount]
579
	 */
580
	public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user) {
581
		$qb = $this->dbConn->getQueryBuilder();
582
		$query = $qb->select('f.fileid')
583
			->selectAlias(
584
				$qb->createFunction('COUNT(' . $qb->getColumnName('c.id') . ')'),
585
				'num_ids'
586
			)
587
			->from('comments', 'c')
588
			->innerJoin('c', 'filecache', 'f', $qb->expr()->andX(
0 ignored issues
show
Documentation introduced by
$qb->expr()->andX($qb->e...ryBuilder::PARAM_INT))) is of type object<OCP\DB\QueryBuilder\ICompositeExpression>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
589
				$qb->expr()->eq('c.object_type', $qb->createNamedParameter('files')),
590
				$qb->expr()->eq('f.fileid', $qb->expr()->castColumn('c.object_id', IQueryBuilder::PARAM_INT))
591
			))
592
			->leftJoin('c', 'comments_read_markers', 'm', $qb->expr()->andX(
0 ignored issues
show
Documentation introduced by
$qb->expr()->andX($qb->e...eter($user->getUID()))) is of type object<OCP\DB\QueryBuilder\ICompositeExpression>, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
593
				$qb->expr()->eq('m.object_type', $qb->createNamedParameter('files')),
594
				$qb->expr()->eq('m.object_id', 'c.object_id'),
595
				$qb->expr()->eq('m.user_id', $qb->createNamedParameter($user->getUID()))
596
			))
597
			->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($folderId)))
598
			->andWhere($qb->expr()->orX(
599
				$qb->expr()->gt('c.creation_timestamp', 'marker_datetime'),
600
				$qb->expr()->isNull('marker_datetime')
601
			))
602
			->groupBy('f.fileid');
603
604
		$resultStatement = $query->execute();
605
606
		$results = [];
607
		while ($row = $resultStatement->fetch()) {
608
			$results[$row['fileid']] = (int) $row['num_ids'];
609
		}
610
		$resultStatement->closeCursor();
611
		return $results;
612
	}
613
614
	/**
615
	 * creates a new comment and returns it. At this point of time, it is not
616
	 * saved in the used data storage. Use save() after setting other fields
617
	 * of the comment (e.g. message or verb).
618
	 *
619
	 * @param string $actorType the actor type (e.g. 'users')
620
	 * @param string $actorId a user id
621
	 * @param string $objectType the object type the comment is attached to
622
	 * @param string $objectId the object id the comment is attached to
623
	 * @return IComment
624
	 * @since 9.0.0
625
	 */
626
	public function create($actorType, $actorId, $objectType, $objectId) {
627
		$comment = new Comment();
628
		$comment
629
			->setActor($actorType, $actorId)
630
			->setObject($objectType, $objectId);
631
		return $comment;
632
	}
633
634
	/**
635
	 * permanently deletes the comment specified by the ID
636
	 *
637
	 * When the comment has child comments, their parent ID will be changed to
638
	 * the parent ID of the item that is to be deleted.
639
	 *
640
	 * @param string $id
641
	 * @return bool
642
	 * @throws \InvalidArgumentException
643
	 * @since 9.0.0
644
	 */
645
	public function delete($id) {
646
		if (!is_string($id)) {
647
			throw new \InvalidArgumentException('Parameter must be string');
648
		}
649
650
		try {
651
			$comment = $this->get($id);
652
		} catch (\Exception $e) {
653
			// Ignore exceptions, we just don't fire a hook then
654
			$comment = null;
655
		}
656
657
		$qb = $this->dbConn->getQueryBuilder();
658
		$query = $qb->delete('comments')
659
			->where($qb->expr()->eq('id', $qb->createParameter('id')))
660
			->setParameter('id', $id);
661
662
		try {
663
			$affectedRows = $query->execute();
664
			$this->uncache($id);
665
		} catch (DriverException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\DBAL\Exception\DriverException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
666
			$this->logger->logException($e, ['app' => 'core_comments']);
667
			return false;
668
		}
669
670
		if ($affectedRows > 0 && $comment instanceof IComment) {
671
			$this->sendEvent(CommentsEvent::EVENT_DELETE, $comment);
672
		}
673
674
		return ($affectedRows > 0);
675
	}
676
677
	/**
678
	 * saves the comment permanently
679
	 *
680
	 * if the supplied comment has an empty ID, a new entry comment will be
681
	 * saved and the instance updated with the new ID.
682
	 *
683
	 * Otherwise, an existing comment will be updated.
684
	 *
685
	 * Throws NotFoundException when a comment that is to be updated does not
686
	 * exist anymore at this point of time.
687
	 *
688
	 * @param IComment $comment
689
	 * @return bool
690
	 * @throws NotFoundException
691
	 * @since 9.0.0
692
	 */
693
	public function save(IComment $comment) {
694
		if ($this->prepareCommentForDatabaseWrite($comment)->getId() === '') {
695
			$result = $this->insert($comment);
696
		} else {
697
			$result = $this->update($comment);
698
		}
699
700
		if ($result && !!$comment->getParentId()) {
701
			$this->updateChildrenInformation(
702
				$comment->getParentId(),
703
				$comment->getCreationDateTime()
704
			);
705
			$this->cache($comment);
706
		}
707
708
		return $result;
709
	}
710
711
	/**
712
	 * inserts the provided comment in the database
713
	 *
714
	 * @param IComment $comment
715
	 * @return bool
716
	 */
717
	protected function insert(IComment &$comment) {
718
		$qb = $this->dbConn->getQueryBuilder();
719
		$affectedRows = $qb
720
			->insert('comments')
721
			->values([
722
				'parent_id' => $qb->createNamedParameter($comment->getParentId()),
723
				'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()),
724
				'children_count' => $qb->createNamedParameter($comment->getChildrenCount()),
725
				'actor_type' => $qb->createNamedParameter($comment->getActorType()),
726
				'actor_id' => $qb->createNamedParameter($comment->getActorId()),
727
				'message' => $qb->createNamedParameter($comment->getMessage()),
728
				'verb' => $qb->createNamedParameter($comment->getVerb()),
729
				'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'),
730
				'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'),
731
				'object_type' => $qb->createNamedParameter($comment->getObjectType()),
732
				'object_id' => $qb->createNamedParameter($comment->getObjectId()),
733
			])
734
			->execute();
735
736
		if ($affectedRows > 0) {
737
			$comment->setId((string)$qb->getLastInsertId());
738
			$this->sendEvent(CommentsEvent::EVENT_ADD, $comment);
739
		}
740
741
		return $affectedRows > 0;
742
	}
743
744
	/**
745
	 * updates a Comment data row
746
	 *
747
	 * @param IComment $comment
748
	 * @return bool
749
	 * @throws NotFoundException
750
	 */
751
	protected function update(IComment $comment) {
752
		// for properly working preUpdate Events we need the old comments as is
753
		// in the DB and overcome caching. Also avoid that outdated information stays.
754
		$this->uncache($comment->getId());
755
		$this->sendEvent(CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId()));
756
		$this->uncache($comment->getId());
757
758
		$qb = $this->dbConn->getQueryBuilder();
759
		$affectedRows = $qb
760
			->update('comments')
761
			->set('parent_id', $qb->createNamedParameter($comment->getParentId()))
762
			->set('topmost_parent_id', $qb->createNamedParameter($comment->getTopmostParentId()))
763
			->set('children_count', $qb->createNamedParameter($comment->getChildrenCount()))
764
			->set('actor_type', $qb->createNamedParameter($comment->getActorType()))
765
			->set('actor_id', $qb->createNamedParameter($comment->getActorId()))
766
			->set('message', $qb->createNamedParameter($comment->getMessage()))
767
			->set('verb', $qb->createNamedParameter($comment->getVerb()))
768
			->set('creation_timestamp', $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'))
769
			->set('latest_child_timestamp', $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'))
770
			->set('object_type', $qb->createNamedParameter($comment->getObjectType()))
771
			->set('object_id', $qb->createNamedParameter($comment->getObjectId()))
772
			->where($qb->expr()->eq('id', $qb->createParameter('id')))
773
			->setParameter('id', $comment->getId())
774
			->execute();
775
776
		if ($affectedRows === 0) {
777
			throw new NotFoundException('Comment to update does ceased to exist');
778
		}
779
780
		$this->sendEvent(CommentsEvent::EVENT_UPDATE, $comment);
781
782
		return $affectedRows > 0;
783
	}
784
785
	/**
786
	 * removes references to specific actor (e.g. on user delete) of a comment.
787
	 * The comment itself must not get lost/deleted.
788
	 *
789
	 * @param string $actorType the actor type (e.g. 'users')
790
	 * @param string $actorId a user id
791
	 * @return boolean
792
	 * @since 9.0.0
793
	 */
794
	public function deleteReferencesOfActor($actorType, $actorId) {
795
		$this->checkRoleParameters('Actor', $actorType, $actorId);
796
797
		$qb = $this->dbConn->getQueryBuilder();
798
		$affectedRows = $qb
799
			->update('comments')
800
			->set('actor_type', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
801
			->set('actor_id', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
802
			->where($qb->expr()->eq('actor_type', $qb->createParameter('type')))
803
			->andWhere($qb->expr()->eq('actor_id', $qb->createParameter('id')))
804
			->setParameter('type', $actorType)
805
			->setParameter('id', $actorId)
806
			->execute();
807
808
		$this->commentsCache = [];
809
810
		return is_int($affectedRows);
811
	}
812
813
	/**
814
	 * deletes all comments made of a specific object (e.g. on file delete)
815
	 *
816
	 * @param string $objectType the object type (e.g. 'files')
817
	 * @param string $objectId e.g. the file id
818
	 * @return boolean
819
	 * @since 9.0.0
820
	 */
821
	public function deleteCommentsAtObject($objectType, $objectId) {
822
		$this->checkRoleParameters('Object', $objectType, $objectId);
823
824
		$qb = $this->dbConn->getQueryBuilder();
825
		$affectedRows = $qb
826
			->delete('comments')
827
			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
828
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
829
			->setParameter('type', $objectType)
830
			->setParameter('id', $objectId)
831
			->execute();
832
833
		$this->commentsCache = [];
834
835
		return is_int($affectedRows);
836
	}
837
838
	/**
839
	 * deletes the read markers for the specified user
840
	 *
841
	 * @param \OCP\IUser $user
842
	 * @return bool
843
	 * @since 9.0.0
844
	 */
845
	public function deleteReadMarksFromUser(IUser $user) {
846
		$qb = $this->dbConn->getQueryBuilder();
847
		$query = $qb->delete('comments_read_markers')
848
			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
849
			->setParameter('user_id', $user->getUID());
850
851
		try {
852
			$affectedRows = $query->execute();
853
		} catch (DriverException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\DBAL\Exception\DriverException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
854
			$this->logger->logException($e, ['app' => 'core_comments']);
855
			return false;
856
		}
857
		return ($affectedRows > 0);
858
	}
859
860
	/**
861
	 * sets the read marker for a given file to the specified date for the
862
	 * provided user
863
	 *
864
	 * @param string $objectType
865
	 * @param string $objectId
866
	 * @param \DateTime $dateTime
867
	 * @param IUser $user
868
	 * @since 9.0.0
869
	 * @suppress SqlInjectionChecker
870
	 */
871
	public function setReadMark($objectType, $objectId, \DateTime $dateTime, IUser $user) {
872
		$this->checkRoleParameters('Object', $objectType, $objectId);
873
874
		$qb = $this->dbConn->getQueryBuilder();
875
		$values = [
876
			'user_id' => $qb->createNamedParameter($user->getUID()),
877
			'marker_datetime' => $qb->createNamedParameter($dateTime, 'datetime'),
878
			'object_type' => $qb->createNamedParameter($objectType),
879
			'object_id' => $qb->createNamedParameter($objectId),
880
		];
881
882
		// Strategy: try to update, if this does not return affected rows, do an insert.
883
		$affectedRows = $qb
884
			->update('comments_read_markers')
885
			->set('user_id', $values['user_id'])
886
			->set('marker_datetime', $values['marker_datetime'])
887
			->set('object_type', $values['object_type'])
888
			->set('object_id', $values['object_id'])
889
			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
890
			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
891
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
892
			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
893
			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
894
			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
895
			->execute();
896
897
		if ($affectedRows > 0) {
898
			return;
899
		}
900
901
		$qb->insert('comments_read_markers')
902
			->values($values)
903
			->execute();
904
	}
905
906
	/**
907
	 * returns the read marker for a given file to the specified date for the
908
	 * provided user. It returns null, when the marker is not present, i.e.
909
	 * no comments were marked as read.
910
	 *
911
	 * @param string $objectType
912
	 * @param string $objectId
913
	 * @param IUser $user
914
	 * @return \DateTime|null
915
	 * @since 9.0.0
916
	 */
917
	public function getReadMark($objectType, $objectId, IUser $user) {
918
		$qb = $this->dbConn->getQueryBuilder();
919
		$resultStatement = $qb->select('marker_datetime')
920
			->from('comments_read_markers')
921
			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
922
			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
923
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
924
			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
925
			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
926
			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
927
			->execute();
928
929
		$data = $resultStatement->fetch();
930
		$resultStatement->closeCursor();
931
		if (!$data || is_null($data['marker_datetime'])) {
932
			return null;
933
		}
934
935
		return new \DateTime($data['marker_datetime']);
936
	}
937
938
	/**
939
	 * deletes the read markers on the specified object
940
	 *
941
	 * @param string $objectType
942
	 * @param string $objectId
943
	 * @return bool
944
	 * @since 9.0.0
945
	 */
946
	public function deleteReadMarksOnObject($objectType, $objectId) {
947
		$this->checkRoleParameters('Object', $objectType, $objectId);
948
949
		$qb = $this->dbConn->getQueryBuilder();
950
		$query = $qb->delete('comments_read_markers')
951
			->where($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
952
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
953
			->setParameter('object_type', $objectType)
954
			->setParameter('object_id', $objectId);
955
956
		try {
957
			$affectedRows = $query->execute();
958
		} catch (DriverException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\DBAL\Exception\DriverException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
959
			$this->logger->logException($e, ['app' => 'core_comments']);
960
			return false;
961
		}
962
		return ($affectedRows > 0);
963
	}
964
965
	/**
966
	 * registers an Entity to the manager, so event notifications can be send
967
	 * to consumers of the comments infrastructure
968
	 *
969
	 * @param \Closure $closure
970
	 */
971
	public function registerEventHandler(\Closure $closure) {
972
		$this->eventHandlerClosures[] = $closure;
973
		$this->eventHandlers = [];
974
	}
975
976
	/**
977
	 * registers a method that resolves an ID to a display name for a given type
978
	 *
979
	 * @param string $type
980
	 * @param \Closure $closure
981
	 * @throws \OutOfBoundsException
982
	 * @since 11.0.0
983
	 *
984
	 * Only one resolver shall be registered per type. Otherwise a
985
	 * \OutOfBoundsException has to thrown.
986
	 */
987 View Code Duplication
	public function registerDisplayNameResolver($type, \Closure $closure) {
988
		if (!is_string($type)) {
989
			throw new \InvalidArgumentException('String expected.');
990
		}
991
		if (isset($this->displayNameResolvers[$type])) {
992
			throw new \OutOfBoundsException('Displayname resolver for this type already registered');
993
		}
994
		$this->displayNameResolvers[$type] = $closure;
995
	}
996
997
	/**
998
	 * resolves a given ID of a given Type to a display name.
999
	 *
1000
	 * @param string $type
1001
	 * @param string $id
1002
	 * @return string
1003
	 * @throws \OutOfBoundsException
1004
	 * @since 11.0.0
1005
	 *
1006
	 * If a provided type was not registered, an \OutOfBoundsException shall
1007
	 * be thrown. It is upon the resolver discretion what to return of the
1008
	 * provided ID is unknown. It must be ensured that a string is returned.
1009
	 */
1010 View Code Duplication
	public function resolveDisplayName($type, $id) {
1011
		if (!is_string($type)) {
1012
			throw new \InvalidArgumentException('String expected.');
1013
		}
1014
		if (!isset($this->displayNameResolvers[$type])) {
1015
			throw new \OutOfBoundsException('No Displayname resolver for this type registered');
1016
		}
1017
		return (string)$this->displayNameResolvers[$type]($id);
1018
	}
1019
1020
	/**
1021
	 * returns valid, registered entities
1022
	 *
1023
	 * @return \OCP\Comments\ICommentsEventHandler[]
1024
	 */
1025 View Code Duplication
	private function getEventHandlers() {
1026
		if (!empty($this->eventHandlers)) {
1027
			return $this->eventHandlers;
1028
		}
1029
1030
		$this->eventHandlers = [];
1031
		foreach ($this->eventHandlerClosures as $name => $closure) {
1032
			$entity = $closure();
1033
			if (!($entity instanceof ICommentsEventHandler)) {
1034
				throw new \InvalidArgumentException('The given entity does not implement the ICommentsEntity interface');
1035
			}
1036
			$this->eventHandlers[$name] = $entity;
1037
		}
1038
1039
		return $this->eventHandlers;
1040
	}
1041
1042
	/**
1043
	 * sends notifications to the registered entities
1044
	 *
1045
	 * @param $eventType
1046
	 * @param IComment $comment
1047
	 */
1048
	private function sendEvent($eventType, IComment $comment) {
1049
		$entities = $this->getEventHandlers();
1050
		$event = new CommentsEvent($eventType, $comment);
1051
		foreach ($entities as $entity) {
1052
			$entity->handle($event);
1053
		}
1054
	}
1055
}
1056