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 +145 -58 1.40trunk View file →
@@ -2,12 +2,9 @@
2 2
3 3 namespace FluentBoards\App\Http\Controllers;
4 4
5 5 use FluentBoards\App\Models\Comment;
6 -use FluentBoards\App\Models\Task;
7 -use FluentBoards\App\Models\User;
8 6 use FluentBoards\App\Services\NotificationService;
9 -use FluentBoards\App\Services\Constant;
10 7 use FluentBoards\App\Services\Helper;
11 8 use FluentBoards\App\Services\UploadService;
12 9 use FluentBoards\Framework\Http\Request\Request;
13 10 use FluentBoards\App\Services\CommentService;
@@ -27,13 +24,13 @@
27 24
28 25 public function getComments(Request $request, $board_id, $task_id)
29 26 {
30 27 try {
31 - $filter = $request->getSafe('filter');
28 + $filter = $request->getSafe('filter', 'sanitize_text_field');
32 29 $per_page = 10;
33 30
34 - $comments = $this->commentService->getComments($task_id, $per_page, $filter);
35 - $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);
36 33
37 34 return $this->sendSuccess([
38 35 'comments' => $comments,
39 36 'total' => $totalComments
@@ -52,13 +49,15 @@
52 49 public function create(Request $request, $board_id, $task_id)
53 50 {
54 51 // TODO: Refactor the whole request and sanitize process here.. minimize the code in this functions.
55 52 $requestData = [
56 - 'parent_id' => $request->parent_id,
57 - 'description' => $request->comment,
58 - 'created_by' => $request->comment_by,
59 - 'task_id' => $task_id,
60 - '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'),
61 60 'board_id' => (int) $board_id,
62 61 ];
63 62 $validationRules = [
64 63 'description' => 'required|string',
@@ -67,9 +66,10 @@
67 66 'task_id' => 'required|integer',
68 67 'type' => 'required|string'
69 68 ];
70 69
71 - if ($request->images) {
70 + $imageIds = $this->getImageIdsFromRequest($request);
71 + if ($imageIds) {
72 72 $validationRules['description'] = 'nullable|string';
73 73 }
74 74
75 75 $commentData = $this->commentSanitizeAndValidate($requestData, $validationRules);
@@ -75,47 +75,54 @@
75 75 $commentData = $this->commentSanitizeAndValidate($requestData, $validationRules);
76 76
77 77
78 78 try {
79 + if (!empty($imageIds)) {
80 + $this->commentService->assertCommentImagesAttachable($imageIds, $board_id, $task_id);
81 + }
82 +
79 83 $rawDescription = $commentData['description'];
80 - $commentData['settings'] = [ 'raw_description' => $rawDescription, 'mentioned_id' => $request->mentionData ];
81 - if($request->mentionData) {
82 - $commentData['description'] = $this->commentService->processMentionAndLink($commentData['description'], $request->mentionData);
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);
83 93 } else {
84 - $commentData['description'] = $this->commentService->checkIfCommentHaveLinks($commentData['description']);
94 + // Process links with UTF-8 support
95 + $commentData['description'] = $this->commentService->checkIfCommentHaveLinks($description);
85 96 }
86 97
87 - $comment = $this->commentService->create($commentData, $task_id);
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 + }
88 103 $comment['user'] = $comment->user;
89 104
90 - $usersToSendEmail = [];
105 + $recipientUserIds = [];
91 106 if ($comment->type == 'reply') {
92 107 $parentComment = Comment::findOrFail($comment->parent_id);
93 108 $commenterId = $parentComment->created_by;
94 109 if ($commenterId != get_current_user_id())
95 110 {
96 - $commenter = User::select('user_email')->findOrFail($commenterId);
97 - $commenterEmail = $commenter->user_email;
98 - $usersToSendEmail[] = $commenterEmail;
111 + $recipientUserIds[] = absint($commenterId);
99 112 }
100 - $this->sendMailAfterComment($comment->id, $usersToSendEmail);
113 + $this->sendMailAfterComment($comment->id, $recipientUserIds);
101 114 } else {
102 - //sending emails to assignees who enabled their email
103 - $usersToSendEmail = $this->notificationService->filterAssigneeToSendEmail($task_id, Constant::BOARD_EMAIL_COMMENT);
104 - $this->sendMailAfterComment($comment->id, $usersToSendEmail);
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);
105 118 }
106 119
107 - if($request->mentionData)
120 + if(!empty($mentionData))
108 121 {
109 - $this->notificationService->mentionInComment($comment, $request->mentionData);
122 + $this->notificationService->mentionInComment($comment, $mentionData);
110 123 }
111 124
112 - if($request->images)
113 - {
114 - $this->commentService->attachCommentImages($comment, $request->images);
115 - $comment->load(['images']);
116 - }
117 -
118 125 if ($comment->type == 'comment')
119 126 {
120 127 $comment->load('replies');
121 128 }
@@ -124,19 +131,16 @@
124 131 'message' => __('Comment has been added', 'fluent-boards'),
125 132 'comment' => $comment
126 133 ], 201);
127 134 } catch (\Exception $e) {
128 - return $this->sendError($e->getMessage(), 404);
135 + return $this->sendError($e->getMessage(), $e->getCode() === 403 ? 403 : 400);
129 136 }
130 137 }
131 138
132 139 public function update(Request $request, $board_id, $comment_id)
133 140 {
134 -// $commentData = $this->commentSanitizeAndValidate($request->all(), [
135 -// 'description' => 'required|string',
136 -// ]);
137 141 $requestData = [
138 - 'description' => $request->comment
142 + 'description' => $request->getSafe('comment', 'sanitize_textarea_field')
139 143 ];
140 144
141 145 $validationRules = [
142 146 'description' => 'required|string'
@@ -141,9 +145,11 @@
141 145 $validationRules = [
142 146 'description' => 'required|string'
143 147 ];
144 148
145 - if ($request->images) {
149 + $hasImagesParam = $this->requestHasImagesArray($request);
150 + $imageIds = $this->getImageIdsFromRequest($request);
151 + if ($hasImagesParam) {
146 152 $validationRules['description'] = 'nullable|string';
147 153 }
148 154
149 155 $commentData = $this->commentSanitizeAndValidate($requestData, $validationRules);
@@ -148,24 +154,34 @@
148 154
149 155 $commentData = $this->commentSanitizeAndValidate($requestData, $validationRules);
150 156
151 157 try {
152 - $comment = $this->commentService->update($commentData, $comment_id, $request->mentionData);
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 + }
153 166
154 - if($request->mentionData)
155 - {
156 - $this->notificationService->mentionInComment($comment, $request->mentionData);
167 + $mentionData = $this->getMentionData($request);
168 +
169 + $comment = $this->commentService->update($commentData, $comment_id, $mentionData, $board_id);
170 +
171 + if (!$comment) {
172 + $errorMessage = __('Unauthorized Action', 'fluent-boards');
173 + return $this->sendError($errorMessage, 401);
157 174 }
158 175
159 - if($request->images)
176 + if(!empty($mentionData))
160 177 {
161 - $this->commentService->attachCommentImages($comment, $request->images);
162 - $comment->load(['images']);
178 + $this->notificationService->mentionInComment($comment, $mentionData);
163 179 }
164 180
165 - if ( !$comment ) {
166 - $errorMessage = __('Unauthorized Action', 'fluent-boards');
167 - return $this->sendError($errorMessage, 401);
181 + if ($hasImagesParam) {
182 + $this->commentService->attachCommentImages($comment, $imageIds);
183 + $comment->load(['images']);
168 184 }
169 185
170 186 $comment->load('user');
171 187
@@ -173,9 +189,9 @@
173 189 'comment' => $comment,
174 190 'message' => __('Comment has been updated', 'fluent-boards'),
175 191 ], 200);
176 192 } catch (\Exception $e) {
177 - return $this->sendError($e->getMessage(), 404);
193 + return $this->sendError($e->getMessage(), $e->getCode() === 403 ? 403 : 404);
178 194 }
179 195 }
180 196
181 197 public function deleteComment($board_id, $comment_id)
@@ -180,9 +196,9 @@
180 196
181 197 public function deleteComment($board_id, $comment_id)
182 198 {
183 199 try {
184 - $this->commentService->delete($comment_id);
200 + $this->commentService->delete($comment_id, $board_id);
185 201
186 202 return $this->sendSuccess([
187 203 'message' => __('Comment has been deleted', 'fluent-boards'),
188 204 ], 200);
@@ -193,9 +209,9 @@
193 209
194 210 public function updateReply(Request $request, $board_id, $reply_id)
195 211 {
196 212 $requestData = [
197 - 'description' => $request->comment
213 + 'description' => $request->getSafe('comment', 'sanitize_textarea_field')
198 214 ];
199 215
200 216 $validationRules = [
201 217 'description' => 'required|string'
@@ -203,9 +219,11 @@
203 219
204 220 $replyData = $this->commentSanitizeAndValidate($requestData, $validationRules);
205 221
206 222 try {
207 - $reply = $this->commentService->update($replyData, $reply_id, $request->mentionData);
223 + $mentionData = $this->getMentionData($request);
224 +
225 + $reply = $this->commentService->update($replyData, $reply_id, $mentionData, $board_id);
208 226
209 227 if (!$reply) {
210 228 $errorMessage = __('Unauthorized Action', 'fluent-boards');
211 229 return $this->sendError($errorMessage, 401);
@@ -215,9 +233,9 @@
215 233 'description' => $reply->description,
216 234 'message' => __('Reply has been updated', 'fluent-boards'),
217 235 ], 200);
218 236 } catch (\Exception $e) {
219 - return $this->sendError($e->getMessage(), 404);
237 + return $this->sendError($e->getMessage(), $e->getCode() === 403 ? 403 : 404);
220 238 }
221 239 }
222 240
223 241 public function deleteReply($board_id, $reply_id)
@@ -222,9 +240,9 @@
222 240
223 241 public function deleteReply($board_id, $reply_id)
224 242 {
225 243 try {
226 - $this->commentService->deleteReply($reply_id);
244 + $this->commentService->deleteReply($reply_id, $board_id);
227 245
228 246 return $this->sendSuccess([
229 247 'message' => __('Reply has been deleted', 'fluent-boards'),
230 248 ], 200);
@@ -232,17 +250,47 @@
232 250 return $this->sendError($e->getMessage(), 404);
233 251 }
234 252 }
235 253
236 - public function sendMailAfterComment($commentId, $usersToSendEmail)
254 + public function sendMailAfterComment($commentId, $recipientUserIds)
237 255 {
238 256 $current_user_id = get_current_user_id();
239 257
240 258 /* this will run in background as soon as possible */
241 259 /* sending Model or Model Instance won't work here */
242 - 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');
243 261 }
244 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 +
245 293 private function commentSanitizeAndValidate($data, array $rules = [])
246 294 {
247 295 $data = Helper::sanitizeComment($data);
248 296
@@ -268,12 +316,14 @@
268 316 ], [
269 317 'file.mimetypes' => __('The file must be a image type.', 'fluent-boards')
270 318 ]);
271 319
320 + (new \FluentBoards\App\Services\TaskService())->findTaskOnBoard($task_id, $board_id);
321 +
272 322 $uploadInfo = UploadService::handleFileUpload( $files, $board_id);
273 323
274 324 $imageData = $uploadInfo[0];
275 - $attachment = $this->commentService->createCommentImage($imageData, $board_id);
325 + $attachment = $this->commentService->createCommentImage($imageData, $board_id, $task_id);
276 326 if(!!defined('FLUENT_BOARDS_PRO_VERSION')) {
277 327 $mediaData = (new AttachmentService())->processMediaData($imageData, $files['file']);
278 328 $attachment['driver'] = $mediaData['driver'];
279 329 $attachment['file_path'] = $mediaData['file_path'];
@@ -286,6 +336,43 @@
286 336 'message' => __('attachment has been added', 'fluent-boards'),
287 337 'imageAttachment' => $attachment
288 338 ], 200);
289 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'));
290 377 }
291 378 }