Completed
Push — master ( ca3aef...5b2d6b )
by Morris
15:49
created

Manager::delete()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 20
nc 9
nop 1
dl 0
loc 31
rs 8.439
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
25
namespace OC\Comments;
26
27
use Doctrine\DBAL\Exception\DriverException;
28
use OCP\Comments\CommentsEvent;
29
use OCP\Comments\IComment;
30
use OCP\Comments\ICommentsEventHandler;
31
use OCP\Comments\ICommentsManager;
32
use OCP\Comments\NotFoundException;
33
use OCP\DB\QueryBuilder\IQueryBuilder;
34
use OCP\IDBConnection;
35
use OCP\IConfig;
36
use OCP\ILogger;
37
use OCP\IUser;
38
39
class Manager implements ICommentsManager {
40
41
	/** @var  IDBConnection */
42
	protected $dbConn;
43
44
	/** @var  ILogger */
45
	protected $logger;
46
47
	/** @var IConfig */
48
	protected $config;
49
50
	/** @var IComment[] */
51
	protected $commentsCache = [];
52
53
	/** @var  \Closure[] */
54
	protected $eventHandlerClosures = [];
55
56
	/** @var  ICommentsEventHandler[] */
57
	protected $eventHandlers = [];
58
59
	/** @var \Closure[] */
60
	protected $displayNameResolvers = [];
61
62
	/**
63
	 * Manager constructor.
64
	 *
65
	 * @param IDBConnection $dbConn
66
	 * @param ILogger $logger
67
	 * @param IConfig $config
68
	 */
69
	public function __construct(
70
		IDBConnection $dbConn,
71
		ILogger $logger,
72
		IConfig $config
73
	) {
74
		$this->dbConn = $dbConn;
75
		$this->logger = $logger;
76
		$this->config = $config;
77
	}
78
79
	/**
80
	 * converts data base data into PHP native, proper types as defined by
81
	 * IComment interface.
82
	 *
83
	 * @param array $data
84
	 * @return array
85
	 */
86
	protected function normalizeDatabaseData(array $data) {
87
		$data['id'] = strval($data['id']);
88
		$data['parent_id'] = strval($data['parent_id']);
89
		$data['topmost_parent_id'] = strval($data['topmost_parent_id']);
90
		$data['creation_timestamp'] = new \DateTime($data['creation_timestamp']);
91 View Code Duplication
		if (!is_null($data['latest_child_timestamp'])) {
92
			$data['latest_child_timestamp'] = new \DateTime($data['latest_child_timestamp']);
93
		}
94
		$data['children_count'] = intval($data['children_count']);
95
		return $data;
96
	}
97
98
	/**
99
	 * prepares a comment for an insert or update operation after making sure
100
	 * all necessary fields have a value assigned.
101
	 *
102
	 * @param IComment $comment
103
	 * @return IComment returns the same updated IComment instance as provided
104
	 *                  by parameter for convenience
105
	 * @throws \UnexpectedValueException
106
	 */
107
	protected function prepareCommentForDatabaseWrite(IComment $comment) {
108
		if (!$comment->getActorType()
109
			|| !$comment->getActorId()
110
			|| !$comment->getObjectType()
111
			|| !$comment->getObjectId()
112
			|| !$comment->getVerb()
113
		) {
114
			throw new \UnexpectedValueException('Actor, Object and Verb information must be provided for saving');
115
		}
116
117
		if ($comment->getId() === '') {
118
			$comment->setChildrenCount(0);
119
			$comment->setLatestChildDateTime(new \DateTime('0000-00-00 00:00:00', new \DateTimeZone('UTC')));
120
			$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...
121
		}
122
123
		if (is_null($comment->getCreationDateTime())) {
124
			$comment->setCreationDateTime(new \DateTime());
125
		}
126
127
		if ($comment->getParentId() !== '0') {
128
			$comment->setTopmostParentId($this->determineTopmostParentId($comment->getParentId()));
129
		} else {
130
			$comment->setTopmostParentId('0');
131
		}
132
133
		$this->cache($comment);
134
135
		return $comment;
136
	}
137
138
	/**
139
	 * returns the topmost parent id of a given comment identified by ID
140
	 *
141
	 * @param string $id
142
	 * @return string
143
	 * @throws NotFoundException
144
	 */
145
	protected function determineTopmostParentId($id) {
146
		$comment = $this->get($id);
147
		if ($comment->getParentId() === '0') {
148
			return $comment->getId();
149
		} else {
150
			return $this->determineTopmostParentId($comment->getId());
151
		}
152
	}
153
154
	/**
155
	 * updates child information of a comment
156
	 *
157
	 * @param string $id
158
	 * @param \DateTime $cDateTime the date time of the most recent child
159
	 * @throws NotFoundException
160
	 */
161
	protected function updateChildrenInformation($id, \DateTime $cDateTime) {
162
		$qb = $this->dbConn->getQueryBuilder();
163
		$query = $qb->select($qb->createFunction('COUNT(`id`)'))
164
			->from('comments')
165
			->where($qb->expr()->eq('parent_id', $qb->createParameter('id')))
166
			->setParameter('id', $id);
167
168
		$resultStatement = $query->execute();
169
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
170
		$resultStatement->closeCursor();
171
		$children = intval($data[0]);
172
173
		$comment = $this->get($id);
174
		$comment->setChildrenCount($children);
175
		$comment->setLatestChildDateTime($cDateTime);
176
		$this->save($comment);
177
	}
178
179
	/**
180
	 * Tests whether actor or object type and id parameters are acceptable.
181
	 * Throws exception if not.
182
	 *
183
	 * @param string $role
184
	 * @param string $type
185
	 * @param string $id
186
	 * @throws \InvalidArgumentException
187
	 */
188
	protected function checkRoleParameters($role, $type, $id) {
189
		if (
190
			!is_string($type) || empty($type)
191
			|| !is_string($id) || empty($id)
192
		) {
193
			throw new \InvalidArgumentException($role . ' parameters must be string and not empty');
194
		}
195
	}
196
197
	/**
198
	 * run-time caches a comment
199
	 *
200
	 * @param IComment $comment
201
	 */
202
	protected function cache(IComment $comment) {
203
		$id = $comment->getId();
204
		if (empty($id)) {
205
			return;
206
		}
207
		$this->commentsCache[strval($id)] = $comment;
208
	}
209
210
	/**
211
	 * removes an entry from the comments run time cache
212
	 *
213
	 * @param mixed $id the comment's id
214
	 */
215
	protected function uncache($id) {
216
		$id = strval($id);
217
		if (isset($this->commentsCache[$id])) {
218
			unset($this->commentsCache[$id]);
219
		}
220
	}
221
222
	/**
223
	 * returns a comment instance
224
	 *
225
	 * @param string $id the ID of the comment
226
	 * @return IComment
227
	 * @throws NotFoundException
228
	 * @throws \InvalidArgumentException
229
	 * @since 9.0.0
230
	 */
231
	public function get($id) {
232
		if (intval($id) === 0) {
233
			throw new \InvalidArgumentException('IDs must be translatable to a number in this implementation.');
234
		}
235
236
		if (isset($this->commentsCache[$id])) {
237
			return $this->commentsCache[$id];
238
		}
239
240
		$qb = $this->dbConn->getQueryBuilder();
241
		$resultStatement = $qb->select('*')
242
			->from('comments')
243
			->where($qb->expr()->eq('id', $qb->createParameter('id')))
244
			->setParameter('id', $id, IQueryBuilder::PARAM_INT)
245
			->execute();
246
247
		$data = $resultStatement->fetch();
248
		$resultStatement->closeCursor();
249
		if (!$data) {
250
			throw new NotFoundException();
251
		}
252
253
		$comment = new Comment($this->normalizeDatabaseData($data));
254
		$this->cache($comment);
255
		return $comment;
256
	}
257
258
	/**
259
	 * returns the comment specified by the id and all it's child comments.
260
	 * At this point of time, we do only support one level depth.
261
	 *
262
	 * @param string $id
263
	 * @param int $limit max number of entries to return, 0 returns all
264
	 * @param int $offset the start entry
265
	 * @return array
266
	 * @since 9.0.0
267
	 *
268
	 * The return array looks like this
269
	 * [
270
	 *   'comment' => IComment, // root comment
271
	 *   'replies' =>
272
	 *   [
273
	 *     0 =>
274
	 *     [
275
	 *       'comment' => IComment,
276
	 *       'replies' => []
277
	 *     ]
278
	 *     1 =>
279
	 *     [
280
	 *       'comment' => IComment,
281
	 *       'replies'=> []
282
	 *     ],
283
	 *     …
284
	 *   ]
285
	 * ]
286
	 */
287
	public function getTree($id, $limit = 0, $offset = 0) {
288
		$tree = [];
289
		$tree['comment'] = $this->get($id);
290
		$tree['replies'] = [];
291
292
		$qb = $this->dbConn->getQueryBuilder();
293
		$query = $qb->select('*')
294
			->from('comments')
295
			->where($qb->expr()->eq('topmost_parent_id', $qb->createParameter('id')))
296
			->orderBy('creation_timestamp', 'DESC')
297
			->setParameter('id', $id);
298
299
		if ($limit > 0) {
300
			$query->setMaxResults($limit);
301
		}
302
		if ($offset > 0) {
303
			$query->setFirstResult($offset);
304
		}
305
306
		$resultStatement = $query->execute();
307
		while ($data = $resultStatement->fetch()) {
308
			$comment = new Comment($this->normalizeDatabaseData($data));
309
			$this->cache($comment);
310
			$tree['replies'][] = [
311
				'comment' => $comment,
312
				'replies' => []
313
			];
314
		}
315
		$resultStatement->closeCursor();
316
317
		return $tree;
318
	}
319
320
	/**
321
	 * returns comments for a specific object (e.g. a file).
322
	 *
323
	 * The sort order is always newest to oldest.
324
	 *
325
	 * @param string $objectType the object type, e.g. 'files'
326
	 * @param string $objectId the id of the object
327
	 * @param int $limit optional, number of maximum comments to be returned. if
328
	 * not specified, all comments are returned.
329
	 * @param int $offset optional, starting point
330
	 * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
331
	 * that may be returned
332
	 * @return IComment[]
333
	 * @since 9.0.0
334
	 */
335
	public function getForObject(
336
		$objectType,
337
		$objectId,
338
		$limit = 0,
339
		$offset = 0,
340
		\DateTime $notOlderThan = null
341
	) {
342
		$comments = [];
343
344
		$qb = $this->dbConn->getQueryBuilder();
345
		$query = $qb->select('*')
346
			->from('comments')
347
			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
348
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
349
			->orderBy('creation_timestamp', 'DESC')
350
			->setParameter('type', $objectType)
351
			->setParameter('id', $objectId);
352
353
		if ($limit > 0) {
354
			$query->setMaxResults($limit);
355
		}
356
		if ($offset > 0) {
357
			$query->setFirstResult($offset);
358
		}
359 View Code Duplication
		if (!is_null($notOlderThan)) {
360
			$query
361
				->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
362
				->setParameter('notOlderThan', $notOlderThan, 'datetime');
363
		}
364
365
		$resultStatement = $query->execute();
366
		while ($data = $resultStatement->fetch()) {
367
			$comment = new Comment($this->normalizeDatabaseData($data));
368
			$this->cache($comment);
369
			$comments[] = $comment;
370
		}
371
		$resultStatement->closeCursor();
372
373
		return $comments;
374
	}
375
376
	/**
377
	 * @param $objectType string the object type, e.g. 'files'
378
	 * @param $objectId string the id of the object
379
	 * @param \DateTime $notOlderThan optional, timestamp of the oldest comments
380
	 * that may be returned
381
	 * @return Int
382
	 * @since 9.0.0
383
	 */
384
	public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null) {
385
		$qb = $this->dbConn->getQueryBuilder();
386
		$query = $qb->select($qb->createFunction('COUNT(`id`)'))
387
			->from('comments')
388
			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
389
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
390
			->setParameter('type', $objectType)
391
			->setParameter('id', $objectId);
392
393 View Code Duplication
		if (!is_null($notOlderThan)) {
394
			$query
395
				->andWhere($qb->expr()->gt('creation_timestamp', $qb->createParameter('notOlderThan')))
396
				->setParameter('notOlderThan', $notOlderThan, 'datetime');
397
		}
398
399
		$resultStatement = $query->execute();
400
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
401
		$resultStatement->closeCursor();
402
		return intval($data[0]);
403
	}
