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 +562 -20 1.212.1.0 View file →
@@ -3,20 +3,31 @@
3 3 namespace FluentBoards\App\Api\Classes;
4 4
5 5 defined('ABSPATH') || exit;
6 6
7 +use Exception;
7 8 use FluentBoards\App\Models\Label;
9 +use FluentBoards\App\Models\Stage;
8 10 use FluentBoards\App\Models\Task;
11 +use FluentBoards\App\Models\TaskMeta;
12 +use FluentBoards\App\Models\Board;
9 13 use FluentBoards\App\Services\BoardService;
10 14 use FluentBoards\App\Services\Helper;
11 15 use FluentBoards\App\Services\PermissionManager;
16 +use FluentBoards\App\Services\TaskService;
17 +use FluentBoardsPro\App\Models\TaskAttachment;
18 +use FluentBoardsPro\App\Services\AttachmentService;
19 +use FluentBoardsPro\App\Services\SubtaskService;
12 20 use FluentCrm\App\Models\Subscriber;
13 21
22 +use FluentBoards\App\Services\Constant;
14 23
24 +
25 +
15 26 /**
16 - * Contacts Class - PHP APi Wrapper
27 + * Tasks Class - PHP API Wrapper
17 28 *
18 - * 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
19 30 *
20 31 * @package FluentBoards\App\Api\Classes
21 32 * @namespace FluentBoards\App\Api\Classes
22 33 *
@@ -25,16 +36,8 @@
25 36 class Tasks
26 37 {
27 38 private $instance = null;
28 39
29 - private $allowedInstanceMethods = [
30 - 'all',
31 - 'get',
32 - 'find',
33 - 'first',
34 - 'paginate'
35 - ];
36 -
37 40 public function __construct(Task $instance)
38 41 {
39 42 $this->instance = $instance;
40 43 }
@@ -54,8 +57,13 @@
54 57 if (!$board_id) {
55 58 return [];
56 59 }
57 60
61 + //checking if current user has access to board
62 + if (!$this->canReadBoard($board_id)) {
63 + return false;
64 + }
65 +
58 66 $query = Task::query()
59 67 ->where('board_id', $board_id)
60 68 ->whereNull('parent_id')
61 69 ->whereNull('archived_at')
@@ -92,10 +100,14 @@
92 100 {
93 101 if (!empty($id)) {
94 102 $task = Task::where('id', $id)->first();
95 103
104 + if (!$task) {
105 + return false;
106 + }
107 +
96 108 //checking if current user has access to board
97 - if (!PermissionManager::userHasPermission($task->board_id)) {
109 + if (!$this->canReadBoard($task->board_id)) {
98 110 return false;
99 111 }
100 112 return $task;
101 113 }
@@ -123,19 +135,29 @@
123 135 }
124 136
125 137 $tasks = $query->get();
126 138
127 - //checking if current user has access to board
128 - if (!PermissionManager::userHasPermission($tasks->board_id)) {
129 - return false;
139 + $permittedTasks = [];
140 +
141 + foreach ($tasks as $task) {
142 + //checking if current user has access to board
143 + if ($this->canReadBoard($task->board_id)) {
144 + $permittedTasks[] = $task;
145 + }
130 146 }
131 147
132 - return $tasks;
148 + return $permittedTasks;
133 149 }
134 150 return false;
135 151 }
136 152
137 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 + */
138 160 public function create($data)
139 161 {
140 162 if (empty($data)) {
141 163 return false;
@@ -144,13 +166,42 @@
144 166 if (empty($data['title']) || empty($data['board_id']) || empty($data['stage_id'])) {
145 167 return false;
146 168 }
147 169
148 - $taskData = Helper::sanitizeTask($data);
170 + if (!$this->canWriteBoard($data['board_id'])) {
171 + return false;
172 + }
149 173
150 - if(!empty($taskData['priority']) && !in_array($taskData['priority'], ['low', 'medium', 'high'])) {
151 - $taskData['priority'] = 'low';
174 + $stage = Stage::where('id', $data['stage_id'])->where('board_id', $data['board_id'])->first();
175 + if (!$stage) {
176 + return false;
152 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 + }
153 204 if(!empty($taskData['status']) && !in_array($taskData['status'], ['open', 'closed'])) {
154 205 $taskData['status'] = 'open';
155 206 }
156 207 if(!empty($data['crm_contact_id'])) {
@@ -158,9 +209,9 @@
158 209 }
159 210
160 211 if(!empty($data['contact_email']) && empty($data['crm_contact_id'])) {
161 212 // Find first if the contact exists
162 - $contact = FluentCrmApi('contacts')->creteOrUpdate([
213 + $contact = FluentCrmApi('contacts')->createOrUpdate([
163 214 'email' => $data['contact_email'],
164 215 'first_name' => $data['contact_first_name'],
165 216 'last_name' => $data['contact_last_name'],
166 217 'status' => 'subscribed'
@@ -225,9 +276,9 @@
225 276 'board_id',
226 277 'stage_id',
227 278 'parent_id',
228 279 'status', // open | closed
229 - 'priority', // low | medium | high
280 + 'priority', // urgent | high | medium | low | blank
230 281 'source',
231 282 'source_id',
232 283 'description',
233 284 'due_at',
@@ -241,6 +292,497 @@
241 292 'assignees', // WP User IDs / WP User Emails as an array
242 293 'labels', // array of label ids or titles [1, 'New']
243 294 ];
244 295
296 + }
297 +
298 + public function addAssignees($task_id, $assignees = [])
299 + {
300 + if(empty($task_id) || empty($assignees)) {
301 + return false;
302 + }
303 +
304 + $task = Task::where('id', $task_id)->first();
305 + $board = Board::findOrFail($task->board_id);
306 +
307 + if(!PermissionManager::isBoardManager($task->board_id))
308 + {
309 + if(!PermissionManager::isAdmin())
310 + {
311 + return false;
312 + }
313 + }
314 +
315 + if(!$task) {
316 + return false;
317 + }
318 +
319 + $boardUsersIds = $board->users->pluck('ID')->toArray();
320 +
321 + foreach($assignees as $assignee)
322 + {
323 + if(in_array($assignee, $boardUsersIds)) { //check if user is a board member
324 + $task->assignees()
325 + ->syncWithoutDetaching([
326 + $assignee => [
327 + 'object_type' => Constant::OBJECT_TYPE_TASK_ASSIGNEE
328 + ]
329 + ]);
330 + $task->watchers()
331 + ->syncWithoutDetaching([
332 + $assignee => [
333 + 'object_type' => Constant::OBJECT_TYPE_USER_TASK_WATCH
334 + ]
335 + ]);
336 + }
337 + }
338 +
339 + return true;
340 + }
341 +
342 + public function removeAssignees($task_id, $assignees = [])
343 + {
344 + if(empty($task_id) || empty($assignees)) {
345 + return false;
346 + }
347 +
348 + $task = Task::where('id', $task_id)->first();
349 + if(!$task) {
350 + return false;
351 + }
352 +
353 + if(!PermissionManager::isBoardManager($task->board_id))
354 + {
355 + if(!PermissionManager::isAdmin())
356 + {
357 + return false;
358 + }
359 + }
360 +
361 + $task->assignees()->detach($assignees);
362 +
363 + $task->watchers()->detach($assignees);
364 +
365 + return true;
366 + }
367 +
368 +
369 +
370 + public function attachLabels($taskId, $labelIds = [])
371 + {
372 + // do code here
373 + if(!$taskId)
374 + {
375 + return false;
376 + }
377 +
378 + $task = Task::findOrFail($taskId);
379 + $board = Board::findOrFail($task->board_id);
380 +
381 + if(!$task)
382 + {
383 + return false;
384 + }
385 +
386 + //checking if current user has access to board
387 + if (!$this->canWriteBoard($task->board_id)) {
388 + return false;
389 + }
390 +
391 + $boardlabelIds = $board->labels->pluck('id')->toArray();
392 +
393 + foreach ($labelIds as $labelId)
394 + {
395 + if(in_array($labelId, $boardlabelIds)) { //check if label is a board label
396 + $task->labels()
397 + ->syncWithoutDetaching([
398 + $labelId => [
399 + 'object_type' => Constant::OBJECT_TYPE_TASK_LABEL
400 + ]
401 + ]);
402 + }
403 + }
404 +
405 + return true;
406 +
407 + }
408 +
409 + public function removeLabels($taskId, $labelIds = [])
410 + {
411 + // do code here
412 + if(!$taskId)
413 + {
414 + return false;
415 + }
416 +
417 + $task = Task::findOrFail($taskId);
418 +
419 + if(!$task)
420 + {
421 + return false;
422 + }
423 +
424 + //checking if current user has access to board
425 + if (!$this->canWriteBoard($task->board_id)) {
426 + return false;
427 + }
428 +
429 + $task->labels()->detach($labelIds);
430 +
431 + return true;
432 + }
433 +
434 + /*
435 + * Change the stage of a task
436 + * @param int $taskId
437 + * @param int $stageId - the new stage id
438 + * @return Task
439 + */
440 + public function changeStage($task, $stageId)
441 + {
442 + // param can be either task object or task Id
443 + if(is_numeric($task)) {
444 + $task = Task::where('id', $task)->first();
445 + }
446 +
447 + if(!$task) {
448 + return false;
449 + }
450 +
451 + //checking if current user has access to board
452 + if (!$this->canWriteBoard($task->board_id)) {
453 + return false;
454 + }
455 +
456 + $stage = Stage::findOrFail($stageId);
457 +
458 + if($stage->board_id != $task->board_id)
459 + {
460 + return false;
461 + }
462 +
463 + $taskService = new TaskService();
464 + $position = $taskService->getLastPositionOfTasks($stageId);
465 +
466 + $task->stage_id = $stageId;
467 + $task->position = $position;
468 + $task->save();
469 +
470 + return $task;
471 + }
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 + */
481 + public function updateProperty($taskId, $property, $value)
482 + {
483 + $taskId = absint($taskId);
484 + $property = sanitize_text_field($property);
485 +
486 + if (!$taskId || !$property) {
487 + return false;
488 + }
489 +
490 + $taskService = new TaskService();
491 + $task = Task::where('id', $taskId)->first();
492 +
493 + if(!$task) {
494 + return false;
495 + }
496 +
497 + if (!$this->canWriteBoard($task->board_id)) {
498 + return false;
499 + }
500 +
501 + $allowedColumns = ['title', 'description', 'due_at', 'priority', 'status', 'source', 'source_id', 'crm_contact_id'];
502 +
503 + if(!in_array($property, $allowedColumns, true)) {
504 + return false;
505 + }
506 +
507 + switch ($property) {
508 + case 'description':
509 + $sanitizedValue = fluent_boards_sanitize_description($value);
510 + break;
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 +
541 + return $task;
542 +
543 + }
544 +
545 + /**
546 + * Create a task attachment
547 + * @param int $boardId
548 + * @param int $taskId
549 + * @param array $data - ['title', 'url'] // url is required
550 + * @throws Exception
551 + */
552 + public function createTaskAttachment(int $boardId, int $taskId, array $data = [])
553 + {
554 + // check if the user has permission to add attachment
555 + if (!$this->hasProAndBoardAccess($boardId) || empty($data) || empty($data['url'])) {
556 + return false;
557 + }
558 +
559 + $task = Task::where('id', $taskId)->where('board_id', $boardId)->first();
560 + if (!$task) {
561 + return false;
562 + }
563 +
564 + $attachmentData = Helper::sanitizeTaskAttachment($data);
565 + return (new AttachmentService())->addTaskAttachment($task->id, $attachmentData);
566 + }
567 +
568 +
569 + /**
570 + * delete a task attachment
571 + * @param $boardId
572 + * @param $taskId
573 + * @param $attachmentId
574 + * @return bool
575 + */
576 + public function deleteTaskAttachment( int $boardId, int $taskId, int $attachmentId)
577 + {
578 + // check if the user has permission to delete attachment
579 + if (!$this->hasProAndBoardAccess($boardId)) {
580 + return false;
581 + }
582 +
583 + $task = Task::where('id', $taskId)->where('board_id', $boardId)->first();
584 + if (!$task) {
585 + return false;
586 + }
587 +
588 + (new AttachmentService())->deleteTaskAttachment($task->id, $attachmentId);
589 +
590 + return true;
591 + }
592 +
593 +
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)
602 + {
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;
669 + }
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 + */
746 + public function __call($method, $params)
747 + {
748 + throw new \Exception(esc_html(sprintf('Method %s does not exist.', $method)));
749 + }
750 +
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 + ])));
245 787 }
246 788 }