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/Services/CommentService.php +150 -22 2.0.10trunk View file →
@@ -291,28 +291,43 @@
291 291 return $comment; // Return original text if processing fails
292 292 }
293 293 }
294 294
295 + /**
296 + * Validate every new image before attaching uploads or removing existing images.
297 + */
295 298 public function attachCommentImages($comment, $imageIds)
296 299 {
300 + $imageIds = $this->normalizeCommentImageIds($imageIds);
297 301
298 - foreach ($imageIds as $imageId)
299 - {
300 - $attachmentObject = CommentImage::findOrFail($imageId);
301 - if($attachmentObject) {
302 - if ($attachmentObject->object_id == $comment->id && $attachmentObject->object_type == Constant::COMMENT_IMAGE) {
303 - continue;
304 - }
302 + $commentImages = CommentImage::where('object_id', $comment->id)
303 + ->where('object_type', Constant::COMMENT_IMAGE)
304 + ->get();
305 +
306 + $attachedImageIds = [];
307 + foreach ($commentImages as $commentImage) {
308 + if (in_array((int) $commentImage->id, $imageIds, true)) {
309 + $attachedImageIds[] = (int) $commentImage->id;
310 + }
311 + }
312 +
313 + $newImageIds = array_values(array_diff($imageIds, $attachedImageIds));
314 + if (!empty($newImageIds)) {
315 + $attachmentObjects = $this->assertCommentImagesAttachable(
316 + $newImageIds,
317 + $comment->board_id,
318 + $comment->task_id
319 + );
320 +
321 + foreach ($attachmentObjects as $attachmentObject) {
305 322 $attachmentObject->object_id = $comment->id;
306 323 $attachmentObject->object_type = Constant::COMMENT_IMAGE;
307 324 $attachmentObject->save();
308 325 }
309 326 }
310 - //if(in_array("banana", $imageIds))
311 - $commentImages = CommentImage::where('object_id', $comment->id)->where('object_type', Constant::COMMENT_IMAGE)->get();
312 327
313 328 foreach ($commentImages as $commentImage) {
314 - if(!in_array($commentImage->id, $imageIds)) {
329 + if (!in_array((int) $commentImage->id, $imageIds, true)) {
315 330 $deletedImage = clone $commentImage;
316 331 $commentImage->delete();
317 332 //do_action('fluent_boards/comment_image_deleted', $deletedImage);
318 333 }
@@ -318,8 +333,114 @@
318 333 }
319 334 }
320 335 }
321 336
337 + /**
338 + * Allow retained images on this comment and validate all newly supplied uploads.
339 + */
340 + public function assertCommentImagesAttachableForComment($comment, $imageIds)
341 + {
342 + $imageIds = $this->normalizeCommentImageIds($imageIds);
343 +
344 + if (empty($imageIds)) {
345 + return [];
346 + }
347 +
348 + $commentImages = CommentImage::where('object_id', $comment->id)
349 + ->where('object_type', Constant::COMMENT_IMAGE)
350 + ->get();
351 +
352 + foreach ($commentImages as $commentImage) {
353 + $key = array_search((int) $commentImage->id, $imageIds, true);
354 + if ($key !== false) {
355 + unset($imageIds[$key]);
356 + }
357 + }
358 +
359 + return $this->assertCommentImagesAttachable(
360 + array_values($imageIds),
361 + $comment->board_id,
362 + $comment->task_id
363 + );
364 + }
365 +
366 + /**
367 + * Reject the entire image list unless every upload is unbound and owned by this actor, board, and task.
368 + */
369 + public function assertCommentImagesAttachable($imageIds, $boardId, $taskId)
370 + {
371 + $imageIds = $this->normalizeCommentImageIds($imageIds);
372 +
373 + if (empty($imageIds)) {
374 + return [];
375 + }
376 +
377 + $currentUserId = get_current_user_id();
378 + if (!$currentUserId) {
379 + throw new \Exception(esc_html__('Invalid comment image attachment', 'fluent-boards'));
380 + }
381 +
382 + $attachments = CommentImage::whereIn('id', $imageIds)
383 + ->where('object_id', 0)
384 + ->where('object_type', Constant::COMMENT_IMAGE)
385 + ->get();
386 +
387 + if (count($attachments) !== count($imageIds)) {
388 + throw new \Exception(esc_html__('Invalid comment image attachment', 'fluent-boards'));
389 + }
390 +
391 + foreach ($attachments as $attachment) {
392 + if (!$this->commentImageScopeMatches($attachment, $boardId, $taskId, $currentUserId)) {
393 + throw new \Exception(esc_html__('Invalid comment image attachment', 'fluent-boards'));
394 + }
395 + }
396 +
397 + return $attachments;
398 + }
399 +
400 + /**
401 + * Fail closed for legacy uploads without recorded board, task, and uploader ownership.
402 + */
403 + private function commentImageScopeMatches($attachment, $boardId, $taskId, $userId)
404 + {
405 + $settings = is_array($attachment->settings) ? $attachment->settings : [];
406 + $scope = isset($settings['comment_image_scope']) && is_array($settings['comment_image_scope'])
407 + ? $settings['comment_image_scope']
408 + : [];
409 +
410 + return intval($scope['board_id'] ?? 0) === intval($boardId)
411 + && intval($scope['task_id'] ?? 0) === intval($taskId)
412 + && intval($scope['created_by'] ?? 0) === intval($userId);
413 + }
414 +
415 + /**
416 + * Record trusted upload ownership in attachment metadata before saving.
417 + */
418 + public function applyCommentImageScope($attachment, $boardId, $taskId, $createdBy = null)
419 + {
420 + $settings = is_array($attachment->settings) ? $attachment->settings : [];
421 + $settings['comment_image_scope'] = [
422 + 'board_id' => intval($boardId),
423 + 'task_id' => intval($taskId),
424 + 'created_by' => intval($createdBy === null ? get_current_user_id() : $createdBy),
425 + ];
426 + $attachment->settings = $settings;
427 +
428 + return $attachment;
429 + }
430 +
431 + /**
432 + * Normalize submitted image IDs and remove duplicates before validating the full list.
433 + */
434 + private function normalizeCommentImageIds($imageIds)
435 + {
436 + if (!is_array($imageIds)) {
437 + return [];
438 + }
439 +
440 + return array_values(array_filter(array_unique(array_map('intval', $imageIds))));
441 + }
442 +
322 443 public function update($commentData, $comment_id, $mentionData, $boardId = null)
323 444 {
324 445 $comment = $boardId ? $this->findCommentOnBoard($comment_id, $boardId) : Comment::findOrFail($comment_id);
325 446
@@ -326,10 +447,19 @@
326 447 if ($comment->created_by != get_current_user_id()) {
327 448 return false;
328 449 }
329 450
330 - $allMentionedIds = array_unique(array_merge($comment->settings['mentioned_id'] ?? [], is_array($mentionData) ? $mentionData : []));
451 + $effectiveBoardId = absint($comment->board_id ?: $boardId);
452 + $notificationService = new NotificationService();
453 + $existingMentionedIds = array_values(array_unique(array_filter(array_map('absint', (array) ($comment->settings['mentioned_id'] ?? [])))));
454 + $newMentionedIds = array_values(array_unique(array_filter(array_map('absint', (array) $mentionData))));
455 + $requestedMentionedIds = array_values(array_unique(array_merge($existingMentionedIds, $newMentionedIds)));
456 + $allMentionedIds = $notificationService->resolveBoardMentionUserIds($effectiveBoardId, $requestedMentionedIds);
331 457
458 + if (array_diff($newMentionedIds, $allMentionedIds)) {
459 + throw new \Exception(esc_html__('One or more mentioned users are not members of this board', 'fluent-boards'), 403);
460 + }
461 +
332 462 if ($allMentionedIds) {
333 463 $processedDescription = $this->processMentionAndLink($commentData['description'], $allMentionedIds);
334 464 } elseif(!$allMentionedIds) {
335 465 $processedDescription = $this->checkIfCommentHaveLinks($commentData['description']);
@@ -418,18 +548,14 @@
418 548 // do_action('fluent_boards/comment_deleted', $taskId);
419 549 }
420 550
421 551 /**
422 - * Adds a task attachment to the specified task.
552 + * Persist an unbound comment upload with trusted board, task, and uploader metadata.
553 + * Legacy uploads without this scope cannot be newly attached to a comment.
423 554 *
424 - * @param int $taskId The ID of the task to which the attachment is added.
425 - * @param string $title The title of the attachment.
426 - * @param string $url The URL of the attachment.
427 - *
428 - * @return Attachment The updated list of task attachments.
429 - * @throws \Exception
555 + * @return CommentImage
430 556 */
431 - public function createCommentImage($data, $boardId)
557 + public function createCommentImage($data, $boardId, $taskId = null)
432 558 {
433 559 /*
434 560 * I will refactor this function later- within March 2024 Last Week
435 561 */
@@ -452,11 +578,14 @@
452 578 $attachment->title = $this->setTitle($attachData['type'], $attachData['name'], $UrlMeta);
453 579 $attachment->file_path = $attachData['type'] != 'url' ? $attachData['file'] : null;
454 580 $attachment->full_url = esc_url($attachData['url']);
455 581 $attachment->file_size = $attachData['size'];
456 - $attachment->settings = $attachData['type'] == 'url' ? [
582 + $settings = $attachData['type'] == 'url' ? [
457 583 'meta' => $UrlMeta
458 - ] : '';
584 + ] : [];
585 + $settings['board_id'] = absint($boardId);
586 + $attachment->settings = $settings;
587 + $this->applyCommentImageScope($attachment, $boardId, $taskId);
459 588 $attachment->driver = 'local';
460 589 $attachment->save();
461 590
462 591 return $attachment;
@@ -466,9 +595,8 @@
466 595 {
467 596 return add_query_arg([
468 597 'fbs' => 1,
469 598 'fbs_type' => 'public_url',
470 - 'fbs_bid' => $boardId,
471 599 'fbs_comment_image' => $attachment->file_hash
472 600 ], site_url('/index.php'));
473 601 }
474 602