PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.0
2.2.1 2.2.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 All 44 releases
← All changes | app/Api/Classes/Tasks.php +299 -36 1.22 → 2.2.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;
@@ -13,19 +14,20 @@
13 14 use FluentBoards\App\Services\Helper;
14 15 use FluentBoards\App\Services\PermissionManager;
15 16 use FluentBoards\App\Services\TaskService;
16 17 use FluentBoardsPro\App\Models\TaskAttachment;
17 -use FluentBoardsPro\App\Services\Attachment;
18 18 use FluentBoardsPro\App\Services\AttachmentService;
19 +use FluentBoardsPro\App\Services\SubtaskService;
19 20 use FluentCrm\App\Models\Subscriber;
21 +
20 22 use FluentBoards\App\Services\Constant;
21 23
22 24
23 25
24 26 /**
25 - * Contacts Class - PHP APi Wrapper
27 + * Tasks Class - PHP API Wrapper
26 28 *
27 - * 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
28 30 *
29 31 * @package FluentBoards\App\Api\Classes
30 32 * @namespace FluentBoards\App\Api\Classes
31 33 *
@@ -34,16 +36,8 @@
34 36 class Tasks
35 37 {
36 38 private $instance = null;
37 39
38 - private $allowedInstanceMethods = [
39 - 'all',
40 - 'get',
41 - 'find',
42 - 'first',
43 - 'paginate'
44 - ];
45 -
46 40 public function __construct(Task $instance)
47 41 {
48 42 $this->instance = $instance;
49 43 }
@@ -64,9 +58,9 @@
64 58 return [];
65 59 }
66 60
67 61 //checking if current user has access to board
68 - if (!PermissionManager::userHasPermission($board_id)) {
62 + if (!$this->canReadBoard($board_id)) {
69 63 return false;
70 64 }
71 65
72 66 $query = Task::query()
@@ -106,10 +100,14 @@
106 100 {
107 101 if (!empty($id)) {
108 102 $task = Task::where('id', $id)->first();
109 103
104 + if (!$task) {
105 + return false;
106 + }
107 +
110 108 //checking if current user has access to board
111 - if (!PermissionManager::userHasPermission($task->board_id)) {
109 + if (!$this->canReadBoard($task->board_id)) {
112 110 return false;
113 111 }
114 112 return $task;
115 113 }
@@ -141,9 +139,9 @@
141 139 $permittedTasks = [];
142 140
143 141 foreach ($tasks as $task) {
144 142 //checking if current user has access to board
145 - if (PermissionManager::userHasPermission($task->board_id)) {
143 + if ($this->canReadBoard($task->board_id)) {
146 144 $permittedTasks[] = $task;
147 145 }
148 146 }
149 147
@@ -152,8 +150,14 @@
152 150 return false;
153 151 }
154 152
155 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 + */
156 160 public function create($data)
157 161 {
158 162 if (empty($data)) {
159 163 return false;
@@ -162,13 +166,42 @@
162 166 if (empty($data['title']) || empty($data['board_id']) || empty($data['stage_id'])) {
163 167 return false;
164 168 }
165 169
166 - $taskData = Helper::sanitizeTask($data);
170 + if (!$this->canWriteBoard($data['board_id'])) {
171 + return false;
172 + }
167 173
168 - if(!empty($taskData['priority']) && !in_array($taskData['priority'], ['low', 'medium', 'high'])) {
169 - $taskData['priority'] = 'low';
174 + $stage = Stage::where('id', $data['stage_id'])->where('board_id', $data['board_id'])->first();
175 + if (!$stage) {
176 + return false;
170 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 + }
171 204 if(!empty($taskData['status']) && !in_array($taskData['status'], ['open', 'closed'])) {
172 205 $taskData['status'] = 'open';
173 206 }
174 207 if(!empty($data['crm_contact_id'])) {
@@ -176,9 +209,9 @@
176 209 }
177 210
178 211 if(!empty($data['contact_email']) && empty($data['crm_contact_id'])) {
179 212 // Find first if the contact exists
180 - $contact = FluentCrmApi('contacts')->creteOrUpdate([
213 + $contact = FluentCrmApi('contacts')->createOrUpdate([
181 214 'email' => $data['contact_email'],
182 215 'first_name' => $data['contact_first_name'],
183 216 'last_name' => $data['contact_last_name'],
184 217 'status' => 'subscribed'
@@ -243,9 +276,9 @@
243 276 'board_id',
244 277 'stage_id',
245 278 'parent_id',
246 279 'status', // open | closed
247 - 'priority', // low | medium | high
280 + 'priority', // urgent | high | medium | low | blank
248 281 'source',
249 282 'source_id',
250 283 'description',
251 284 'due_at',
@@ -350,9 +383,9 @@
350 383 return false;
351 384 }
352 385
353 386 //checking if current user has access to board
354 - if (!PermissionManager::userHasPermission($task->board_id)) {
387 + if (!$this->canWriteBoard($task->board_id)) {
355 388 return false;
356 389 }
357 390
358 391 $boardlabelIds = $board->labels->pluck('id')->toArray();
@@ -388,9 +421,9 @@
388 421 return false;
389 422 }
390 423
391 424 //checking if current user has access to board
392 - if (!PermissionManager::userHasPermission($task->board_id)) {
425 + if (!$this->canWriteBoard($task->board_id)) {
393 426 return false;
394 427 }
395 428
396 429 $task->labels()->detach($labelIds);
@@ -415,9 +448,9 @@
415 448 return false;
416 449 }
417 450
418 451 //checking if current user has access to board
419 - if (!PermissionManager::userHasPermission($task->board_id)) {
452 + if (!$this->canWriteBoard($task->board_id)) {
420 453 return false;
421 454 }
422 455
423 456 $stage = Stage::findOrFail($stageId);
@@ -436,10 +469,25 @@
436 469
437 470 return $task;
438 471 }
439 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 + */
440 481 public function updateProperty($taskId, $property, $value)
441 482 {
483 + $taskId = absint($taskId);
484 + $property = sanitize_text_field($property);
485 +
486 + if (!$taskId || !$property) {
487 + return false;
488 + }
489 +
442 490 $taskService = new TaskService();
443 491 $task = Task::where('id', $taskId)->first();
444 492
445 493 if(!$task) {
@@ -445,16 +493,52 @@
445 493 if(!$task) {
446 494 return false;
447 495 }
448 496
497 + if (!$this->canWriteBoard($task->board_id)) {
498 + return false;
499 + }
500 +
449 501 $allowedColumns = ['title', 'description', 'due_at', 'priority', 'status', 'source', 'source_id', 'crm_contact_id'];
450 502
451 - if(!in_array($property, $allowedColumns)) {
503 + if(!in_array($property, $allowedColumns, true)) {
452 504 return false;
453 505 }
454 506
455 - $taskService->updateTaskProperty($task, $property, $value);
507 + switch ($property) {
508 + case 'description':
509 + $sanitizedValue = fluent_boards_sanitize_description($value);
510 + break;
456 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 +
457 541 return $task;
458 542
459 543 }
460 544
@@ -462,15 +546,14 @@
462 546 * Create a task attachment
463 547 * @param int $boardId
464 548 * @param int $taskId
465 549 * @param array $data - ['title', 'url'] // url is required
466 - * @return false|TaskAttachment $attachment
467 550 * @throws Exception
468 551 */
469 - public function createTaskAttachment(int $boardId, int $taskId, array $data = []): false|TaskAttachment
552 + public function createTaskAttachment(int $boardId, int $taskId, array $data = [])
470 553 {
471 554 // check if the user has permission to add attachment
472 - if (!defined('FLUENT_BOARDS_PRO_VERSION') || !PermissionManager::userHasPermission($boardId) || empty($data) || empty($data['url'])) {
555 + if (!$this->hasProAndBoardAccess($boardId) || empty($data) || empty($data['url'])) {
473 556 return false;
474 557 }
475 558
476 559 $task = Task::where('id', $taskId)->where('board_id', $boardId)->first();
@@ -476,9 +559,8 @@
476 559 $task = Task::where('id', $taskId)->where('board_id', $boardId)->first();
477 560 if (!$task) {
478 561 return false;
479 562 }
480 - error_log('hello world');
481 563
482 564 $attachmentData = Helper::sanitizeTaskAttachment($data);
483 565 return (new AttachmentService())->addTaskAttachment($task->id, $attachmentData);
484 566 }
@@ -490,12 +572,12 @@
490 572 * @param $taskId
491 573 * @param $attachmentId
492 574 * @return bool
493 575 */
494 - public function deleteTaskAttachment( int $boardId, int $taskId, int $attachmentId): bool
576 + public function deleteTaskAttachment( int $boardId, int $taskId, int $attachmentId)
495 577 {
496 578 // check if the user has permission to delete attachment
497 - if (!defined('FLUENT_BOARDS_PRO_VERSION') || !PermissionManager::userHasPermission($boardId)) {
579 + if (!$this->hasProAndBoardAccess($boardId)) {
498 580 return false;
499 581 }
500 582
501 583 $task = Task::where('id', $taskId)->where('board_id', $boardId)->first();
@@ -508,18 +590,199 @@
508 590 return true;
509 591 }
510 592
511 593
512 - 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)
513 602 {
514 - 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;
515 669 }
516 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 + */
517 746 public function __call($method, $params)
518 747 {
519 - if (in_array($method, $this->allowedInstanceMethods)) {
520 - return call_user_func_array([$this->instance, $method], $params);
521 - }
748 + throw new \Exception(esc_html(sprintf('Method %s does not exist.', $method)));
749 + }
522 750
523 - 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 + ])));
524 787 }
525 788 }