404
405
	/**
406
	 * Get the number of unread comments for all files in a folder
407
	 *
408
	 * @param int $folderId
409
	 * @param IUser $user
410
	 * @return array [$fileId => $unreadCount]
411
	 */
412
	public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user) {
413
		$qb = $this->dbConn->getQueryBuilder();
414
		$query = $qb->select('f.fileid')
415
			->selectAlias(
416
				$qb->createFunction('COUNT(' . $qb->getColumnName('c.id') . ')'),
417
				'num_ids'
418
			)
419
			->from('comments', 'c')
420
			->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...
421
				$qb->expr()->eq('c.object_type', $qb->createNamedParameter('files')),
422
				$qb->expr()->eq('f.fileid', $qb->expr()->castColumn('c.object_id', IQueryBuilder::PARAM_INT))
423
			))
424
			->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...
425
				$qb->expr()->eq('m.object_type', $qb->createNamedParameter('files')),
426
				$qb->expr()->eq('m.object_id', 'c.object_id'),
427
				$qb->expr()->eq('m.user_id', $qb->createNamedParameter($user->getUID()))
428
			))
429
			->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($folderId)))
430
			->andWhere($qb->expr()->orX(
431
				$qb->expr()->gt('c.creation_timestamp', 'marker_datetime'),
432
				$qb->expr()->isNull('marker_datetime')
433
			))
