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/Api/Classes/Tasks.php +219 -34 1.30 → 2.1.0 View file →
@@ -7,8 +7,9 @@
7 7 use Exception;
8 8 use FluentBoards\App\Models\Label;
9 9 use FluentBoards\App\Models\Stage;
10 10 use FluentBoards\App\Models\Task;
11 +use FluentBoards\App\Models\TaskMeta;
11 12 use FluentBoards\App\Models\Board;
12 13 use FluentBoards\App\Services\BoardService;
13 14 use FluentBoards\App\Services\Helper;
14 15 use FluentBoards\App\Services\PermissionManager;
@@ -22,11 +23,11 @@
22 23
23 24
24 25
25 26 /**
26 - * Contacts Class - PHP APi Wrapper
27 + * Tasks Class - PHP API Wrapper
27 28 *
28 - * Contacts API Wrapper Class that can be used as <code>FluentBoardsApi('tasks')</code> to get the class instance
29 + * Tasks API Wrapper Class that can be used as <code>FluentBoardsApi('tasks')</code> to get the class instance
29 30 *
30 31 * @package FluentBoards\App\Api\Classes
31 32 * @namespace FluentBoards\App\Api\Classes
32 33 *
@@ -35,16 +36,8 @@
35 36 class Tasks
36 37 {
37 38 private $instance = null;
38 39
39 - private $allowedInstanceMethods = [
40 - 'all',
41 - 'get',
42 - 'find',
43 - 'first',
44 - 'paginate'
45 - ];
46 -
47 40 public function __construct(Task $instance)
48 41 {
49 42 $this->instance = $instance;
50 43 }
@@ -65,9 +58,9 @@
65 58 return [];
66 59 }
67 60
68 61 //checking if current user has access to board
69 - if (!PermissionManager::userHasPermission($board_id)) {
62 + if (!$this->canReadBoard($board_id)) {
70 63 return false;
71 64 }
72 65
73 66 $query = Task::query()
@@ -107,10 +100,14 @@
107 100 {
108 101 if (!empty($id)) {
109 102 $task = Task::where('id', $id)->first();
110 103
104 + if (!$task) {
105 + return false;
106 + }
107 +
111 108 //checking if current user has access to board
112 - if (!PermissionManager::userHasPermission($task->board_id)) {
109 + if (!$this->canReadBoard($task->board_id)) {
113 110 return false;
114 111 }
115 112 return $task;
116 113 }
@@ -142,9 +139,9 @@
142 139 $permittedTasks = [];
143 140
144 141 foreach ($tasks as $task) {
145 142 //checking if current user has access to board
146 - if (PermissionManager::userHasPermission($task->board_id)) {
143 + if ($this->canReadBoard($task->board_id)) {
147 144 $permittedTasks[] = $task;
148 145 }
149 146 }
150 147
@@ -153,8 +150,14 @@
153 150 return false;
154 151 }
155 152
156 153
154 + /**
155 + * Create a task only when the current user can write to the target board.
156 + *
157 + * @param array $data
158 + * @return Task|false
159 + */
157 160 public function create($data)
158 161 {
159 162 if (empty($data)) {
160 163 return false;
@@ -163,13 +166,42 @@
163 166 if (empty($data['title']) || empty($data['board_id']) || empty($data['stage_id'])) {
164 167 return false;
165 168 }
166 169
167 - $taskData = Helper::sanitizeTask($data);
170 + if (!$this->canWriteBoard($data['board_id'])) {
171 + return false;
172 + }
168 173
169 - if(!empty($taskData['priority']) && !in_array($taskData['priority'], ['low', 'medium', 'high'])) {
170 - $taskData['priority'] = 'low';
174 + $stage = Stage::where('id', $data['stage_id'])->where('board_id', $data['board_id'])->first();
175 + if (!$stage) {
176 + return false;
171 177 }
178 +
179 + $taskData = Helper::sanitizeTaskForWebHook($data);
180 + if (is_string($data['assignees'])) {
181 + $data['assignees'] = json_decode($data['assignees'], true);
182 + if (!is_array($data['assignees'])) {
183 + $data['assignees'] = [$data['assignees']]; // Wrap single value in an array
184 + }
185 + } elseif (is_int($data['assignees'])) {
186 + $data['assignees'] = [$data['assignees']]; // Wrap integer in an array
187 + }
188 +
189 + if (is_string($data['labels'])) {
190 + $data['labels'] = json_decode($data['labels'], true);
191 + if (!is_array($data['labels'])) {
192 + $data['labels'] = [$data['labels']]; // Wrap single value in an array
193 + }
194 + } elseif (is_int($data['labels'])) {
195 + $data['labels'] = [$data['labels']]; // Wrap integer in an array
196 + }
197 +
198 +
199 +
200 +
201 + if(!empty($taskData['priority']) && !in_array($taskData['priority'], $this->getAllowedTaskPriorities(), true)) {
202 + $taskData['priority'] = '';
203 + }
172 204 if(!empty($taskData['status']) && !in_array($taskData['status'], ['open', 'closed'])) {
173 205 $taskData['status'] = 'open';
174 206 }
175 207 if(!empty($data['crm_contact_id'])) {
@@ -177,9 +209,9 @@
177 209 }
178 210
179 211 if(!empty($data['contact_email']) && empty($data['crm_contact_id'])) {
180 212 // Find first if the contact exists
181 - $contact = FluentCrmApi('contacts')->creteOrUpdate([
213 + $contact = FluentCrmApi('contacts')->createOrUpdate([
182 214 'email' => $data['contact_email'],
183 215 'first_name' => $data['contact_first_name'],
184 216 'last_name' => $data['contact_last_name'],
185 217 'status' => 'subscribed'
@@ -244,9 +276,9 @@
244 276 'board_id',
245 277 'stage_id',
246 278 'parent_id',
247 279 'status', // open | closed
248 - 'priority', // low | medium | high
280 + 'priority', // urgent | high | medium | low | blank
249 281 'source',
250 282 'source_id',
251 283 'description',
252 284 'due_at',
@@ -351,9 +383,9 @@
351 383 return false;
352 384 }
353 385
354 386 //checking if current user has access to board
355 - if (!PermissionManager::userHasPermission($task->board_id)) {
387 + if (!$this->canWriteBoard($task->board_id)) {
356 388 return false;
357 389 }
358 390
359 391 $boardlabelIds = $board->labels->pluck('id')->toArray();
@@ -389,9 +421,9 @@
389 421 return false;
390 422 }
391 423
392 424 //checking if current user has access to board
393 - if (!PermissionManager::userHasPermission($task->board_id)) {
425 + if (!$this->canWriteBoard($task->board_id)) {
394 426 return false;
395 427 }
396 428
397 429 $task->labels()->detach($labelIds);
@@ -416,9 +448,9 @@
416 448 return false;
417 449 }
418 450
419 451 //checking if current user has access to board
420 - if (!PermissionManager::userHasPermission($task->board_id)) {
452 + if (!$this->canWriteBoard($task->board_id)) {
421 453 return false;
422 454 }
423 455
424 456 $stage = Stage::findOrFail($stageId);
@@ -437,10 +469,25 @@
437 469
438 470 return $task;
439 471 }
440 472
473 + /**
474 + * Update a task property only when the current user can write to the task board.
475 + *
476 + * @param int $taskId
477 + * @param string $property
478 + * @param mixed $value
479 + * @return Task|false
480 + */
441 481 public function updateProperty($taskId, $property, $value)
442 482 {
483 + $taskId = absint($taskId);
484 + $property = sanitize_text_field($property);
485 +
486 + if (!$taskId || !$property) {
487 + return false;
488 + }
489 +
443 490 $taskService = new TaskService();
444 491 $task = Task::where('id', $taskId)->first();
445 492
446 493 if(!$task) {
@@ -446,16 +493,52 @@
446 493 if(!$task) {
447 494 return false;
448 495 }
449 496
497 + if (!$this->canWriteBoard($task->board_id)) {
498 + return false;
499 + }
500 +
450 501 $allowedColumns = ['title', 'description', 'due_at', 'priority', 'status', 'source', 'source_id', 'crm_contact_id'];
451 502
452 - if(!in_array($property, $allowedColumns)) {
503 + if(!in_array($property, $allowedColumns, true)) {
453 504 return false;
454 505 }
455 506
456 - $taskService->updateTaskProperty($task, $property, $value);
507 + switch ($property) {
508 + case 'description':
509 + $sanitizedValue = fluent_boards_sanitize_description($value);
510 + break;
457 511
512 + case 'due_at':
513 + $sanitizedValue = $value === null || $value === '' ? null : sanitize_text_field((string) $value);
514 + break;
515 +
516 + case 'priority':
517 + $sanitizedValue = $value === null ? null : sanitize_text_field((string) $value);
518 + if ($sanitizedValue !== null && $sanitizedValue !== '' && !in_array($sanitizedValue, $this->getAllowedTaskPriorities(), true)) {
519 + return false;
520 + }
521 + break;
522 +
523 + case 'status':
524 + $sanitizedValue = sanitize_text_field((string) $value);
525 + if (!in_array($sanitizedValue, ['open', 'closed'], true)) {
526 + return false;
527 + }
528 + break;
529 +
530 + case 'crm_contact_id':
531 + $sanitizedValue = $value === null || $value === '' ? null : absint($value);
532 + break;
533 +
534 + default:
535 + $sanitizedValue = sanitize_text_field((string) $value);
536 + break;
537 + }
538 +
539 + $task = $taskService->updateTaskProperty($property, $sanitizedValue, $task);
540 +
458 541 return $task;
459 542
460 543 }
461 544
@@ -514,9 +597,9 @@
514 597 * @param int $taskId
515 598 * @param $data
516 599 * @return bool|Task
517 600 */
518 - public function createSubtask(int $boardId, int $taskId, $data): Task|bool
601 + public function createSubtask(int $boardId, int $taskId, $data)
519 602 {
520 603 if (!$this->hasProAndBoardAccess($boardId)) {
521 604 return false;
522 605 }
@@ -527,11 +610,63 @@
527 610 }
528 611
529 612 $data['board_id'] = $boardId;
530 613 $data['parent_id'] = $task->id;
614 + unset($data['started_at']);
531 615
616 + // Ensure group_id is provided and valid
617 + if (!empty($data['group_id'])) {
618 + // Validate that the group belongs to the parent task
619 + $group = TaskMeta::where('id', $data['group_id'])
620 + ->where('task_id', $task->id)
621 + ->where('key', Constant::SUBTASK_GROUP_NAME)
622 + ->first();
532 623
533 - return $this->create($data);
624 + if (!$group) {
625 + // Invalid group_id provided, create a default group instead
626 + $group = TaskMeta::where('task_id', $task->id)
627 + ->where('key', Constant::SUBTASK_GROUP_NAME)
628 + ->first();
629 +
630 + if (!$group) {
631 + $group = TaskMeta::create([
632 + 'task_id' => $task->id,
633 + 'key' => Constant::SUBTASK_GROUP_NAME,
634 + 'value' => __('Untitled Group', 'fluent-boards')
635 + ]);
636 + }
637 +
638 + $data['group_id'] = $group->id;
639 + }
640 + } else {
641 + // No group_id provided, find or create a default group
642 + $group = TaskMeta::where('task_id', $task->id)
643 + ->where('key', Constant::SUBTASK_GROUP_NAME)
644 + ->first();
645 +
646 + if (!$group) {
647 + $group = TaskMeta::create([
648 + 'task_id' => $task->id,
649 + 'key' => Constant::SUBTASK_GROUP_NAME,
650 + 'value' => __('Untitled Group', 'fluent-boards')
651 + ]);
652 + }
653 +
654 + $data['group_id'] = $group->id;
655 + }
656 +
657 + $subtask = $this->create($data);
658 +
659 + if ($subtask) {
660 + // Link subtask to group
661 + TaskMeta::create([
662 + 'task_id' => $subtask->id,
663 + 'key' => Constant::SUBTASK_GROUP_CHILD,
664 + 'value' => $data['group_id']
665 + ]);
666 + }
667 +
668 + return $subtask;
534 669 }
535 670
536 671
537 672 /**
@@ -547,9 +682,19 @@
547 682 {
548 683 if (!$this->hasProAndBoardAccess($boardId)) {
549 684 return false;
550 685 }
551 - return $this->updateProperty($subtaskId, $property, $value);
686 +
687 + $subtask = Task::where('id', $subtaskId)->where('parent_id', $taskId)->where('board_id', $boardId)->first();
688 + if (!$subtask) {
689 + return false;
690 + }
691 +
692 + if ($property === 'started_at') {
693 + $value = null;
694 + }
695 +
696 + return $this->updateProperty($subtask->id, $property, $value);
552 697 }
553 698
554 699
555 700 /**
@@ -564,8 +709,12 @@
564 709 return false;
565 710 }
566 711
567 712 $subtask = Task::where('id', $subtaskId)->where('parent_id', $taskId)->where('board_id', $boardId)->first();
713 + if (!$subtask) {
714 + return false;
715 + }
716 +
568 717 $deletedTask = clone $subtask;
569 718
570 719 $options = null;
571 720 //if we need to do something before a task is deleted
@@ -582,22 +731,58 @@
582 731 * @return bool
583 732 */
584 733 private function hasProAndBoardAccess($boardId)
585 734 {
586 - return defined('FLUENT_BOARDS_PRO_VERSION') && PermissionManager::userHasPermission($boardId);
735 + return defined('FLUENT_BOARDS_PRO_VERSION') && $this->canWriteBoard($boardId);
587 736 }
588 737
589 738
590 - public function getInstance()
739 + /**
740 + * Block raw model proxy calls so board access cannot be bypassed.
741 + *
742 + * @param string $method
743 + * @param array $params
744 + * @throws \Exception
745 + */
746 + public function __call($method, $params)
591 747 {
592 - return $this->instance;
748 + throw new \Exception(esc_html(sprintf('Method %s does not exist.', $method)));
593 749 }
594 750
595 - public function __call($method, $params)
751 + /**
752 + * Check if the current user can read a board.
753 + *
754 + * @param int $boardId
755 + * @return bool
756 + */
757 + private function canReadBoard($boardId)
596 758 {
597 - if (in_array($method, $this->allowedInstanceMethods)) {
598 - return call_user_func_array([$this->instance, $method], $params);
599 - }
759 + return PermissionManager::userHasBoardPermission($boardId, 'GET');
760 + }
600 761
601 - throw new \Exception("Method {$method} does not exist.");
762 + /**
763 + * Check if the current user can write to a board.
764 + *
765 + * @param int $boardId
766 + * @return bool
767 + */
768 + private function canWriteBoard($boardId)
769 + {
770 + return PermissionManager::userHasBoardPermission($boardId, 'POST');
771 + }
772 +
773 + /**
774 + * Get priority keys allowed by the task priority filter.
775 + *
776 + * @return array
777 + */
778 + private function getAllowedTaskPriorities()
779 + {
780 + return array_map('strval', array_keys(apply_filters('fluent_boards/task_priorities', [
781 + '' => __('No priority', 'fluent-boards'),
782 + 'urgent' => __('Urgent', 'fluent-boards'),
783 + 'high' => __('High', 'fluent-boards'),
784 + 'medium' => __('Medium', 'fluent-boards'),
785 + 'low' => __('Low', 'fluent-boards'),
786 + ])));
602 787 }
603 788 }