Add RPC controller
authorMarcel Werk <burntime@woltlab.com>
Fri, 14 Jun 2024 10:54:06 +0000 (12:54 +0200)
committerMarcel Werk <burntime@woltlab.com>
Fri, 14 Jun 2024 10:54:06 +0000 (12:54 +0200)
16 files changed:
wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/CreateComment.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/DeleteComment.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/EditComment.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/EnableComment.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/RenderComment.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/RenderComments.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/TCommentMessageValidator.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/UpdateComment.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/CreateResponse.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/DeleteResponse.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/EditResponse.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/EnableResponse.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/RenderResponse.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/RenderResponses.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/UpdateResponse.class.php [new file with mode: 0644]

index ee6720baf8ba76e68f8932a11c7c8364217fe9a6..6b06c3296d6f42e42d7820d676d297ed65403313 100644 (file)
@@ -120,6 +120,20 @@ return static function (): void {
             $event->register(new \wcf\system\endpoint\controller\core\files\PostGenerateThumbnails);
             $event->register(new \wcf\system\endpoint\controller\core\files\PostUpload);
             $event->register(new \wcf\system\endpoint\controller\core\files\upload\PostChunk);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\CreateComment);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\DeleteComment);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\EditComment);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\EnableComment);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\RenderComments);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\RenderComment);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\UpdateComment);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\responses\CreateResponse);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\responses\DeleteResponse);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\responses\EditResponse);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\responses\EnableResponse);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\responses\RenderResponse);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\responses\RenderResponses);