434
			->groupBy('f.fileid');
435
436
		$resultStatement = $query->execute();
437
438
		$results = [];
439
		while ($row = $resultStatement->fetch()) {
440
			$results[$row['fileid']] = (int) $row['num_ids'];
441
		}
442
		$resultStatement->closeCursor();
443
		return $results;
444
	}
445
446
	/**
447
	 * creates a new comment and returns it. At this point of time, it is not
448
	 * saved in the used data storage. Use save() after setting other fields
449
	 * of the comment (e.g. message or verb).
450
	 *
451
	 * @param string $actorType the actor type (e.g. 'users')
452
	 * @param string $actorId a user id
453
	 * @param string $objectType the object type the comment is attached to
454
	 * @param string $objectId the object id the comment is attached to
455
	 * @return IComment
456
	 * @since 9.0.0
457
	 */
458
	public function create($actorType, $actorId, $objectType, $objectId) {
459
		$comment = new Comment();
460
		$comment
461
			->setActor($actorType, $actorId)
462
			->setObject($objectType, $objectId);
463
		return $comment;
464
	}
465
466
	/**
467
	 * permanently deletes the comment specified by the ID
468
	 *
469
	 * When the comment has child comments, their parent ID will be changed to
470
	 * the parent ID of the item that is to be deleted.
471
	 *
472
	 * @param string $id
473
	 * @return bool
474
	 * @throws \InvalidArgumentException
475
	 * @since 9.0.0
476
	 */
