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 +298 -31 1.23 → 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;
@@ -14,16 +15,19 @@
14 15 use FluentBoards\App\Services\PermissionManager;
15 16 use FluentBoards\App\Services\TaskService;
16 17 use FluentBoardsPro\App\Models\TaskAttachment;
17 18 use FluentBoardsPro\App\Services\AttachmentService;
19 +use FluentBoardsPro\App\Services\SubtaskService;
20 +use FluentCrm\App\Models\Subscriber;
21 +
18 22 use FluentBoards\App\Services\Constant;
19 23
20 24
21 25
22 26 /**
23 - * Contacts Class - PHP APi Wrapper
27 + * Tasks Class - PHP API Wrapper
24 28 *
25 - * 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
26 30 *
27 31 * @package FluentBoards\App\Api\Classes
28 32 * @namespace FluentBoards\App\Api\Classes
29 33 *
@@ -32,16 +36,8 @@
32 36 class Tasks
33 37 {
34 38 private $instance = null;
35 39
36 - private $allowedInstanceMethods = [
37 - 'all',
38 - 'get',
39 - 'find',
40 - 'first',
41 - 'paginate'
42 - ];
43 -
44 40 public function __construct(Task $instance)
45 41 {
46 42 $this->instance = $instance;
47 43 }
@@ -62,9 +58,9 @@
62 58 return [];
63 59 }
64 60
65 61 //checking if current user has access to board
66 - if (!PermissionManager::userHasPermission($board_id)) {
62 + if (!$this->canReadBoard($board_id)) {
67 63 return false;
68 64 }
69 65
70 66 $query = Task::query()
@@ -104,10 +100,14 @@
104 100 {
105 101 if (!empty($id)) {
106 102 $task = Task::where('id', $id)->first();
107 103
104 + if (!$task) {
105 + return false;
106 + }
107 +
108 108 //checking if current user has access to board
109 - if (!PermissionManager::userHasPermission($task->board_id)) {
109 + if (!$this->canReadBoard($task->board_id)) {
110 110 return false;
111 111 }
112 112 return $task;
113 113 }
@@ -139,9 +139,9 @@
139 139 $permittedTasks = [];
140 140
141 141 foreach ($tasks as $task) {
142 142 //checking if current user has access to board
143 - if (PermissionManager::userHasPermission($task->board_id)) {
143 + if ($this->canReadBoard($task->board_id)) {
144 144 $permittedTasks[] = $task;
145 145 }
146 146 }
147 147
@@ -150,8 +150,14 @@
150 150 return false;
151 151 }
152 152
153 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 + */
154 160 public function create($data)
155 161 {
156 162 if (empty($data)) {
157 163 return false;
@@ -160,13 +166,42 @@
160 166 if (empty($data['title']) || empty($data['board_id']) || empty($data['stage_id'])) {
161 167 return false;
162 168 }
163 169
164 - $taskData = Helper::sanitizeTask($data);
170 + if (!$this->canWriteBoard($data['board_id'])) {
171 + return false;
172 + }
165 173
166 - if(!empty($taskData['priority']) && !in_array($taskData['priority'], ['low', 'medium', 'high'])) {
167 - $taskData['priority'] = 'low';
174 + $stage = Stage::where('id', $data['stage_id'])->where('board_id', $data['board_id'])->first();
175 + if (!$stage) {
176 + return false;
168 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 + }
169 204 if(!empty($taskData['status']) && !in_array($taskData['status'], ['open', 'closed'])) {
170 205 $taskData['status'] = 'open';
171 206 }
172 207 if(!empty($data['crm_contact_id'])) {
@@ -174,9 +209,9 @@
174 209 }
175 210
176 211 if(!empty($data['contact_email']) && empty($data['crm_contact_id'])) {
177 212 // Find first if the contact exists
178 - $contact = FluentCrmApi('contacts')->creteOrUpdate([
213 + $contact = FluentCrmApi('contacts')->createOrUpdate([
179 214 'email' => $data['contact_email'],
180 215 'first_name' => $data['contact_first_name'],
181 216 'last_name' => $data['contact_last_name'],
182 217 'status' => 'subscribed'
@@ -241,9 +276,9 @@
241 276 'board_id',
242 277 'stage_id',
243 278 'parent_id',
244 279 'status', // open | closed
245 - 'priority', // low | medium | high
280 + 'priority', // urgent | high | medium | low | blank
246 281 'source',
247 282 'source_id',
248 283 'description',
249 284 'due_at',
@@ -348,9 +383,9 @@
348 383 return false;
349 384 }
350 385
351 386 //checking if current user has access to board
352 - if (!PermissionManager::userHasPermission($task->board_id)) {
387 + if (!$this->canWriteBoard($task->board_id)) {
353 388 return false;
354 389 }
355 390
356 391 $boardlabelIds = $board->labels->pluck('id')->toArray();
@@ -386,9 +421,9 @@
386 421 return false;
387 422 }
388 423
389 424 //checking if current user has access to board
390 - if (!PermissionManager::userHasPermission($task->board_id)) {
425 + if (!$this->canWriteBoard($task->board_id)) {
391 426 return false;
392 427 }
393 428
394 429 $task->labels()->detach($labelIds);
@@ -413,9 +448,9 @@
413 448 return false;
414 449 }
415 450
416 451 //checking if current user has access to board
417 - if (!PermissionManager::userHasPermission($task->board_id)) {
452 + if (!$this->canWriteBoard($task->board_id)) {
418 453 return false;
419 454 }
420 455
421 456 $stage = Stage::findOrFail($stageId);
@@ -434,10 +469,25 @@
434 469
435 470 return $task;
436 471 }
437 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 + */
438 481 public function updateProperty($taskId, $property, $value)
439 482 {
483 + $taskId = absint($taskId);
484 + $property = sanitize_text_field($property);
485 +
486 + if (!$taskId || !$property) {
487 + return false;
488 + }
489 +
440 490 $taskService = new TaskService();
441 491 $task = Task::where('id', $taskId)->first();
442 492
443 493 if(!$task) {
@@ -443,16 +493,52 @@
443 493 if(!$task) {
444 494 return false;
445 495 }
446 496
497 + if (!$this->canWriteBoard($task->board_id)) {
498 + return false;
499 + }
500 +
447 501 $allowedColumns = ['title', 'description', 'due_at', 'priority', 'status', 'source', 'source_id', 'crm_contact_id'];
448 502
449 - if(!in_array($property, $allowedColumns)) {
503 + if(!in_array($property, $allowedColumns, true)) {
450 504 return false;
451 505 }
452 506
453 - $taskService->updateTaskProperty($task, $property, $value);
507 + switch ($property) {
508 + case 'description':
509 + $sanitizedValue = fluent_boards_sanitize_description($value);
510 + break;
454 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 +
455 541 return $task;
456 542
457 543 }
458 544
@@ -465,9 +551,9 @@
465 551 */
466 552 public function createTaskAttachment(int $boardId, int $taskId, array $data = [])
467 553 {
468 554 // check if the user has permission to add attachment
469 - if (!defined('FLUENT_BOARDS_PRO_VERSION') || !PermissionManager::userHasPermission($boardId) || empty($data) || empty($data['url'])) {
555 + if (!$this->hasProAndBoardAccess($boardId) || empty($data) || empty($data['url'])) {
470 556 return false;
471 557 }
472 558
473 559 $task = Task::where('id', $taskId)->where('board_id', $boardId)->first();
@@ -489,9 +575,9 @@
489 575 */
490 576 public function deleteTaskAttachment( int $boardId, int $taskId, int $attachmentId)
491 577 {
492 578 // check if the user has permission to delete attachment
493 - if (!defined('FLUENT_BOARDS_PRO_VERSION') || !PermissionManager::userHasPermission($boardId)) {
579 + if (!$this->hasProAndBoardAccess($boardId)) {
494 580 return false;
495 581 }
496 582
497 583 $task = Task::where('id', $taskId)->where('board_id', $boardId)->first();
@@ -504,18 +590,199 @@
504 590 return true;
505 591 }
506 592
507 593
508 - public function getInstance()
594 + /**
595 + * Create a subtask
596 + * @param int $boardId
597 + * @param int $taskId
598 + * @param $data
599 + * @return bool|Task
600 + */
601 + public function createSubtask(int $boardId, int $taskId, $data)
509 602 {
510 - return $this->instance;
603 + if (!$this->hasProAndBoardAccess($boardId)) {
604 + return false;
605 + }
606 +
607 + $task = Task::where('id', $taskId)->where('board_id', $boardId)->first();
608 + if (!$task) {
609 + return false;
610 + }
611 +
612 + $data['board_id'] = $boardId;
613 + $data['parent_id'] = $task->id;
614 + unset($data['started_at']);
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();
623 +
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;
511 669 }
512 670
671 +
672 + /**
673 + * Update a subtask
674 + * @param int $boardId
675 + * @param int $taskId
676 + * @param int $subtaskId
677 + * @param string $property
678 + * @param mixed $value
679 + *
680 + */
681 + public function updateSubtask($boardId, $taskId, $subtaskId, $property, $value)
682 + {
683 + if (!$this->hasProAndBoardAccess($boardId)) {
684 + return false;
685 + }
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);
697 + }
698 +
699 +
700 + /**
701 + * Delete a subtask
702 + * @param int $boardId
703 + * @param int $taskId
704 + * @param int $subtaskId
705 + */
706 + public function deleteSubtask($boardId, $taskId, $subtaskId)
707 + {
708 + if (!$this->hasProAndBoardAccess($boardId)) {
709 + return false;
710 + }
711 +
712 + $subtask = Task::where('id', $subtaskId)->where('parent_id', $taskId)->where('board_id', $boardId)->first();
713 + if (!$subtask) {
714 + return false;
715 + }
716 +
717 + $deletedTask = clone $subtask;
718 +
719 + $options = null;
720 + //if we need to do something before a task is deleted
721 + do_action('fluent_boards/before_task_deleted', $subtask, $options);
722 +
723 + ( new SubtaskService() )->deleteSubtask($subtask);
724 +
725 + do_action('fluent_boards/subtask_deleted_activity', $deletedTask->parent_id, $deletedTask->title);
726 + }
727 +
728 + /**
729 + * Check if the user has pro version and has access to the board
730 + * @param $boardId
731 + * @return bool
732 + */
733 + private function hasProAndBoardAccess($boardId)
734 + {
735 + return defined('FLUENT_BOARDS_PRO_VERSION') && $this->canWriteBoard($boardId);
736 + }
737 +
738 +
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 + */
513 746 public function __call($method, $params)
514 747 {
515 - if (in_array($method, $this->allowedInstanceMethods)) {
516 - return call_user_func_array([$this->instance, $method], $params);
517 - }
748 + throw new \Exception(esc_html(sprintf('Method %s does not exist.', $method)));
749 + }
518 750
519 - throw new \Exception("Method {$method} does not exist.");
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)
758 + {
759 + return PermissionManager::userHasBoardPermission($boardId, 'GET');
760 + }
761 +
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 + ])));
520 787 }
521 788 }