PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / trunk
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration vtrunk
2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 1.45 All 41 releases
← All changes | app/Http/Controllers/CommentController.php +245 -42 1.21trunk View file →
@@ -1,14 +1,15 @@
1 1 <?php
2 2
3 3 namespace FluentBoards\App\Http\Controllers;
4 4
5 -use FluentBoards\App\Models\Task;
5 +use FluentBoards\App\Models\Comment;
6 6 use FluentBoards\App\Services\NotificationService;
7 -use FluentBoards\App\Services\Constant;
8 7 use FluentBoards\App\Services\Helper;
8 +use FluentBoards\App\Services\UploadService;
9 9 use FluentBoards\Framework\Http\Request\Request;
10 10 use FluentBoards\App\Services\CommentService;
11 +use FluentBoardsPro\App\Services\AttachmentService;
11 12
12 13 class CommentController extends Controller
13 14 {
14 15 private $commentService;
@@ -23,13 +24,13 @@
23 24
24 25 public function getComments(Request $request, $board_id, $task_id)
25 26 {
26 27 try {
27 - $filter = $request->getSafe('filter');
28 + $filter = $request->getSafe('filter', 'sanitize_text_field');
28 29 $per_page = 10;
29 30
30 - $comments = $this->commentService->getComments($task_id, $per_page, $filter);
31 - $totalComments = $this->commentService->getTotal($task_id);
31 + $comments = $this->commentService->getComments($task_id, $per_page, $filter, $board_id);
32 + $totalComments = $this->commentService->getTotal($task_id, $board_id);
32 33
33 34 return $this->sendSuccess([
34 35 'comments' => $comments,
35 36 'total' => $totalComments
@@ -48,72 +49,156 @@
48 49 public function create(Request $request, $board_id, $task_id)
49 50 {
50 51 // TODO: Refactor the whole request and sanitize process here.. minimize the code in this functions.
51 52 $requestData = [
52 - 'parent_id' => $request->parent_id,
53 - 'description' => $request->comment,
54 - 'created_by' => $request->comment_by,
55 - 'task_id' => $task_id,
56 - 'type' => $request->comment_type ? $request->comment_type : 'comment',
53 + 'parent_id' => $request->getSafe('parent_id', function ($value) {
54 + return (empty($value)) ? null : intval( $value);
55 + }, null),
56 + 'description' => $request->getSafe('comment', 'sanitize_textarea_field'),
57 + 'created_by' => get_current_user_id(),
58 + 'task_id' => (int) $task_id,
59 + 'type' => $request->getSafe('comment_type', 'sanitize_text_field', 'comment'),
57 60 'board_id' => (int) $board_id,
58 61 ];
59 -
60 - $commentData = $this->commentSanitizeAndValidate($requestData, [
62 + $validationRules = [
61 63 'description' => 'required|string',
62 64 'created_by' => 'required|integer',
63 65 'board_id' => 'required|integer',
64 66 'task_id' => 'required|integer',
65 67 'type' => 'required|string'
66 - ]);
68 + ];
67 69
70 + $imageIds = $this->getImageIdsFromRequest($request);
71 + if ($imageIds) {
72 + $validationRules['description'] = 'nullable|string';
73 + }
74 +
75 + $commentData = $this->commentSanitizeAndValidate($requestData, $validationRules);
76 +
77 +
68 78 try {
69 - $comment = $this->commentService->create($commentData, $task_id);
79 + if (!empty($imageIds)) {
80 + $this->commentService->assertCommentImagesAttachable($imageIds, $board_id, $task_id);
81 + }
82 +
83 + $rawDescription = $commentData['description'];
84 + $mentionData = $this->getMentionData($request, $board_id);
85 + $commentData['settings'] = [ 'raw_description' => $rawDescription, 'mentioned_id' => $mentionData ];
86 +
87 + // Ensure UTF-8 encoding for comment description
88 + $description = mb_convert_encoding($commentData['description'], 'UTF-8', 'auto');
89 +
90 + if(!empty($mentionData)) {
91 + // Process mentions and links with UTF-8 support
92 + $commentData['description'] = $this->commentService->processMentionAndLink($description, $mentionData);
93 + } else {
94 + // Process links with UTF-8 support
95 + $commentData['description'] = $this->commentService->checkIfCommentHaveLinks($description);
96 + }
97 +
98 + $comment = $this->commentService->create($commentData, $task_id, $board_id);
99 + if (!empty($imageIds)) {
100 + $this->commentService->attachCommentImages($comment, $imageIds);
101 + $comment->load(['images']);
102 + }
70 103 $comment['user'] = $comment->user;
71 - //sending emails to assignees who enabled their email
72 - $usersToSendEmail = $this->notificationService->filterAssigneeToSendEmail($task_id, Constant::BOARD_EMAIL_COMMENT);
73 104
74 - $this->sendMailAfterComment($comment->id, $usersToSendEmail);
105 + $recipientUserIds = [];
106 + if ($comment->type == 'reply') {
107 + $parentComment = Comment::findOrFail($comment->parent_id);
108 + $commenterId = $parentComment->created_by;
109 + if ($commenterId != get_current_user_id())
110 + {
111 + $recipientUserIds[] = absint($commenterId);
112 + }
113 + $this->sendMailAfterComment($comment->id, $recipientUserIds);
114 + } else {
115 + // Queue revocable IDs; the worker rechecks membership and preferences before sending.
116 + $recipientUserIds = $this->notificationService->getCommentRecipientUserIds($task_id);
117 + $this->sendMailAfterComment($comment->id, $recipientUserIds);
118 + }
75 119
76 -// if($request->mentions)
77 -// {
78 -// do_action('fluent_boards/mention_comment_notification', $task_id, get_current_user_id(), $request->mentions);
79 -// }
120 + if(!empty($mentionData))
121 + {
122 + $this->notificationService->mentionInComment($comment, $mentionData);
123 + }
80 124
125 + if ($comment->type == 'comment')
126 + {
127 + $comment->load('replies');
128 + }
129 +
81 130 return $this->sendSuccess([
82 131 'message' => __('Comment has been added', 'fluent-boards'),
83 132 'comment' => $comment
84 133 ], 201);
85 134 } catch (\Exception $e) {
86 - return $this->sendError($e->getMessage(), 404);
135 + return $this->sendError($e->getMessage(), $e->getCode() === 403 ? 403 : 400);
87 136 }
88 137 }
89 138
90 - public function update(Request $request, $comment_id)
139 + public function update(Request $request, $board_id, $comment_id)
91 140 {
92 - $commentData = $this->commentSanitizeAndValidate($request->all(), [
93 - 'description' => 'required|string',
94 - ]);
141 + $requestData = [
142 + 'description' => $request->getSafe('comment', 'sanitize_textarea_field')
143 + ];
144 +
145 + $validationRules = [
146 + 'description' => 'required|string'
147 + ];
148 +
149 + $hasImagesParam = $this->requestHasImagesArray($request);
150 + $imageIds = $this->getImageIdsFromRequest($request);
151 + if ($hasImagesParam) {
152 + $validationRules['description'] = 'nullable|string';
153 + }
154 +
155 + $commentData = $this->commentSanitizeAndValidate($requestData, $validationRules);
156 +
95 157 try {
96 - $comment = $this->commentService->update($commentData, $comment_id);
158 + if ($hasImagesParam) {
159 + $commentForImages = $this->commentService->findCommentOnBoard($comment_id, $board_id);
160 + if ($commentForImages->created_by != get_current_user_id()) {
161 + $errorMessage = __('Unauthorized Action', 'fluent-boards');
162 + return $this->sendError($errorMessage, 401);
163 + }
164 + $this->commentService->assertCommentImagesAttachableForComment($commentForImages, $imageIds);
165 + }
97 166
98 - if ( !$comment ) {
167 + $mentionData = $this->getMentionData($request);
168 +
169 + $comment = $this->commentService->update($commentData, $comment_id, $mentionData, $board_id);
170 +
171 + if (!$comment) {
99 172 $errorMessage = __('Unauthorized Action', 'fluent-boards');
100 173 return $this->sendError($errorMessage, 401);
101 174 }
102 175
176 + if(!empty($mentionData))
177 + {
178 + $this->notificationService->mentionInComment($comment, $mentionData);
179 + }
180 +
181 + if ($hasImagesParam) {
182 + $this->commentService->attachCommentImages($comment, $imageIds);
183 + $comment->load(['images']);
184 + }
185 +
186 + $comment->load('user');
187 +
103 188 return $this->sendSuccess([
104 - 'description' => $comment->description,
189 + 'comment' => $comment,
105 190 'message' => __('Comment has been updated', 'fluent-boards'),
106 191 ], 200);
107 192 } catch (\Exception $e) {
108 - return $this->sendError($e->getMessage(), 404);
193 + return $this->sendError($e->getMessage(), $e->getCode() === 403 ? 403 : 404);
109 194 }
110 195 }
111 196
112 - public function delete($comment_id)
197 + public function deleteComment($board_id, $comment_id)
113 198 {
114 199 try {
115 - $this->commentService->delete($comment_id);
200 + $this->commentService->delete($comment_id, $board_id);
116 201
117 202 return $this->sendSuccess([
118 203 'message' => __('Comment has been deleted', 'fluent-boards'),
119 204 ], 200);
@@ -121,15 +206,24 @@
121 206 return $this->sendError($e->getMessage(), 404);
122 207 }
123 208 }
124 209
125 - public function updateReply(Request $request, $task_id)
210 + public function updateReply(Request $request, $board_id, $reply_id)
126 211 {
127 - $replyData = $this->commentSanitizeAndValidate($request->all(), [
128 - 'description' => 'required|string',
129 - ]);
212 + $requestData = [
213 + 'description' => $request->getSafe('comment', 'sanitize_textarea_field')
214 + ];
215 +
216 + $validationRules = [
217 + 'description' => 'required|string'
218 + ];
219 +
220 + $replyData = $this->commentSanitizeAndValidate($requestData, $validationRules);
221 +
130 222 try {
131 - $reply = $this->commentService->updateReply($replyData, $task_id);
223 + $mentionData = $this->getMentionData($request);
224 +
225 + $reply = $this->commentService->update($replyData, $reply_id, $mentionData, $board_id);
132 226
133 227 if (!$reply) {
134 228 $errorMessage = __('Unauthorized Action', 'fluent-boards');
135 229 return $this->sendError($errorMessage, 401);
@@ -139,16 +233,16 @@
139 233 'description' => $reply->description,
140 234 'message' => __('Reply has been updated', 'fluent-boards'),
141 235 ], 200);
142 236 } catch (\Exception $e) {
143 - return $this->sendError($e->getMessage(), 404);
237 + return $this->sendError($e->getMessage(), $e->getCode() === 403 ? 403 : 404);
144 238 }
145 239 }
146 240
147 - public function deleteReply($comment_id)
241 + public function deleteReply($board_id, $reply_id)
148 242 {
149 243 try {
150 - $this->commentService->deleteReply($comment_id);
244 + $this->commentService->deleteReply($reply_id, $board_id);
151 245
152 246 return $this->sendSuccess([
153 247 'message' => __('Reply has been deleted', 'fluent-boards'),
154 248 ], 200);
@@ -156,20 +250,129 @@
156 250 return $this->sendError($e->getMessage(), 404);
157 251 }
158 252 }
159 253
160 - public function sendMailAfterComment($commentId, $usersToSendEmail)
254 + public function sendMailAfterComment($commentId, $recipientUserIds)
161 255 {
162 256 $current_user_id = get_current_user_id();
163 257
164 258 /* this will run in background as soon as possible */
165 259 /* sending Model or Model Instance won't work here */
166 - as_enqueue_async_action('fluent_boards/one_time_schedule_send_email_for_comment', [$commentId, $usersToSendEmail, $current_user_id], 'fluent-boards');
260 + as_enqueue_async_action('fluent_boards/one_time_schedule_send_email_for_comment', [$commentId, $recipientUserIds, $current_user_id], 'fluent-boards');
167 261 }
168 262
263 + /**
264 + * Sanitize mention IDs and optionally verify board membership before a create.
265 + *
266 + * @param Request $request
267 + * @param int $boardId
268 + * @return array
269 + * @throws \Exception
270 + */
271 + private function getMentionData(Request $request, $boardId = null)
272 + {
273 + $rawMentionData = $request->getSafe('mentionData');
274 + if (!is_array($rawMentionData)) {
275 + return [];
276 + }
277 +
278 + $mentionData = array_values(array_unique(array_filter(array_map('absint', $rawMentionData))));
279 +
280 + if (!$boardId) {
281 + return $mentionData;
282 + }
283 +
284 + $boardMemberIds = $this->notificationService->resolveBoardMentionUserIds($boardId, $mentionData);
285 +
286 + if (array_diff($mentionData, $boardMemberIds)) {
287 + throw new \Exception(esc_html__('One or more mentioned users are not members of this board', 'fluent-boards'), 403);
288 + }
289 +
290 + return $boardMemberIds;
291 + }
292 +
169 293 private function commentSanitizeAndValidate($data, array $rules = [])
170 294 {
171 295 $data = Helper::sanitizeComment($data);
172 296
173 297 return $this->validate($data, $rules);
298 + }
299 +
300 + public function handleImageUpload(Request $request, $board_id, $task_id)
301 + {
302 + $allowedTypes = implode(',', [
303 + "image/jpeg",
304 + "image/gif",
305 + "image/png",
306 + "image/bmp",
307 + "image/tiff",
308 + "image/webp",
309 + "image/avif",
310 + "image/x-icon",
311 + "image/heic",
312 + ]);
313 +
314 + $files = $this->validate($request->files(), [
315 + 'file' => 'mimetypes:' . $allowedTypes,
316 + ], [
317 + 'file.mimetypes' => __('The file must be a image type.', 'fluent-boards')
318 + ]);
319 +
320 + (new \FluentBoards\App\Services\TaskService())->findTaskOnBoard($task_id, $board_id);
321 +
322 + $uploadInfo = UploadService::handleFileUpload( $files, $board_id);
323 +
324 + $imageData = $uploadInfo[0];
325 + $attachment = $this->commentService->createCommentImage($imageData, $board_id, $task_id);
326 + if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
327 + $mediaData = (new AttachmentService())->processMediaData($imageData, $files['file']);
328 + $attachment['driver'] = $mediaData['driver'];
329 + $attachment['file_path'] = $mediaData['file_path'];
330 + $attachment['full_url'] = $mediaData['full_url'];
331 + $attachment->save();
332 + }
333 + $attachment->public_url = $this->commentService->createPublicUrl($attachment, $board_id);
334 +
335 + return $this->sendSuccess([
336 + 'message' => __('attachment has been added', 'fluent-boards'),
337 + 'imageAttachment' => $attachment
338 + ], 200);
339 +
340 + }
341 +
342 + public function updateCommentPrivacy($board_id, $comment_id)
343 + {
344 + $comment = $this->commentService->findCommentOnBoard($comment_id, $board_id);
345 +
346 + // Check if user has permission to update the comment
347 + if ($comment->created_by != get_current_user_id()) {
348 + return $this->sendError(__('Unauthorized Action', 'fluent-boards'), 401);
349 + }
350 +
351 + // Toggle privacy
352 + $comment->privacy = ($comment->privacy === 'public') ? 'private' : 'public';
353 + $comment->save();
354 +
355 + return $this->sendSuccess([
356 + 'comment' => $comment,
357 + $privacy = $comment->privacy == 'public' ? __('public', 'fluent-boards') : __('private', 'fluent-boards'),
358 + // translators: %s is the privacy setting (public or private)
359 + 'message' => sprintf(__('This comment is now %s', 'fluent-boards'), $privacy),
360 + ], 200);
361 + }
362 +
363 + private function getImageIdsFromRequest(Request $request)
364 + {
365 + $images = $request->getSafe('images');
366 +
367 + if (!$images || !is_array($images)) {
368 + return [];
369 + }
370 +
371 + return array_values(array_filter(array_unique(array_map('intval', $images))));
372 + }
373 +
374 + private function requestHasImagesArray(Request $request)
375 + {
376 + return $request->exists('images') && is_array($request->getSafe('images'));
174 377 }
175 378 }