diff options
Diffstat (limited to 'lib/private/Comments')
-rw-r--r-- | lib/private/Comments/Comment.php | 252 | ||||
-rw-r--r-- | lib/private/Comments/Manager.php | 377 | ||||
-rw-r--r-- | lib/private/Comments/ManagerFactory.php | 26 |
3 files changed, 273 insertions, 382 deletions
diff --git a/lib/private/Comments/Comment.php b/lib/private/Comments/Comment.php index f9e5c166872..7190f252c82 100644 --- a/lib/private/Comments/Comment.php +++ b/lib/private/Comments/Comment.php @@ -1,27 +1,9 @@ <?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; @@ -30,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' => '', @@ -42,6 +24,7 @@ class Comment implements IComment { 'objectType' => '', 'objectId' => '', 'referenceId' => null, + 'metaData' => null, 'creationDT' => null, 'latestChildDT' => null, 'reactions' => null, @@ -51,31 +34,30 @@ class Comment implements IComment { /** * 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 @@ -87,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.'); } @@ -102,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.'); } @@ -127,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.'); } @@ -153,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.'); } @@ -178,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']; } @@ -192,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; @@ -212,25 +185,16 @@ 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]+\"|\"group\/[a-z0-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 []; } $mentionIds = array_unique($mentions[0]); @@ -239,11 +203,36 @@ class Comment implements IComment { }); $result = []; foreach ($mentionIds as $mentionId) { + // Cut-off the @ and remove wrapping double-quotes + /** @var non-empty-lowercase-string $cleanId */ $cleanId = trim(substr($mentionId, 1), '"'); - if (strpos($cleanId, 'guest/') === 0) { + + if (str_starts_with($cleanId, 'guest/')) { $result[] = ['type' => 'guest', 'id' => $cleanId]; - } elseif (strpos($cleanId, 'group/') === 0) { - $result[] = ['type' => 'group', 'id' => substr($cleanId, 6)]; + } 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' => $cleanId]; } @@ -252,23 +241,21 @@ class Comment implements IComment { } /** - * 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.'); } @@ -277,34 +264,29 @@ 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($actorId) || $actorId === '' @@ -317,76 +299,68 @@ 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']; } /** * @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($objectId) || trim($objectId) === '' @@ -399,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 { @@ -409,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 { @@ -431,6 +402,34 @@ 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'] ?? []; } @@ -463,9 +462,8 @@ class Comment implements IComment { * 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 00cf323bfbf..047fa361dad 100644 --- a/lib/private/Comments/Manager.php +++ b/lib/private/Comments/Manager.php @@ -1,35 +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@protonmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Simounet <contact@simounet.net> - * @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\DAV\Connector\Sabre\File; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Comments\CommentsEvent; use OCP\Comments\IComment; @@ -37,58 +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\IEmojiHelper; -use OCP\IUser; 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 IEmojiHelper */ - protected $emojiHelper; - - /** @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, - IEmojiHelper $emojiHelper, - IInitialStateService $initialStateService) { - $this->dbConn = $dbConn; - $this->logger = $logger; - $this->config = $config; - $this->timeFactory = $timeFactory; - $this->emojiHelper = $emojiHelper; - $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, + ) { } /** @@ -96,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']; @@ -110,7 +72,8 @@ class Manager implements ICommentsManager { $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); @@ -134,11 +97,6 @@ class Manager implements ICommentsManager { return $data; } - - /** - * @param array $data - * @return IComment - */ public function getCommentFromData(array $data): IComment { return new Comment($this->normalizeDatabaseData($data)); } @@ -152,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() @@ -172,7 +130,9 @@ class Manager implements ICommentsManager { $comment->setLatestChildDateTime(null); } - if (is_null($comment->getCreationDateTime())) { + try { + $comment->getCreationDateTime(); + } catch (\LogicException $e) { $comment->setCreationDateTime(new \DateTime()); } @@ -191,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(); @@ -210,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') @@ -237,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) @@ -248,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; @@ -264,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]); @@ -275,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.'); } @@ -309,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'] = []; @@ -378,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( @@ -390,7 +320,7 @@ class Manager implements ICommentsManager { $objectId, $limit = 0, $offset = 0, - \DateTime $notOlderThan = null + ?\DateTime $notOlderThan = null, ) { $comments = []; @@ -432,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, @@ -443,7 +373,8 @@ 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, @@ -452,7 +383,8 @@ class Manager implements ICommentsManager { $lastKnownCommentId, $sortDirection, $limit, - $includeLastKnown + $includeLastKnown, + $topmostParentId, ); } @@ -463,9 +395,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[] + * @param string $topmostParentId Limit the comments to a list of replies and its original root comment + * @return list<IComment> */ public function getCommentsWithVerbForObjectSinceComment( string $objectType, @@ -474,7 +407,8 @@ class Manager implements ICommentsManager { int $lastKnownCommentId, string $sortDirection = 'asc', int $limit = 30, - bool $includeLastKnown = false + bool $includeLastKnown = false, + string $topmostParentId = '', ): array { $comments = []; @@ -494,6 +428,13 @@ class Manager implements ICommentsManager { $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, @@ -511,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 ) @@ -534,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(); @@ -565,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') @@ -599,7 +555,7 @@ 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 = []; @@ -618,7 +574,7 @@ 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(); @@ -631,7 +587,7 @@ class Manager implements ICommentsManager { if ($search !== '') { $query->where($query->expr()->iLike('message', $query->createNamedParameter( - '%' . $this->dbConn->escapeLikeParameter($search). '%' + '%' . $this->dbConn->escapeLikeParameter($search) . '%' ))); } @@ -664,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') @@ -731,7 +687,7 @@ class Manager implements ICommentsManager { $result = $query->executeQuery(); while ($row = $result->fetch()) { - $unreadComments[$row['object_id']] = (int) $row['num_comments']; + $unreadComments[$row['object_id']] = (int)$row['num_comments']; } $result->closeCursor(); } @@ -779,7 +735,7 @@ class Manager implements ICommentsManager { $data = $result->fetch(); $result->closeCursor(); - return (int) ($data['num_messages'] ?? 0); + return (int)($data['num_messages'] ?? 0); } /** @@ -796,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 !== '') { @@ -807,7 +763,7 @@ class Manager implements ICommentsManager { $data = $result->fetch(); $result->closeCursor(); - return (int) ($data['id'] ?? 0); + return (int)($data['id'] ?? 0); } /** @@ -825,7 +781,7 @@ class Manager implements ICommentsManager { string $objectId, string $verb, string $actorType, - array $actors + array $actors, ): array { $lastComments = []; @@ -852,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; + }); } /** @@ -1031,6 +958,7 @@ class Manager implements ICommentsManager { ->select('message_id') ->from('reactions') ->where($qb->expr()->eq('parent_id', $qb->createNamedParameter($parentId))) + ->orderBy('message_id', 'DESC') ->executeQuery(); $commentIds = []; @@ -1106,22 +1034,29 @@ class Manager implements ICommentsManager { if (!$commentIds) { return []; } - $query = $this->dbConn->getQueryBuilder(); + $chunks = array_chunk($commentIds, 500); + + $query = $this->dbConn->getQueryBuilder(); $query->select('*') ->from('comments') - ->where($query->expr()->in('id', $query->createNamedParameter($commentIds, IQueryBuilder::PARAM_STR_ARRAY))) + ->where($query->expr()->in('id', $query->createParameter('ids'))) ->orderBy('creation_timestamp', 'DESC') ->addOrderBy('id', 'DESC'); $comments = []; - $result = $query->executeQuery(); - while ($data = $result->fetch()) { - $comment = $this->getCommentFromData($data); - $this->cache($comment); - $comments[] = $comment; + 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(); } - $result->closeCursor(); + return $comments; } @@ -1156,7 +1091,7 @@ class Manager implements ICommentsManager { $result = $this->update($comment); } - if ($result && !!$comment->getParentId()) { + if ($result && (bool)$comment->getParentId()) { $this->updateChildrenInformation( $comment->getParentId(), $comment->getCreationDateTime() @@ -1174,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 = [ @@ -1205,12 +1124,10 @@ class Manager implements ICommentsManager { '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(); @@ -1236,7 +1153,7 @@ class Manager implements ICommentsManager { ->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(); + $exists = (int)$result->fetchOne(); if (!$exists) { $qb = $this->dbConn->getQueryBuilder(); try { @@ -1313,12 +1230,7 @@ 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); @@ -1329,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') @@ -1343,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'); @@ -1633,6 +1543,7 @@ class Manager implements ICommentsManager { foreach ($entities as $entity) { $entity->handle($event); } + $this->eventDispatcher->dispatchTyped($event); } /** @@ -1652,7 +1563,7 @@ class Manager implements ICommentsManager { $qb = $this->dbConn->getQueryBuilder(); $qb->delete('comments') ->where($qb->expr()->lte('expire_date', - $qb->createNamedParameter($this->timeFactory->getDateTime(), IQueryBuilder::PARAM_DATE))) + $qb->createNamedParameter($this->timeFactory->getDateTime(), IQueryBuilder::PARAM_DATETIME_MUTABLE))) ->andWhere($qb->expr()->eq('object_type', $qb->createNamedParameter($objectType))); if ($objectId !== '') { diff --git a/lib/private/Comments/ManagerFactory.php b/lib/private/Comments/ManagerFactory.php index 2b59a284b61..2436ca74c66 100644 --- a/lib/private/Comments/ManagerFactory.php +++ b/lib/private/Comments/ManagerFactory.php @@ -1,27 +1,9 @@ <?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; |