Completed
Push — master ( ce76f4...3efcd8 )
by Sujith
20:38 queued 07:42
created

Manager::uncache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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