+            $event->register(new \wcf\system\endpoint\controller\core\comments\responses\UpdateResponse);
             $event->register(new \wcf\system\endpoint\controller\core\messages\GetMentionSuggestions);
             $event->register(new \wcf\system\endpoint\controller\core\sessions\DeleteSession);
         }
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/CreateComment.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/CreateComment.class.php
new file mode 100644 (file)
index 0000000..1465a02
--- /dev/null
@@ -0,0 +1,108 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\event\message\MessageSpamChecking;
+use wcf\http\Helper;
+use wcf\system\comment\CommentHandler;
+use wcf\system\endpoint\IController;
+use wcf\system\endpoint\PostRequest;
+use wcf\system\event\EventHandler;
+use wcf\system\exception\NamedUserException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\exception\UserInputException;
+use wcf\system\flood\FloodControl;
+use wcf\system\WCF;
+use wcf\util\UserUtil;
+
+/**
+ * API endpoint for the creation of new comments.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[PostRequest('/core/comments')]
+final class CreateComment implements IController
+{
+    use TCommentMessageValidator;
+
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        try {
+            CommentHandler::enforceFloodControl();
+        } catch (NamedUserException $e) {
+            throw new UserInputException('message', $e->getMessage());
+        }
+
+        $parameters = Helper::mapApiParameters($request, CreateCommentParameters::class);
+        $objectType = CommentHandler::getInstance()->getObjectType($parameters->objectTypeID);
+        if ($objectType === null) {
+            throw new UserInputException('objectTypeID');
+        }
+
+        if (!$objectType->getProcessor()->canAdd($parameters->objectID)) {
+            throw new PermissionDeniedException();
+        }
+
+        $username = '';
+        if (!WCF::getUser()->userID) {
+            $username = UserUtil::verifyGuestToken($parameters->guestToken);
+            if ($username === null) {
+                throw new UserInputException('guestToken');
+            }
+        }
+
+        $isDisabled = !$objectType->getProcessor()->canAddWithoutApproval($parameters->objectID);
+
+        $htmlInputProcessor = $this->validateMessage($parameters->message);
+
+        $event = new MessageSpamChecking(
+            $htmlInputProcessor,
+            WCF::getUser()->userID ? WCF::getUser() : null,
+            UserUtil::getIpAddress(),
+        );
+        EventHandler::getInstance()->fire($event);
+        if ($event->defaultPrevented()) {
+            $isDisabled = true;
+        }
+
+        $comment = (new \wcf\system\comment\command\CreateComment(
+            $objectType,
+            $parameters->objectID,
+            $htmlInputProcessor,
+            WCF::getUser()->userID ? WCF::getUser() : null,
+            $username,
+            $isDisabled,
+        ))();
+
+        FloodControl::getInstance()->registerContent('com.woltlab.wcf.comment');
+
+        return new JsonResponse([
+            'commentID' => $comment->commentID,
+        ]);
+    }
+}
+
+/** @internal */
+final class CreateCommentParameters
+{
+    public function __construct(
+        /** @var positive-int **/
+        public readonly int $objectID,
+
+        /** @var positive-int **/
+        public readonly int $objectTypeID,
+
+        /** @var non-empty-string */
+        public readonly string $message,
+
+        public readonly string $guestToken,
+    ) {
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/DeleteComment.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/DeleteComment.class.php
new file mode 100644 (file)
index 0000000..047f13b
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\comment\Comment;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\http\Helper;
+use wcf\system\endpoint\DeleteRequest;
+use wcf\system\endpoint\IController;
+use wcf\system\exception\PermissionDeniedException;
+
+/**
+ * API endpoint for the deletion of comments.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[DeleteRequest('/core/comments/{id:\d+}')]
+final class DeleteComment implements IController
+{
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $comment = Helper::fetchObjectFromRequestParameter($variables['id'], Comment::class);
+
+        $this->assertCommentIsDeletable($comment);
+
+        (new \wcf\system\comment\command\DeleteComments([$comment]))();
+
+        return new JsonResponse([]);
+    }
+
+    private function assertCommentIsDeletable(Comment $comment): void
+    {
+        $objectType = ObjectTypeCache::getInstance()->getObjectType($comment->objectTypeID);
+        if (!$objectType->getProcessor()->canDeleteComment($comment)) {
+            throw new PermissionDeniedException();
+        }
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/EditComment.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/EditComment.class.php
new file mode 100644 (file)
index 0000000..ab1dcbe
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\comment\Comment;
+use wcf\http\Helper;
+use wcf\system\bbcode\BBCodeHandler;
+use wcf\system\comment\CommentHandler;
+use wcf\system\comment\manager\ICommentManager;
+use wcf\system\endpoint\GetRequest;
+use wcf\system\endpoint\IController;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\html\upcast\HtmlUpcastProcessor;
+use wcf\system\WCF;
+
+/**
+ * API endpoint for starting the editing of a comment.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[GetRequest('/core/comments/{id:\d+}/edit')]
+final class EditComment implements IController
+{
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $comment = Helper::fetchObjectFromRequestParameter($variables['id'], Comment::class);
+
+        $this->assertCommentIsEditable($comment);
+
+        BBCodeHandler::getInstance()->setDisallowedBBCodes(\explode(
+            ',',
+            WCF::getSession()->getPermission('user.comment.disallowedBBCodes')
+        ));
+
+        $upcastProcessor = new HtmlUpcastProcessor();
+        $upcastProcessor->process($comment->message, 'com.woltlab.wcf.comment');
+
+        return new JsonResponse([
+            'template' => WCF::getTPL()->fetch('commentEditor', 'wcf', [
+                'comment' => $comment,
+                'text' => $upcastProcessor->getHtml(),
+                'wysiwygSelector' => 'commentEditor' . $comment->commentID,
+            ]),
+        ]);
+    }
+
+    private function assertCommentIsEditable(Comment $comment): void
+    {
+        $processor = CommentHandler::getInstance()->getObjectType($comment->objectTypeID)->getProcessor();
+        \assert($processor instanceof ICommentManager);
+        if (!$processor->canEditComment($comment)) {
+            throw new PermissionDeniedException();
+        }
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/EnableComment.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/EnableComment.class.php
new file mode 100644 (file)
index 0000000..b189f53
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\comment\Comment;
+use wcf\http\Helper;
+use wcf\system\comment\CommentHandler;
+use wcf\system\comment\manager\ICommentManager;
+use wcf\system\endpoint\IController;
+use wcf\system\endpoint\PostRequest;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+
+/**
+ * API endpoint for enabling comments.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[PostRequest('/core/comments/{id:\d+}/enable')]
+final class EnableComment implements IController
+{
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $comment = Helper::fetchObjectFromRequestParameter($variables['id'], Comment::class);
+
+        $this->assertCommentCanBeEnabled($comment);
+
+        (new \wcf\system\comment\command\PublishComment($comment))();
+
+        return new JsonResponse([]);
+    }
+
+    private function assertCommentCanBeEnabled(Comment $comment): void
+    {
+        if (!$comment->isDisabled) {
+            throw new IllegalLinkException();
+        }
+
+        $processor = CommentHandler::getInstance()->getObjectType($comment->objectTypeID)->getProcessor();
+        \assert($processor instanceof ICommentManager);
+        if (!$processor->canModerate($comment->objectTypeID, $comment->objectID)) {
+            throw new PermissionDeniedException();
+        }
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/RenderComment.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/RenderComment.class.php
new file mode 100644 (file)
index 0000000..3644c88
--- /dev/null
@@ -0,0 +1,231 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\comment\Comment;
+use wcf\data\comment\response\CommentResponse;
+use wcf\data\comment\response\StructuredCommentResponse;
+use wcf\data\comment\StructuredComment;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\http\Helper;
+use wcf\system\comment\CommentHandler;
+use wcf\system\endpoint\GetRequest;
+use wcf\system\endpoint\IController;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
+use wcf\system\reaction\ReactionHandler;
+use wcf\system\WCF;
+
+/**
+ * API endpoint for the rendering of a single comment.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[GetRequest('/core/comments/{id:\d+}/render')]
+final class RenderComment implements IController
+{
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $comment = Helper::fetchObjectFromRequestParameter($variables['id'], Comment::class);
+
+        $parameters = Helper::mapQueryParameters(
+            $request->getQueryParams(),
+            <<<'EOT'
+                array {
+                    responseID: null|positive-int,
+                    messageOnly: null|bool,
+                    objectTypeID: null|positive-int,
+                }
+                EOT,
+        );
+
+        $this->assertCommentIsAccessible($comment, $parameters['objectTypeID']);
+        $response = null;
+        if ($parameters['responseID']) {
+            $response = Helper::fetchObjectFromRequestParameter($parameters['responseID'], CommentResponse::class);
+            $this->assertResponseIsAccessible($comment, $response);
+        }
+
+        $this->markNotificationsAsRead($comment, $response);
+
+        return new JsonResponse(
+            $this->renderComment($comment, $response, $parameters['messageOnly'] ?? false),
+        );
+    }
+
+    private function assertCommentIsAccessible(Comment $comment, ?int $objectTypeID = null): void
+    {
+        $objectType = ObjectTypeCache::getInstance()->getObjectType($comment->objectTypeID);
+        if ($objectTypeID !== null) {
+            if ($objectType->objectTypeID !== $objectTypeID) {
+                throw new IllegalLinkException();
+            }
+        }
+
+        $commentProcessor = $objectType->getProcessor();
+        if (!$commentProcessor->isAccessible($comment->objectID)) {
+            throw new PermissionDeniedException();
+        }
+        if ($comment->isDisabled && !$commentProcessor->canModerate($comment->objectTypeID, $comment->objectID)) {
+            throw new PermissionDeniedException();
+        }
+    }
+
+    private function assertResponseIsAccessible(Comment $comment, CommentResponse $response): void
+    {
+        $objectType = ObjectTypeCache::getInstance()->getObjectType($comment->objectTypeID);
+        $commentProcessor = $objectType->getProcessor();
+
+        if ($response->commentID != $comment->commentID) {
+            throw new PermissionDeniedException();
+        }
+        if ($response->isDisabled && !$commentProcessor->canModerate($comment->objectTypeID, $comment->objectID)) {
+            throw new PermissionDeniedException();
+        }
+    }
+
+    private function markNotificationsAsRead(Comment $comment, ?CommentResponse $response = null)
+    {
+        $objectType = CommentHandler::getInstance()->getObjectType($comment->objectTypeID)->objectType;
+        if ($response === null) {
+            CommentHandler::getInstance()->markNotificationsAsConfirmedForComments(
+                $objectType,
+                [new StructuredComment($comment)]
+            );
+        } else {
+            CommentHandler::getInstance()->markNotificationsAsConfirmedForResponses(
+                $objectType,
+                [$response]
+            );
+        }
+    }
+
+    private function renderComment(Comment $comment, ?CommentResponse $response = null, bool $messageOnly = false): array
+    {
+        if ($comment->hasEmbeddedObjects) {
+            MessageEmbeddedObjectManager::getInstance()->loadObjects(
+                'com.woltlab.wcf.comment',
+                [$comment->getObjectID()]
+            );
+        }
+
+        if ($messageOnly) {
+            $returnValue = [
+                'template' => $comment->getFormattedMessage(),
+            ];
+
+            if ($response !== null) {
+                $returnValue['response'] = $this->renderResponse($response, $messageOnly);
+            }
+
+            return $returnValue;
+        }
+
+        $commentProcessor = ObjectTypeCache::getInstance()->getObjectType($comment->objectTypeID)->getProcessor();
+
+        $structuredComment = new StructuredComment($comment);
+        $structuredComment->setIsDeletable($commentProcessor->canDeleteComment($comment));
+        $structuredComment->setIsEditable($commentProcessor->canEditComment($comment));
+
+        if ($response !== null) {
+            // check if response is not visible
+            /** @var CommentResponse $visibleResponse */
+            foreach ($comment as $visibleResponse) {
+                if ($visibleResponse->responseID == $response->responseID) {
+                    $response = null;
+                    break;
+                }
+            }
+        }
+
+        // This functions renders a single comment without rendering its responses.
+        // We need to prevent the setting of the data attribute for the last response time
+        // so that the loading of the responses by the user works correctly.
+        if ($comment->responses) {
+            WCF::getTPL()->assign('ignoreLastResponseTime', true);
+        }
+
+        WCF::getTPL()->assign([
+            'commentCanAdd' => $commentProcessor->canAdd(
+                $comment->objectID
+            ),
+            'commentCanModerate' => $commentProcessor->canModerate(
+                $comment->objectTypeID,
+                $comment->objectID
+            ),
+            'commentList' => [$structuredComment],
+            'commentManager' => $commentProcessor,
+        ]);
+
+        // load like data
+        if (MODULE_LIKE) {
+            $likeData = [];
+            $commentObjectType = ReactionHandler::getInstance()->getObjectType('com.woltlab.wcf.comment');
+            ReactionHandler::getInstance()->loadLikeObjects($commentObjectType, [$comment->commentID]);
+            $likeData['comment'] = ReactionHandler::getInstance()->getLikeObjects($commentObjectType);
+
+            $responseIDs = [];
+            foreach ($structuredComment as $visibleResponse) {
+                $responseIDs[] = $visibleResponse->responseID;
+            }
+
+            if ($response !== null) {
+                $responseIDs[] = $response->responseID;
+            }
+
+            if (!empty($responseIDs)) {
+                $responseObjectType = ReactionHandler::getInstance()->getObjectType('com.woltlab.wcf.comment.response');
+                ReactionHandler::getInstance()->loadLikeObjects($responseObjectType, $responseIDs);
+                $likeData['response'] = ReactionHandler::getInstance()->getLikeObjects($responseObjectType);
+            }
+
+            WCF::getTPL()->assign('likeData', $likeData);
+        }
+
+        $returnValue = [
+            'template' => WCF::getTPL()->fetch('commentList'),
+        ];
+        if ($response !== null) {
+            $returnValue['response'] = $this->renderResponse($response);
+        }
+
+        return $returnValue;
+    }
+
+    private function renderResponse(CommentResponse $response, bool $messageOnly = false): string
+    {
+        if ($response->hasEmbeddedObjects) {
+            MessageEmbeddedObjectManager::getInstance()->loadObjects(
+                'com.woltlab.wcf.comment.response',
+                [$response->getObjectID()]
+            );
+        }
+
+        if ($messageOnly) {
+            return $response->getFormattedMessage();
+        }
+
+        $commentProcessor = ObjectTypeCache::getInstance()->getObjectType($response->getComment()->objectTypeID)->getProcessor();
+
+        $structedResponse = new StructuredCommentResponse($response);
+        $structedResponse->setIsDeletable($commentProcessor->canDeleteResponse($response));
+        $structedResponse->setIsEditable($commentProcessor->canEditResponse($response));
+
+        return WCF::getTPL()->fetch('commentResponseList', 'wcf', [
+            'responseList' => [$structedResponse],
+            'commentCanModerate' => $commentProcessor->canModerate(
+                $response->getComment()->objectTypeID,
+                $response->getComment()->objectID
+            ),
+            'commentManager' => $commentProcessor,
+        ]);
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/RenderComments.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/RenderComments.class.php
new file mode 100644 (file)
index 0000000..f63b13b
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\comment\StructuredCommentList;
+use wcf\http\Helper;
+use wcf\system\comment\CommentHandler;
+use wcf\system\endpoint\GetRequest;
+use wcf\system\endpoint\IController;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\exception\UserInputException;
+use wcf\system\WCF;
+
+/**
+ * API endpoint for loading additional rendered comments.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[GetRequest('/core/comments/render')]
+final class RenderComments implements IController
+{
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $parameters = Helper::mapApiParameters($request, RenderCommentsParameters::class);
+        $objectType = CommentHandler::getInstance()->getObjectType($parameters->objectTypeID);
+        if ($objectType === null) {
+            throw new UserInputException('objectTypeID');
+        }
+
+        if (!$objectType->getProcessor()->isAccessible($parameters->objectID)) {
+            throw new PermissionDeniedException();
+        }
+
+        $commentList = $this->getCommentList(
+            $parameters->objectTypeID,
+            $parameters->objectID,
+            $parameters->lastCommentTime
+        );
+
+        CommentHandler::getInstance()->markNotificationsAsConfirmedForComments(
+            $objectType->objectType,
+            $commentList->getObjects()
+        );
+
+        return new JsonResponse([
+            'lastCommentTime' => $commentList->getMinCommentTime(),
+            'template' => WCF::getTPL()->fetch('commentList', 'wcf', [
+                'commentList' => $commentList,
+                'likeData' => MODULE_LIKE ? $commentList->getLikeData() : [],
+            ]),
+        ]);
+    }
+
+    private function getCommentList(int $objectTypeID, int $objectID, int $lastCommentTime): StructuredCommentList
+    {
+        $commentList = CommentHandler::getInstance()->getCommentList(
+            CommentHandler::getInstance()->getObjectType($objectTypeID)->getProcessor(),
+            $objectTypeID,
+            $objectID,
+            false
+        );
+        if ($lastCommentTime) {
+            $commentList->getConditionBuilder()->add("comment.time < ?", [$lastCommentTime]);
+        }
+        $commentList->readObjects();
+
+        return $commentList;
+    }
+}
+
+/** @internal */
+final class RenderCommentsParameters
+{
+    public function __construct(
+        /** @var positive-int **/
+        public readonly int $objectID,
+
+        /** @var positive-int **/
+        public readonly int $objectTypeID,
+
+        public readonly int $lastCommentTime = 0,
+    ) {
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/TCommentMessageValidator.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/TCommentMessageValidator.class.php
new file mode 100644 (file)
index 0000000..d069b13
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments;
+
+use wcf\system\bbcode\BBCodeHandler;
+use wcf\system\comment\CommentHandler;
+use wcf\system\exception\UserInputException;
+use wcf\system\html\input\HtmlInputProcessor;
+use wcf\system\WCF;
+use wcf\util\MessageUtil;
+
+/**
+ * Trait that provides helper methods for comment controllers.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+trait TCommentMessageValidator
+{
+    private function validateMessage(string $message, bool $isResponse = false, int $objectID = 0): HtmlInputProcessor
+    {
+        $message = MessageUtil::stripCrap($message);
+        if ($message === '') {
+            throw new UserInputException('message');
+        }
+
+        CommentHandler::enforceCensorship($message);
+
+        BBCodeHandler::getInstance()->setDisallowedBBCodes(\explode(
+            ',',
+            WCF::getSession()->getPermission('user.comment.disallowedBBCodes')
+        ));
+
+        $htmlInputProcessor = new HtmlInputProcessor();
+        if ($isResponse) {
+            $htmlInputProcessor->process(
+                $message,
+                'com.woltlab.wcf.comment.response',
+                $objectID
+            );
+        } else {
+            $htmlInputProcessor->process(
+                $message,
+                'com.woltlab.wcf.comment',
+                $objectID
+            );
+        }
+
+        // search for disallowed bbcodes
+        $disallowedBBCodes = $htmlInputProcessor->validate();
+        if (!empty($disallowedBBCodes)) {
+            throw new UserInputException(
+                'text',
+                WCF::getLanguage()->getDynamicVariable(
+                    'wcf.message.error.disallowedBBCodes',
+                    ['disallowedBBCodes' => $disallowedBBCodes]
+                )
+            );
+        }
+
+        if ($htmlInputProcessor->appearsToBeEmpty()) {
+            throw new UserInputException('message');
+        }
+
+        $commentTextContent = $htmlInputProcessor->getTextContent();
+        if (\mb_strlen($commentTextContent) > WCF::getSession()->getPermission('user.comment.maxLength')) {
+            throw new UserInputException(
+                'text',
+                WCF::getLanguage()->getDynamicVariable(
+                    'wcf.message.error.tooLong',
+                    ['maxTextLength' => WCF::getSession()->getPermission('user.comment.maxLength')]
+                )
+            );
+        }
+
+        return $htmlInputProcessor;
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/UpdateComment.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/UpdateComment.class.php
new file mode 100644 (file)
index 0000000..418d77f
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\comment\Comment;
+use wcf\event\message\MessageSpamChecking;
+use wcf\http\Helper;
+use wcf\system\comment\CommentHandler;
+use wcf\system\comment\manager\ICommentManager;
+use wcf\system\endpoint\IController;
+use wcf\system\endpoint\PostRequest;
+use wcf\system\event\EventHandler;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\WCF;
+use wcf\util\UserUtil;
+
+/**
+ * API endpoint for the update of comments.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[PostRequest('/core/comments/{id:\d+}')]
+final class UpdateComment implements IController
+{
+    use TCommentMessageValidator;
+
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $comment = Helper::fetchObjectFromRequestParameter($variables['id'], Comment::class);
+
+        $this->assertCommentIsEditable($comment);
+
+        $parameters = Helper::mapApiParameters($request, UpdateCommentParameters::class);
+
+        $htmlInputProcessor = $this->validateMessage($parameters->message, false, $comment->commentID);
+
+        $event = new MessageSpamChecking(
+            $htmlInputProcessor,
+            WCF::getUser()->userID ? WCF::getUser() : null,
+            UserUtil::getIpAddress(),
+        );
+        EventHandler::getInstance()->fire($event);
+        if ($event->defaultPrevented()) {
+            throw new PermissionDeniedException();
+        }
+
+        (new \wcf\system\comment\command\UpdateComment(
+            $comment,
+            $htmlInputProcessor,
+        ))();
+
+        return new JsonResponse([]);
+    }
+
+    private function assertCommentIsEditable(Comment $comment): void
+    {
+        $processor = CommentHandler::getInstance()->getObjectType($comment->objectTypeID)->getProcessor();
+        \assert($processor instanceof ICommentManager);
+        if (!$processor->canEditComment($comment)) {
+            throw new PermissionDeniedException();
+        }
+    }
+}
+
+/** @internal */
+final class UpdateCommentParameters
+{
+    public function __construct(
+        /** @var non-empty-string */
+        public readonly string $message,
+    ) {
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/CreateResponse.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/CreateResponse.class.php
new file mode 100644 (file)
index 0000000..487657c
--- /dev/null
@@ -0,0 +1,117 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments\responses;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\comment\Comment;
+use wcf\data\object\type\ObjectType;
+use wcf\event\message\MessageSpamChecking;
+use wcf\http\Helper;
+use wcf\system\comment\CommentHandler;
+use wcf\system\comment\manager\ICommentManager;
+use wcf\system\endpoint\controller\core\comments\TCommentMessageValidator;
+use wcf\system\endpoint\IController;
+use wcf\system\endpoint\PostRequest;
+use wcf\system\event\EventHandler;
+use wcf\system\exception\NamedUserException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\exception\UserInputException;
+use wcf\system\flood\FloodControl;
+use wcf\system\WCF;
+use wcf\util\UserUtil;
+
+/**
+ * API endpoint for the creation of new responses.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[PostRequest('/core/comments/responses')]
+final class CreateResponse implements IController
+{
+    use TCommentMessageValidator;
+
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        try {
+            CommentHandler::enforceFloodControl();
+        } catch (NamedUserException $e) {
+            throw new UserInputException('message', $e->getMessage());
+        }
+
+        $parameters = Helper::mapApiParameters($request, CreateResponseParameters::class);
+        $comment = Helper::fetchObjectFromRequestParameter($parameters->commentID, Comment::class);
+        $objectType = CommentHandler::getInstance()->getObjectType($comment->objectTypeID);
+
+        $this->assertResponseIsPossible($objectType, $comment);
+
+        $username = '';
+        if (!WCF::getUser()->userID) {
+            $username = UserUtil::verifyGuestToken($parameters->guestToken);
+            if ($username === null) {
+                throw new UserInputException('guestToken');
+            }
+        }
+
+        $isDisabled = !$objectType->getProcessor()->canAddWithoutApproval($comment->objectID);
+
+        $htmlInputProcessor = $this->validateMessage($parameters->message, true);
+
+        $event = new MessageSpamChecking(
+            $htmlInputProcessor,
+            WCF::getUser()->userID ? WCF::getUser() : null,
+            UserUtil::getIpAddress(),
+        );
+        EventHandler::getInstance()->fire($event);
+        if ($event->defaultPrevented()) {
+            $isDisabled = true;
+        }
+
+        $response = (new \wcf\system\comment\response\command\CreateResponse(
+            $comment,
+            $htmlInputProcessor,
+            WCF::getUser()->userID ? WCF::getUser() : null,
+            $username,
+            $isDisabled,
+        ))();
+
+        FloodControl::getInstance()->registerContent('com.woltlab.wcf.comment');
+
+        return new JsonResponse([
+            'responseID' => $response->responseID,
+        ]);
+    }
+
+    private function assertResponseIsPossible(ObjectType $objectType, Comment $comment): void
+    {
+        $processor = $objectType->getProcessor();
+        assert($processor instanceof ICommentManager);
+        if (!$processor->canAdd($comment->objectID)) {
+            throw new PermissionDeniedException();
+        }
+
+        if ($comment->isDisabled && !$processor->canModerate($comment->objectTypeID, $comment->objectID)) {
+            throw new PermissionDeniedException();
+        }
+    }
+}
+
+/** @internal */
+final class CreateResponseParameters
+{
+    public function __construct(
+        /** @var positive-int **/
+        public readonly int $commentID,
+
+        /** @var non-empty-string */
+        public readonly string $message,
+
+        public readonly string $guestToken,
+    ) {
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/DeleteResponse.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/DeleteResponse.class.php
new file mode 100644 (file)
index 0000000..b2acc4e
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments\responses;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\comment\response\CommentResponse;
+use wcf\http\Helper;
+use wcf\system\comment\CommentHandler;
+use wcf\system\endpoint\DeleteRequest;
+use wcf\system\endpoint\IController;
+use wcf\system\exception\PermissionDeniedException;
+
+/**
+ * API endpoint for the deletion of responses.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[DeleteRequest('/core/comments/responses/{id:\d+}')]
+final class DeleteResponse implements IController
+{
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $response = Helper::fetchObjectFromRequestParameter($variables['id'], CommentResponse::class);
+
+        $this->assertResponseIsDeletable($response);
+
+        (new \wcf\system\comment\response\command\DeleteResponses([$response]))();
+
+        return new JsonResponse([]);
+    }
+
+    private function assertResponseIsDeletable(CommentResponse $response): void
+    {
+        $manager = CommentHandler::getInstance()->getCommentManagerByID($response->getComment()->objectTypeID);
+        if (!$manager->canDeleteResponse($response)) {
+            throw new PermissionDeniedException();
+        }
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/EditResponse.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/EditResponse.class.php
new file mode 100644 (file)
index 0000000..5b67a19
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments\responses;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\comment\response\CommentResponse;
+use wcf\http\Helper;
+use wcf\system\bbcode\BBCodeHandler;
+use wcf\system\comment\CommentHandler;
+use wcf\system\endpoint\GetRequest;
+use wcf\system\endpoint\IController;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\html\upcast\HtmlUpcastProcessor;
+use wcf\system\WCF;
+
+/**
+ * API endpoint for starting the editing of a comment response.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[GetRequest('/core/comments/responses/{id:\d+}/edit')]
+final class EditResponse implements IController
+{
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $response = Helper::fetchObjectFromRequestParameter($variables['id'], CommentResponse::class);
+
+        $this->assertResponseIsEditable($response);
+
+        BBCodeHandler::getInstance()->setDisallowedBBCodes(\explode(
+            ',',
+            WCF::getSession()->getPermission('user.comment.disallowedBBCodes')
+        ));
+
+        $upcastProcessor = new HtmlUpcastProcessor();
+        $upcastProcessor->process($response->message, 'com.woltlab.wcf.comment.response');
+
+        return new JsonResponse([
+            'template' => WCF::getTPL()->fetch('commentResponseEditor', 'wcf', [
+                'response' => $response,
+                'text' => $upcastProcessor->getHtml(),
+                'wysiwygSelector' => 'commentResponseEditor' . $response->responseID,
+            ]),
+        ]);
+    }
+
+    private function assertResponseIsEditable(CommentResponse $response): void
+    {
+        $manager = CommentHandler::getInstance()->getCommentManagerByID($response->getComment()->objectTypeID);
+        if (!$manager->canEditResponse($response)) {
+            throw new PermissionDeniedException();
+        }
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/EnableResponse.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/EnableResponse.class.php
new file mode 100644 (file)
index 0000000..8b7b8b1
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments\responses;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\comment\response\CommentResponse;
+use wcf\http\Helper;
+use wcf\system\comment\CommentHandler;
+use wcf\system\comment\manager\ICommentManager;
+use wcf\system\endpoint\IController;
+use wcf\system\endpoint\PostRequest;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+
+/**
+ * API endpoint for enabling responses.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[PostRequest('/core/comments/responses/{id:\d+}/enable')]
+final class EnableResponse implements IController
+{
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $response = Helper::fetchObjectFromRequestParameter($variables['id'], CommentResponse::class);
+
+        $this->assertResponseCanBeEnabled($response);
+
+        (new \wcf\system\comment\response\command\PublishResponse($response))();
+
+        return new JsonResponse([]);
+    }
+
+    private function assertResponseCanBeEnabled(CommentResponse $response): void
+    {
+        if (!$response->isDisabled) {
+            throw new IllegalLinkException();
+        }
+
+        $comment = $response->getComment();
+        $processor = CommentHandler::getInstance()->getObjectType($comment->objectTypeID)->getProcessor();
+        \assert($processor instanceof ICommentManager);
+        if (!$processor->canModerate($comment->objectTypeID, $comment->objectID)) {
+            throw new PermissionDeniedException();
+        }
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/RenderResponse.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/RenderResponse.class.php
new file mode 100644 (file)
index 0000000..714d58c
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments\responses;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\comment\response\CommentResponse;
+use wcf\data\comment\response\StructuredCommentResponse;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\http\Helper;
+use wcf\system\comment\CommentHandler;
+use wcf\system\endpoint\GetRequest;
+use wcf\system\endpoint\IController;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
+use wcf\system\WCF;
+
+/**
+ * API endpoint for the rendering of a single comment response.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[GetRequest('/core/comments/responses/{id:\d+}/render')]
+final class RenderResponse implements IController
+{
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $response = Helper::fetchObjectFromRequestParameter($variables['id'], CommentResponse::class);
+        $parameters = Helper::mapQueryParameters(
+            $request->getQueryParams(),
+            <<<'EOT'
+                array {
+                    messageOnly: null|bool,
+                    objectTypeID: null|positive-int,
+                }
+                EOT,
+        );
+
+        $this->assertResponseIsAccessible($response, $parameters['objectTypeID']);
+        $this->markNotificationsAsRead($response);
+
+        return new JsonResponse([
+            'template' => $this->renderResponse($response, $parameters['messageOnly'] ?? false),
+        ]);
+    }
+
+    private function assertResponseIsAccessible(CommentResponse $response, ?int $objectTypeID = null): void
+    {
+        $comment = $response->getComment();
+        $objectType = ObjectTypeCache::getInstance()->getObjectType($comment->objectTypeID);
+        if ($objectTypeID !== null) {
+            if ($objectType->objectTypeID !== $objectTypeID) {
+                throw new IllegalLinkException();
+            }
+        }
+        $commentProcessor = $objectType->getProcessor();
+
+        if (!$commentProcessor->isAccessible($comment->objectID)) {
+            throw new PermissionDeniedException();
+        }
+        if ($response->commentID != $comment->commentID) {
+            throw new PermissionDeniedException();
+        }
+        if ($response->isDisabled && !$commentProcessor->canModerate($comment->objectTypeID, $comment->objectID)) {
+            throw new PermissionDeniedException();
+        }
+    }
+
+    private function markNotificationsAsRead(CommentResponse $response): void
+    {
+        $objectType = CommentHandler::getInstance()->getObjectType($response->getComment()->objectTypeID)->objectType;
+        CommentHandler::getInstance()->markNotificationsAsConfirmedForResponses(
+            $objectType,
+            [$response]
+        );
+    }
+
+    private function renderResponse(CommentResponse $response, bool $messageOnly = false): string
+    {
+        if ($response->hasEmbeddedObjects) {
+            MessageEmbeddedObjectManager::getInstance()->loadObjects(
+                'com.woltlab.wcf.comment.response',
+                [$response->getObjectID()]
+            );
+        }
+
+        if ($messageOnly) {
+            return $response->getFormattedMessage();
+        }
+
+        $commentProcessor = ObjectTypeCache::getInstance()->getObjectType($response->getComment()->objectTypeID)->getProcessor();
+
+        $structedResponse = new StructuredCommentResponse($response);
+        $structedResponse->setIsDeletable($commentProcessor->canDeleteResponse($response));
+        $structedResponse->setIsEditable($commentProcessor->canEditResponse($response));
+
+        return WCF::getTPL()->fetch('commentResponseList', 'wcf', [
+            'responseList' => [$structedResponse],
+            'commentCanModerate' => $commentProcessor->canModerate(
+                $response->getComment()->objectTypeID,
+                $response->getComment()->objectID
+            ),
+            'commentManager' => $commentProcessor,
+        ]);
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/RenderResponses.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/RenderResponses.class.php
new file mode 100644 (file)
index 0000000..b269a11
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments\responses;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\comment\Comment;
+use wcf\data\comment\response\StructuredCommentResponseList;
+use wcf\http\Helper;
+use wcf\system\comment\CommentHandler;
+use wcf\system\comment\manager\ICommentManager;
+use wcf\system\endpoint\GetRequest;
+use wcf\system\endpoint\IController;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\WCF;
+
+/**
+ * API endpoint for loading additional rendered responses.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[GetRequest('/core/comments/responses/render')]
+final class RenderResponses implements IController
+{
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $parameters = Helper::mapApiParameters($request, RenderReponsesParameters::class);
+        $comment = Helper::fetchObjectFromRequestParameter($parameters->commentID, Comment::class);
+        $commentManager = CommentHandler::getInstance()->getObjectType($comment->objectTypeID)->getProcessor();
+        assert($commentManager instanceof ICommentManager);
+
+        if (!$commentManager->isAccessible($comment->objectID)) {
+            throw new PermissionDeniedException();
+        }
+
+        $commentCanModerate = $commentManager->canModerate(
+            $comment->objectTypeID,
+            $comment->objectID
+        );
+
+        // get response list
+        $responseList = new StructuredCommentResponseList($commentManager, $comment);
+        if ($parameters->lastResponseID) {
+            $responseList->getConditionBuilder()->add(
+                "(comment_response.time > ? OR (comment_response.time = ? && comment_response.responseID > ?))",
+                [
+                    $parameters->lastResponseTime,
+                    $parameters->lastResponseTime,
+                    $parameters->lastResponseID,
+                ]
+            );
+        } else {
+            $responseList->getConditionBuilder()->add(
+                "comment_response.time > ?",
+                [$parameters->lastResponseTime]
+            );
+        }
+        if (!$commentCanModerate) {
+            $responseList->getConditionBuilder()->add("comment_response.isDisabled = ?", [0]);
+        }
+        $responseList->readObjects();
+
+        $lastResponseTime = $lastResponseID = 0;
+        foreach ($responseList as $response) {
+            if (!$lastResponseTime) {
+                $lastResponseTime = $response->time;
+            }
+            if (!$lastResponseID) {
+                $lastResponseID = $response->responseID;
+            }
+
+            $lastResponseTime = \max($lastResponseTime, $response->time);
+            $lastResponseID = \max($lastResponseID, $response->responseID);
+        }
+
+        CommentHandler::getInstance()->markNotificationsAsConfirmedForResponses(
+            CommentHandler::getInstance()->getObjectType($comment->objectTypeID)->objectType,
+            $responseList->getObjects()
+        );
+
+        return new JsonResponse([
+            'lastResponseTime' => $lastResponseTime,
+            'lastResponseID' => $lastResponseID,
+            'template' => WCF::getTPL()->fetch('commentResponseList', 'wcf', [
+                'commentCanModerate' => $commentCanModerate,
+                'likeData' => MODULE_LIKE ? $responseList->getLikeData() : [],
+                'responseList' => $responseList,
+                'commentManager' => $commentManager,
+            ]),
+        ]);
+    }
+}
+
+/** @internal */
+final class RenderReponsesParameters
+{
+    public function __construct(
+        /** @var positive-int **/
+        public readonly int $commentID,
+
+        /** @var positive-int **/
+        public readonly int $lastResponseTime,
+
+        /** @var positive-int **/
+        public readonly int $lastResponseID,
+
+        public readonly bool $loadAllResponses,
+    ) {
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/UpdateResponse.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/comments/responses/UpdateResponse.class.php
new file mode 100644 (file)
index 0000000..aa499b2
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\comments\responses;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use wcf\data\comment\response\CommentResponse;
+use wcf\event\message\MessageSpamChecking;
+use wcf\http\Helper;
+use wcf\system\comment\CommentHandler;
+use wcf\system\endpoint\controller\core\comments\TCommentMessageValidator;
+use wcf\system\endpoint\IController;
+use wcf\system\endpoint\PostRequest;
+use wcf\system\event\EventHandler;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\WCF;
+use wcf\util\UserUtil;
+
+/**
+ * API endpoint for the update of responses.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+#[PostRequest('/core/comments/responses/{id:\d+}')]
+final class UpdateResponse implements IController
+{
+    use TCommentMessageValidator;
+
+    #[\Override]
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $response = Helper::fetchObjectFromRequestParameter($variables['id'], CommentResponse::class);
+
+        $this->assertResponseIsEditable($response);
+
+        $parameters = Helper::mapApiParameters($request, UpdateCommentParameters::class);
+
+        $htmlInputProcessor = $this->validateMessage($parameters->message, true, $response->responseID);
+
+        $event = new MessageSpamChecking(
+            $htmlInputProcessor,
+            WCF::getUser()->userID ? WCF::getUser() : null,
+            UserUtil::getIpAddress(),
+        );
+        EventHandler::getInstance()->fire($event);
+        if ($event->defaultPrevented()) {
+            throw new PermissionDeniedException();
+        }
+
+        (new \wcf\system\comment\response\command\UpdateResponse(
+            $response,
+            $htmlInputProcessor,
+        ))();
+
+        return new JsonResponse([]);
+    }
+
+    private function assertResponseIsEditable(CommentResponse $response): void
+    {
+        $manager = CommentHandler::getInstance()->getCommentManagerByID($response->getComment()->objectTypeID);
+        if (!$manager->canEditResponse($response)) {
+            throw new PermissionDeniedException();
+        }
+    }
+}
+
+/** @internal */
+final class UpdateCommentParameters
+{
+    public function __construct(
+        /** @var non-empty-string */
+        public readonly string $message,
+    ) {
+    }
+}