Add commands
authorMarcel Werk <burntime@woltlab.com>
Fri, 14 Jun 2024 10:53:14 +0000 (12:53 +0200)
committerMarcel Werk <burntime@woltlab.com>
Fri, 14 Jun 2024 10:53:14 +0000 (12:53 +0200)
wcfsetup/install/files/lib/system/comment/command/CreateComment.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/comment/command/DeleteComments.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/comment/command/PublishComment.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/comment/command/UpdateComment.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/comment/response/command/CreateResponse.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/comment/response/command/DeleteResponses.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/comment/response/command/PublishResponse.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/comment/response/command/UpdateResponse.class.php [new file with mode: 0644]

diff --git a/wcfsetup/install/files/lib/system/comment/command/CreateComment.class.php b/wcfsetup/install/files/lib/system/comment/command/CreateComment.class.php
new file mode 100644 (file)
index 0000000..2fb16d5
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+namespace wcf\system\comment\command;
+
+use wcf\data\comment\Comment;
+use wcf\data\comment\CommentAction;
+use wcf\data\comment\CommentEditor;
+use wcf\data\object\type\ObjectType;
+use wcf\data\user\User;
+use wcf\event\comment\CommentCreated;
+use wcf\system\event\EventHandler;
+use wcf\system\html\input\HtmlInputProcessor;
+use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
+use wcf\system\moderation\queue\ModerationQueueActivationManager;
+
+/**
+ * Creates a new 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
+ */
+final class CreateComment
+{
+    public function __construct(
+        private readonly ObjectType $objectType,
+        private readonly int $objectID,
+        private readonly HtmlInputProcessor $htmlInputProcessor,
+        private readonly ?User $user = null,
+        private readonly string $username = '',
+        private readonly bool $isDisabled = false,
+    ) {
+    }
+
+    public function __invoke(): Comment
+    {
+        $action = new CommentAction([], 'create', [
+            'data' => [
+                'objectTypeID' => $this->objectType->objectTypeID,
+                'objectID' => $this->objectID,
+                'time' => TIME_NOW,
+                'userID' => $this->user ? $this->user->userID : null,
+                'username' => $this->user ? $this->user->username : $this->username,
+                'message' => $this->htmlInputProcessor->getHtml(),
+                'responses' => 0,
+                'responseIDs' => \serialize([]),
+                'enableHtml' => 1,
+                'isDisabled' => $this->isDisabled ? 1 : 0,
+            ]
+        ]);
+        /** @var Comment $comment */
+        $comment = $action->executeAction()['returnValues'];
+
+        if (!$comment->isDisabled) {
+            (new PublishComment($comment))();
+        } else {
+            ModerationQueueActivationManager::getInstance()->addModeratedContent(
+                'com.woltlab.wcf.comment.comment',
+                $comment->commentID
+            );
+        }
+
+        $this->htmlInputProcessor->setObjectID($comment->getObjectID());
+        if (MessageEmbeddedObjectManager::getInstance()->registerObjects($this->htmlInputProcessor)) {
+            (new CommentEditor($comment))->update([
+                'hasEmbeddedObjects' => 1,
+            ]);
+            $comment = new Comment($comment->getObjectID());
+        }
+
+        $event = new CommentCreated($comment);
+        EventHandler::getInstance()->fire($event);
+
+        return $comment;
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/comment/command/DeleteComments.class.php b/wcfsetup/install/files/lib/system/comment/command/DeleteComments.class.php
new file mode 100644 (file)
index 0000000..70674ce
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+
+namespace wcf\system\comment\command;
+
+use wcf\data\comment\Comment;
+use wcf\data\comment\CommentAction;
+use wcf\data\comment\response\CommentResponseList;
+use wcf\data\object\type\ObjectType;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\event\comment\CommentsDeleted;
+use wcf\system\comment\response\command\DeleteResponses;
+use wcf\system\event\EventHandler;
+use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
+use wcf\system\moderation\queue\ModerationQueueManager;
+use wcf\system\reaction\ReactionHandler;
+use wcf\system\user\activity\event\UserActivityEventHandler;
+use wcf\system\user\notification\UserNotificationHandler;
+
+/**
+ * Deletes a bunch of comments that belong to the same object type.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ *
+ * @property-read int[] $commentIDs
+ * @property-read Comment[] $comments
+ */
+final class DeleteComments
+{
+    private readonly ObjectType $objectType;
+    private readonly array $commentIDs;
+
+    public function __construct(
+        private readonly array $comments,
+        private readonly bool $updateCounters = true,
+    ) {
+        $this->commentIDs = \array_map(fn (Comment $comment): int => $comment->commentID, $this->comments);
+        foreach ($this->comments as $comment) {
+            if (!isset($this->objectType)) {
+                $this->objectType = ObjectTypeCache::getInstance()->getObjectType($comment->objectTypeID);
+            } else if ($this->objectType->objectTypeID !== $comment->objectTypeID) {
+                throw new \InvalidArgumentException('Given comments do not belong to the same object type.');
+            }
+        }
+    }
+
+    public function __invoke(): void
+    {
+        $this->deleteActivityEvents();
+        $this->deleteNotifications();
+        $this->deleteReactions();
+        $this->deleteModerationQueues();
+        $this->deleteMessageEmbeddedObjects();
+        $this->deleteResponses();
+
+        $action = new CommentAction($this->commentIDs, 'delete');
+        $action->executeAction();
+
+        $this->updateCounters();
+
+        $event = new CommentsDeleted($this->comments);
+        EventHandler::getInstance()->fire($event);
+    }
+
+    private function deleteActivityEvents(): void
+    {
+        if (UserActivityEventHandler::getInstance()->getObjectTypeID($this->objectType->objectType . '.recentActivityEvent')) {
+            UserActivityEventHandler::getInstance()->removeEvents(
+                $this->objectType->objectType . '.recentActivityEvent',
+                $this->commentIDs
+            );
+        }
+    }
+
+    private function deleteNotifications(): void
+    {
+        if (UserNotificationHandler::getInstance()->getObjectTypeID($this->objectType->objectType . '.notification')) {
+            UserNotificationHandler::getInstance()->removeNotifications(
+                $this->objectType->objectType . '.notification',
+                $this->commentIDs
+            );
+        }
+    }
+
+    private function deleteReactions(): void
+    {
+        ReactionHandler::getInstance()->removeReactions(
+            'com.woltlab.wcf.comment',
+            $this->commentIDs,
+            UserNotificationHandler::getInstance()->getObjectTypeID($this->objectType->objectType . '.like.notification')
+                ? [$this->objectType->objectType . '.like.notification']
+                : []
+        );
+    }
+
+    private function deleteResponses(): void
+    {
+        $responseList = new CommentResponseList();
+        $responseList->getConditionBuilder()->add('comment_response.commentID IN (?)', [$this->commentIDs]);
+        $responseList->readObjectIDs();
+        if (!\count($responseList->getObjectIDs())) {
+            return;
+        }
+
+        (new DeleteResponses($responseList->getObjects(), $this->updateCounters))();
+    }
+
+    private function deleteModerationQueues(): void
+    {
+        ModerationQueueManager::getInstance()->removeQueues(
+            'com.woltlab.wcf.comment.comment',
+            $this->commentIDs
+        );
+    }
+
+    private function deleteMessageEmbeddedObjects(): void
+    {
+        MessageEmbeddedObjectManager::getInstance()->removeObjects(
+            'com.woltlab.wcf.comment',
+            $this->commentIDs
+        );
+    }
+
+    private function updateCounters(): void
+    {
+        if (!$this->updateCounters) {
+            return;
+        }
+
+        foreach ($this->comments as $comment) {
+            if (!$comment->isDisabled) {
+                $this->objectType->getProcessor()->updateCounter($comment->objectID, -1);
+            }
+        }
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/comment/command/PublishComment.class.php b/wcfsetup/install/files/lib/system/comment/command/PublishComment.class.php
new file mode 100644 (file)
index 0000000..f162642
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+
+namespace wcf\system\comment\command;
+
+use wcf\data\comment\Comment;
+use wcf\data\comment\CommentEditor;
+use wcf\data\object\type\ObjectType;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\event\comment\CommentPublished;
+use wcf\system\event\EventHandler;
+use wcf\system\user\activity\event\UserActivityEventHandler;
+use wcf\system\user\notification\object\CommentUserNotificationObject;
+use wcf\system\user\notification\object\type\ICommentUserNotificationObjectType;
+use wcf\system\user\notification\object\type\IMultiRecipientCommentUserNotificationObjectType;
+use wcf\system\user\notification\UserNotificationHandler;
+
+/**
+ * Publishes 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
+ */
+final class PublishComment
+{
+    private readonly ObjectType $objectType;
+
+    public function __construct(
+        private readonly Comment $comment,
+    ) {
+        $this->objectType = ObjectTypeCache::getInstance()->getObjectType($this->comment->objectTypeID);
+    }
+
+    public function __invoke(): void
+    {
+        if ($this->comment->isDisabled) {
+            (new CommentEditor($this->comment))->update([
+                'isDisabled' => 0
+            ]);
+        }
+        $this->objectType->getProcessor()->updateCounter($this->comment->objectID, 1);
+
+        $this->fireActivityEvent();
+        $this->fireNotificationEvent();
+
+        $event = new CommentPublished($this->comment);
+        EventHandler::getInstance()->fire($event);
+    }
+
+    private function fireActivityEvent(): void
+    {
+        if (
+            $this->comment->userID
+            && UserActivityEventHandler::getInstance()->getObjectTypeID(
+                $this->objectType->objectType . '.recentActivityEvent'
+            )
+        ) {
+            UserActivityEventHandler::getInstance()->fireEvent(
+                $this->objectType->objectType . '.recentActivityEvent',
+                $this->comment->commentID,
+                null,
+                $this->comment->userID,
+                $this->comment->time
+            );
+        }
+    }
+
+    private function fireNotificationEvent(): void
+    {
+        if (!UserNotificationHandler::getInstance()->getEvent($this->objectType->objectType . '.notification', 'comment')) {
+            return;
+        }
+
+        $notificationObject = new CommentUserNotificationObject($this->comment);
+        $notificationObjectType = UserNotificationHandler::getInstance()->getObjectTypeProcessor(
+            $this->objectType->objectType . '.notification'
+        );
+
+        if ($notificationObjectType instanceof IMultiRecipientCommentUserNotificationObjectType) {
+            $recipientIDs = $notificationObjectType->getRecipientIDs($this->comment);
+        } else {
+            $recipientIDs = [];
+        }
+
+        if ($notificationObjectType instanceof ICommentUserNotificationObjectType) {
+            $recipientIDs[] = $notificationObjectType->getOwnerID($this->comment->commentID);
+        }
+
+        // make sure that the comment's author gets no notification
+        $recipientIDs = \array_diff($recipientIDs, [$this->comment->getUserID()]);
+
+        UserNotificationHandler::getInstance()->fireEvent(
+            'comment',
+            $this->objectType->objectType . '.notification',
+            $notificationObject,
+            $recipientIDs
+        );
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/comment/command/UpdateComment.class.php b/wcfsetup/install/files/lib/system/comment/command/UpdateComment.class.php
new file mode 100644 (file)
index 0000000..a3bfa43
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+namespace wcf\system\comment\command;
+
+use wcf\data\comment\Comment;
+use wcf\data\comment\CommentAction;
+use wcf\event\comment\CommentUpdated;
+use wcf\system\event\EventHandler;
+use wcf\system\html\input\HtmlInputProcessor;
+use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
+
+/**
+ * Updates 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
+ */
+final class UpdateComment
+{
+    public function __construct(
+        private readonly Comment $comment,
+        private readonly HtmlInputProcessor $htmlInputProcessor,
+    ) {
+    }
+
+    public function __invoke(): void
+    {
+        $data = [
+            'message' => $this->htmlInputProcessor->getHtml(),
+        ];
+
+        $this->htmlInputProcessor->setObjectID($this->comment->getObjectID());
+        $hasEmbeddedObjects = MessageEmbeddedObjectManager::getInstance()->registerObjects($this->htmlInputProcessor);
+        if ($this->comment->hasEmbeddedObjects != $hasEmbeddedObjects) {
+            $data['hasEmbeddedObjects'] = $this->comment->hasEmbeddedObjects ? 0 : 1;
+        }
+
+        $action = new CommentAction([$this->comment], 'update', [
+            'data' => $data,
+        ]);
+        $action->executeAction();
+
+        $event = new CommentUpdated(new Comment($this->comment->commentID));
+        EventHandler::getInstance()->fire($event);
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/comment/response/command/CreateResponse.class.php b/wcfsetup/install/files/lib/system/comment/response/command/CreateResponse.class.php
new file mode 100644 (file)
index 0000000..5dcd773
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+
+namespace wcf\system\comment\response\command;
+
+use wcf\data\comment\Comment;
+use wcf\data\comment\CommentEditor;
+use wcf\data\comment\response\CommentResponse;
+use wcf\data\comment\response\CommentResponseAction;
+use wcf\data\comment\response\CommentResponseEditor;
+use wcf\data\user\User;
+use wcf\event\comment\response\ResponseCreated;
+use wcf\system\event\EventHandler;
+use wcf\system\html\input\HtmlInputProcessor;
+use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
+use wcf\system\moderation\queue\ModerationQueueActivationManager;
+
+/**
+ * Creates a new 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
+ */
+final class CreateResponse
+{
+    public function __construct(
+        private readonly Comment $comment,
+        private readonly HtmlInputProcessor $htmlInputProcessor,
+        private readonly ?User $user = null,
+        private readonly string $username = '',
+        private readonly bool $isDisabled = false,
+    ) {
+    }
+
+    public function __invoke(): CommentResponse
+    {
+        $action = new CommentResponseAction([], 'create', [
+            'data' => [
+                'commentID' => $this->comment->commentID,
+                'time' => TIME_NOW,
+                'userID' => $this->user ? $this->user->userID : null,
+                'username' => $this->user ? $this->user->username : $this->username,
+                'message' => $this->htmlInputProcessor->getHtml(),
+                'enableHtml' => 1,
+                'isDisabled' => $this->isDisabled ? 1 : 0,
+            ]
+        ]);
+        /** @var CommentResponse $response */
+        $response = $action->executeAction()['returnValues'];
+
+        $this->updateResponseData($response);
+
+        if (!$response->isDisabled) {
+            (new PublishResponse($response))();
+        } else {
+            ModerationQueueActivationManager::getInstance()->addModeratedContent(
+                'com.woltlab.wcf.comment.response',
+                $response->responseID
+            );
+        }
+
+        $this->htmlInputProcessor->setObjectID($response->getObjectID());
+        if (MessageEmbeddedObjectManager::getInstance()->registerObjects($this->htmlInputProcessor)) {
+            (new CommentResponseEditor($response))->update([
+                'hasEmbeddedObjects' => 1,
+            ]);
+            $response = new CommentResponse($response->getObjectID());
+        }
+
+        $event = new ResponseCreated($response);
+        EventHandler::getInstance()->fire($event);
+
+        return $response;
+    }
+
+    private function updateResponseData(CommentResponse $response): void
+    {
+        $unfilteredResponseIDs = $this->comment->getUnfilteredResponseIDs();
+        if (\count($unfilteredResponseIDs) < 5) {
+            $unfilteredResponseIDs[] = $response->responseID;
+        }
+        $unfilteredResponses = $this->comment->unfilteredResponses + 1;
+
+        (new CommentEditor($this->comment))->update([
+            'unfilteredResponseIDs' => \serialize($unfilteredResponseIDs),
+            'unfilteredResponses' => $unfilteredResponses,
+        ]);
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/comment/response/command/DeleteResponses.class.php b/wcfsetup/install/files/lib/system/comment/response/command/DeleteResponses.class.php
new file mode 100644 (file)
index 0000000..bf9016a
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+
+namespace wcf\system\comment\response\command;
+
+use wcf\data\comment\CommentEditor;
+use wcf\data\comment\CommentList;
+use wcf\data\comment\response\CommentResponse;
+use wcf\data\comment\response\CommentResponseAction;
+use wcf\data\object\type\ObjectType;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\event\comment\response\ResponsesDeleted;
+use wcf\system\event\EventHandler;
+use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
+use wcf\system\moderation\queue\ModerationQueueManager;
+use wcf\system\reaction\ReactionHandler;
+use wcf\system\user\activity\event\UserActivityEventHandler;
+use wcf\system\user\notification\UserNotificationHandler;
+
+/**
+ * Deletes a bunch of comment responses that belong to the same object type.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ *
+ * @property-read int[] $responseIDs
+ * @property-read CommentResponse[] $responses
+ */
+final class DeleteResponses
+{
+    private readonly ObjectType $objectType;
+    private readonly array $responseIDs;
+
+    public function __construct(
+        private readonly array $responses,
+        private readonly bool $updateCounters = true,
+    ) {
+        $this->responseIDs = \array_map(fn (CommentResponse $response): int => $response->responseID, $this->responses);
+        foreach ($this->responses as $response) {
+            if (!isset($this->objectType)) {
+                $this->objectType = ObjectTypeCache::getInstance()->getObjectType($response->getComment()->objectTypeID);
+            }
+        }
+    }
+
+    public function __invoke(): void
+    {
+        $this->deleteActivityEvents();
+        $this->deleteNotifications();
+        $this->deleteReactions();
+        $this->deleteModerationQueues();
+        $this->deleteMessageEmbeddedObjects();
+
+        $action = new CommentResponseAction($this->responseIDs, 'delete');
+        $action->executeAction();
+
+        $this->updateCounters();
+
+        $event = new ResponsesDeleted($this->responses);
+        EventHandler::getInstance()->fire($event);
+    }
+
+    private function deleteActivityEvents(): void
+    {
+        if (UserActivityEventHandler::getInstance()->getObjectTypeID($this->objectType->objectType . '.response.recentActivityEvent')) {
+            UserActivityEventHandler::getInstance()->removeEvents(
+                $this->objectType->objectType . '.response.recentActivityEvent',
+                $this->responseIDs
+            );
+        }
+    }
+
+    private function deleteNotifications(): void
+    {
+        if (UserNotificationHandler::getInstance()->getObjectTypeID($this->objectType->objectType . '.response.notification')) {
+            UserNotificationHandler::getInstance()->removeNotifications(
+                $this->objectType->objectType . '.response.notification',
+                $this->responseIDs
+            );
+        }
+    }
+
+    private function deleteReactions(): void
+    {
+        ReactionHandler::getInstance()->removeReactions(
+            'com.woltlab.wcf.comment.response',
+            $this->responseIDs,
+            UserNotificationHandler::getInstance()->getObjectTypeID($this->objectType->objectType . '.response.like.notification')
+                ? [$this->objectType->objectType . '.response.like.notification']
+                : []
+        );
+    }
+
+    private function deleteModerationQueues(): void
+    {
+        ModerationQueueManager::getInstance()->removeQueues(
+            'com.woltlab.wcf.comment.response',
+            $this->responseIDs
+        );
+    }
+
+    private function deleteMessageEmbeddedObjects(): void
+    {
+        MessageEmbeddedObjectManager::getInstance()->removeObjects(
+            'com.woltlab.wcf.comment.response',
+            $this->responseIDs
+        );
+    }
+
+    private function updateCounters(): void
+    {
+        if (!$this->updateCounters) {
+            return;
+        }
+
+        foreach ($this->responses as $response) {
+            $commentIDs[] = $response->commentID;
+        }
+
+        $commentList = new CommentList();
+        $commentList->setObjectIDs(\array_unique($commentIDs));
+        $commentList->readObjects();
+        $comments = $commentList->getObjects();
+
+        foreach ($comments as $comment) {
+            $commentEditor = new CommentEditor($comment);
+            $commentEditor->updateResponseIDs();
+            $commentEditor->updateUnfilteredResponseIDs();
+            $commentEditor->updateResponses();
+            $commentEditor->updateUnfilteredResponses();
+        }
+
+        foreach ($this->responses as $response) {
+            if (!$response->isDisabled) {
+                $this->objectType->getProcessor()->updateCounter($comments[$response->commentID]->objectID, -1);
+            }
+        }
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/comment/response/command/PublishResponse.class.php b/wcfsetup/install/files/lib/system/comment/response/command/PublishResponse.class.php
new file mode 100644 (file)
index 0000000..bcd4fe7
--- /dev/null
@@ -0,0 +1,142 @@
+<?php
+
+namespace wcf\system\comment\response\command;
+
+use wcf\data\comment\Comment;
+use wcf\data\comment\CommentEditor;
+use wcf\data\comment\response\CommentResponse;
+use wcf\data\comment\response\CommentResponseEditor;
+use wcf\data\object\type\ObjectType;
+use wcf\event\comment\response\ResponsePublished;
+use wcf\system\comment\CommentHandler;
+use wcf\system\event\EventHandler;
+use wcf\system\user\activity\event\UserActivityEventHandler;
+use wcf\system\user\notification\object\CommentResponseUserNotificationObject;
+use wcf\system\user\notification\object\type\ICommentUserNotificationObjectType;
+use wcf\system\user\notification\object\type\IMultiRecipientCommentUserNotificationObjectType;
+use wcf\system\user\notification\UserNotificationHandler;
+
+/**
+ * Publishes 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
+ */
+final class PublishResponse
+{
+    private readonly ObjectType $objectType;
+    private readonly Comment $comment;
+
+    public function __construct(
+        private readonly CommentResponse $response,
+    ) {
+        $this->comment = $response->getComment();
+        $this->objectType = CommentHandler::getInstance()->getObjectType($this->comment->objectTypeID);
+    }
+
+    public function __invoke(): void
+    {
+        if ($this->response->isDisabled) {
+            (new CommentResponseEditor($this->response))->update([
+                'isDisabled' => 0,
+            ]);
+        }
+
+        $commentEditor = new CommentEditor($this->comment);
+        $commentEditor->updateCounters(['responses' => 1]);
+        // do not prepend the response id as the approved response can appear anywhere
+        $commentEditor->updateResponseIDs();
+
+        $this->objectType->getProcessor()->updateCounter($this->comment->objectID, 1);
+
+        $this->fireActivityEvent();
+        $this->fireNotificationEvent();
+
+        $event = new ResponsePublished($this->response);
+        EventHandler::getInstance()->fire($event);
+    }
+
+    private function fireActivityEvent(): void
+    {
+        if (
+            $this->response->userID
+            && UserActivityEventHandler::getInstance()->getObjectTypeID(
+                $this->objectType->objectType . '.response.recentActivityEvent'
+            )
+        ) {
+            UserActivityEventHandler::getInstance()->fireEvent(
+                $this->objectType->objectType . '.response.recentActivityEvent',
+                $this->response->responseID,
+                null,
+                $this->response->userID,
+                $this->response->time
+            );
+        }
+    }
+
+    private function fireNotificationEvent(): void
+    {
+        if (
+            !UserNotificationHandler::getInstance()->getObjectTypeID($this->objectType->objectType . '.notification')
+            || (
+                !UserNotificationHandler::getInstance()->getEvent($this->objectType->objectType . '.response.notification', 'commentResponse')
+                && !UserNotificationHandler::getInstance()->getEvent($this->objectType->objectType . '.response.notification', 'commentResponseOwner')
+            )
+        ) {
+            return;
+        }
+
+        $notificationObject = new CommentResponseUserNotificationObject($this->response);
+        $notificationObjectType = UserNotificationHandler::getInstance()->getObjectTypeProcessor($this->objectType->objectType . '.notification');
+
+        if ($notificationObjectType instanceof IMultiRecipientCommentUserNotificationObjectType) {
+            $recipientIDs = $notificationObjectType->getRecipientIDs($this->comment);
+        } else {
+            $recipientIDs = [];
+        }
+
+        $recipientIDs[] = $this->comment->userID;
+
+        $userID = 0;
+        if ($notificationObjectType instanceof ICommentUserNotificationObjectType) {
+            $userID = $notificationObjectType->getOwnerID($this->comment->commentID);
+        }
+
+        // make sure that the response's author gets no notification
+        $recipientIDs = \array_diff($recipientIDs, [$this->response->getUserID()]);
+
+        if (UserNotificationHandler::getInstance()->getEvent($this->objectType->objectType . '.response.notification', 'commentResponse')) {
+            UserNotificationHandler::getInstance()->fireEvent(
+                'commentResponse',
+                $this->objectType->objectType . '.response.notification',
+                $notificationObject,
+                $recipientIDs,
+                [
+                    'commentID' => $this->comment->commentID,
+                    'objectID' => $this->comment->objectID,
+                    'userID' => $this->comment->userID,
+                ]
+            );
+        }
+
+        // notify the container owner
+        if (UserNotificationHandler::getInstance()->getEvent($this->objectType->objectType . '.response.notification', 'commentResponseOwner')) {
+            if ($userID && $userID != $this->comment->userID && $userID != $this->response->getUserID()) {
+                UserNotificationHandler::getInstance()->fireEvent(
+                    'commentResponseOwner',
+                    $this->objectType->objectType . '.response.notification',
+                    $notificationObject,
+                    [$userID],
+                    [
+                        'commentID' => $this->comment->commentID,
+                        'objectID' => $this->comment->objectID,
+                        'objectUserID' => $userID,
+                        'userID' => $this->comment->userID,
+                    ]
+                );
+            }
+        }
+    }
+}
diff --git a/wcfsetup/install/files/lib/system/comment/response/command/UpdateResponse.class.php b/wcfsetup/install/files/lib/system/comment/response/command/UpdateResponse.class.php
new file mode 100644 (file)
index 0000000..0624370
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+namespace wcf\system\comment\response\command;
+
+use wcf\data\comment\response\CommentResponse;
+use wcf\data\comment\response\CommentResponseAction;
+use wcf\event\comment\response\ResponseUpdated;
+use wcf\system\event\EventHandler;
+use wcf\system\html\input\HtmlInputProcessor;
+use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
+
+/**
+ * Updates 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
+ */
+final class UpdateResponse
+{
+    public function __construct(
+        private readonly CommentResponse $response,
+        private readonly HtmlInputProcessor $htmlInputProcessor,
+    ) {
+    }
+
+    public function __invoke(): void
+    {
+        $data = [
+            'message' => $this->htmlInputProcessor->getHtml(),
+        ];
+
+        $this->htmlInputProcessor->setObjectID($this->response->getObjectID());
+        $hasEmbeddedObjects = MessageEmbeddedObjectManager::getInstance()->registerObjects($this->htmlInputProcessor);
+        if ($this->response->hasEmbeddedObjects != $hasEmbeddedObjects) {
+            $data['hasEmbeddedObjects'] = $this->response->hasEmbeddedObjects ? 0 : 1;
+        }
+
+        $action = new CommentResponseAction([$this->response], 'update', [
+            'data' => $data,
+        ]);
+        $action->executeAction();
+
+        $event = new ResponseUpdated(new CommentResponse($this->response->commentID));
+        EventHandler::getInstance()->fire($event);
+    }
+}