| @@ -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 | |