477
	public function delete($id) {
478
		if (!is_string($id)) {
479
			throw new \InvalidArgumentException('Parameter must be string');
480
		}
481
482
		try {
483
			$comment = $this->get($id);
484
		} catch (\Exception $e) {
485
			// Ignore exceptions, we just don't fire a hook then
486
			$comment = null;
487
		}
488
489
		$qb = $this->dbConn->getQueryBuilder();
490
		$query = $qb->delete('comments')
491
			->where($qb->expr()->eq('id', $qb->createParameter('id')))
492
			->setParameter('id', $id);
493
494
		try {
495
			$affectedRows = $query->execute();
496
			$this->uncache($id);
497
		} 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...
498
			$this->logger->logException($e, ['app' => 'core_comments']);
499
			return false;
500
		}
501
502
		if ($affectedRows > 0 && $comment instanceof IComment) {
503
			$this->sendEvent(CommentsEvent::EVENT_DELETE, $comment);
504
		}
505
506
		return ($affectedRows > 0);
507
	}
508
509
	/**
510
	 * saves the comment permanently
511
	 *
512
	 * if the supplied comment has an empty ID, a new entry comment will be
513
	 * saved and the instance updated with the new ID.
514
	 *
515
	 * Otherwise, an existing comment will be updated.
516
	 *
517
	 * Throws NotFoundException when a comment that is to be updated does not
518
	 * exist anymore at this point of time.
519
	 *
520
	 * @param IComment $comment
521
	 * @return bool
522
	 * @throws NotFoundException
523
	 * @since 9.0.0
524
	 */
525
	public function save(IComment $comment) {
526
		if ($this->prepareCommentForDatabaseWrite($comment)->getId() === '') {
527
			$result = $this->insert($comment);
528
		} else {
529
			$result = $this->update($comment);
530
		}
531
532
		if ($result && !!$comment->getParentId()) {
533
			$this->updateChildrenInformation(
534
				$comment->getParentId(),
535
				$comment->getCreationDateTime()
536
			);
537
			$this->cache($comment);
538
		}
539
540
		return $result;
541
	}
