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
fluent-boards / app / Api / Classes / Tasks.php

Tasks.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.1.0, at app/Api/Classes/Tasks.php

789 lines 22.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\App\Api\Classes;
4
5 defined('ABSPATH') || exit;
6
7 use Exception;
8 use FluentBoards\App\Models\Label;
9 use FluentBoards\App\Models\Stage;
10 use FluentBoards\App\Models\Task;
11 use FluentBoards\App\Models\TaskMeta;
12 use FluentBoards\App\Models\Board;
13 use FluentBoards\App\Services\BoardService;
14 use FluentBoards\App\Services\Helper;
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;
20 use FluentCrm\App\Models\Subscriber;
21
22 use FluentBoards\App\Services\Constant;
23
24
25
26 /**
27 * Tasks Class - PHP API Wrapper
28 *
29 * Tasks API Wrapper Class that can be used as <code>FluentBoardsApi('tasks')</code> to get the class instance
30 *
31 * @package FluentBoards\App\Api\Classes
32 * @namespace FluentBoards\App\Api\Classes
33 *
34 * @version 1.0.0
35 */
36 class Tasks
37 {
38 private $instance = null;
39
40 public function __construct(Task $instance)
41 {
42 $this->instance = $instance;
43 }
44
45 /**
46 * Get Tasks by board
47 *
48 * Use:
49 * <code>FluentBoardsApi('tasks')->getTasksByBoard();</code>
50 *
51 * @param string|int $board_id
52 * @param array $with
53 * @return array|Task Model
54 */
55 public function getTasksByBoard($board_id, $with = [])
56 {
57 if (!$board_id) {
58 return [];
59 }
60
61 //checking if current user has access to board
62 if (!$this->canReadBoard($board_id)) {
63 return false;
64 }
65
66 $query = Task::query()
67 ->where('board_id', $board_id)
68 ->whereNull('parent_id')
69 ->whereNull('archived_at')
70 ->orderBy('due_at', 'ASC');
71
72 if (!empty($with)) {
73 $query->with($with);
74 }
75
76 $tasks = $query->get();
77
78 $tasks->transform(function ($task) {
79 $task->isOverdue = $task->isOverdue();
80 $task->isUpcoming = $task->upcoming();
81 $task->contact = Helper::crm_contact($task->crm_contact_id);
82 $task->is_watching = $task->isWatching();
83 return $task;
84 });
85
86 return $tasks;
87 }
88
89
90 /**
91 * Get Task by id
92 *
93 * Use:
94 * <code>FluentBoardsApi('tasks')->getTask($id);</code>
95 *
96 * @param int|string $id Task id
97 * @return false|Task Model
98 */
99 public function getTask($id)
100 {
101 if (!empty($id)) {
102 $task = Task::where('id', $id)->first();
103
104 if (!$task) {
105 return false;
106 }
107
108 //checking if current user has access to board
109 if (!$this->canReadBoard($task->board_id)) {
110 return false;
111 }
112 return $task;
113 }
114 return false;
115 }
116
117
118 /**
119 * Get Task Created by
120 *
121 * Use:
122 * <code>FluentBoardsApi('tasks')->getTasksCreatedBy($userId);</code>
123 *
124 * @param int|string $userId Task id
125 * @param array $with
126 * @return false|Task Model
127 */
128 public function getTasksCreatedBy($userId, $with = [])
129 {
130 if (!empty($userId)) {
131 $query = Task::where('created_by', $userId);
132
133 if (!empty($with)) {
134 $query->with($with);
135 }
136
137 $tasks = $query->get();
138
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 }
146 }
147
148 return $permittedTasks;
149 }
150 return false;
151 }
152
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 */
160 public function create($data)
161 {
162 if (empty($data)) {
163 return false;
164 }
165
166 if (empty($data['title']) || empty($data['board_id']) || empty($data['stage_id'])) {
167 return false;
168 }
169
170 if (!$this->canWriteBoard($data['board_id'])) {
171 return false;
172 }
173
174 $stage = Stage::where('id', $data['stage_id'])->where('board_id', $data['board_id'])->first();
175 if (!$stage) {
176 return false;
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 }
204 if(!empty($taskData['status']) && !in_array($taskData['status'], ['open', 'closed'])) {
205 $taskData['status'] = 'open';
206 }
207 if(!empty($data['crm_contact_id'])) {
208 $taskData['crm_contact_id'] = $data['crm_contact_id'];
209 }
210
211 if(!empty($data['contact_email']) && empty($data['crm_contact_id'])) {
212 // Find first if the contact exists
213 $contact = FluentCrmApi('contacts')->createOrUpdate([
214 'email' => $data['contact_email'],
215 'first_name' => $data['contact_first_name'],
216 'last_name' => $data['contact_last_name'],
217 'status' => 'subscribed'
218 ]);
219 $taskData['crm_contact_id'] = $contact->id;
220 }
221
222 if(!empty($data['assignees']) && is_array($data['assignees'])) {
223 // check if the assignees are valid, they have to be wp user ids
224 $users = get_users(['include' => $data['assignees']]);
225 $taskData['assignees'] = array_map(function($user) {
226 return $user->ID;
227 }, $users);
228 // after successful task creation we have to add them as board members
229 }
230
231 if(!empty($data['labels']) && is_array($data['labels'])) {
232 $labelIds = [];
233 // check if the labels are valid, they have to be label ids
234 foreach ($data['labels'] as $label) {
235 if(is_numeric($label)) {
236 $labelModel = Label::where('id', $label)->where('board_id', $data['board_id'])->first();
237 if($labelModel) {
238 $labelId = $labelModel->id;
239 $labelIds[] = $labelId;
240 }
241 continue;
242 }
243 if(is_string($label)) {
244 $labelModel = Label::where('board_id', $data['board_id'])
245 ->where(function($query) use ($label) {
246 $query->where('title', $label)
247 ->orWhere('slug', $label);
248 })->first();
249 if($labelModel) {
250 $labelId = $labelModel->id;
251 $labelIds[] = $labelId;
252 }
253 }
254 }
255 $taskData['labels'] = $labelIds;
256 // we have to map the labels after task creation
257 }
258
259 $task = $this->instance->createTask($taskData);
260 // push assignees to board Members
261 $boardService = new BoardService();
262 if(!empty($taskData['assignees'])) {
263 foreach ($taskData['assignees'] as $assigneeId) {
264 $boardService->addMembersInBoard($data['board_id'], $assigneeId);
265 }
266 }
267
268 return $task;
269 }
270
271
272 public function createTask($data = [])
273 {
274 $defaultData = [
275 'title',
276 'board_id',
277 'stage_id',
278 'parent_id',
279 'status', // open | closed
280 'priority', // urgent | high | medium | low | blank
281 'source',
282 'source_id',
283 'description',
284 'due_at',
285 'crm_contact_id',
286 // <or>
287 'contact_email',
288 'contact_first_name', // this is the full name
289 'contact_last_name', // this is the full name
290 'contact_status', // default subscribed
291 // </or>
292 'assignees', // WP User IDs / WP User Emails as an array
293 'labels', // array of label ids or titles [1, 'New']
294 ];
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 ])));
787 }
788 }
789