aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Comments
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Comments')
-rw-r--r--lib/private/Comments/Comment.php307
-rw-r--r--lib/private/Comments/Manager.php725
-rw-r--r--lib/private/Comments/ManagerFactory.php28
3 files changed, 668 insertions, 392 deletions
diff --git a/lib/private/Comments/Comment.php b/lib/private/Comments/Comment.php
index 8517bef5893..7190f252c82 100644
--- a/lib/private/Comments/Comment.php
+++ b/lib/private/Comments/Comment.php
@@ -1,29 +1,10 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OC\Comments;
use OCP\Comments\IComment;
@@ -31,11 +12,11 @@ use OCP\Comments\IllegalIDChangeException;
use OCP\Comments\MessageTooLongException;
class Comment implements IComment {
- protected $data = [
+ protected array $data = [
'id' => '',
'parentId' => '0',
'topmostParentId' => '0',
- 'childrenCount' => '0',
+ 'childrenCount' => 0,
'message' => '',
'verb' => '',
'actorType' => '',
@@ -43,38 +24,40 @@ class Comment implements IComment {
'objectType' => '',
'objectId' => '',
'referenceId' => null,
+ 'metaData' => null,
'creationDT' => null,
'latestChildDT' => null,
+ 'reactions' => null,
+ 'expire_date' => null,
];
/**
* Comment constructor.
*
- * @param array $data optional, array with keys according to column names from
- * the comments database scheme
+ * @param array $data optional, array with keys according to column names from
+ * the comments database scheme
*/
- public function __construct(array $data = null) {
+ public function __construct(?array $data = null) {
if (is_array($data)) {
$this->fromArray($data);
}
}
/**
- * returns the ID of the comment
+ * Returns the ID of the comment
*
* It may return an empty string, if the comment was not stored.
* It is expected that the concrete Comment implementation gives an ID
* by itself (e.g. after saving).
*
- * @return string
* @since 9.0.0
*/
- public function getId() {
+ public function getId(): string {
return $this->data['id'];
}
/**
- * sets the ID of the comment and returns itself
+ * Sets the ID of the comment and returns itself
*
* It is only allowed to set the ID only, if the current id is an empty
* string (which means it is not stored in a database, storage or whatever
@@ -86,7 +69,7 @@ class Comment implements IComment {
* @throws IllegalIDChangeException
* @since 9.0.0
*/
- public function setId($id) {
+ public function setId($id): IComment {
if (!is_string($id)) {
throw new \InvalidArgumentException('String expected.');
}
@@ -101,23 +84,21 @@ class Comment implements IComment {
}
/**
- * returns the parent ID of the comment
+ * Returns the parent ID of the comment
*
- * @return string
* @since 9.0.0
*/
- public function getParentId() {
+ public function getParentId(): string {
return $this->data['parentId'];
}
/**
- * sets the parent ID and returns itself
+ * Sets the parent ID and returns itself
*
* @param string $parentId
- * @return IComment
* @since 9.0.0
*/
- public function setParentId($parentId) {
+ public function setParentId($parentId): IComment {
if (!is_string($parentId)) {
throw new \InvalidArgumentException('String expected.');
}
@@ -126,24 +107,22 @@ class Comment implements IComment {
}
/**
- * returns the topmost parent ID of the comment
+ * Returns the topmost parent ID of the comment
*
- * @return string
* @since 9.0.0
*/
- public function getTopmostParentId() {
+ public function getTopmostParentId(): string {
return $this->data['topmostParentId'];
}
/**
- * sets the topmost parent ID and returns itself
+ * Sets the topmost parent ID and returns itself
*
* @param string $id
- * @return IComment
* @since 9.0.0
*/
- public function setTopmostParentId($id) {
+ public function setTopmostParentId($id): IComment {
if (!is_string($id)) {
throw new \InvalidArgumentException('String expected.');
}
@@ -152,23 +131,21 @@ class Comment implements IComment {
}
/**
- * returns the number of children
+ * Returns the number of children
*
- * @return int
* @since 9.0.0
*/
- public function getChildrenCount() {
+ public function getChildrenCount(): int {
return $this->data['childrenCount'];
}
/**
- * sets the number of children
+ * Sets the number of children
*
* @param int $count
- * @return IComment
* @since 9.0.0
*/
- public function setChildrenCount($count) {
+ public function setChildrenCount($count): IComment {
if (!is_int($count)) {
throw new \InvalidArgumentException('Integer expected.');
}
@@ -177,12 +154,10 @@ class Comment implements IComment {
}
/**
- * returns the message of the comment
- *
- * @return string
+ * Returns the message of the comment
* @since 9.0.0
*/
- public function getMessage() {
+ public function getMessage(): string {
return $this->data['message'];
}
@@ -191,17 +166,16 @@ class Comment implements IComment {
*
* @param string $message
* @param int $maxLength
- * @return IComment
* @throws MessageTooLongException
* @since 9.0.0
*/
- public function setMessage($message, $maxLength = self::MAX_MESSAGE_LENGTH) {
+ public function setMessage($message, $maxLength = self::MAX_MESSAGE_LENGTH): IComment {
if (!is_string($message)) {
throw new \InvalidArgumentException('String expected.');
}
$message = trim($message);
if ($maxLength && mb_strlen($message, 'UTF-8') > $maxLength) {
- throw new MessageTooLongException('Comment message must not exceed ' . $maxLength. ' characters');
+ throw new MessageTooLongException('Comment message must not exceed ' . $maxLength . ' characters');
}
$this->data['message'] = $message;
return $this;
@@ -211,61 +185,77 @@ class Comment implements IComment {
* returns an array containing mentions that are included in the comment
*
* @return array each mention provides a 'type' and an 'id', see example below
+ * @psalm-return list<array{type: 'guest'|'email'|'federated_group'|'group'|'federated_team'|'team'|'federated_user'|'user', id: non-empty-lowercase-string}>
+ * @since 30.0.2 Type 'email' is supported
+ * @since 29.0.0 Types 'federated_group', 'federated_team', 'team' and 'federated_user' are supported
+ * @since 23.0.0 Type 'group' is supported
+ * @since 17.0.0 Type 'guest' is supported
* @since 11.0.0
- *
- * The return array looks like:
- * [
- * [
- * 'type' => 'user',
- * 'id' => 'citizen4'
- * ],
- * [
- * 'type' => 'group',
- * 'id' => 'media'
- * ],
- * …
- * ]
- *
*/
- public function getMentions() {
- $ok = preg_match_all("/\B(?<![^a-z0-9_\-@\.\'\s])@(\"guest\/[a-f0-9]+\"|\"[a-z0-9_\-@\.\' ]+\"|[a-z0-9_\-@\.\']+)/i", $this->getMessage(), $mentions);
- if (!$ok || !isset($mentions[0]) || !is_array($mentions[0])) {
+ public function getMentions(): array {
+ $ok = preg_match_all("/\B(?<![^a-z0-9_\-@\.\'\s])@(\"(guest|email)\/[a-f0-9]+\"|\"(?:federated_)?(?:group|team|user){1}\/[a-z0-9_\-@\.\' \/:]+\"|\"[a-z0-9_\-@\.\' ]+\"|[a-z0-9_\-@\.\']+)/i", $this->getMessage(), $mentions);
+ if (!$ok || !isset($mentions[0])) {
return [];
}
- $uids = array_unique($mentions[0]);
- usort($uids, static function ($uid1, $uid2) {
- return mb_strlen($uid2) <=> mb_strlen($uid1);
+ $mentionIds = array_unique($mentions[0]);
+ usort($mentionIds, static function ($mentionId1, $mentionId2) {
+ return mb_strlen($mentionId2) <=> mb_strlen($mentionId1);
});
$result = [];
- foreach ($uids as $uid) {
- $cleanUid = trim(substr($uid, 1), '"');
- if (strpos($cleanUid, 'guest/') === 0) {
- $result[] = ['type' => 'guest', 'id' => $cleanUid];
+ foreach ($mentionIds as $mentionId) {
+ // Cut-off the @ and remove wrapping double-quotes
+ /** @var non-empty-lowercase-string $cleanId */
+ $cleanId = trim(substr($mentionId, 1), '"');
+
+ if (str_starts_with($cleanId, 'guest/')) {
+ $result[] = ['type' => 'guest', 'id' => $cleanId];
+ } elseif (str_starts_with($cleanId, 'email/')) {
+ /** @var non-empty-lowercase-string $cleanId */
+ $cleanId = substr($cleanId, 6);
+ $result[] = ['type' => 'email', 'id' => $cleanId];
+ } elseif (str_starts_with($cleanId, 'federated_group/')) {
+ /** @var non-empty-lowercase-string $cleanId */
+ $cleanId = substr($cleanId, 16);
+ $result[] = ['type' => 'federated_group', 'id' => $cleanId];
+ } elseif (str_starts_with($cleanId, 'group/')) {
+ /** @var non-empty-lowercase-string $cleanId */
+ $cleanId = substr($cleanId, 6);
+ $result[] = ['type' => 'group', 'id' => $cleanId];
+ } elseif (str_starts_with($cleanId, 'federated_team/')) {
+ /** @var non-empty-lowercase-string $cleanId */
+ $cleanId = substr($cleanId, 15);
+ $result[] = ['type' => 'federated_team', 'id' => $cleanId];
+ } elseif (str_starts_with($cleanId, 'team/')) {
+ /** @var non-empty-lowercase-string $cleanId */
+ $cleanId = substr($cleanId, 5);
+ $result[] = ['type' => 'team', 'id' => $cleanId];
+ } elseif (str_starts_with($cleanId, 'federated_user/')) {
+ /** @var non-empty-lowercase-string $cleanId */
+ $cleanId = substr($cleanId, 15);
+ $result[] = ['type' => 'federated_user', 'id' => $cleanId];
} else {
- $result[] = ['type' => 'user', 'id' => $cleanUid];
+ $result[] = ['type' => 'user', 'id' => $cleanId];
}
}
return $result;
}
/**
- * returns the verb of the comment
+ * Returns the verb of the comment
*
- * @return string
* @since 9.0.0
*/
- public function getVerb() {
+ public function getVerb(): string {
return $this->data['verb'];
}
/**
- * sets the verb of the comment, e.g. 'comment' or 'like'
+ * Sets the verb of the comment, e.g. 'comment' or 'like'
*
* @param string $verb
- * @return IComment
* @since 9.0.0
*/
- public function setVerb($verb) {
+ public function setVerb($verb): IComment {
if (!is_string($verb) || !trim($verb)) {
throw new \InvalidArgumentException('Non-empty String expected.');
}
@@ -274,36 +264,31 @@ class Comment implements IComment {
}
/**
- * returns the actor type
- *
- * @return string
+ * Returns the actor type
* @since 9.0.0
*/
- public function getActorType() {
+ public function getActorType(): string {
return $this->data['actorType'];
}
/**
- * returns the actor ID
- *
- * @return string
+ * Returns the actor ID
* @since 9.0.0
*/
- public function getActorId() {
+ public function getActorId(): string {
return $this->data['actorId'];
}
/**
- * sets (overwrites) the actor type and id
+ * Sets (overwrites) the actor type and id
*
* @param string $actorType e.g. 'users'
* @param string $actorId e.g. 'zombie234'
- * @return IComment
* @since 9.0.0
*/
- public function setActor($actorType, $actorId) {
+ public function setActor($actorType, $actorId): IComment {
if (
- !is_string($actorType) || !trim($actorType)
+ !is_string($actorType) || !trim($actorType)
|| !is_string($actorId) || $actorId === ''
) {
throw new \InvalidArgumentException('String expected.');
@@ -314,82 +299,70 @@ class Comment implements IComment {
}
/**
- * returns the creation date of the comment.
+ * Returns the creation date of the comment.
*
* If not explicitly set, it shall default to the time of initialization.
- *
- * @return \DateTime
* @since 9.0.0
+ * @throws \LogicException if creation date time is not set yet
*/
- public function getCreationDateTime() {
+ public function getCreationDateTime(): \DateTime {
+ if (!isset($this->data['creationDT'])) {
+ throw new \LogicException('Cannot get creation date before setting one or writting to database');
+ }
return $this->data['creationDT'];
}
/**
- * sets the creation date of the comment and returns itself
- *
- * @param \DateTime $timestamp
- * @return IComment
+ * Sets the creation date of the comment and returns itself
* @since 9.0.0
*/
- public function setCreationDateTime(\DateTime $timestamp) {
- $this->data['creationDT'] = $timestamp;
+ public function setCreationDateTime(\DateTime $dateTime): IComment {
+ $this->data['creationDT'] = $dateTime;
return $this;
}
/**
- * returns the DateTime of the most recent child, if set, otherwise null
- *
- * @return \DateTime|null
+ * Returns the DateTime of the most recent child, if set, otherwise null
* @since 9.0.0
*/
- public function getLatestChildDateTime() {
+ public function getLatestChildDateTime(): ?\DateTime {
return $this->data['latestChildDT'];
}
/**
- * sets the date of the most recent child
- *
- * @param \DateTime $dateTime
- * @return IComment
- * @since 9.0.0
+ * @inheritDoc
*/
- public function setLatestChildDateTime(\DateTime $dateTime = null) {
+ public function setLatestChildDateTime(?\DateTime $dateTime = null): IComment {
$this->data['latestChildDT'] = $dateTime;
return $this;
}
/**
- * returns the object type the comment is attached to
- *
- * @return string
+ * Returns the object type the comment is attached to
* @since 9.0.0
*/
- public function getObjectType() {
+ public function getObjectType(): string {
return $this->data['objectType'];
}
/**
- * returns the object id the comment is attached to
- *
- * @return string
+ * Returns the object id the comment is attached to
* @since 9.0.0
*/
- public function getObjectId() {
+ public function getObjectId(): string {
return $this->data['objectId'];
}
/**
- * sets (overwrites) the object of the comment
+ * Sets (overwrites) the object of the comment
*
* @param string $objectType e.g. 'files'
* @param string $objectId e.g. '16435'
- * @return IComment
* @since 9.0.0
*/
- public function setObject($objectType, $objectId) {
+ public function setObject($objectType, $objectId): IComment {
if (
- !is_string($objectType) || !trim($objectType)
+ !is_string($objectType) || !trim($objectType)
|| !is_string($objectId) || trim($objectId) === ''
) {
throw new \InvalidArgumentException('String expected.');
@@ -400,9 +373,7 @@ class Comment implements IComment {
}
/**
- * returns the reference id of the comment
- *
- * @return string|null
+ * Returns the reference id of the comment
* @since 19.0.0
*/
public function getReferenceId(): ?string {
@@ -410,10 +381,9 @@ class Comment implements IComment {
}
/**
- * sets (overwrites) the reference id of the comment
+ * Sets (overwrites) the reference id of the comment
*
* @param string $referenceId e.g. sha256 hash sum
- * @return IComment
* @since 19.0.0
*/
public function setReferenceId(?string $referenceId): IComment {
@@ -430,13 +400,70 @@ class Comment implements IComment {
}
/**
+ * @inheritDoc
+ */
+ public function getMetaData(): ?array {
+ if ($this->data['metaData'] === null) {
+ return null;
+ }
+
+ try {
+ $metaData = json_decode($this->data['metaData'], true, flags: JSON_THROW_ON_ERROR);
+ } catch (\JsonException $e) {
+ return null;
+ }
+ return is_array($metaData) ? $metaData : null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setMetaData(?array $metaData): IComment {
+ if ($metaData === null) {
+ $this->data['metaData'] = null;
+ } else {
+ $this->data['metaData'] = json_encode($metaData, JSON_THROW_ON_ERROR);
+ }
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getReactions(): array {
+ return $this->data['reactions'] ?? [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setReactions(?array $reactions): IComment {
+ $this->data['reactions'] = $reactions;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setExpireDate(?\DateTime $dateTime): IComment {
+ $this->data['expire_date'] = $dateTime;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getExpireDate(): ?\DateTime {
+ return $this->data['expire_date'];
+ }
+
+ /**
* sets the comment data based on an array with keys as taken from the
* database.
*
* @param array $data
- * @return IComment
*/
- protected function fromArray($data) {
+ protected function fromArray($data): IComment {
foreach (array_keys($data) as $key) {
// translate DB keys to internal setter names
$setter = 'set' . implode('', array_map('ucfirst', explode('_', $key)));
diff --git a/lib/private/Comments/Manager.php b/lib/private/Comments/Manager.php
index 1cc82b5c2ce..047fa361dad 100644
--- a/lib/private/Comments/Manager.php
+++ b/lib/private/Comments/Manager.php
@@ -1,36 +1,14 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OC\Comments;
use Doctrine\DBAL\Exception\DriverException;
-use Doctrine\DBAL\Exception\InvalidFieldNameException;
-use OCA\Comments\AppInfo\Application;
+use OCA\DAV\Connector\Sabre\File;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\CommentsEvent;
use OCP\Comments\IComment;
@@ -38,52 +16,42 @@ use OCP\Comments\ICommentsEventHandler;
use OCP\Comments\ICommentsManager;
use OCP\Comments\NotFoundException;
use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\FileInfo;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
use OCP\IConfig;
use OCP\IDBConnection;
-use OCP\IUser;
+use OCP\IEmojiHelper;
use OCP\IInitialStateService;
+use OCP\IUser;
+use OCP\PreConditionNotMetException;
use OCP\Util;
use Psr\Log\LoggerInterface;
class Manager implements ICommentsManager {
-
- /** @var IDBConnection */
- protected $dbConn;
-
- /** @var LoggerInterface */
- protected $logger;
-
- /** @var IConfig */
- protected $config;
-
- /** @var ITimeFactory */
- protected $timeFactory;
-
- /** @var IInitialStateService */
- protected $initialStateService;
-
/** @var IComment[] */
- protected $commentsCache = [];
+ protected array $commentsCache = [];
- /** @var \Closure[] */
- protected $eventHandlerClosures = [];
+ /** @var \Closure[] */
+ protected array $eventHandlerClosures = [];
- /** @var ICommentsEventHandler[] */
- protected $eventHandlers = [];
+ /** @var ICommentsEventHandler[] */
+ protected array $eventHandlers = [];
/** @var \Closure[] */
- protected $displayNameResolvers = [];
-
- public function __construct(IDBConnection $dbConn,
- LoggerInterface $logger,
- IConfig $config,
- ITimeFactory $timeFactory,
- IInitialStateService $initialStateService) {
- $this->dbConn = $dbConn;
- $this->logger = $logger;
- $this->config = $config;
- $this->timeFactory = $timeFactory;
- $this->initialStateService = $initialStateService;
+ protected array $displayNameResolvers = [];
+
+ public function __construct(
+ protected IDBConnection $dbConn,
+ protected LoggerInterface $logger,
+ protected IConfig $config,
+ protected ITimeFactory $timeFactory,
+ protected IEmojiHelper $emojiHelper,
+ protected IInitialStateService $initialStateService,
+ protected IRootFolder $rootFolder,
+ protected IEventDispatcher $eventDispatcher,
+ ) {
}
/**
@@ -91,9 +59,8 @@ class Manager implements ICommentsManager {
* IComment interface.
*
* @param array $data
- * @return array
*/
- protected function normalizeDatabaseData(array $data) {
+ protected function normalizeDatabaseData(array $data): array {
$data['id'] = (string)$data['id'];
$data['parent_id'] = (string)$data['parent_id'];
$data['topmost_parent_id'] = (string)$data['topmost_parent_id'];
@@ -101,16 +68,35 @@ class Manager implements ICommentsManager {
if (!is_null($data['latest_child_timestamp'])) {
$data['latest_child_timestamp'] = new \DateTime($data['latest_child_timestamp']);
}
+ if (!is_null($data['expire_date'])) {
+ $data['expire_date'] = new \DateTime($data['expire_date']);
+ }
$data['children_count'] = (int)$data['children_count'];
- $data['reference_id'] = $data['reference_id'] ?? null;
+ $data['reference_id'] = $data['reference_id'];
+ $data['meta_data'] = json_decode($data['meta_data'], true);
+ if ($this->supportReactions()) {
+ if ($data['reactions'] !== null) {
+ $list = json_decode($data['reactions'], true);
+ // Ordering does not work on the database with group concat and Oracle,
+ // So we simply sort on the output.
+ if (is_array($list)) {
+ uasort($list, static function ($a, $b) {
+ if ($a === $b) {
+ return 0;
+ }
+ return ($a > $b) ? -1 : 1;
+ });
+ $data['reactions'] = $list;
+ } else {
+ $data['reactions'] = [];
+ }
+ } else {
+ $data['reactions'] = [];
+ }
+ }
return $data;
}
-
- /**
- * @param array $data
- * @return IComment
- */
public function getCommentFromData(array $data): IComment {
return new Comment($this->normalizeDatabaseData($data));
}
@@ -124,7 +110,7 @@ class Manager implements ICommentsManager {
* by parameter for convenience
* @throws \UnexpectedValueException
*/
- protected function prepareCommentForDatabaseWrite(IComment $comment) {
+ protected function prepareCommentForDatabaseWrite(IComment $comment): IComment {
if (!$comment->getActorType()
|| $comment->getActorId() === ''
|| !$comment->getObjectType()
@@ -134,13 +120,19 @@ class Manager implements ICommentsManager {
throw new \UnexpectedValueException('Actor, Object and Verb information must be provided for saving');
}
+ if ($comment->getVerb() === 'reaction' && !$this->emojiHelper->isValidSingleEmoji($comment->getMessage())) {
+ // 4 characters: laptop + person + gender + skin color => "🧑🏽‍💻" is a single emoji from the picker
+ throw new \UnexpectedValueException('Reactions can only be a single emoji');
+ }
+
if ($comment->getId() === '') {
$comment->setChildrenCount(0);
- $comment->setLatestChildDateTime(new \DateTime('0000-00-00 00:00:00', new \DateTimeZone('UTC')));
$comment->setLatestChildDateTime(null);
}
- if (is_null($comment->getCreationDateTime())) {
+ try {
+ $comment->getCreationDateTime();
+ } catch (\LogicException $e) {
$comment->setCreationDateTime(new \DateTime());
}
@@ -159,10 +151,9 @@ class Manager implements ICommentsManager {
* returns the topmost parent id of a given comment identified by ID
*
* @param string $id
- * @return string
* @throws NotFoundException
*/
- protected function determineTopmostParentId($id) {
+ protected function determineTopmostParentId($id): string {
$comment = $this->get($id);
if ($comment->getParentId() === '0') {
return $comment->getId();
@@ -178,7 +169,7 @@ class Manager implements ICommentsManager {
* @param \DateTime $cDateTime the date time of the most recent child
* @throws NotFoundException
*/
- protected function updateChildrenInformation($id, \DateTime $cDateTime) {
+ protected function updateChildrenInformation($id, \DateTime $cDateTime): void {
$qb = $this->dbConn->getQueryBuilder();
$query = $qb->select($qb->func()->count('id'))
->from('comments')
@@ -205,7 +196,7 @@ class Manager implements ICommentsManager {
* @param string $id
* @throws \InvalidArgumentException
*/
- protected function checkRoleParameters($role, $type, $id) {
+ protected function checkRoleParameters($role, $type, $id): void {
if (
!is_string($type) || empty($type)
|| !is_string($id) || empty($id)
@@ -216,10 +207,8 @@ class Manager implements ICommentsManager {
/**
* run-time caches a comment
- *
- * @param IComment $comment
*/
- protected function cache(IComment $comment) {
+ protected function cache(IComment $comment): void {
$id = $comment->getId();
if (empty($id)) {
return;
@@ -232,7 +221,7 @@ class Manager implements ICommentsManager {
*
* @param mixed $id the comment's id
*/
- protected function uncache($id) {
+ protected function uncache($id): void {
$id = (string)$id;
if (isset($this->commentsCache[$id])) {
unset($this->commentsCache[$id]);
@@ -243,12 +232,11 @@ class Manager implements ICommentsManager {
* returns a comment instance
*
* @param string $id the ID of the comment
- * @return IComment
* @throws NotFoundException
* @throws \InvalidArgumentException
* @since 9.0.0
*/
- public function get($id) {
+ public function get($id): IComment {
if ((int)$id === 0) {
throw new \InvalidArgumentException('IDs must be translatable to a number in this implementation.');
}
@@ -277,35 +265,9 @@ class Manager implements ICommentsManager {
}
/**
- * returns the comment specified by the id and all it's child comments.
- * At this point of time, we do only support one level depth.
- *
- * @param string $id
- * @param int $limit max number of entries to return, 0 returns all
- * @param int $offset the start entry
- * @return array
- * @since 9.0.0
- *
- * The return array looks like this
- * [
- * 'comment' => IComment, // root comment
- * 'replies' =>
- * [
- * 0 =>
- * [
- * 'comment' => IComment,
- * 'replies' => []
- * ]
- * 1 =>
- * [
- * 'comment' => IComment,
- * 'replies'=> []
- * ],
- * …
- * ]
- * ]
+ * @inheritDoc
*/
- public function getTree($id, $limit = 0, $offset = 0) {
+ public function getTree($id, $limit = 0, $offset = 0): array {
$tree = [];
$tree['comment'] = $this->get($id);
$tree['replies'] = [];
@@ -346,11 +308,11 @@ class Manager implements ICommentsManager {
* @param string $objectType the object type, e.g. 'files'
* @param string $objectId the id of the object
* @param int $limit optional, number of maximum comments to be returned. if
- * not specified, all comments are returned.
+ * not specified, all comments are returned.
* @param int $offset optional, starting point
* @param \DateTime $notOlderThan optional, timestamp of the oldest comments
- * that may be returned
- * @return IComment[]
+ * that may be returned
+ * @return list<IComment>
* @since 9.0.0
*/
public function getForObject(
@@ -358,7 +320,7 @@ class Manager implements ICommentsManager {
$objectId,
$limit = 0,
$offset = 0,
- \DateTime $notOlderThan = null
+ ?\DateTime $notOlderThan = null,
) {
$comments = [];
@@ -400,10 +362,10 @@ class Manager implements ICommentsManager {
* @param int $lastKnownCommentId the last known comment (will be used as offset)
* @param string $sortDirection direction of the comments (`asc` or `desc`)
* @param int $limit optional, number of maximum comments to be returned. if
- * set to 0, all comments are returned.
+ * set to 0, all comments are returned.
* @param bool $includeLastKnown
- * @return IComment[]
- * @return array
+ * @param string $topmostParentId Limit the comments to a list of replies and its original root comment
+ * @return list<IComment>
*/
public function getForObjectSince(
string $objectType,
@@ -411,7 +373,42 @@ class Manager implements ICommentsManager {
int $lastKnownCommentId,
string $sortDirection = 'asc',
int $limit = 30,
- bool $includeLastKnown = false
+ bool $includeLastKnown = false,
+ string $topmostParentId = '',
+ ): array {
+ return $this->getCommentsWithVerbForObjectSinceComment(
+ $objectType,
+ $objectId,
+ [],
+ $lastKnownCommentId,
+ $sortDirection,
+ $limit,
+ $includeLastKnown,
+ $topmostParentId,
+ );
+ }
+
+ /**
+ * @param string $objectType the object type, e.g. 'files'
+ * @param string $objectId the id of the object
+ * @param string[] $verbs List of verbs to filter by
+ * @param int $lastKnownCommentId the last known comment (will be used as offset)
+ * @param string $sortDirection direction of the comments (`asc` or `desc`)
+ * @param int $limit optional, number of maximum comments to be returned. if
+ * set to 0, all comments are returned.
+ * @param bool $includeLastKnown
+ * @param string $topmostParentId Limit the comments to a list of replies and its original root comment
+ * @return list<IComment>
+ */
+ public function getCommentsWithVerbForObjectSinceComment(
+ string $objectType,
+ string $objectId,
+ array $verbs,
+ int $lastKnownCommentId,
+ string $sortDirection = 'asc',
+ int $limit = 30,
+ bool $includeLastKnown = false,
+ string $topmostParentId = '',
): array {
$comments = [];
@@ -427,6 +424,17 @@ class Manager implements ICommentsManager {
$query->setMaxResults($limit);
}
+ if (!empty($verbs)) {
+ $query->andWhere($query->expr()->in('verb', $query->createNamedParameter($verbs, IQueryBuilder::PARAM_STR_ARRAY)));
+ }
+
+ if ($topmostParentId !== '') {
+ $query->andWhere($query->expr()->orX(
+ $query->expr()->eq('id', $query->createNamedParameter($topmostParentId)),
+ $query->expr()->eq('topmost_parent_id', $query->createNamedParameter($topmostParentId)),
+ ));
+ }
+
$lastKnownComment = $lastKnownCommentId > 0 ? $this->getLastKnownComment(
$objectType,
$objectId,
@@ -444,14 +452,14 @@ class Manager implements ICommentsManager {
$query->expr()->orX(
$query->expr()->lt(
'creation_timestamp',
- $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
- IQueryBuilder::PARAM_DATE
+ $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATETIME_MUTABLE),
+ IQueryBuilder::PARAM_DATETIME_MUTABLE
),
$query->expr()->andX(
$query->expr()->eq(
'creation_timestamp',
- $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
- IQueryBuilder::PARAM_DATE
+ $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATETIME_MUTABLE),
+ IQueryBuilder::PARAM_DATETIME_MUTABLE
),
$idComparison
)
@@ -467,20 +475,36 @@ class Manager implements ICommentsManager {
$query->expr()->orX(
$query->expr()->gt(
'creation_timestamp',
- $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
- IQueryBuilder::PARAM_DATE
+ $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATETIME_MUTABLE),
+ IQueryBuilder::PARAM_DATETIME_MUTABLE
),
$query->expr()->andX(
$query->expr()->eq(
'creation_timestamp',
- $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATE),
- IQueryBuilder::PARAM_DATE
+ $query->createNamedParameter($lastKnownCommentDateTime, IQueryBuilder::PARAM_DATETIME_MUTABLE),
+ IQueryBuilder::PARAM_DATETIME_MUTABLE
),
$idComparison
)
)
);
}
+ } elseif ($lastKnownCommentId > 0) {
+ // We didn't find the "$lastKnownComment" but we still use the ID as an offset.
+ // This is required as a fall-back for expired messages in talk and deleted comments in other apps.
+ if ($sortDirection === 'desc') {
+ if ($includeLastKnown) {
+ $query->andWhere($query->expr()->lte('id', $query->createNamedParameter($lastKnownCommentId)));
+ } else {
+ $query->andWhere($query->expr()->lt('id', $query->createNamedParameter($lastKnownCommentId)));
+ }
+ } else {
+ if ($includeLastKnown) {
+ $query->andWhere($query->expr()->gte('id', $query->createNamedParameter($lastKnownCommentId)));
+ } else {
+ $query->andWhere($query->expr()->gt('id', $query->createNamedParameter($lastKnownCommentId)));
+ }
+ }
}
$resultStatement = $query->execute();
@@ -498,11 +522,10 @@ class Manager implements ICommentsManager {
* @param string $objectType the object type, e.g. 'files'
* @param string $objectId the id of the object
* @param int $id the comment to look for
- * @return Comment|null
*/
protected function getLastKnownComment(string $objectType,
- string $objectId,
- int $id) {
+ string $objectId,
+ int $id): ?IComment {
$query = $this->dbConn->getQueryBuilder();
$query->select('*')
->from('comments')
@@ -532,12 +555,12 @@ class Manager implements ICommentsManager {
* @param string $verb Limit the verb of the comment
* @param int $offset
* @param int $limit
- * @return IComment[]
+ * @return list<IComment>
*/
public function search(string $search, string $objectType, string $objectId, string $verb, int $offset, int $limit = 50): array {
$objectIds = [];
if ($objectId) {
- $objectIds[] = $objectIds;
+ $objectIds[] = $objectId;
}
return $this->searchForObjects($search, $objectType, $objectIds, $verb, $offset, $limit);
}
@@ -551,20 +574,23 @@ class Manager implements ICommentsManager {
* @param string $verb Limit the verb of the comment
* @param int $offset
* @param int $limit
- * @return IComment[]
+ * @return list<IComment>
*/
public function searchForObjects(string $search, string $objectType, array $objectIds, string $verb, int $offset, int $limit = 50): array {
$query = $this->dbConn->getQueryBuilder();
$query->select('*')
->from('comments')
- ->where($query->expr()->iLike('message', $query->createNamedParameter(
- '%' . $this->dbConn->escapeLikeParameter($search). '%'
- )))
->orderBy('creation_timestamp', 'DESC')
->addOrderBy('id', 'DESC')
->setMaxResults($limit);
+ if ($search !== '') {
+ $query->where($query->expr()->iLike('message', $query->createNamedParameter(
+ '%' . $this->dbConn->escapeLikeParameter($search) . '%'
+ )));
+ }
+
if ($objectType !== '') {
$query->andWhere($query->expr()->eq('object_type', $query->createNamedParameter($objectType)));
}
@@ -594,12 +620,12 @@ class Manager implements ICommentsManager {
* @param $objectType string the object type, e.g. 'files'
* @param $objectId string the id of the object
* @param \DateTime $notOlderThan optional, timestamp of the oldest comments
- * that may be returned
+ * that may be returned
* @param string $verb Limit the verb of the comment - Added in 14.0.0
* @return Int
* @since 9.0.0
*/
- public function getNumberOfCommentsForObject($objectType, $objectId, \DateTime $notOlderThan = null, $verb = '') {
+ public function getNumberOfCommentsForObject($objectType, $objectId, ?\DateTime $notOlderThan = null, $verb = '') {
$qb = $this->dbConn->getQueryBuilder();
$query = $qb->select($qb->func()->count('id'))
->from('comments')
@@ -634,6 +660,7 @@ class Manager implements ICommentsManager {
* @since 21.0.0
*/
public function getNumberOfUnreadCommentsForObjects(string $objectType, array $objectIds, IUser $user, $verb = ''): array {
+ $unreadComments = [];
$query = $this->dbConn->getQueryBuilder();
$query->select('c.object_id', $query->func()->count('c.id', 'num_comments'))
->from('comments', 'c')
@@ -643,7 +670,7 @@ class Manager implements ICommentsManager {
$query->expr()->eq('c.object_id', 'm.object_id')
))
->where($query->expr()->eq('c.object_type', $query->createNamedParameter($objectType)))
- ->andWhere($query->expr()->in('c.object_id', $query->createNamedParameter($objectIds, IQueryBuilder::PARAM_STR_ARRAY)))
+ ->andWhere($query->expr()->in('c.object_id', $query->createParameter('ids')))
->andWhere($query->expr()->orX(
$query->expr()->gt('c.creation_timestamp', 'm.marker_datetime'),
$query->expr()->isNull('m.marker_datetime')
@@ -654,12 +681,16 @@ class Manager implements ICommentsManager {
$query->andWhere($query->expr()->eq('c.verb', $query->createNamedParameter($verb)));
}
- $result = $query->execute();
$unreadComments = array_fill_keys($objectIds, 0);
- while ($row = $result->fetch()) {
- $unreadComments[$row['object_id']] = (int) $row['num_comments'];
+ foreach (array_chunk($objectIds, 1000) as $chunk) {
+ $query->setParameter('ids', $chunk, IQueryBuilder::PARAM_STR_ARRAY);
+
+ $result = $query->executeQuery();
+ while ($row = $result->fetch()) {
+ $unreadComments[$row['object_id']] = (int)$row['num_comments'];
+ }
+ $result->closeCursor();
}
- $result->closeCursor();
return $unreadComments;
}
@@ -673,6 +704,22 @@ class Manager implements ICommentsManager {
* @since 21.0.0
*/
public function getNumberOfCommentsForObjectSinceComment(string $objectType, string $objectId, int $lastRead, string $verb = ''): int {
+ if ($verb !== '') {
+ return $this->getNumberOfCommentsWithVerbsForObjectSinceComment($objectType, $objectId, $lastRead, [$verb]);
+ }
+
+ return $this->getNumberOfCommentsWithVerbsForObjectSinceComment($objectType, $objectId, $lastRead, []);
+ }
+
+ /**
+ * @param string $objectType
+ * @param string $objectId
+ * @param int $lastRead
+ * @param string[] $verbs
+ * @return int
+ * @since 24.0.0
+ */
+ public function getNumberOfCommentsWithVerbsForObjectSinceComment(string $objectType, string $objectId, int $lastRead, array $verbs): int {
$query = $this->dbConn->getQueryBuilder();
$query->select($query->func()->count('id', 'num_messages'))
->from('comments')
@@ -680,15 +727,15 @@ class Manager implements ICommentsManager {
->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
->andWhere($query->expr()->gt('id', $query->createNamedParameter($lastRead)));
- if ($verb !== '') {
- $query->andWhere($query->expr()->eq('verb', $query->createNamedParameter($verb)));
+ if (!empty($verbs)) {
+ $query->andWhere($query->expr()->in('verb', $query->createNamedParameter($verbs, IQueryBuilder::PARAM_STR_ARRAY)));
}
- $result = $query->execute();
+ $result = $query->executeQuery();
$data = $result->fetch();
$result->closeCursor();
- return (int) ($data['num_messages'] ?? 0);
+ return (int)($data['num_messages'] ?? 0);
}
/**
@@ -705,7 +752,7 @@ class Manager implements ICommentsManager {
->from('comments')
->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
- ->andWhere($query->expr()->lt('creation_timestamp', $query->createNamedParameter($beforeDate, IQueryBuilder::PARAM_DATE)))
+ ->andWhere($query->expr()->lt('creation_timestamp', $query->createNamedParameter($beforeDate, IQueryBuilder::PARAM_DATETIME_MUTABLE)))
->orderBy('creation_timestamp', 'desc');
if ($verb !== '') {
@@ -716,7 +763,7 @@ class Manager implements ICommentsManager {
$data = $result->fetch();
$result->closeCursor();
- return (int) ($data['id'] ?? 0);
+ return (int)($data['id'] ?? 0);
}
/**
@@ -734,7 +781,7 @@ class Manager implements ICommentsManager {
string $objectId,
string $verb,
string $actorType,
- array $actors
+ array $actors,
): array {
$lastComments = [];
@@ -761,54 +808,25 @@ class Manager implements ICommentsManager {
/**
* Get the number of unread comments for all files in a folder
*
+ * This is unused since 8bd39fccf411195839f2dadee085fad18ec52c23
+ *
* @param int $folderId
* @param IUser $user
* @return array [$fileId => $unreadCount]
*/
public function getNumberOfUnreadCommentsForFolder($folderId, IUser $user) {
- $qb = $this->dbConn->getQueryBuilder();
-
- $query = $qb->select('f.fileid')
- ->addSelect($qb->func()->count('c.id', 'num_ids'))
- ->from('filecache', 'f')
- ->leftJoin('f', 'comments', 'c', $qb->expr()->andX(
- $qb->expr()->eq('f.fileid', $qb->expr()->castColumn('c.object_id', IQueryBuilder::PARAM_INT)),
- $qb->expr()->eq('c.object_type', $qb->createNamedParameter('files'))
- ))
- ->leftJoin('c', 'comments_read_markers', 'm', $qb->expr()->andX(
- $qb->expr()->eq('c.object_id', 'm.object_id'),
- $qb->expr()->eq('m.object_type', $qb->createNamedParameter('files'))
- ))
- ->where(
- $qb->expr()->andX(
- $qb->expr()->eq('f.parent', $qb->createNamedParameter($folderId)),
- $qb->expr()->orX(
- $qb->expr()->eq('c.object_type', $qb->createNamedParameter('files')),
- $qb->expr()->isNull('c.object_type')
- ),
- $qb->expr()->orX(
- $qb->expr()->eq('m.object_type', $qb->createNamedParameter('files')),
- $qb->expr()->isNull('m.object_type')
- ),
- $qb->expr()->orX(
- $qb->expr()->eq('m.user_id', $qb->createNamedParameter($user->getUID())),
- $qb->expr()->isNull('m.user_id')
- ),
- $qb->expr()->orX(
- $qb->expr()->gt('c.creation_timestamp', 'm.marker_datetime'),
- $qb->expr()->isNull('m.marker_datetime')
- )
- )
- )->groupBy('f.fileid');
-
- $resultStatement = $query->execute();
-
- $results = [];
- while ($row = $resultStatement->fetch()) {
- $results[$row['fileid']] = (int) $row['num_ids'];
+ $directory = $this->rootFolder->getFirstNodeById($folderId);
+ if (!$directory instanceof Folder) {
+ return [];
}
- $resultStatement->closeCursor();
- return $results;
+ $children = $directory->getDirectoryListing();
+ $ids = array_map(fn (FileInfo $child) => (string)$child->getId(), $children);
+
+ $ids[] = (string)$directory->getId();
+ $counts = $this->getNumberOfUnreadCommentsForObjects('files', $ids, $user);
+ return array_filter($counts, function (int $count) {
+ return $count > 0;
+ });
}
/**
@@ -871,12 +889,177 @@ class Manager implements ICommentsManager {
}
if ($affectedRows > 0 && $comment instanceof IComment) {
+ if ($comment->getVerb() === 'reaction_deleted') {
+ $this->deleteReaction($comment);
+ }
$this->sendEvent(CommentsEvent::EVENT_DELETE, $comment);
}
return ($affectedRows > 0);
}
+ private function deleteReaction(IComment $reaction): void {
+ $qb = $this->dbConn->getQueryBuilder();
+ $qb->delete('reactions')
+ ->where($qb->expr()->eq('parent_id', $qb->createNamedParameter($reaction->getParentId())))
+ ->andWhere($qb->expr()->eq('message_id', $qb->createNamedParameter($reaction->getId())))
+ ->executeStatement();
+ $this->sumReactions($reaction->getParentId());
+ }
+
+ /**
+ * Get comment related with user reaction
+ *
+ * Throws PreConditionNotMetException when the system haven't the minimum requirements to
+ * use reactions
+ *
+ * @param int $parentId
+ * @param string $actorType
+ * @param string $actorId
+ * @param string $reaction
+ * @return IComment
+ * @throws NotFoundException
+ * @throws PreConditionNotMetException
+ * @since 24.0.0
+ */
+ public function getReactionComment(int $parentId, string $actorType, string $actorId, string $reaction): IComment {
+ $this->throwIfNotSupportReactions();
+ $qb = $this->dbConn->getQueryBuilder();
+ $messageId = $qb
+ ->select('message_id')
+ ->from('reactions')
+ ->where($qb->expr()->eq('parent_id', $qb->createNamedParameter($parentId)))
+ ->andWhere($qb->expr()->eq('actor_type', $qb->createNamedParameter($actorType)))
+ ->andWhere($qb->expr()->eq('actor_id', $qb->createNamedParameter($actorId)))
+ ->andWhere($qb->expr()->eq('reaction', $qb->createNamedParameter($reaction)))
+ ->executeQuery()
+ ->fetchOne();
+ if (!$messageId) {
+ throw new NotFoundException('Comment related with reaction not found');
+ }
+ return $this->get($messageId);
+ }
+
+ /**
+ * Retrieve all reactions of a message
+ *
+ * Throws PreConditionNotMetException when the system haven't the minimum requirements to
+ * use reactions
+ *
+ * @param int $parentId
+ * @return IComment[]
+ * @throws PreConditionNotMetException
+ * @since 24.0.0
+ */
+ public function retrieveAllReactions(int $parentId): array {
+ $this->throwIfNotSupportReactions();
+ $qb = $this->dbConn->getQueryBuilder();
+ $result = $qb
+ ->select('message_id')
+ ->from('reactions')
+ ->where($qb->expr()->eq('parent_id', $qb->createNamedParameter($parentId)))
+ ->orderBy('message_id', 'DESC')
+ ->executeQuery();
+
+ $commentIds = [];
+ while ($data = $result->fetch()) {
+ $commentIds[] = $data['message_id'];
+ }
+
+ return $this->getCommentsById($commentIds);
+ }
+
+ /**
+ * Retrieve all reactions with specific reaction of a message
+ *
+ * Throws PreConditionNotMetException when the system haven't the minimum requirements to
+ * use reactions
+ *
+ * @param int $parentId
+ * @param string $reaction
+ * @return IComment[]
+ * @throws PreConditionNotMetException
+ * @since 24.0.0
+ */
+ public function retrieveAllReactionsWithSpecificReaction(int $parentId, string $reaction): array {
+ $this->throwIfNotSupportReactions();
+ $qb = $this->dbConn->getQueryBuilder();
+ $result = $qb
+ ->select('message_id')
+ ->from('reactions')
+ ->where($qb->expr()->eq('parent_id', $qb->createNamedParameter($parentId)))
+ ->andWhere($qb->expr()->eq('reaction', $qb->createNamedParameter($reaction)))
+ ->executeQuery();
+
+ $commentIds = [];
+ while ($data = $result->fetch()) {
+ $commentIds[] = $data['message_id'];
+ }
+ $comments = [];
+ if ($commentIds) {
+ $comments = $this->getCommentsById($commentIds);
+ }
+
+ return $comments;
+ }
+
+ /**
+ * Support reactions
+ *
+ * @return bool
+ * @since 24.0.0
+ */
+ public function supportReactions(): bool {
+ return $this->emojiHelper->doesPlatformSupportEmoji();
+ }
+
+ /**
+ * @throws PreConditionNotMetException
+ * @since 24.0.0
+ */
+ private function throwIfNotSupportReactions() {
+ if (!$this->supportReactions()) {
+ throw new PreConditionNotMetException('The database does not support reactions');
+ }
+ }
+
+ /**
+ * Get all comments on list
+ *
+ * @param int[] $commentIds
+ * @return IComment[]
+ * @since 24.0.0
+ */
+ private function getCommentsById(array $commentIds): array {
+ if (!$commentIds) {
+ return [];
+ }
+
+ $chunks = array_chunk($commentIds, 500);
+
+ $query = $this->dbConn->getQueryBuilder();
+ $query->select('*')
+ ->from('comments')
+ ->where($query->expr()->in('id', $query->createParameter('ids')))
+ ->orderBy('creation_timestamp', 'DESC')
+ ->addOrderBy('id', 'DESC');
+
+ $comments = [];
+ foreach ($chunks as $ids) {
+ $query->setParameter('ids', $ids, IQueryBuilder::PARAM_STR_ARRAY);
+
+ $result = $query->executeQuery();
+ while ($data = $result->fetch()) {
+ $comment = $this->getCommentFromData($data);
+ $this->cache($comment);
+ $comments[] = $comment;
+ }
+ $result->closeCursor();
+ }
+
+ return $comments;
+ }
+
/**
* saves the comment permanently
*
@@ -888,19 +1071,27 @@ class Manager implements ICommentsManager {
* Throws NotFoundException when a comment that is to be updated does not
* exist anymore at this point of time.
*
+ * Throws PreConditionNotMetException when the system haven't the minimum requirements to
+ * use reactions
+ *
* @param IComment $comment
* @return bool
* @throws NotFoundException
+ * @throws PreConditionNotMetException
* @since 9.0.0
*/
public function save(IComment $comment) {
+ if ($comment->getVerb() === 'reaction') {
+ $this->throwIfNotSupportReactions();
+ }
+
if ($this->prepareCommentForDatabaseWrite($comment)->getId() === '') {
$result = $this->insert($comment);
} else {
$result = $this->update($comment);
}
- if ($result && !!$comment->getParentId()) {
+ if ($result && (bool)$comment->getParentId()) {
$this->updateChildrenInformation(
$comment->getParentId(),
$comment->getCreationDateTime()
@@ -918,22 +1109,6 @@ class Manager implements ICommentsManager {
* @return bool
*/
protected function insert(IComment $comment): bool {
- try {
- $result = $this->insertQuery($comment, true);
- } catch (InvalidFieldNameException $e) {
- // The reference id field was only added in Nextcloud 19.
- // In order to not cause too long waiting times on the update,
- // it was decided to only add it lazy, as it is also not a critical
- // feature, but only helps to have a better experience while commenting.
- // So in case the reference_id field is missing,
- // we simply save the comment without that field.
- $result = $this->insertQuery($comment, false);
- }
-
- return $result;
- }
-
- protected function insertQuery(IComment $comment, bool $tryWritingReferenceId): bool {
$qb = $this->dbConn->getQueryBuilder();
$values = [
@@ -948,24 +1123,99 @@ class Manager implements ICommentsManager {
'latest_child_timestamp' => $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'),
'object_type' => $qb->createNamedParameter($comment->getObjectType()),
'object_id' => $qb->createNamedParameter($comment->getObjectId()),
+ 'expire_date' => $qb->createNamedParameter($comment->getExpireDate(), 'datetime'),
+ 'reference_id' => $qb->createNamedParameter($comment->getReferenceId()),
+ 'meta_data' => $qb->createNamedParameter(json_encode($comment->getMetaData())),
];
- if ($tryWritingReferenceId) {
- $values['reference_id'] = $qb->createNamedParameter($comment->getReferenceId());
- }
-
$affectedRows = $qb->insert('comments')
->values($values)
->execute();
if ($affectedRows > 0) {
$comment->setId((string)$qb->getLastInsertId());
+ if ($comment->getVerb() === 'reaction') {
+ $this->addReaction($comment);
+ }
$this->sendEvent(CommentsEvent::EVENT_ADD, $comment);
}
return $affectedRows > 0;
}
+ private function addReaction(IComment $reaction): void {
+ // Prevent violate constraint
+ $qb = $this->dbConn->getQueryBuilder();
+ $qb->select($qb->func()->count('*'))
+ ->from('reactions')
+ ->where($qb->expr()->eq('parent_id', $qb->createNamedParameter($reaction->getParentId())))
+ ->andWhere($qb->expr()->eq('actor_type', $qb->createNamedParameter($reaction->getActorType())))
+ ->andWhere($qb->expr()->eq('actor_id', $qb->createNamedParameter($reaction->getActorId())))
+ ->andWhere($qb->expr()->eq('reaction', $qb->createNamedParameter($reaction->getMessage())));
+ $result = $qb->executeQuery();
+ $exists = (int)$result->fetchOne();
+ if (!$exists) {
+ $qb = $this->dbConn->getQueryBuilder();
+ try {
+ $qb->insert('reactions')
+ ->values([
+ 'parent_id' => $qb->createNamedParameter($reaction->getParentId()),
+ 'message_id' => $qb->createNamedParameter($reaction->getId()),
+ 'actor_type' => $qb->createNamedParameter($reaction->getActorType()),
+ 'actor_id' => $qb->createNamedParameter($reaction->getActorId()),
+ 'reaction' => $qb->createNamedParameter($reaction->getMessage()),
+ ])
+ ->executeStatement();
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage(), [
+ 'exception' => $e,
+ 'app' => 'core_comments',
+ ]);
+ }
+ }
+ $this->sumReactions($reaction->getParentId());
+ }
+
+ private function sumReactions(string $parentId): void {
+ $totalQuery = $this->dbConn->getQueryBuilder();
+ $totalQuery
+ ->selectAlias(
+ $totalQuery->func()->concat(
+ $totalQuery->expr()->literal('"'),
+ 'reaction',
+ $totalQuery->expr()->literal('":'),
+ $totalQuery->func()->count('id')
+ ),
+ 'colonseparatedvalue'
+ )
+ ->selectAlias($totalQuery->func()->count('id'), 'total')
+ ->from('reactions', 'r')
+ ->where($totalQuery->expr()->eq('r.parent_id', $totalQuery->createNamedParameter($parentId)))
+ ->groupBy('r.reaction')
+ ->orderBy('total', 'DESC')
+ ->addOrderBy('r.reaction', 'ASC')
+ ->setMaxResults(20);
+
+ $jsonQuery = $this->dbConn->getQueryBuilder();
+ $jsonQuery
+ ->selectAlias(
+ $jsonQuery->func()->concat(
+ $jsonQuery->expr()->literal('{'),
+ $jsonQuery->func()->groupConcat('colonseparatedvalue'),
+ $jsonQuery->expr()->literal('}')
+ ),
+ 'json'
+ )
+ ->from($jsonQuery->createFunction('(' . $totalQuery->getSQL() . ')'), 'json');
+
+ $qb = $this->dbConn->getQueryBuilder();
+ $qb
+ ->update('comments')
+ ->set('reactions', $qb->createFunction('(' . $jsonQuery->getSQL() . ')'))
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter($parentId)))
+ ->executeStatement();
+ }
+
/**
* updates a Comment data row
*
@@ -980,11 +1230,10 @@ class Manager implements ICommentsManager {
$this->sendEvent(CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId()));
$this->uncache($comment->getId());
- try {
- $result = $this->updateQuery($comment, true);
- } catch (InvalidFieldNameException $e) {
- // See function insert() for explanation
- $result = $this->updateQuery($comment, false);
+ $result = $this->updateQuery($comment);
+
+ if ($comment->getVerb() === 'reaction_deleted') {
+ $this->deleteReaction($comment);
}
$this->sendEvent(CommentsEvent::EVENT_UPDATE, $comment);
@@ -992,7 +1241,7 @@ class Manager implements ICommentsManager {
return $result;
}
- protected function updateQuery(IComment $comment, bool $tryWritingReferenceId): bool {
+ protected function updateQuery(IComment $comment): bool {
$qb = $this->dbConn->getQueryBuilder();
$qb
->update('comments')
@@ -1006,14 +1255,12 @@ class Manager implements ICommentsManager {
->set('creation_timestamp', $qb->createNamedParameter($comment->getCreationDateTime(), 'datetime'))
->set('latest_child_timestamp', $qb->createNamedParameter($comment->getLatestChildDateTime(), 'datetime'))
->set('object_type', $qb->createNamedParameter($comment->getObjectType()))
- ->set('object_id', $qb->createNamedParameter($comment->getObjectId()));
-
- if ($tryWritingReferenceId) {
- $qb->set('reference_id', $qb->createNamedParameter($comment->getReferenceId()));
- }
-
- $affectedRows = $qb->where($qb->expr()->eq('id', $qb->createNamedParameter($comment->getId())))
- ->execute();
+ ->set('object_id', $qb->createNamedParameter($comment->getObjectId()))
+ ->set('expire_date', $qb->createNamedParameter($comment->getExpireDate(), 'datetime'))
+ ->set('reference_id', $qb->createNamedParameter($comment->getReferenceId()))
+ ->set('meta_data', $qb->createNamedParameter(json_encode($comment->getMetaData())))
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter($comment->getId())));
+ $affectedRows = $qb->executeStatement();
if ($affectedRows === 0) {
throw new NotFoundException('Comment to update does ceased to exist');
@@ -1296,6 +1543,7 @@ class Manager implements ICommentsManager {
foreach ($entities as $entity) {
$entity->handle($event);
}
+ $this->eventDispatcher->dispatchTyped($event);
}
/**
@@ -1304,7 +1552,28 @@ class Manager implements ICommentsManager {
* @since 21.0.0
*/
public function load(): void {
- $this->initialStateService->provideInitialState(Application::APP_ID, 'max-message-length', IComment::MAX_MESSAGE_LENGTH);
- Util::addScript(Application::APP_ID, 'comments-app');
+ $this->initialStateService->provideInitialState('comments', 'max-message-length', IComment::MAX_MESSAGE_LENGTH);
+ Util::addScript('comments', 'comments-app');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function deleteCommentsExpiredAtObject(string $objectType, string $objectId = ''): bool {
+ $qb = $this->dbConn->getQueryBuilder();
+ $qb->delete('comments')
+ ->where($qb->expr()->lte('expire_date',
+ $qb->createNamedParameter($this->timeFactory->getDateTime(), IQueryBuilder::PARAM_DATETIME_MUTABLE)))
+ ->andWhere($qb->expr()->eq('object_type', $qb->createNamedParameter($objectType)));
+
+ if ($objectId !== '') {
+ $qb->andWhere($qb->expr()->eq('object_id', $qb->createNamedParameter($objectId)));
+ }
+
+ $affectedRows = $qb->executeStatement();
+
+ $this->commentsCache = [];
+
+ return $affectedRows > 0;
}
}
diff --git a/lib/private/Comments/ManagerFactory.php b/lib/private/Comments/ManagerFactory.php
index ec2cb1e69a1..2436ca74c66 100644
--- a/lib/private/Comments/ManagerFactory.php
+++ b/lib/private/Comments/ManagerFactory.php
@@ -1,29 +1,10 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OC\Comments;
use OCP\Comments\ICommentsManager;
@@ -31,7 +12,6 @@ use OCP\Comments\ICommentsManagerFactory;
use OCP\IServerContainer;
class ManagerFactory implements ICommentsManagerFactory {
-
/**
* Server container
*