542
543
	/**
544
	 * inserts the provided comment in the database
545
	 *
546
	 * @param IComment $comment
547
	 * @return bool
548
	 */
549
	protected function insert(IComment &$comment) {
550
		$qb = $this->dbConn->getQueryBuilder();
551
		$affectedRows = $qb
552
			->insert('comments')
553
			->values([
554
				'parent_id' => $qb->createNamedParameter($comment->getParentId()),
555
				'topmost_parent_id' => $qb->createNamedParameter($comment->getTopmostParentId()),
556
				'children_count' => $qb->createNamedParameter($comment->getChildrenCount()),
557
				'actor_type' => $qb->createNamedParameter($comment->getActorType()),
558
				'actor_id' => $qb->createNamedParameter($comment->getActorId()),
559
				'message' => $qb->createNamedParameter($comment->getMessage()),
560
				'verb' => $qb->createNamedParameter($comment->getVerb()),
561
				'creation_timestamp' => $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'),
562
				'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'),
563
				'object_type' => $qb->createNamedParameter($comment->getObjectType()),
564
				'object_id' => $qb->createNamedParameter($comment->getObjectId()),
565
			])
566
			->execute();
567
568
		if ($affectedRows > 0) {
569
			$comment->setId(strval($qb->getLastInsertId()));
570
			$this->sendEvent(CommentsEvent::EVENT_ADD, $comment);
571
		}
572
573
		return $affectedRows > 0;
574
	}
575
576
	/**
577
	 * updates a Comment data row
578
	 *
579
	 * @param IComment $comment
580
	 * @return bool
581
	 * @throws NotFoundException
582
	 */
583
	protected function update(IComment $comment) {
584
		// for properly working preUpdate Events we need the old comments as is
585
		// in the DB and overcome caching. Also avoid that outdated information stays.
586
		$this->uncache($comment->getId());
587
		$this->sendEvent(CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId()));
588
		$this->uncache($comment->getId());
589
590
		$qb = $this->dbConn->getQueryBuilder();
591
		$affectedRows = $qb
592
			->update('comments')
593
			->set('parent_id', $qb->createNamedParameter($comment->getParentId()))
594
			->set('topmost_parent_id', $qb->createNamedParameter($comment->getTopmostParentId()))
595
			->set('children_count', $qb->createNamedParameter($comment->getChildrenCount()))
596
			->set('actor_type', $qb->createNamedParameter($comment->getActorType()))
597
			->set('actor_id', $qb->createNamedParameter($comment->getActorId()))
598
			->set('message', $qb->createNamedParameter($comment->getMessage()))
599
			->set('verb', $qb->createNamedParameter($comment->getVerb()))
600
			->set('creation_timestamp', $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'))
601
			->set('latest_child_timestamp', $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'))
602
			->set('object_type', $qb->createNamedParameter($comment->getObjectType()))
603
			->set('object_id', $qb->createNamedParameter($comment->getObjectId()))
604
			->where($qb->expr()->eq('id', $qb->createParameter('id')))
605
			->setParameter('id', $comment->getId())
606
			->execute();
607
608
		if ($affectedRows === 0) {
609
			throw new NotFoundException('Comment to update does ceased to exist');
610
		}
611
612
		$this->sendEvent(CommentsEvent::EVENT_UPDATE, $comment);
613
614
		return $affectedRows > 0;
615
	}
616
617
	/**
618
	 * removes references to specific actor (e.g. on user delete) of a comment.
619
	 * The comment itself must not get lost/deleted.
620
	 *
621
	 * @param string $actorType the actor type (e.g. 'users')
622
	 * @param string $actorId a user id
623
	 * @return boolean
624
	 * @since 9.0.0
625
	 */
626
	public function deleteReferencesOfActor($actorType, $actorId) {
627
		$this->checkRoleParameters('Actor', $actorType, $actorId);
628
629
		$qb = $this->dbConn->getQueryBuilder();
630
		$affectedRows = $qb
631
			->update('comments')
632
			->set('actor_type', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
633
			->set('actor_id', $qb->createNamedParameter(ICommentsManager::DELETED_USER))
634
			->where($qb->expr()->eq('actor_type', $qb->createParameter('type')))
635
			->andWhere($qb->expr()->eq('actor_id', $qb->createParameter('id')))
636
			->setParameter('type', $actorType)
637
			->setParameter('id', $actorId)
638
			->execute();
639
640
		$this->commentsCache = [];
641
642
		return is_int($affectedRows);
643
	}
644
645
	/**
646
	 * deletes all comments made of a specific object (e.g. on file delete)
647
	 *
648
	 * @param string $objectType the object type (e.g. 'files')
649
	 * @param string $objectId e.g. the file id
650
	 * @return boolean
651
	 * @since 9.0.0
652
	 */
653
	public function deleteCommentsAtObject($objectType, $objectId) {
654
		$this->checkRoleParameters('Object', $objectType, $objectId);
655
656
		$qb = $this->dbConn->getQueryBuilder();
657
		$affectedRows = $qb
658
			->delete('comments')
659
			->where($qb->expr()->eq('object_type', $qb->createParameter('type')))
660
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('id')))
661
			->setParameter('type', $objectType)
662
			->setParameter('id', $objectId)
663
			->execute();
664
665
		$this->commentsCache = [];
666
667
		return is_int($affectedRows);
668
	}
