Completed
Push — master ( cde7f5...cd12e2 )
by Joas
14:47
created

Manager::deleteReadMarksOnObject()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 14
nc 2
nop 2
dl 0
loc 18
rs 9.4285
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 Thomas Müller <[email protected]>
8
 *
9
 * @license AGPL-3.0
10
 *
11
 * This code is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License, version 3,
13
 * as published by the Free Software Foundation.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License, version 3,
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
22
 *
23
 */
24
namespace OC\Comments;
25
26
use Doctrine\DBAL\Exception\DriverException;
27
use OCP\Comments\CommentsEvent;
28
use OCP\Comments\IComment;
29
use OCP\Comments\ICommentsEventHandler;
30
use OCP\Comments\ICommentsManager;
31
use OCP\Comments\NotFoundException;
32
use OCP\DB\QueryBuilder\IQueryBuilder;
33
use OCP\IDBConnection;
34
use OCP\IConfig;
35
use OCP\ILogger;
36
use OCP\IUser;
37
38
class Manager implements ICommentsManager {
39
40
	/** @var  IDBConnection */
41
	protected $dbConn;
42
43
	/** @var  ILogger */
44
	protected $logger;
45
46
	/** @var IConfig */
47
	protected $config;
48
49
	/** @var IComment[]  */
50
	protected $commentsCache = [];
51
52
	/** @var  \Closure[] */
53
	protected $eventHandlerClosures = [];
54
55
	/** @var  ICommentsEventHandler[] */
56
	protected $eventHandlers = [];
57
58
	/** @var \Closure[] */
59
	protected $displayNameResolvers = [];
60
61
	/**
62
	 * Manager constructor.
63
	 *
64
	 * @param IDBConnection $dbConn
65
	 * @param ILogger $logger
66
	 * @param IConfig $config
67
	 */
68
	public function __construct(
69
		IDBConnection $dbConn,
70
		ILogger $logger,
71
		IConfig $config
72
	) {
73
		$this->dbConn = $dbConn;
74
		$this->logger = $logger;
75
		$this->config = $config;
76
	}
77
78
	/**
79
	 * converts data base data into PHP native, proper types as defined by
80
	 * IComment interface.
81
	 *
82
	 * @param array $data
83
	 * @return array
84
	 */
85
	protected function normalizeDatabaseData(array $data) {
86
		$data['id'] = strval($data['id']);
87
		$data['parent_id'] = strval($data['parent_id']);
88
		$data['topmost_parent_id'] = strval($data['topmost_parent_id']);
89
		$data['creation_timestamp'] = new \DateTime($data['creation_timestamp']);
90 View Code Duplication
		if (!is_null($data['latest_child_timestamp'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
91
			$data['latest_child_timestamp'] = new \DateTime($data['latest_child_timestamp']);
92
		}
93
		$data['children_count'] = intval($data['children_count']);
94
		return $data;
95
	}
96
97
	/**
98
	 * prepares a comment for an insert or update operation after making sure
99
	 * all necessary fields have a value assigned.
100
	 *
101
	 * @param IComment $comment
102
	 * @return IComment returns the same updated IComment instance as provided
103
	 *                  by parameter for convenience
104
	 * @throws \UnexpectedValueException
105
	 */
106
	protected function prepareCommentForDatabaseWrite(IComment $comment) {
107
		if(    !$comment->getActorType()
108
			|| !$comment->getActorId()
109
			|| !$comment->getObjectType()
110
			|| !$comment->getObjectId()
111
			|| !$comment->getVerb()
112
		) {
113
			throw new \UnexpectedValueException('Actor, Object and Verb information must be provided for saving');
114
		}
115
116
		if($comment->getId() === '') {
117
			$comment->setChildrenCount(0);
118
			$comment->setLatestChildDateTime(new \DateTime('0000-00-00 00:00:00', new \DateTimeZone('UTC')));
119
			$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...
120
		}
121
122
		if(is_null($comment->getCreationDateTime())) {
123
			$comment->setCreationDateTime(new \DateTime());
124
		}
125
126
		if($comment->getParentId() !== '0') {
127
			$comment->setTopmostParentId($this->determineTopmostParentId($comment->getParentId()));
128
		} else {
129
			$comment->setTopmostParentId('0');
130
		}
131
132
		$this->cache($comment);
133
134
		return $comment;
135
	}
136
137
	/**
138
	 * returns the topmost parent id of a given comment identified by ID
139
	 *
140
	 * @param string $id
141
	 * @return string
142
	 * @throws NotFoundException
143
	 */
144
	protected function determineTopmostParentId($id) {
145
		$comment = $this->get($id);
146
		if($comment->getParentId() === '0') {
147
			return $comment->getId();
148
		} else {
149
			return $this->determineTopmostParentId($comment->getId());
150
		}
151
	}
152
153
	/**
154
	 * updates child information of a comment
155
	 *
156
	 * @param string	$id
157
	 * @param \DateTime	$cDateTime	the date time of the most recent child
158
	 * @throws NotFoundException
159
	 */
160
	protected function updateChildrenInformation($id, \DateTime $cDateTime) {
161
		$qb = $this->dbConn->getQueryBuilder();
162
		$query = $qb->select($qb->createFunction('COUNT(`id`)'))
163
				->from('comments')
164
				->where($qb->expr()->eq('parent_id', $qb->createParameter('id')))
165
				->setParameter('id', $id);
166
167
		$resultStatement = $query->execute();
168
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
169
		$resultStatement->closeCursor();
170
		$children = intval($data[0]);
171
172
		$comment = $this->get($id);
173
		$comment->setChildrenCount($children);
174
		$comment->setLatestChildDateTime($cDateTime);
175
		$this->save($comment);
176
	}
177
178
	/**
179
	 * Tests whether actor or object type and id parameters are acceptable.
180
	 * Throws exception if not.
181
	 *
182
	 * @param string $role
183
	 * @param string $type
184
	 * @param string $id
185
	 * @throws \InvalidArgumentException
186
	 */
187
	protected function checkRoleParameters($role, $type, $id) {
188
		if(
189
			   !is_string($type) || empty($type)
190
			|| !is_string($id) || empty($id)
191
		) {
192
			throw new \InvalidArgumentException($role . ' parameters must be string and not empty');
193
		}
194
	}
195
196
	/**
197
	 * run-time caches a comment
198
	 *
199
	 * @param IComment $comment
200
	 */
201
	protected function cache(IComment $comment) {
202
		$id = $comment->getId();
203
		if(empty($id)) {
204
			return;
205
		}
206
		$this->commentsCache[strval($id)] = $comment;
207
	}
208
209
	/**
210
	 * removes an entry from the comments run time cache
211
	 *
212
	 * @param mixed $id the comment's id
213
	 */
214
	protected function uncache($id) {
215
		$id = strval($id);
216
		if (isset($this->commentsCache[$id])) {
217
			unset($this->commentsCache[$id]);
218
		}
219
	}
220
221
	/**
222
	 * returns a comment instance
223
	 *
224
	 * @param string $id the ID of the comment
225
	 * @return IComment
226
	 * @throws NotFoundException
227
	 * @throws \InvalidArgumentException
228
	 * @since 9.0.0
229
	 */
230
	public function get($id) {
231
		if(intval($id) === 0) {
232
			throw new \InvalidArgumentException('IDs must be translatable to a number in this implementation.');
233
		}
234
235
		if(isset($this->commentsCache[$id])) {
236
			return $this->commentsCache[$id];
237
		}
238
239
		$qb = $this->dbConn->getQueryBuilder();
240
		$resultStatement = $qb->select('*')
241
			->from('comments')
242
			->where($qb->expr()->eq('id', $qb->createParameter('id')))
243
			->setParameter('id', $id, IQueryBuilder::PARAM_INT)
244
			->execute();
245
246
		$data = $resultStatement->fetch();
247
		$resultStatement->closeCursor();
248
		if(!$data) {
249
			throw new NotFoundException();
250
		}
251
252
		$comment = new Comment($this->normalizeDatabaseData($data));
253
		$this->cache($comment);
254
		return $comment;
255
	}
256
257
	/**
258
	 * returns the comment specified by the id and all it's child comments.
259
	 * At this point of time, we do only support one level depth.
260
	 *
261
	 * @param string $id
262
	 * @param int $limit max number of entries to return, 0 returns all
263
	 * @param int $offset the start entry
264
	 * @return array
265
	 * @since 9.0.0
266
	 *
267
	 * The return array looks like this
268
	 * [
269
	 *   'comment' => IComment, // root comment
270
	 *   'replies' =>
271
	 *   [
272
	 *     0 =>
273
	 *     [
274
	 *       'comment' => IComment,
275
	 *       'replies' => []
276
	 *     ]
277
	 *     1 =>
278
	 *     [
279
	 *       'comment' => IComment,
280
	 *       'replies'=> []
281
	 *     ],
282
	 *     …
283
	 *   ]
284
	 * ]
285
	 */
286
	public function getTree($id, $limit = 0, $offset = 0) {
287
		$tree = [];
288
		$tree['comment'] = $this->get($id);
289
		$tree['replies'] = [];
290
291
		$qb = $this->dbConn->getQueryBuilder();
292
		$query = $qb->select('*')
293
				->from('comments')
294
				->where($qb->expr()->eq('topmost_parent_id', $qb->createParameter('id')))
295
				->orderBy('creation_timestamp', 'DESC')
296
				->setParameter('id', $id);
297
298
		if($limit > 0) {
299
			$query->setMaxResults($limit);
300
		}
301
		if($offset > 0) {
302
			$query->setFirstResult($offset);
303
		}
304
305
		$resultStatement = $query->execute();
306
		while($data = $resultStatement->fetch()) {
307
			$comment = new Comment($this->normalizeDatabaseData($data));
308
			$this->cache($comment);
309
			$tree['replies'][] = [
310
				'comment' => $comment,
311
				'replies' => []
312
			];
313
		}
314
		$resultStatement->closeCursor();
315
316
		return $tree;
317
	}
318
319
	/**
320
	 * returns comments for a specific object (e.g. a file).
321
	 *
322
	 * The sort order is always newest to oldest.
323
	 *
324
	 * @param string $objectType the object type, e.g. 'files'
325
	 * @param string $objectId the id of the object
326
	 * @param int $limit optional, number of maximum comments to be returned. if
327
	 * not specified, all comments are returned.
328
	 * @param int $offset optional, starting point
329
	 * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
330
	 * that may be returned
331
	 * @return IComment[]
332
	 * @since 9.0.0
333
	 */
334
	public function getForObject(
335
			$objectType,
336
			$objectId,
337
			$limit = 0,
338
			$offset = 0,
339
			\DateTime $notOlderThan = null
340
	) {
341
		$comments = [];
342
343
		$qb = $this->dbConn->getQueryBuilder();
344
		$query = $qb->select('*')
345
				->from('comments')
346
				->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
347
				->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
348
				->orderBy('creation_timestamp', 'DESC')
349
				->setParameter('type', $objectType)
350
				->setParameter('id', $objectId);
351
352
		if($limit > 0) {
353
			$query->setMaxResults($limit);
354
		}
355
		if($offset > 0) {
356
			$query->setFirstResult($offset);
357
		}
358 View Code Duplication
		if(!is_null($notOlderThan)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
359
			$query
360
				->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
361
				->setParameter('notOlderThan', $notOlderThan, 'datetime');
362
		}
363
364
		$resultStatement = $query->execute();
365
		while($data = $resultStatement->fetch()) {
366
			$comment = new Comment($this->normalizeDatabaseData($data));
367
			$this->cache($comment);
368
			$comments[] = $comment;
369
		}
370
		$resultStatement->closeCursor();
371
372
		return $comments;
373
	}
374
375
	/**
376
	 * @param $objectType string the object type, e.g. 'files'
377
	 * @param $objectId string the id of the object
378
	 * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
379
	 * that may be returned
380
	 * @return Int
381
	 * @since 9.0.0
382
	 */
383
	public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null) {
384
		$qb = $this->dbConn->getQueryBuilder();
385
		$query = $qb->select($qb->createFunction('COUNT(`id`)'))
386
				->from('comments')
387
				->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
388
				->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
389
				->setParameter('type', $objectType)
390
				->setParameter('id', $objectId);
391
392 View Code Duplication
		if(!is_null($notOlderThan)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
393
			$query
394
				->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
395
				->setParameter('notOlderThan', $notOlderThan, 'datetime');
396
		}
397
398
		$resultStatement = $query->execute();
399
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
400
		$resultStatement->closeCursor();
401
		return intval($data[0]);
402
	}
403
404
	/**
405
	 * creates a new comment and returns it. At this point of time, it is not
406
	 * saved in the used data storage. Use save() after setting other fields
407
	 * of the comment (e.g. message or verb).
408
	 *
409
	 * @param string $actorType the actor type (e.g. 'users')
410
	 * @param string $actorId a user id
411
	 * @param string $objectType the object type the comment is attached to
412
	 * @param string $objectId the object id the comment is attached to
413
	 * @return IComment
414
	 * @since 9.0.0
415
	 */
416
	public function create($actorType, $actorId, $objectType, $objectId) {
417
		$comment = new Comment();
418
		$comment
419
			->setActor($actorType, $actorId)
420
			->setObject($objectType, $objectId);
421
		return $comment;
422
	}
423
424
	/**
425
	 * permanently deletes the comment specified by the ID
426
	 *
427
	 * When the comment has child comments, their parent ID will be changed to
428
	 * the parent ID of the item that is to be deleted.
429
	 *
430
	 * @param string $id
431
	 * @return bool
432
	 * @throws \InvalidArgumentException
433
	 * @since 9.0.0
434
	 */
435
	public function delete($id) {
436
		if(!is_string($id)) {
437
			throw new \InvalidArgumentException('Parameter must be string');
438
		}
439
440
		try {
441
			$comment = $this->get($id);
442
		} catch (\Exception $e) {
443
			// Ignore exceptions, we just don't fire a hook then
444
			$comment = null;
445
		}
446
447
		$qb = $this->dbConn->getQueryBuilder();
448
		$query = $qb->delete('comments')
449
			->where($qb->expr()->eq('id', $qb->createParameter('id')))
450
			->setParameter('id', $id);
451
452
		try {
453
			$affectedRows = $query->execute();
454
			$this->uncache($id);
455
		} 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...
456
			$this->logger->logException($e, ['app' => 'core_comments']);
457
			return false;
458
		}
459
460
		if ($affectedRows > 0 && $comment instanceof IComment) {
461
			$this->sendEvent(CommentsEvent::EVENT_DELETE, $comment);
462
		}
463
464
		return ($affectedRows > 0);
465
	}
466
467
	/**
468
	 * saves the comment permanently
469
	 *
470
	 * if the supplied comment has an empty ID, a new entry comment will be
471
	 * saved and the instance updated with the new ID.
472
	 *
473
	 * Otherwise, an existing comment will be updated.
474
	 *
475
	 * Throws NotFoundException when a comment that is to be updated does not
476
	 * exist anymore at this point of time.
477
	 *
478
	 * @param IComment $comment
479
	 * @return bool
480
	 * @throws NotFoundException
481
	 * @since 9.0.0
482
	 */
483
	public function save(IComment $comment) {
484
		if($this->prepareCommentForDatabaseWrite($comment)->getId() === '') {
485
			$result = $this->insert($comment);
486
		} else {
487
			$result = $this->update($comment);
488
		}
489
490
		if($result && !!$comment->getParentId()) {
491
			$this->updateChildrenInformation(
492
					$comment->getParentId(),
493
					$comment->getCreationDateTime()
494
			);
495
			$this->cache($comment);
496
		}
497
498
		return $result;
499
	}
500
501
	/**
502
	 * inserts the provided comment in the database
503
	 *
504
	 * @param IComment $comment
505
	 * @return bool
506
	 */
507
	protected function insert(IComment &$comment) {
508
		$qb = $this->dbConn->getQueryBuilder();
509
		$affectedRows = $qb
510
			->insert('comments')
511
			->values([
512
				'parent_id'					=> $qb->createNamedParameter($comment->getParentId()),
513
				'topmost_parent_id' 		=> $qb->createNamedParameter($comment->getTopmostParentId()),
514
				'children_count' 			=> $qb->createNamedParameter($comment->getChildrenCount()),
515
				'actor_type' 				=> $qb->createNamedParameter($comment->getActorType()),
516
				'actor_id' 					=> $qb->createNamedParameter($comment->getActorId()),
517
				'message' 					=> $qb->createNamedParameter($comment->getMessage()),
518
				'verb' 						=> $qb->createNamedParameter($comment->getVerb()),
519
				'creation_timestamp' 		=> $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'),
520
				'latest_child_timestamp'	=> $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'),
521
				'object_type' 				=> $qb->createNamedParameter($comment->getObjectType()),
522
				'object_id' 				=> $qb->createNamedParameter($comment->getObjectId()),
523
			])
524
			->execute();
525
526
		if ($affectedRows > 0) {
527
			$comment->setId(strval($qb->getLastInsertId()));
528
			$this->sendEvent(CommentsEvent::EVENT_ADD, $comment);
529
		}
530
531
		return $affectedRows > 0;
532
	}
533
534
	/**
535
	 * updates a Comment data row
536
	 *
537
	 * @param IComment $comment
538
	 * @return bool
539
	 * @throws NotFoundException
540
	 */
541
	protected function update(IComment $comment) {
542
		// for properly working preUpdate Events we need the old comments as is
543
		// in the DB and overcome caching. Also avoid that outdated information stays.
544
		$this->uncache($comment->getId());
545
		$this->sendEvent(CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId()));
546
		$this->uncache($comment->getId());
547
548
		$qb = $this->dbConn->getQueryBuilder();
549
		$affectedRows = $qb
550
			->update('comments')
551
				->set('parent_id',				$qb->createNamedParameter($comment->getParentId()))
552
				->set('topmost_parent_id', 		$qb->createNamedParameter($comment->getTopmostParentId()))
553
				->set('children_count',			$qb->createNamedParameter($comment->getChildrenCount()))
554
				->set('actor_type', 			$qb->createNamedParameter($comment->getActorType()))
555
				->set('actor_id', 				$qb->createNamedParameter($comment->getActorId()))
556
				->set('message',				$qb->createNamedParameter($comment->getMessage()))
557
				->set('verb',					$qb->createNamedParameter($comment->getVerb()))
558
				->set('creation_timestamp',		$qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'))
559
				->set('latest_child_timestamp',	$qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'))
560
				->set('object_type',			$qb->createNamedParameter($comment->getObjectType()))
561
				->set('object_id',				$qb->createNamedParameter($comment->getObjectId()))
562
			->where($qb->expr()->eq('id', $qb->createParameter('id')))
563
			->setParameter('id', $comment->getId())
564
			->execute();
565
566
		if($affectedRows === 0) {
567
			throw new NotFoundException('Comment to update does ceased to exist');
568
		}
569
570
		$this->sendEvent(CommentsEvent::EVENT_UPDATE, $comment);
571
572
		return $affectedRows > 0;
573
	}
574
575
	/**
576
	 * removes references to specific actor (e.g. on user delete) of a comment.
577
	 * The comment itself must not get lost/deleted.
578
	 *
579
	 * @param string $actorType the actor type (e.g. 'users')
580
	 * @param string $actorId a user id
581
	 * @return boolean
582
	 * @since 9.0.0
583
	 */
584
	public function deleteReferencesOfActor($actorType, $actorId) {
585
		$this->checkRoleParameters('Actor', $actorType, $actorId);
586
587
		$qb = $this->dbConn->getQueryBuilder();
588
		$affectedRows = $qb
589
			->update('comments')
590
			->set('actor_type',	$qb->createNamedParameter(ICommentsManager::DELETED_USER))
591
			->set('actor_id',	$qb->createNamedParameter(ICommentsManager::DELETED_USER))
592
			->where($qb->expr()->eq('actor_type', $qb->createParameter('type')))
593
			->andWhere($qb->expr()->eq('actor_id', $qb->createParameter('id')))
594
			->setParameter('type', $actorType)
595
			->setParameter('id', $actorId)
596
			->execute();
597
598
		$this->commentsCache = [];
599
600
		return is_int($affectedRows);
601
	}
602
603
	/**
604
	 * deletes all comments made of a specific object (e.g. on file delete)
605
	 *
606
	 * @param string $objectType the object type (e.g. 'files')
607
	 * @param string $objectId e.g. the file id
608
	 * @return boolean
609
	 * @since 9.0.0
610
	 */
611
	public function deleteCommentsAtObject($objectType, $objectId) {
612
		$this->checkRoleParameters('Object', $objectType, $objectId);
613
614
		$qb = $this->dbConn->getQueryBuilder();
615
		$affectedRows = $qb
616
			->delete('comments')
617
			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
618
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
619
			->setParameter('type', $objectType)
620
			->setParameter('id', $objectId)
621
			->execute();
622
623
		$this->commentsCache = [];
624
625
		return is_int($affectedRows);
626
	}
627
628
	/**
629
	 * deletes the read markers for the specified user
630
	 *
631
	 * @param \OCP\IUser $user
632
	 * @return bool
633
	 * @since 9.0.0
634
	 */
635
	public function deleteReadMarksFromUser(IUser $user) {
636
		$qb = $this->dbConn->getQueryBuilder();
637
		$query = $qb->delete('comments_read_markers')
638
			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
639
			->setParameter('user_id', $user->getUID());
640
641
		try {
642
			$affectedRows = $query->execute();
643
		} 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...
644
			$this->logger->logException($e, ['app' => 'core_comments']);
645
			return false;
646
		}
647
		return ($affectedRows > 0);
648
	}
649
650
	/**
651
	 * sets the read marker for a given file to the specified date for the
652
	 * provided user
653
	 *
654
	 * @param string $objectType
655
	 * @param string $objectId
656
	 * @param \DateTime $dateTime
657
	 * @param IUser $user
658
	 * @since 9.0.0
659
	 */
660
	public function setReadMark($objectType, $objectId, \DateTime $dateTime, IUser $user) {
661
		$this->checkRoleParameters('Object', $objectType, $objectId);
662
663
		$qb = $this->dbConn->getQueryBuilder();
664
		$values = [
665
			'user_id'         => $qb->createNamedParameter($user->getUID()),
666
			'marker_datetime' => $qb->createNamedParameter($dateTime, 'datetime'),
667
			'object_type'     => $qb->createNamedParameter($objectType),
668
			'object_id'       => $qb->createNamedParameter($objectId),
669
		];
670
671
		// Strategy: try to update, if this does not return affected rows, do an insert.
672
		$affectedRows = $qb
673
			->update('comments_read_markers')
674
			->set('user_id',         $values['user_id'])
675
			->set('marker_datetime', $values['marker_datetime'])
676
			->set('object_type',     $values['object_type'])
677
			->set('object_id',       $values['object_id'])
678
			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
679
			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
680
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
681
			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
682
			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
683
			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
684
			->execute();
685
686
		if ($affectedRows > 0) {
687
			return;
688
		}
689
690
		$qb->insert('comments_read_markers')
691
			->values($values)
692
			->execute();
693
	}
694
695
	/**
696
	 * returns the read marker for a given file to the specified date for the
697
	 * provided user. It returns null, when the marker is not present, i.e.
698
	 * no comments were marked as read.
699
	 *
700
	 * @param string $objectType
701
	 * @param string $objectId
702
	 * @param IUser $user
703
	 * @return \DateTime|null
704
	 * @since 9.0.0
705
	 */
706
	public function getReadMark($objectType, $objectId, IUser $user) {
707
		$qb = $this->dbConn->getQueryBuilder();
708
		$resultStatement = $qb->select('marker_datetime')
709
			->from('comments_read_markers')
710
			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
711
			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
712
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
713
			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
714
			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
715
			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
716
			->execute();
717
718
		$data = $resultStatement->fetch();
719
		$resultStatement->closeCursor();
720
		if(!$data || is_null($data['marker_datetime'])) {
721
			return null;
722
		}
723
724
		return new \DateTime($data['marker_datetime']);
725
	}
726
727
	/**
728
	 * deletes the read markers on the specified object
729
	 *
730
	 * @param string $objectType
731
	 * @param string $objectId
732
	 * @return bool
733
	 * @since 9.0.0
734
	 */
735
	public function deleteReadMarksOnObject($objectType, $objectId) {
736
		$this->checkRoleParameters('Object', $objectType, $objectId);
737
738
		$qb = $this->dbConn->getQueryBuilder();
739
		$query = $qb->delete('comments_read_markers')
740
			->where($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
741
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
742
			->setParameter('object_type', $objectType)
743
			->setParameter('object_id', $objectId);
744
745
		try {
746
			$affectedRows = $query->execute();
747
		} 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...
748
			$this->logger->logException($e, ['app' => 'core_comments']);
749
			return false;
750
		}
751
		return ($affectedRows > 0);
752
	}
753
754
	/**
755
	 * registers an Entity to the manager, so event notifications can be send
756
	 * to consumers of the comments infrastructure
757
	 *
758
	 * @param \Closure $closure
759
	 */
760
	public function registerEventHandler(\Closure $closure) {
761
		$this->eventHandlerClosures[] = $closure;
762
		$this->eventHandlers = [];
763
	}
764
765
	/**
766
	 * registers a method that resolves an ID to a display name for a given type
767
	 *
768
	 * @param string $type
769
	 * @param \Closure $closure
770
	 * @throws \OutOfBoundsException
771
	 * @since 9.2.0
772
	 *
773
	 * Only one resolver shall be registered per type. Otherwise a
774
	 * \OutOfBoundsException has to thrown.
775
	 */
776 View Code Duplication
	public function registerDisplayNameResolver($type, \Closure $closure) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
777
		if(!is_string($type)) {
778
			throw new \InvalidArgumentException('String expected.');
779
		}
780
		if(isset($this->displayNameResolvers[$type])) {
781
			throw new \OutOfBoundsException('Displayname resolver for this type already registered');
782
		}
783
		$this->displayNameResolvers[$type] = $closure;
784
	}
