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')); } }