669
670
	/**
671
	 * deletes the read markers for the specified user
672
	 *
673
	 * @param \OCP\IUser $user
674
	 * @return bool
675
	 * @since 9.0.0
676
	 */
677
	public function deleteReadMarksFromUser(IUser $user) {
678
		$qb = $this->dbConn->getQueryBuilder();
679
		$query = $qb->delete('comments_read_markers')
680
			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
681
			->setParameter('user_id', $user->getUID());
682
683
		try {
684
			$affectedRows = $query->execute();
685
		} 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...
686
			$this->logger->logException($e, ['app' => 'core_comments']);
687
			return false;
688
		}
689
		return ($affectedRows > 0);
690
	}
691
692
	/**
693
	 * sets the read marker for a given file to the specified date for the
694
	 * provided user
695
	 *
696
	 * @param string $objectType
697
	 * @param string $objectId
698
	 * @param \DateTime $dateTime
699
	 * @param IUser $user
700
	 * @since 9.0.0
701
	 * @suppress SqlInjectionChecker
702
	 */
703
	public function setReadMark($objectType, $objectId, \DateTime $dateTime, IUser $user) {
704
		$this->checkRoleParameters('Object', $objectType, $objectId);
705
706
		$qb = $this->dbConn->getQueryBuilder();
707
		$values = [
708
			'user_id' => $qb->createNamedParameter($user->getUID()),
709
			'marker_datetime' => $qb->createNamedParameter($dateTime, 'datetime'),
710
			'object_type' => $qb->createNamedParameter($objectType),
711
			'object_id' => $qb->createNamedParameter($objectId),
712
		];
713
714
		// Strategy: try to update, if this does not return affected rows, do an insert.
715
		$affectedRows = $qb
716
			->update('comments_read_markers')
717
			->set('user_id', $values['user_id'])
718
			->set('marker_datetime', $values['marker_datetime'])
719
			->set('object_type', $values['object_type'])
720
			->set('object_id', $values['object_id'])
721
			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
722
			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
723
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
724
			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
725
			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
726
			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
727
			->execute();
728
729
		if ($affectedRows > 0) {
730
			return;
731
		}
732
733
		$qb->insert('comments_read_markers')
734
			->values($values)
735
			->execute();
736
	}
737
738
	/**
739
	 * returns the read marker for a given file to the specified date for the
740
	 * provided user. It returns null, when the marker is not present, i.e.
741
	 * no comments were marked as read.
742
	 *
743
	 * @param string $objectType
744
	 * @param string $objectId
745
	 * @param IUser $user
746
	 * @return \DateTime|null
747
	 * @since 9.0.0
748
	 */
