PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
2.1.0 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 All 42 releases
← All changes | app/Services/TaskService.php +155 -24 2.0.122.1.0 View file →
@@ -1,11 +1,13 @@
1 1 <?php
2 2
3 3 namespace FluentBoards\App\Services;
4 4
5 +use FluentBoards\Framework\Database\Orm\ModelNotFoundException;
5 6 use FluentBoards\App\App;
6 7 use FluentBoards\App\Models\Attachment;
7 8 use FluentBoards\App\Models\Comment;
9 +use FluentBoards\App\Models\Notification;
8 10 use FluentBoards\App\Models\NotificationUser;
9 11 use FluentBoards\App\Models\TaskImage;
10 12 use FluentBoards\App\Services\Constant;
11 13 use FluentBoards\App\Models\Label;
@@ -31,14 +33,15 @@
31 33 * Resolve a task only when it belongs to the requested board.
32 34 *
33 35 * Subtasks normally carry the same board_id as their parent, but the parent
34 36 * fallback protects older data where that relationship may be incomplete.
37 + * Missing or mismatched tasks use the router's deliberate 404 response.
35 38 *
36 39 * @param int $taskId
37 40 * @param int $boardId
38 41 * @param bool $allowParentFallback
39 42 * @return Task
40 - * @throws \Exception
43 + * @throws ModelNotFoundException
41 44 */
42 45 public function findTaskOnBoard($taskId, $boardId, $allowParentFallback = true)
43 46 {
44 47 $taskId = absint($taskId);
@@ -44,9 +47,9 @@
44 47 $taskId = absint($taskId);
45 48 $boardId = absint($boardId);
46 49
47 50 if (!$taskId || !$boardId) {
48 - throw new \Exception(esc_html__('Task not found', 'fluent-boards'));
51 + throw new ModelNotFoundException(esc_html__('Task not found', 'fluent-boards'));
49 52 }
50 53
51 54 $task = Task::where('id', $taskId)
52 55 ->where('board_id', $boardId)
@@ -70,9 +73,9 @@
70 73 }
71 74 }
72 75 }
73 76
74 - throw new \Exception(esc_html__('Task not found', 'fluent-boards'));
77 + throw new ModelNotFoundException(esc_html__('Task not found', 'fluent-boards'));
75 78 }
76 79
77 80 private function normalizeTaskDescriptionForEditor(Task $task)
78 81 {
@@ -786,42 +789,136 @@
786 789 $this->deleteTask($subtask);
787 790 }
788 791 }
789 792
793 + $this->deleteTasksBatch([$task]);
794 + }
795 +
796 + /**
797 + * Delete the supplied tasks and their owned records without discovering children.
798 + *
799 + * @param iterable $tasks
800 + * @param bool $manageTransaction Set false only when the caller owns an active transaction.
801 + * @return void
802 + * @throws \Throwable
803 + */
804 + public function deleteTasksBatch($tasks, $manageTransaction = true)
805 + {
806 + if (!is_array($tasks) && !($tasks instanceof \Traversable)) {
807 + $tasks = [$tasks];
808 + }
809 +
810 + $deletedTasks = [];
811 + $taskBoardIds = [];
812 + foreach ($tasks as $task) {
813 + if (!$task instanceof Task) {
814 + continue;
815 + }
816 +
817 + $taskId = (int) $task->id;
818 + if ($taskId < 1) {
819 + continue;
820 + }
821 +
822 + $deletedTasks[$taskId] = clone $task;
823 + $taskBoardIds[$taskId] = (int) $task->board_id;
824 + }
825 +
826 + if (!$deletedTasks) {
827 + return;
828 + }
829 +
830 + ksort($deletedTasks, SORT_NUMERIC);
831 + $taskIds = array_keys($deletedTasks);
790 832 $dbInstance = App::getInstance('db');
791 - $dbInstance->beginTransaction();
792 833
793 - try {
794 - $deletedTask = clone $task;
795 - $task->watchers()->detach();
796 - $task->assignees()->detach();
834 + if (!$manageTransaction && !$dbInstance->inTransaction()) {
835 + throw new \RuntimeException(__('An active transaction is required for caller-managed task deletion.', 'fluent-boards'));
836 + }
797 837
798 - $notificationIds = $task->notifications->pluck('id');
799 - $task->notifications()->delete();
800 - NotificationUser::whereIn('notification_id', $notificationIds)->delete();
838 + if ($manageTransaction) {
839 + $dbInstance->beginTransaction();
840 + }
801 841
802 - $task->labels()->detach();
842 + try {
843 + $relationTypes = [
844 + Constant::OBJECT_TYPE_USER_TASK_WATCH,
845 + Constant::OBJECT_TYPE_TASK_ASSIGNEE,
846 + Constant::OBJECT_TYPE_TASK_LABEL,
847 + ];
803 848
804 849 if (defined('FLUENT_BOARDS_PRO_VERSION')) {
805 - $task->customFields()->detach();
806 - $this->deleteTaskAttachments($task);
850 + $relationTypes[] = \FluentBoardsPro\App\Services\Constant::TASK_CUSTOM_FIELD;
807 851 }
808 852
809 - $this->deleteTimeTrackingRecords($task->id, false);
810 - TaskMeta::where('task_id', $task->id)->delete();
853 + Relation::whereIn('object_id', $taskIds)
854 + ->whereIn('object_type', $relationTypes)
855 + ->delete();
811 856
812 - if (!$task->delete()) {
857 + Relation::where('object_type', Constant::OBJECT_TYPE_TASK_DEPENDENCY)
858 + ->where(function ($query) use ($taskIds) {
859 + $query->whereIn('object_id', $taskIds)
860 + ->orWhereIn('foreign_id', $taskIds);
861 + })
862 + ->delete();
863 +
864 + $notificationIds = Notification::whereIn('task_id', $taskIds)->pluck('id')->toArray();
865 + NotificationUser::whereIn('notification_id', $notificationIds)->delete();
866 + Notification::whereIn('task_id', $taskIds)->delete();
867 +
868 + $this->deleteTaskAttachmentsBatch($taskIds, $taskBoardIds);
869 + Activity::whereIn('object_id', $taskIds)
870 + ->where('object_type', Constant::ACTIVITY_TASK)
871 + ->delete();
872 + Meta::whereIn('object_id', $taskIds)
873 + ->where('object_type', Constant::REPEAT_TASK_META)
874 + ->delete();
875 + $this->deleteTimeTrackingRecords($taskIds, false);
876 + TaskMeta::whereIn('task_id', $taskIds)->delete();
877 +
878 + $deletedCount = Task::whereIn('id', $taskIds)->delete();
879 + if ($deletedCount !== count($taskIds)) {
813 880 throw new \RuntimeException(__('Task could not be deleted.', 'fluent-boards'));
814 881 }
815 882
816 - do_action('fluent_boards/task_deleted', $deletedTask);
817 - $dbInstance->commit();
883 + $this->dispatchTaskDeletedHooksAfterCommit($dbInstance, $deletedTasks);
884 +
885 + if ($manageTransaction) {
886 + $dbInstance->commit();
887 + }
818 888 } catch (\Throwable $e) {
819 - $dbInstance->rollBack();
820 - throw $e; // Re-throw the exception after rolling back
889 + if ($manageTransaction) {
890 + $dbInstance->rollBack();
891 + }
892 +
893 + throw $e;
821 894 }
895 + }
822 896
897 + /**
898 + * Dispatch task deletion hooks after the outermost transaction commits.
899 + *
900 + * @param mixed $dbInstance
901 + * @param array $deletedTasks
902 + * @return void
903 + */
904 + private function dispatchTaskDeletedHooksAfterCommit($dbInstance, $deletedTasks)
905 + {
906 + $dbInstance->afterCommit(function () use ($deletedTasks) {
907 + foreach ($deletedTasks as $deletedTask) {
908 + try {
909 + do_action('fluent_boards/task_deleted', $deletedTask);
910 + } catch (\Throwable $e) {
911 + error_log(sprintf(
912 + 'FluentBoards: Failed to dispatch committed task deletion hook for task %d: %s',
913 + (int) $deletedTask->id,
914 + sanitize_text_field($e->getMessage())
915 + ));
916 + }
917 + }
918 + });
823 919 }
920 +
824 921 public function deleteTaskForBulk($task)
825 922 {
826 923 // If this is a parent task, delete all subtasks first
827 924 if (!$task->parent_id) {
@@ -831,8 +928,10 @@
831 928 $this->deleteTaskForBulk($subtask);
832 929 }
833 930 }
834 931
932 + $this->deleteTimeTrackingRecords($task->id, false);
933 +
835 934 $deleted = $task->delete();
836 935
837 936 if ($deleted) {
838 937
@@ -848,9 +947,9 @@
848 947 //task labels removed
849 948 $task->labels()->detach();
850 949
851 950 //task custom field value
852 - if (defined('FLUENT_BOARDS_PRO_VERSION')) {
951 + if (defined('FLUENT_BOARDS_PRO_VERSION')) {
853 952 $task->customFields()->detach();
854 953 $this->deleteTaskAttachments($task);
855 954 }
856 955
@@ -2125,9 +2224,9 @@
2125 2224
2126 2225 // Fetch comments and activities separately
2127 2226 $comments = [];
2128 2227 if ($feedType !== 'activities') {
2129 - $comments = $task->comments()->with('user')->orderBy('created_at', 'desc')->get()->toArray();
2228 + $comments = $task->comments()->with(['user', 'replies.user'])->orderBy('created_at', 'desc')->get()->toArray();
2130 2229 }
2131 2230
2132 2231 $activities = [];
2133 2232 if ($feedType !== 'comments') {
@@ -2379,12 +2478,38 @@
2379 2478 foreach ($attachments as $attachment) {
2380 2479 $deletedAttachment = clone $attachment;
2381 2480 $attachment->delete();
2382 2481
2383 - do_action('fluent_boards/task_attachment_deleted', $deletedAttachment);
2482 + do_action('fluent_boards/task_attachment_deleted', $deletedAttachment, $task->board_id);
2384 2483 }
2385 2484 }
2386 2485
2486 + /**
2487 + * Delete task attachments one at a time so each attachment-deleted hook is preserved.
2488 + *
2489 + * @param array $taskIds
2490 + * @param array $taskBoardIds
2491 + * @return void
2492 + */
2493 + private function deleteTaskAttachmentsBatch($taskIds, $taskBoardIds)
2494 + {
2495 + if (!defined('FLUENT_BOARDS_PRO_VERSION')) {
2496 + return;
2497 + }
2498 +
2499 + $attachments = TaskAttachment::whereIn('object_id', $taskIds)
2500 + ->where('object_type', Constant::TASK_ATTACHMENT)
2501 + ->get();
2502 +
2503 + foreach ($attachments as $attachment) {
2504 + $deletedAttachment = clone $attachment;
2505 + $attachment->delete();
2506 + $boardId = $taskBoardIds[(int) $attachment->object_id] ?? null;
2507 +
2508 + do_action('fluent_boards/task_attachment_deleted', $deletedAttachment, $boardId);
2509 + }
2510 + }
2511 +
2387 2512 public function cloneTask(int $taskId, $taskData, $boardId = null): Task
2388 2513 {
2389 2514 global $wpdb;
2390 2515 $attachmentFileService = new AttachmentFileService();
@@ -2638,8 +2763,14 @@
2638 2763 if ($images->count() > 0) {
2639 2764 foreach ($images as $image) {
2640 2765 $clonedImage = $image->replicate();
2641 2766 $clonedImage->object_id = $clonedCommentOrReply->id;
2767 + (new CommentService())->applyCommentImageScope(
2768 + $clonedImage,
2769 + $clonedCommentOrReply->board_id,
2770 + $clonedCommentOrReply->task_id,
2771 + $clonedCommentOrReply->created_by
2772 + );
2642 2773 $clonedImage->save();
2643 2774 }
2644 2775 }
2645 2776 }