# fluent-boards/trunk/app/Http/Controllers/CommentController.php

FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration, version trunk. 379 lines.

- Page: https://pluginprobe.com/plugins/fluent-boards/trunk/code/app/Http/Controllers/CommentController.php
- Raw: https://pluginprobe.com/plugins/fluent-boards/trunk/raw/app/Http/Controllers/CommentController.php
- Modified: 2026-09-09T13:50:06+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/fluent-boards/trunk/code/app/Http/Controllers/CommentController.php#L10-L20`.

```php
<?php

namespace FluentBoards\App\Http\Controllers;

use FluentBoards\App\Models\Comment;
use FluentBoards\App\Services\NotificationService;
use FluentBoards\App\Services\Helper;
use FluentBoards\App\Services\UploadService;
use FluentBoards\Framework\Http\Request\Request;
use FluentBoards\App\Services\CommentService;
use FluentBoardsPro\App\Services\AttachmentService;

class CommentController extends Controller
{
    private $commentService;
    private $notificationService;

    public function __construct(CommentService $commentService, NotificationService $notificationService)
    {
        parent::__construct();
        $this->commentService = $commentService;
        $this->notificationService = $notificationService;
    }

    public function getComments(Request $request, $board_id, $task_id)
    {
        try {
            $filter = $request->getSafe('filter', 'sanitize_text_field');
            $per_page =  10;

            $comments = $this->commentService->getComments($task_id, $per_page, $filter, $board_id);
            $totalComments = $this->commentService->getTotal($task_id, $board_id);

            return $this->sendSuccess([
                'comments' => $comments,
                'total' => $totalComments
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 404);
        }
    }

    /*
     * handles comment or reply creation
     * @param $board_id int
     * @param $task_id int
     * @return json
     */
    public function create(Request $request, $board_id, $task_id)
    {
        // TODO: Refactor the whole request and sanitize process here.. minimize the code in this functions.
        $requestData = [
            'parent_id'     => $request->getSafe('parent_id', function ($value) {
                return (empty($value)) ? null : intval( $value);
            }, null),
            'description'   => $request->getSafe('comment', 'sanitize_textarea_field'),
            'created_by'    => get_current_user_id(),
            'task_id'       => (int) $task_id,
            'type'          => $request->getSafe('comment_type', 'sanitize_text_field', 'comment'),
            'board_id'      => (int) $board_id,
        ];
        $validationRules = [
            'description'   => 'required|string',
            'created_by'    => 'required|integer',
            'board_id'      => 'required|integer',
            'task_id'       => 'required|integer',
            'type'          => 'required|string'
        ];

        $imageIds = $this->getImageIdsFromRequest($request);
        if ($imageIds) {
            $validationRules['description'] = 'nullable|string';
        }

        $commentData = $this->commentSanitizeAndValidate($requestData, $validationRules);


        try {
            if (!empty($imageIds)) {
                $this->commentService->assertCommentImagesAttachable($imageIds, $board_id, $task_id);
            }

            $rawDescription = $commentData['description'];
            $mentionData = $this->getMentionData($request, $board_id);
            $commentData['settings'] = [ 'raw_description' => $rawDescription, 'mentioned_id' => $mentionData ];

            // Ensure UTF-8 encoding for comment description
            $description = mb_convert_encoding($commentData['description'], 'UTF-8', 'auto');

            if(!empty($mentionData)) {
                // Process mentions and links with UTF-8 support
                $commentData['description'] = $this->commentService->processMentionAndLink($description, $mentionData);
            } else {
                // Process links with UTF-8 support
                $commentData['description'] = $this->commentService->checkIfCommentHaveLinks($description);
            }

            $comment = $this->commentService->create($commentData, $task_id, $board_id);
            if (!empty($imageIds)) {
                $this->commentService->attachCommentImages($comment, $imageIds);
                $comment->load(['images']);
            }
            $comment['user'] = $comment->user;

            $recipientUserIds = [];
            if ($comment->type == 'reply') {
                $parentComment = Comment::findOrFail($comment->parent_id);
                $commenterId = $parentComment->created_by;
                if ($commenterId != get_current_user_id())
                {
                    $recipientUserIds[] = absint($commenterId);
                }
                $this->sendMailAfterComment($comment->id, $recipientUserIds);
            } else {
                // Queue revocable IDs; the worker rechecks membership and preferences before sending.
                $recipientUserIds = $this->notificationService->getCommentRecipientUserIds($task_id);
                $this->sendMailAfterComment($comment->id, $recipientUserIds);
            }

            if(!empty($mentionData))
            {
                $this->notificationService->mentionInComment($comment, $mentionData);
            }

            if ($comment->type == 'comment')
            {
                $comment->load('replies');
            }

            return $this->sendSuccess([
                'message' => __('Comment has been added', 'fluent-boards'),
                'comment' => $comment
            ], 201);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), $e->getCode() === 403 ? 403 : 400);
        }
    }

    public function update(Request $request, $board_id, $comment_id)
    {
        $requestData = [
            'description'   => $request->getSafe('comment', 'sanitize_textarea_field')
        ];

        $validationRules = [
            'description'   => 'required|string'
        ];

        $hasImagesParam = $this->requestHasImagesArray($request);
        $imageIds = $this->getImageIdsFromRequest($request);
        if ($hasImagesParam) {
            $validationRules['description'] = 'nullable|string';
        }

        $commentData = $this->commentSanitizeAndValidate($requestData, $validationRules);

        try {
            if ($hasImagesParam) {
                $commentForImages = $this->commentService->findCommentOnBoard($comment_id, $board_id);
                if ($commentForImages->created_by != get_current_user_id()) {
                    $errorMessage = __('Unauthorized Action', 'fluent-boards');
                    return $this->sendError($errorMessage, 401);
                }
                $this->commentService->assertCommentImagesAttachableForComment($commentForImages, $imageIds);
            }

            $mentionData = $this->getMentionData($request);
            
            $comment = $this->commentService->update($commentData, $comment_id, $mentionData, $board_id);

            if (!$comment) {
                $errorMessage = __('Unauthorized Action', 'fluent-boards');
                return $this->sendError($errorMessage, 401);
            }

            if(!empty($mentionData))
            {
                $this->notificationService->mentionInComment($comment, $mentionData);
            }

            if ($hasImagesParam) {
                $this->commentService->attachCommentImages($comment, $imageIds);
                $comment->load(['images']);
            }

            $comment->load('user');

            return $this->sendSuccess([
                'comment' => $comment,
                'message'     => __('Comment has been updated', 'fluent-boards'),
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), $e->getCode() === 403 ? 403 : 404);
        }
    }

    public function deleteComment($board_id, $comment_id)
    {
        try {
            $this->commentService->delete($comment_id, $board_id);

            return $this->sendSuccess([
                'message' => __('Comment has been deleted', 'fluent-boards'),
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 404);
        }
    }

    public function updateReply(Request $request, $board_id, $reply_id)
    {
        $requestData = [
            'description'   => $request->getSafe('comment', 'sanitize_textarea_field')
        ];

        $validationRules = [
            'description'   => 'required|string'
        ];

        $replyData = $this->commentSanitizeAndValidate($requestData, $validationRules);

        try {
            $mentionData = $this->getMentionData($request);
            
            $reply = $this->commentService->update($replyData, $reply_id, $mentionData, $board_id);

            if (!$reply) {
                $errorMessage = __('Unauthorized Action', 'fluent-boards');
                return $this->sendError($errorMessage, 401);
            }

            return $this->sendSuccess([
                'description' => $reply->description,
                'message'     => __('Reply has been updated', 'fluent-boards'),
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), $e->getCode() === 403 ? 403 : 404);
        }
    }

    public function deleteReply($board_id, $reply_id)
    {
        try {
            $this->commentService->deleteReply($reply_id, $board_id);

            return $this->sendSuccess([
                'message' => __('Reply has been deleted', 'fluent-boards'),
            ], 200);
        } catch (\Exception $e) {
            return $this->sendError($e->getMessage(), 404);
        }
    }

    public function sendMailAfterComment($commentId, $recipientUserIds)
    {
        $current_user_id = get_current_user_id();

        /* this will run in background as soon as possible */
        /* sending Model or Model Instance won't work here */
        as_enqueue_async_action('fluent_boards/one_time_schedule_send_email_for_comment', [$commentId, $recipientUserIds, $current_user_id], 'fluent-boards');
    }

    /**
     * Sanitize mention IDs and optionally verify board membership before a create.
     *
     * @param Request $request
     * @param int $boardId
     * @return array
     * @throws \Exception
     */
    private function getMentionData(Request $request, $boardId = null)
    {
        $rawMentionData = $request->getSafe('mentionData');
        if (!is_array($rawMentionData)) {
            return [];
        }

        $mentionData = array_values(array_unique(array_filter(array_map('absint', $rawMentionData))));

        if (!$boardId) {
            return $mentionData;
        }

        $boardMemberIds = $this->notificationService->resolveBoardMentionUserIds($boardId, $mentionData);

        if (array_diff($mentionData, $boardMemberIds)) {
            throw new \Exception(esc_html__('One or more mentioned users are not members of this board', 'fluent-boards'), 403);
        }

        return $boardMemberIds;
    }

    private function commentSanitizeAndValidate($data, array $rules = [])
    {
        $data = Helper::sanitizeComment($data);

        return $this->validate($data, $rules);
    }

    public function handleImageUpload(Request $request, $board_id, $task_id)
    {
        $allowedTypes = implode(',', [
            "image/jpeg",
            "image/gif",
            "image/png",
            "image/bmp",
            "image/tiff",
            "image/webp",
            "image/avif",
            "image/x-icon",
            "image/heic",
        ]);

        $files = $this->validate($request->files(), [
            'file' => 'mimetypes:' . $allowedTypes,
        ], [
            'file.mimetypes' => __('The file must be a image type.', 'fluent-boards')
        ]);

        (new \FluentBoards\App\Services\TaskService())->findTaskOnBoard($task_id, $board_id);

        $uploadInfo = UploadService::handleFileUpload( $files, $board_id);

        $imageData = $uploadInfo[0];
        $attachment = $this->commentService->createCommentImage($imageData, $board_id, $task_id);
        if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
            $mediaData = (new AttachmentService())->processMediaData($imageData, $files['file']);
            $attachment['driver'] = $mediaData['driver'];
            $attachment['file_path'] = $mediaData['file_path'];
            $attachment['full_url'] = $mediaData['full_url'];
            $attachment->save();
        }
        $attachment->public_url = $this->commentService->createPublicUrl($attachment, $board_id);

        return $this->sendSuccess([
            'message'    => __('attachment has been added', 'fluent-boards'),
            'imageAttachment' => $attachment
        ], 200);

    }

    public function updateCommentPrivacy($board_id, $comment_id)
    {
        $comment = $this->commentService->findCommentOnBoard($comment_id, $board_id);

        // Check if user has permission to update the comment
        if ($comment->created_by != get_current_user_id()) {
            return $this->sendError(__('Unauthorized Action', 'fluent-boards'), 401);
        }

        // Toggle privacy
        $comment->privacy = ($comment->privacy === 'public') ? 'private' : 'public';
        $comment->save();

        return $this->sendSuccess([
            'comment' => $comment,
            $privacy = $comment->privacy == 'public' ? __('public', 'fluent-boards') : __('private', 'fluent-boards'),
            // translators: %s is the privacy setting (public or private)
            'message' => sprintf(__('This comment is now %s', 'fluent-boards'), $privacy),
        ], 200);
    }

    private function getImageIdsFromRequest(Request $request)
    {
        $images = $request->getSafe('images');

        if (!$images || !is_array($images)) {
            return [];
        }

        return array_values(array_filter(array_unique(array_map('intval', $images))));
    }

    private function requestHasImagesArray(Request $request)
    {
        return $request->exists('images') && is_array($request->getSafe('images'));
    }
}

```