749
	public function getReadMark($objectType, $objectId, IUser $user) {
750
		$qb = $this->dbConn->getQueryBuilder();
751
		$resultStatement = $qb->select('marker_datetime')
752
			->from('comments_read_markers')
753
			->where($qb->expr()->eq('user_id', $qb->createParameter('user_id')))
754
			->andWhere($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
755
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
756
			->setParameter('user_id', $user->getUID(), IQueryBuilder::PARAM_STR)
757
			->setParameter('object_type', $objectType, IQueryBuilder::PARAM_STR)
758
			->setParameter('object_id', $objectId, IQueryBuilder::PARAM_STR)
759
			->execute();
760
761
		$data = $resultStatement->fetch();
762
		$resultStatement->closeCursor();
763
		if (!$data || is_null($data['marker_datetime'])) {
764
			return null;
765
		}
766
767
		return new \DateTime($data['marker_datetime']);
768
	}
769
770
	/**
771
	 * deletes the read markers on the specified object
772
	 *
773
	 * @param string $objectType
774
	 * @param string $objectId
775
	 * @return bool
776
	 * @since 9.0.0
777
	 */
778
	public function deleteReadMarksOnObject($objectType, $objectId) {
779
		$this->checkRoleParameters('Object', $objectType, $objectId);
780
781
		$qb = $this->dbConn->getQueryBuilder();
782
		$query = $qb->delete('comments_read_markers')
783
			->where($qb->expr()->eq('object_type', $qb->createParameter('object_type')))
784
			->andWhere($qb->expr()->eq('object_id', $qb->createParameter('object_id')))
785
			->setParameter('object_type', $objectType)
786
			->setParameter('object_id', $objectId);
787
788
		try {
789
			$affectedRows = $query->execute();
790
		} 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...
791
			$this->logger->logException($e, ['app' => 'core_comments']);
792
			return false;
793
		}
794
		return ($affectedRows > 0);
795
	}
796
797
	/**
798
	 * registers an Entity to the manager, so event notifications can be send
799
	 * to consumers of the comments infrastructure
800
	 *
801
	 * @param \Closure $closure
802
	 */
803
	public function registerEventHandler(\Closure $closure) {
804
		$this->eventHandlerClosures[] = $closure;
805
		$this->eventHandlers = [];
806
	}
807
808
	/**
809
	 * registers a method that resolves an ID to a display name for a given type
810
	 *
811
	 * @param string $type
812
	 * @param \Closure $closure
813
	 * @throws \OutOfBoundsException
814
	 * @since 11.0.0
815
	 *
816
	 * Only one resolver shall be registered per type. Otherwise a
817
	 * \OutOfBoundsException has to thrown.
818
	 */
819 View Code Duplication
	public function registerDisplayNameResolver($type, \Closure $closure) {
820
		if (!is_string($type)) {
821
			throw new \InvalidArgumentException('String expected.');
822
		}
823
		if (isset($this->displayNameResolvers[$type])) {
824
			throw new \OutOfBoundsException('Displayname resolver for this type already registered');
825
		}
826
		$this->displayNameResolvers[$type] = $closure;
827
	}
828
829
	/**
830
	 * resolves a given ID of a given Type to a display name.
831
	 *
832
	 * @param string $type
833
	 * @param string $id
834
	 * @return string
835
	 * @throws \OutOfBoundsException
836
	 * @since 11.0.0
837
	 *
838
	 * If a provided type was not registered, an \OutOfBoundsException shall
839
	 * be thrown. It is upon the resolver discretion what to return of the
840
	 * provided ID is unknown. It must be ensured that a string is returned.
841
	 */
842 View Code Duplication
	public function resolveDisplayName($type, $id) {
843
		if (!is_string($type)) {
844
			throw new \InvalidArgumentException('String expected.');
845
		}
846
		if (!isset($this->displayNameResolvers[$type])) {
847
			throw new \OutOfBoundsException('No Displayname resolver for this type registered');
848
		}
849
		return (string)$this->displayNameResolvers[$type]($id);
850
	}
851
852
	/**
853
	 * returns valid, registered entities
854
	 *
855
	 * @return \OCP\Comments\ICommentsEventHandler[]
856
	 */
857 View Code Duplication
	private function getEventHandlers() {
858
		if (!empty($this->eventHandlers)) {
859
			return $this->eventHandlers;
860
		}
861
862
		$this->eventHandlers = [];
863
		foreach ($this->eventHandlerClosures as $name => $closure) {
864
			$entity = $closure();
865
			if (!($entity instanceof ICommentsEventHandler)) {
866
				throw new \InvalidArgumentException('The given entity does not implement the ICommentsEntity interface');
867
			}
868
			$this->eventHandlers[$name] = $entity;
869
		}
870
871
		return $this->eventHandlers;
872
	}
873
874
	/**
875
	 * sends notifications to the registered entities
876
	 *
877
	 * @param $eventType
878
	 * @param IComment $comment
879
	 */
880
	private function sendEvent($eventType, IComment $comment) {
881
		$entities = $this->getEventHandlers();
882
		$event = new CommentsEvent($eventType, $comment);
883
		foreach ($entities as $entity) {
884
			$entity->handle($event);
885
		}
886
	}
887
}
888