785
786
	/**
787
	 * resolves a given ID of a given Type to a display name.
788
	 *
789
	 * @param string $type
790
	 * @param string $id
791
	 * @return string
792
	 * @throws \OutOfBoundsException
793
	 * @since 9.2.0
794
	 *
795
	 * If a provided type was not registered, an \OutOfBoundsException shall
796
	 * be thrown. It is upon the resolver discretion what to return of the
797
	 * provided ID is unknown. It must be ensured that a string is returned.
798
	 */
799 View Code Duplication
	public function resolveDisplayName($type, $id) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
800
		if(!is_string($type)) {
801
			throw new \InvalidArgumentException('String expected.');
802
		}
803
		if(!isset($this->displayNameResolvers[$type])) {
804
			throw new \OutOfBoundsException('No Displayname resolver for this type registered');
805
		}
806
		return (string)$this->displayNameResolvers[$type]($id);
807
	}
808
809
	/**
810
	 * returns valid, registered entities
811
	 *
812
	 * @return \OCP\Comments\ICommentsEventHandler[]
813
	 */
814 View Code Duplication
	private function getEventHandlers() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
815
		if(!empty($this->eventHandlers)) {
816
			return $this->eventHandlers;
817
		}
818
819
		$this->eventHandlers = [];
820
		foreach ($this->eventHandlerClosures as $name => $closure) {
821
			$entity = $closure();
822
			if (!($entity instanceof ICommentsEventHandler)) {
823
				throw new \InvalidArgumentException('The given entity does not implement the ICommentsEntity interface');
824
			}
825
			$this->eventHandlers[$name] = $entity;
826
		}
827
828
		return $this->eventHandlers;
829
	}
830
831
	/**
832
	 * sends notifications to the registered entities
833
	 *
834
	 * @param $eventType
835
	 * @param IComment $comment
836
	 */
837
	private function sendEvent($eventType, IComment $comment) {
838
		$entities = $this->getEventHandlers();
839
		$event = new CommentsEvent($eventType, $comment);
840
		foreach ($entities as $entity) {
841
			$entity->handle($event);
842
		}
843
	}
844
}
845