From 3f05e9280642694a7b6d38d0a9f9957cb6b12956 Mon Sep 17 00:00:00 2001 From: dartcafe Date: Thu, 21 Dec 2023 09:10:51 +0100 Subject: [PATCH 1/9] delete and restore comments Signed-off-by: dartcafe --- appinfo/routes.php | 2 + lib/Controller/CommentController.php | 15 ++++ lib/Controller/PublicController.php | 12 ++++ lib/Cron/JanitorCron.php | 21 ++++-- lib/Db/Comment.php | 9 +++ lib/Db/CommentMapper.php | 31 ++++---- lib/Event/CommentDeleteEvent.php | 2 +- lib/Event/CommentEvent.php | 1 + lib/Migration/TableSchema.php | 1 + lib/Service/ActivityService.php | 6 ++ lib/Service/AnonymizeService.php | 2 +- lib/Service/CommentService.php | 37 +++++----- src/js/Api/modules/comments.js | 9 +++ src/js/Api/modules/public.js | 10 +++ .../Actions/modules/ActionDelete.vue | 18 ++++- src/js/components/Comments/CommentItem.vue | 54 ++++++++++---- src/js/components/Comments/Comments.vue | 30 ++++---- src/js/helpers/modules/arrayHelper.js | 70 ++++++++++++++++++- src/js/store/modules/comments.js | 51 ++++++++++++-- 19 files changed, 304 insertions(+), 77 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index c0498c6b9..0b62809b0 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -38,6 +38,7 @@ ['name' => 'public#add_option', 'url' => '/s/{token}/option', 'verb' => 'POST'], ['name' => 'public#delete_option', 'url' => '/s/{token}/option/{optionId}', 'verb' => 'DELETE'], ['name' => 'public#add_comment', 'url' => '/s/{token}/comment', 'verb' => 'POST'], + ['name' => 'public#restore_comment', 'url' => '/s/{token}/comment/{commentId}/restore', 'verb' => 'PUT', 'postfix' => 'auth'], ['name' => 'public#delete_comment', 'url' => '/s/{token}/comment/{commentId}', 'verb' => 'DELETE', 'postfix' => 'public'], ['name' => 'public#subscribe', 'url' => '/s/{token}/subscribe', 'verb' => 'PUT'], ['name' => 'public#unsubscribe', 'url' => '/s/{token}/unsubscribe', 'verb' => 'PUT'], @@ -114,6 +115,7 @@ ['name' => 'comment#list', 'url' => '/poll/{pollId}/comments', 'verb' => 'GET'], ['name' => 'comment#add', 'url' => '/poll/{pollId}/comment', 'verb' => 'POST'], + ['name' => 'comment#restore', 'url' => '/comment/{commentId}/restore', 'verb' => 'PUT', 'postfix' => 'auth'], ['name' => 'comment#delete', 'url' => '/comment/{commentId}', 'verb' => 'DELETE', 'postfix' => 'auth'], ['name' => 'system#user_search', 'url' => '/search/users/{query}', 'verb' => 'GET'], diff --git a/lib/Controller/CommentController.php b/lib/Controller/CommentController.php index d207a571f..01666007a 100644 --- a/lib/Controller/CommentController.php +++ b/lib/Controller/CommentController.php @@ -74,4 +74,19 @@ public function delete(int $commentId): JSONResponse { 'comment' => $this->commentService->delete($comment, $this->acl->setPollId($comment->getPollId())) ]); } + + /** + * Restore deleted Comment + */ + #[NoAdminRequired] + public function restore(int $commentId, ?array $commentToRestore = null): JSONResponse { + $comment = $this->commentService->get($commentId); + if ($commentToRestore) { + $comment->setComment($commentToRestore['comment'] ?? $comment->getComment()); + } + + return $this->response(fn () => [ + 'comment' => $this->commentService->delete($comment, $this->acl->setPollId($comment->getPollId()), true) + ]); + } } diff --git a/lib/Controller/PublicController.php b/lib/Controller/PublicController.php index 23329c808..0913f960d 100644 --- a/lib/Controller/PublicController.php +++ b/lib/Controller/PublicController.php @@ -217,6 +217,18 @@ public function deleteComment(int $commentId, string $token): JSONResponse { ], $token); } + /** + * Restore deleted Comment + */ + #[PublicPage] + public function restoreComment(int $commentId, string $token): JSONResponse { + $comment = $this->commentService->get($commentId); + + return $this->response(fn () => [ + 'comment' => $this->commentService->delete($comment, $this->acl->setToken($token, Acl::PERMISSION_COMMENT_ADD), true) + ]); + } + /** * Get subscription status */ diff --git a/lib/Cron/JanitorCron.php b/lib/Cron/JanitorCron.php index 5e57edaec..e65a614da 100644 --- a/lib/Cron/JanitorCron.php +++ b/lib/Cron/JanitorCron.php @@ -25,6 +25,7 @@ namespace OCA\Polls\Cron; +use OCA\Polls\Db\CommentMapper; use OCA\Polls\Db\LogMapper; use OCA\Polls\Db\PollMapper; use OCA\Polls\Db\WatchMapper; @@ -39,7 +40,8 @@ public function __construct( protected ITimeFactory $time, private LogMapper $logMapper, private PollMapper $pollMapper, - private WatchMapper $watchMapper + private WatchMapper $watchMapper, + private CommentMapper $commentMapper, ) { parent::__construct($time); parent::setInterval(86400); // run once a day @@ -54,14 +56,23 @@ public function __construct( * @return void */ protected function run($argument) { - $this->logMapper->deleteProcessedEntries(); // delete processed log entries - $this->logMapper->deleteOldEntries(time() - (86400 * 7)); // delete entries older than 7 days - $this->watchMapper->deleteOldEntries(time() - 86400); // delete entries older than 1 day + // delete processed log entries + $this->logMapper->deleteProcessedEntries(); + + // delete entries older than 7 days + $this->logMapper->deleteOldEntries(time() - (86400 * 7)); + + // delete entries older than 1 day + $this->watchMapper->deleteOldEntries(time() - 86400); + // purge entries virtually deleted more than 12 hour ago + $this->commentMapper->purgeDeletedComments(time() - 4320); + + // archive polls after defined days after closing date if ($this->appSettings->getBooleanSetting(AppSettings::SETTING_AUTO_ARCHIVE) && $this->appSettings->getIntegerSetting(AppSettings::SETTING_AUTO_ARCHIVE_OFFSET) > 0) { $this->pollMapper->archiveExpiredPolls( time() - ($this->appSettings->getAutoarchiveOffset() * 86400) - ); // archive polls after defined days after closing date + ); } } } diff --git a/lib/Db/Comment.php b/lib/Db/Comment.php index 265b5fef0..90de46c5c 100644 --- a/lib/Db/Comment.php +++ b/lib/Db/Comment.php @@ -40,6 +40,10 @@ * @method void setComment(string $value) * @method int getTimestamp() * @method void setTimestamp(integer $value) + * @method int getDeleted() + * @method void setDeleted(integer $value) + * @method int getParent() + * @method void setParent(integer $value) */ class Comment extends EntityWithUser implements JsonSerializable { public const TABLE = 'polls_comments'; @@ -49,7 +53,9 @@ class Comment extends EntityWithUser implements JsonSerializable { protected int $pollId = 0; protected string $userId = ''; protected int $timestamp = 0; + protected int $deleted = 0; protected ?string $comment = null; + protected int $parent = 0; public function __construct() { $this->addType('pollId', 'int'); @@ -66,6 +72,8 @@ public function jsonSerialize(): array { 'timestamp' => $this->getTimestamp(), 'comment' => $this->getComment(), 'user' => $this->getUser(), + 'parent' => $this->getParent(), + 'deleted' => $this->getDeleted(), 'subComments' => $this->getSubComments(), ]; } @@ -74,6 +82,7 @@ public function addSubComment(Comment $comment): void { $this->subComments[] = [ 'id' => $comment->getId(), 'comment' => $comment->getComment(), + 'deleted' => $comment->getDeleted(), 'timestamp' => $this->getTimestamp(), ]; } diff --git a/lib/Db/CommentMapper.php b/lib/Db/CommentMapper.php index 92bce62cf..67f9fd844 100644 --- a/lib/Db/CommentMapper.php +++ b/lib/Db/CommentMapper.php @@ -51,12 +51,17 @@ public function find(int $id): Comment { } /** + * @param int $pollId id of poll to get comments from + * @param bool $getDeleted Get deleted comments as well * @throws \OCP\AppFramework\Db\DoesNotExistException if not found * @return Comment[] */ - public function findByPoll(int $pollId): array { + public function findByPoll(int $pollId, bool $getDeleted = false): array { $qb = $this->buildQuery(); $qb->where($qb->expr()->eq(self::TABLE . '.poll_id', $qb->createNamedParameter($pollId, IQueryBuilder::PARAM_INT))); + if (!$getDeleted) { + $qb->andWhere($qb->expr()->eq(self::TABLE . '.deleted', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); + } return $this->findEntities($qb); } @@ -70,20 +75,6 @@ public function deleteByPoll(int $pollId): void { $qb->executeStatement(); } - /** - * @return void - */ - public function deleteComment(int $id): void { - $qb = $this->db->getQueryBuilder(); - - $qb->delete($this->getTableName(), self::TABLE) - ->where( - $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) - ); - - $qb->executeStatement(); - } - /** * @return void */ @@ -95,6 +86,16 @@ public function renameUserId(string $userId, string $replacementName): void { ->executeStatement(); } + public function purgeDeletedComments(int $offset): void { + $query = $this->db->getQueryBuilder(); + $query->delete($this->getTableName()) + ->where( + $query->expr()->lt('deleted', $query->createNamedParameter($offset)) + ); + $query->executeStatement(); + + } + /** * Build the enhanced query with joined tables */ diff --git a/lib/Event/CommentDeleteEvent.php b/lib/Event/CommentDeleteEvent.php index 5efa622b9..f1403529e 100644 --- a/lib/Event/CommentDeleteEvent.php +++ b/lib/Event/CommentDeleteEvent.php @@ -31,6 +31,6 @@ public function __construct( ) { parent::__construct($comment); $this->log = false; - $this->eventId = self::DELETE; + $this->eventId = $comment->getDeleted() ? self::DELETE : self::RESTORE; } } diff --git a/lib/Event/CommentEvent.php b/lib/Event/CommentEvent.php index 5297a078c..ee734f4b3 100644 --- a/lib/Event/CommentEvent.php +++ b/lib/Event/CommentEvent.php @@ -28,6 +28,7 @@ abstract class CommentEvent extends BaseEvent { public const ADD = 'comment_add'; public const DELETE = 'comment_delete'; + public const RESTORE = 'comment_restore'; public function __construct( protected Comment $comment, diff --git a/lib/Migration/TableSchema.php b/lib/Migration/TableSchema.php index fe5d6f7dd..d1fdd0192 100644 --- a/lib/Migration/TableSchema.php +++ b/lib/Migration/TableSchema.php @@ -210,6 +210,7 @@ abstract class TableSchema { 'user_id' => ['type' => Types::STRING, 'options' => ['notnull' => true, 'default' => '', 'length' => 256]], 'comment' => ['type' => Types::STRING, 'options' => ['notnull' => false, 'default' => null, 'length' => 1024]], 'timestamp' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], + 'deleted' => ['type' => Types::BIGINT, 'options' => ['notnull' => true, 'default' => 0, 'length' => 20]], ], Share::TABLE => [ 'id' => ['type' => Types::BIGINT, 'options' => ['autoincrement' => true, 'notnull' => true, 'length' => 20]], diff --git a/lib/Service/ActivityService.php b/lib/Service/ActivityService.php index e3ffc58a9..0b3cbca6d 100644 --- a/lib/Service/ActivityService.php +++ b/lib/Service/ActivityService.php @@ -122,6 +122,12 @@ private function getMatchedMessages(): array { self::FIRST_PERSON_FILTERED => $this->l10n->t('You have deleted a comment'), self::THIRD_PERSON_FILTERED => $this->l10n->t('{actor} has deleted a comment'), ], + CommentEvent::RESTORE => [ + self::FIRST_PERSON_FULL => $this->l10n->t('You have restored a comment from poll {pollTitle}'), + self::THIRD_PERSON_FULL => $this->l10n->t('{actor} has restored a comment from poll {pollTitle}'), + self::FIRST_PERSON_FILTERED => $this->l10n->t('You have restored a comment'), + self::THIRD_PERSON_FILTERED => $this->l10n->t('{actor} has restored a comment'), + ], OptionEvent::ADD => [ self::FIRST_PERSON_FULL => $this->l10n->t('You have added an option to poll {pollTitle}'), self::THIRD_PERSON_FULL => $this->l10n->t('{actor} has added an option to poll {pollTitle}'), diff --git a/lib/Service/AnonymizeService.php b/lib/Service/AnonymizeService.php index b84be82dc..f8df4a286 100644 --- a/lib/Service/AnonymizeService.php +++ b/lib/Service/AnonymizeService.php @@ -67,7 +67,7 @@ public function anonymize(array &$array): void { public function set(int $pollId, string $userId): void { $this->userId = $userId; $votes = $this->voteMapper->findByPoll($pollId); - $comments = $this->commentMapper->findByPoll($pollId); + $comments = $this->commentMapper->findByPoll($pollId, true); $options = $this->optionMapper->findByPoll($pollId); $i = 0; diff --git a/lib/Service/CommentService.php b/lib/Service/CommentService.php index 5fe2c925b..869375aba 100644 --- a/lib/Service/CommentService.php +++ b/lib/Service/CommentService.php @@ -74,22 +74,21 @@ private function listFlat(Acl $acl) : array { public function list(Acl $acl): array { $comments = $this->listFlat($acl); $timeTolerance = 5 * 60; // treat comments within 5 minutes as one comment - $groupedComments = []; - - foreach ($comments as $comment) { - // Create a new comment if comment is from another user than the last in the list - // or the timespan beteen comments is less than the tolerance (i.e. 5 minutes) - if (!count($groupedComments) - || !($comment->getDisplayName() === end($groupedComments)->getDisplayName() - && $comment->getTimestamp() - end($groupedComments)->getTimestamp() < $timeTolerance) - ) { - $groupedComments[] = $comment; + $tempId = null; + $tempUserId = null; + $tempTimestamp = null; + + foreach ($comments as &$comment) { + if ($comment->getUserId() === $tempUserId && $comment->getTimestamp() - $tempTimestamp < $timeTolerance) { + $comment->setParent($tempId); + } else { + $tempUserId = $comment->getUserId(); + $tempId = $comment->getId(); + $tempTimestamp = $comment->getTimestamp(); } - - // Add current comment as subComment element - $groupedComments[array_key_last($groupedComments)]->addSubComment($comment); } - return $groupedComments; + + return $comments; } /** @@ -116,15 +115,17 @@ public function add(string $message, Acl $acl): Comment { /** * Delete comment + * @param Comment $comment Comment to delete or restore + * @param Acl $acl Acl + * @param bool $restore Set true, if comment is to be restored */ - public function delete(Comment $comment, Acl $acl): Comment { + public function delete(Comment $comment, Acl $acl, bool $restore = false): Comment { $acl->validatePollId($comment->getPollId()); - if (!$acl->getIsOwner()) { $acl->validateUserId($comment->getUserId()); } - - $this->commentMapper->delete($comment); + $comment->setDeleted($restore ? 0 : time()); + $this->commentMapper->update($comment); $this->eventDispatcher->dispatchTyped(new CommentDeleteEvent($comment)); return $comment; diff --git a/src/js/Api/modules/comments.js b/src/js/Api/modules/comments.js index 7d4d2bc8b..b37724860 100644 --- a/src/js/Api/modules/comments.js +++ b/src/js/Api/modules/comments.js @@ -50,6 +50,15 @@ const comments = { cancelToken: cancelTokenHandlerObject[this.deleteComment.name].handleRequestCancellation().token, }) }, + restoreComment(commentId) { + return httpInstance.request({ + method: 'PUT', + url: `comment/${commentId}/restore`, + params: { time: +new Date() }, + + cancelToken: cancelTokenHandlerObject[this.restoreComment.name].handleRequestCancellation().token, + }) + }, } const cancelTokenHandlerObject = createCancelTokenHandler(comments) diff --git a/src/js/Api/modules/public.js b/src/js/Api/modules/public.js index e3506c3ea..bcb3bd50f 100644 --- a/src/js/Api/modules/public.js +++ b/src/js/Api/modules/public.js @@ -123,6 +123,16 @@ const publicPoll = { }) }, + restoreComment(shareToken, commentId) { + return httpInstance.request({ + method: 'PUT', + url: `s/${shareToken}/comment/${commentId}/restore`, + params: { time: +new Date() }, + + cancelToken: cancelTokenHandlerObject[this.restoreComment.name].handleRequestCancellation().token, + }) + }, + getShare(shareToken) { return httpInstance.request({ method: 'GET', diff --git a/src/js/components/Actions/modules/ActionDelete.vue b/src/js/components/Actions/modules/ActionDelete.vue index 41420ccde..b8142510a 100644 --- a/src/js/components/Actions/modules/ActionDelete.vue +++ b/src/js/components/Actions/modules/ActionDelete.vue @@ -26,7 +26,10 @@ type="tertiary" :aria-label="computedTitle">