PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / trunk
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration vtrunk
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 1.45 All 41 releases
← All changes | app/Http/Controllers/TaskController.php +135 -19 1.95.2trunk View file →
@@ -37,10 +37,25 @@
37 37 public function getTopTasksForBoards()
38 38 {
39 39 $userId = get_current_user_id();
40 40 $task_ids = PermissionManager::getTaskIdsWatchByUser($userId);
41 - $tasksArray = $this->taskService->getTasksForBoards(['assigned', 'overdue', 'upcoming', 'completed', 'others'], 6, $task_ids);
42 - $taskCounts = $this->taskService->getTaskCountsForBoards(['assigned', 'overdue', 'upcoming', 'completed', 'others'], $task_ids);
41 + $boardIds = PermissionManager::getBoardIdsForUser($userId);
42 + $taskCategories = ['due_today', 'assigned', 'overdue', 'upcoming', 'mentioned', 'completed', 'others'];
43 + $tasksArray = $this->taskService->getTasksForBoards($taskCategories, 6, $task_ids);
44 + $taskCounts = $this->taskService->getTaskCountsForBoards($taskCategories, $task_ids);
45 + $taskCounts['all_boards'] = empty($boardIds)
46 + ? 0
47 + : (int) Board::whereIn('id', $boardIds)
48 + ->whereNull('archived_at')
49 + ->excludeTemplates()
50 + ->count();
51 + $taskCounts['all_tasks'] = empty($task_ids)
52 + ? 0
53 + : (int) Task::whereIn('id', $task_ids)
54 + ->whereNull('archived_at')
55 + ->whereNull('parent_id')
56 + ->onActiveAvailableBoards()
57 + ->count();
43 58
44 59 return [
45 60 'data' => $tasksArray,
46 61 'counts' => $taskCounts,
@@ -72,11 +87,9 @@
72 87 // Process each task
73 88 $this->processTasks($tasks, $board);
74 89
75 90 if ($board->type === 'roadmap') {
76 - foreach ($tasks as $task) {
77 - $task->vote_statistics = $this->taskService->getIdeaVoteStatistics($task->id);
78 - }
91 + $this->taskService->loadIdeaVoteStatistics($tasks);
79 92 }
80 93
81 94 return [
82 95 'tasks' => $tasks,
@@ -426,13 +439,32 @@
426 439 }
427 440 }
428 441 }
429 442
430 -
443 + /**
444 + * Create a task with field-specific sanitization for its request data.
445 + *
446 + * @param Request $request
447 + * @param int $board_id
448 + * @return mixed
449 + */
431 450 public function create(Request $request, $board_id)
432 451 {
433 452 $board_id = absint($board_id);
434 - $taskData = $this->taskSanitizeAndValidate($request->getSafe('task'), [
453 + $safeTaskData = $request->getSafe('task');
454 + $rawTaskData = $request->get('task', []);
455 +
456 + // Milkdown serializes pasted URLs as <https://...>, which generic text
457 + // sanitization removes as a tag.
458 + if (
459 + is_array($safeTaskData) &&
460 + is_array($rawTaskData) &&
461 + array_key_exists('description', $rawTaskData)
462 + ) {
463 + $safeTaskData['description'] = fluent_boards_sanitize_description($rawTaskData['description']);
464 + }
465 +
466 + $taskData = $this->taskSanitizeAndValidate($safeTaskData, [
435 467 'title' => 'required|string',
436 468 'board_id' => 'required|numeric',
437 469 'stage_id' => 'required|numeric',
438 470 'priority' => 'nullable|string',
@@ -440,17 +472,28 @@
440 472 'is_template' => 'string',
441 473 ]);
442 474
443 475 try {
476 + if (isset($taskData['assignees'])) {
477 + $taskData['assignees'] = array_filter(array_map('intval', (array) $taskData['assignees']));
478 + }
479 +
480 + if (isset($taskData['labels'])) {
481 + $taskData['labels'] = array_filter(array_map('intval', (array) $taskData['labels']));
482 + }
483 +
444 484 if ($taskData['board_id'] != $board_id) {
445 485 throw new \Exception(esc_html__('Board id is not valid', 'fluent-boards'));
446 486 }
447 487
448 488 $task = $this->taskService->createTask($taskData, $board_id);
489 + $message = $task->type === 'roadmap'
490 + ? __('Idea has been successfully created', 'fluent-boards')
491 + : __('Task has been successfully created', 'fluent-boards');
449 492
450 493 return $this->sendSuccess([
451 494 'task' => $task,
452 - 'message' => __('Task has been successfully created', 'fluent-boards'),
495 + 'message' => $message,
453 496 'updatedTasks' => $this->taskService->getLastOneMinuteUpdatedTasks($task->board_id)
454 497 ], 201);
455 498 } catch (\Exception $e) {
456 499 return $this->sendError($e->getMessage(), 400);
@@ -481,8 +524,9 @@
481 524
482 525 $task->load(['board', 'stage', 'labels', 'assignees','watchers']);
483 526
484 527 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
528 + $task->watchers = Helper::sanitizeUserCollections($task->watchers);
485 529
486 530 $task->isOverdue = $task->isOverdue();
487 531 $task->contact = Task::lead_contact($task->crm_contact_id);
488 532 $task->board->stages = $stageService->stagesByBoardId($board_id);
@@ -710,9 +754,9 @@
710 754 $task_id = absint($task_id);
711 755 //Properties in col: settings, assignees,crm_contact_id, archived_at(AUTO_SET_TIMESTAMP) , status, title, description, priority, is_watching, is_template
712 756 $col = $request->getSafe('property', 'sanitize_text_field');
713 757 if ($col === 'description') {
714 - $value = $request->getSafe('value', 'wp_kses_post');
758 + $value = $request->getSafe('value', 'fluent_boards_sanitize_description');
715 759 } elseif ($col === 'settings' || $col === 'assignees') {
716 760 $value = $request->get('value');
717 761 if (is_array($value) && isset($value['cover']) && is_array($value['cover'])) {
718 762 if (isset($value['cover']['backgroundColor'])) {
@@ -718,8 +762,19 @@
718 762 if (isset($value['cover']['backgroundColor'])) {
719 763 $value['cover']['backgroundColor'] = sanitize_text_field($value['cover']['backgroundColor']);
720 764 }
721 765 }
766 + } elseif ($col === 'is_watching') {
767 + $value = $request->get('value');
768 + if (is_array($value)) {
769 + $action = isset($value['action']) ? sanitize_text_field($value['action']) : 'start';
770 + $value = [
771 + 'userId' => isset($value['userId']) ? absint($value['userId']) : 0,
772 + 'action' => in_array($action, ['start', 'stop'], true) ? $action : 'start',
773 + ];
774 + } else {
775 + $value = sanitize_text_field($value);
776 + }
722 777 } else {
723 778 $value = $request->getSafe('value', 'sanitize_text_field');
724 779 }
725 780
@@ -738,8 +793,12 @@
738 793 if ($col === 'parent_id' && $validatedData[$col]) {
739 794 $this->taskService->findTaskOnBoard($validatedData[$col], $board_id, false);
740 795 }
741 796
797 + if ($task->parent_id && $col === 'started_at') {
798 + $validatedData[$col] = null;
799 + }
800 +
742 801 $oldDateValue = null;
743 802 if (in_array($col, ['due_at', 'started_at'])) {
744 803 $oldDateValue = $task->{$col};
745 804 }
@@ -755,8 +814,13 @@
755 814 $task->contact = Helper::crm_contact($task->crm_contact_id);
756 815 $task->is_watching = $task->isWatching();
757 816 $task->assignees = Helper::sanitizeUserCollections($task->assignees);
758 817
818 + if ($col === 'is_watching') {
819 + $task->load('watchers');
820 + $task->watchers = Helper::sanitizeUserCollections($task->watchers);
821 + }
822 +
759 823 if ($task->parent_id) {
760 824 $task->subtask_group_id = TaskMeta::where('task_id', $task->id)->where('key', Constant::SUBTASK_GROUP_CHILD)->value('value');
761 825 }
762 826
@@ -781,8 +845,28 @@
781 845 'updatedTasks' => $updatedTasks
782 846 ];
783 847 }
784 848
849 + /**
850 + * Remove a Fluent Support association from a task without deleting the ticket.
851 + *
852 + * @param int $board_id
853 + * @param int $task_id
854 + * @return mixed
855 + */
856 + public function removeSupportTicketLink($board_id, $task_id)
857 + {
858 + $boardId = absint($board_id);
859 + $taskId = absint($task_id);
860 + $task = $this->taskService->removeSupportTicketLink($taskId, $boardId);
861 +
862 + return $this->sendSuccess([
863 + 'message' => __('Support ticket link has been removed', 'fluent-boards'),
864 + 'task' => $task,
865 + 'updatedTasks' => [$task],
866 + ]);
867 + }
868 +
785 869 public function updateTaskDates(Request $request, $board_id, $task_id)
786 870 {
787 871 $board_id = absint($board_id);
788 872 $task_id = absint($task_id);
@@ -803,10 +887,16 @@
803 887 $hasRemindAt = array_key_exists('remind_at', $payload);
804 888
805 889 $startAt = $hasStartAt ? $request->getSafe('started_at', 'sanitize_text_field', NULL) : $task->started_at;
806 890 $dueAt = $hasDueAt ? $request->getSafe('due_at', 'sanitize_text_field', NULL) : $task->due_at;
891 + $isSubtask = (bool) $task->parent_id;
807 892
808 - if ($hasStartAt && $hasDueAt && $startAt && $dueAt) {
893 + if ($isSubtask) {
894 + $startAt = null;
895 + $hasStartAt = $hasStartAt || (bool) $task->started_at;
896 + }
897 +
898 + if (!$isSubtask && $hasStartAt && $hasDueAt && $startAt && $dueAt) {
809 899 if (strtotime($startAt) > strtotime($dueAt)) {
810 900 $startAt = substr($dueAt, 0, 10) . ' 00:00:00';
811 901 }
812 902 }
@@ -1031,8 +1121,11 @@
1031 1121 }
1032 1122
1033 1123 return [$col => $sanitizedAndValidatedValue];
1034 1124 }
1125 + if ('is_watching' == $col && is_array($value)) {
1126 + return [$col => $value];
1127 + }
1035 1128 $data = Helper::sanitizeTask([$col => $value]);
1036 1129
1037 1130 return $this->validate($data, [
1038 1131 $col => $rule,
@@ -1217,9 +1310,10 @@
1217 1310 // Pagination parameters
1218 1311 $page = $request->getSafe('page', 'intval', 1);
1219 1312 $perPage = $request->getSafe('per_page', 'intval', 10);
1220 1313 $filter = $request->getSafe('filter', 'sanitize_text_field', 'newest'); // Filter for comments and activities
1221 - $commentsAndActivities = $this->taskService->getCommentsAndActivities($task_id, $perPage, $page, $filter, $board_id);
1314 + $feedType = $request->getSafe('feed_type', 'sanitize_text_field', 'all');
1315 + $commentsAndActivities = $this->taskService->getCommentsAndActivities($task_id, $perPage, $page, $filter, $board_id, $feedType);
1222 1316 // Return the response with the task, paginated comments and activities, total count, current page, and items per page
1223 1317 return $this->sendSuccess([
1224 1318 'comments_and_activities' => $commentsAndActivities,
1225 1319 ]);
@@ -1317,12 +1411,16 @@
1317 1411 (new \FluentBoards\App\Services\UploadService)->validateFile($file);
1318 1412
1319 1413 $uploadInfo = UploadService::handleFileUpload( $request->files(), $board_id);
1320 1414 $task = $this->taskService->createTaskFromImage($board_id, $stageId, $uploadInfo, $file);
1415 + $message = $task->type === 'roadmap'
1416 + ? __('Idea has been created', 'fluent-boards')
1417 + : __('Task has been created', 'fluent-boards');
1418 +
1321 1419 return $this->sendSuccess([
1322 1420 'task' => $task,
1323 1421 'updatedTasks' => $this->taskService->getLastOneMinuteUpdatedTasks($board_id),
1324 - 'message' => __('Task has been created', 'fluent-boards'),
1422 + 'message' => $message,
1325 1423 ], 200);
1326 1424
1327 1425 }
1328 1426
@@ -1395,42 +1493,48 @@
1395 1493 public function getTaskTabsConfig()
1396 1494 {
1397 1495 $default_config = [
1398 1496 [
1497 + 'name' => 'due_today',
1498 + 'label' => __('Due Today', 'fluent-boards'),
1499 + 'visible' => 'true',
1500 + 'order' => 1
1501 + ],
1502 + [
1399 1503 'name' => 'assigned',
1400 1504 'label' => __('Assigned', 'fluent-boards'),
1401 1505 'visible' => 'true',
1402 - 'order' => 1
1506 + 'order' => 2
1403 1507 ],
1404 1508 [
1405 1509 'name' => 'upcoming',
1406 1510 'label' => __('Upcoming', 'fluent-boards'),
1407 1511 'visible' => 'true',
1408 - 'order' => 2
1512 + 'order' => 3
1409 1513 ],
1410 1514 [
1411 1515 'name' => 'overdue',
1412 1516 'label' => __('Overdue', 'fluent-boards'),
1413 1517 'visible' => 'true',
1414 - 'order' => 3
1518 + 'order' => 4
1415 1519 ],
1416 1520 [
1417 1521 'name' => 'mentioned',
1418 1522 'label' => __('Mentioned', 'fluent-boards'),
1419 1523 'visible' => 'true',
1420 - 'order' => 4
1524 + 'order' => 5
1421 1525 ],
1422 1526 [
1423 1527 'name' => 'completed',
1424 1528 'label' => __('Completed', 'fluent-boards'),
1425 1529 'visible' => 'true',
1426 - 'order' => 5
1530 + 'order' => 6
1427 1531 ],
1428 1532 [
1429 1533 'name' => 'others',
1430 1534 'label' => __('Others', 'fluent-boards'),
1431 1535 'visible' => 'true',
1432 - 'order' => 6
1536 + 'order' => 7
1433 1537 ]
1434 1538 ];
1435 1539 $availableTabNames = array_column($default_config, 'name');
1436 1540
@@ -1459,10 +1563,21 @@
1459 1563
1460 1564 if (!empty($missingTabs)) {
1461 1565 $newConfig = [];
1462 1566 $order = 1;
1567 + $addedDueToday = false;
1463 1568 $addedAssigned = false;
1464 1569 foreach ($config as $tab) {
1570 + if (!$addedDueToday) {
1571 + $dueTodayTab = array_filter($missingTabs, fn($t) => $t['name'] === 'due_today');
1572 + if (!empty($dueTodayTab)) {
1573 + $dueTodayTab = reset($dueTodayTab);
1574 + $dueTodayTab['order'] = $order++;
1575 + $newConfig[] = $dueTodayTab;
1576 + $addedDueToday = true;
1577 + }
1578 + }
1579 +
1465 1580 if ($tab['name'] === 'upcoming' && !$addedAssigned) {
1466 1581 $assignedTab = array_filter($missingTabs, fn($t) => $t['name'] === 'assigned');
1467 1582 if (!empty($assignedTab)) {
1468 1583 $assignedTab = reset($assignedTab);
@@ -1474,9 +1589,9 @@
1474 1589 $tab['order'] = $order++;
1475 1590 $newConfig[] = $tab;
1476 1591 }
1477 1592 foreach ($missingTabs as $missingTab) {
1478 - if ($missingTab['name'] !== 'assigned') {
1593 + if (!in_array($missingTab['name'], ['assigned', 'due_today'], true)) {
1479 1594 $missingTab['order'] = $order++;
1480 1595 $newConfig[] = $missingTab;
1481 1596 }
1482 1597 }
@@ -1491,8 +1606,9 @@
1491 1606 }
1492 1607
1493 1608 // Always apply fresh translations based on tab name
1494 1609 $labelMap = [
1610 + 'due_today' => __('Due Today', 'fluent-boards'),
1495 1611 'assigned' => __('Assigned', 'fluent-boards'),
1496 1612 'upcoming' => __('Upcoming', 'fluent-boards'),
1497 1613 'overdue' => __('Overdue', 'fluent-boards'),
1498 1614 'mentioned' => __('Mentioned', 'fluent-boards'),