PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.95.3
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.95.3
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
fluent-boards / app / Modules / MCP / Tools / TaskTools.php

TaskTools.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 1.95.3, at app/Modules/MCP/Tools/TaskTools.php

361 lines 12.7 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\Modules\MCP\Tools;
4
5 use FluentBoards\App\Models\Stage;
6 use FluentBoards\App\Models\Task;
7 use FluentBoards\App\Modules\MCP\Helpers\MCPHelper;
8 use FluentBoards\App\Services\Constant;
9 use FluentBoards\App\Services\TaskService;
10
11 /**
12 * Task read/write MCP tools.
13 */
14 class TaskTools
15 {
16 public static function canMoveTask($params = [])
17 {
18 $sourceBoardId = isset($params['board_id']) ? absint($params['board_id']) : 0;
19 $targetBoardId = !empty($params['target_board_id']) ? absint($params['target_board_id']) : $sourceBoardId;
20
21 if (!$sourceBoardId || !$targetBoardId) {
22 return false;
23 }
24
25 return MCPHelper::canWriteBoard($sourceBoardId) && MCPHelper::canWriteBoard($targetBoardId);
26 }
27
28 public static function listTasks($params = [])
29 {
30 $board = MCPHelper::resolveBoard($params);
31 if (is_wp_error($board)) {
32 return $board;
33 }
34
35 if (!MCPHelper::canReadBoard($board->id)) {
36 return MCPHelper::error('forbidden', __('You do not have access to this board', 'fluent-boards'));
37 }
38
39 $pagination = MCPHelper::normalizePagination($params);
40 $args = [
41 'page' => $pagination['page'],
42 'per_page' => $pagination['per_page'],
43 'sort_by' => !empty($params['sort_by']) ? sanitize_text_field($params['sort_by']) : 'position',
44 'sort_direction' => !empty($params['sort_direction']) ? sanitize_text_field($params['sort_direction']) : 'asc',
45 'search' => !empty($params['search']) ? sanitize_text_field($params['search']) : '',
46 'include_archived' => !empty($params['include_archived']),
47 'stage' => isset($params['stage']) ? (array) $params['stage'] : [],
48 'task_status' => isset($params['task_status']) ? (array) $params['task_status'] : [],
49 'priority' => isset($params['priority']) ? (array) $params['priority'] : [],
50 'assignee' => isset($params['assignee']) ? (array) $params['assignee'] : [],
51 'labels' => isset($params['labels']) ? (array) $params['labels'] : [],
52 'due_date' => isset($params['due_date']) ? (array) $params['due_date'] : [],
53 ];
54
55 $paginated = (new TaskService())->getTableTasks($board->id, $args);
56
57 return [
58 'items' => MCPHelper::formatTaskList($paginated->items()),
59 'pagination' => [
60 'total' => (int) $paginated->total(),
61 'current_page' => (int) $paginated->currentPage(),
62 'per_page' => (int) $paginated->perPage(),
63 'last_page' => (int) $paginated->lastPage(),
64 ],
65 ];
66 }
67
68 public static function getTask($params = [])
69 {
70 $task = MCPHelper::resolveTask($params);
71 if (is_wp_error($task)) {
72 return $task;
73 }
74
75 if (!MCPHelper::canReadBoard($task->board_id)) {
76 return MCPHelper::error('forbidden', __('You do not have access to this task', 'fluent-boards'));
77 }
78
79 return [
80 'task' => MCPHelper::formatTask($task),
81 ];
82 }
83
84 public static function createTask($params = [])
85 {
86 $board = MCPHelper::resolveBoard($params);
87 if (is_wp_error($board)) {
88 return $board;
89 }
90
91 if (!MCPHelper::canWriteBoard($board->id)) {
92 return MCPHelper::error('forbidden', __('You do not have permission to create tasks on this board', 'fluent-boards'));
93 }
94
95 $title = isset($params['title']) ? sanitize_text_field($params['title']) : '';
96 if ($title === '') {
97 return MCPHelper::error('invalid_param', __('Task title is required', 'fluent-boards'));
98 }
99
100 $stage = MCPHelper::assertStageBelongsToBoard($params['stage_id'] ?? 0, $board->id);
101 if (is_wp_error($stage)) {
102 return $stage;
103 }
104
105 $taskData = [
106 'title' => $title,
107 'board_id' => (int) $board->id,
108 'stage_id' => (int) $stage->id,
109 ];
110
111 if (!empty($params['priority'])) {
112 if (!in_array($params['priority'], ['low', 'medium', 'high'], true)) {
113 return MCPHelper::error('invalid_param', __('Invalid task priority', 'fluent-boards'), [
114 'allowed' => ['low', 'medium', 'high'],
115 ]);
116 }
117 $taskData['priority'] = sanitize_text_field($params['priority']);
118 }
119
120 foreach (['due_at', 'started_at'] as $field) {
121 if (!empty($params[$field])) {
122 $taskData[$field] = sanitize_text_field($params[$field]);
123 }
124 }
125
126 if (!empty($params['description'])) {
127 $taskData['description'] = wp_kses_post($params['description']);
128 }
129
130 if (!empty($params['crm_contact_id'])) {
131 $taskData['crm_contact_id'] = absint($params['crm_contact_id']);
132 }
133
134 $task = (new TaskService())->createTask($taskData, $board->id);
135
136 return [
137 'task' => MCPHelper::formatTaskSummary($task),
138 'message' => __('Task has been successfully created', 'fluent-boards'),
139 ];
140 }
141
142 public static function updateTask($params = [])
143 {
144 $task = MCPHelper::resolveTask($params);
145 if (is_wp_error($task)) {
146 return $task;
147 }
148
149 if (!MCPHelper::canWriteBoard($task->board_id)) {
150 return MCPHelper::error('forbidden', __('You do not have permission to update this task', 'fluent-boards'));
151 }
152
153 $service = new TaskService();
154 $updatable = [
155 'title' => 'text',
156 'description' => 'html',
157 'status' => 'text',
158 'priority' => 'nullable_text',
159 'due_at' => 'nullable_text',
160 'started_at' => 'nullable_text',
161 'crm_contact_id' => 'nullable_int',
162 'settings' => 'array',
163 ];
164
165 foreach ($updatable as $field => $type) {
166 if (!array_key_exists($field, $params)) {
167 continue;
168 }
169
170 $validationError = self::validateUpdateField($field, $params[$field]);
171 if (is_wp_error($validationError)) {
172 return $validationError;
173 }
174
175 $value = self::sanitizeUpdateValue($params[$field], $type);
176 $task = $service->updateTaskProperty($field, $value, $task);
177 }
178
179 if (array_key_exists('assignees', $params)) {
180 $task = self::syncAssignees($task, MCPHelper::sanitizeIdArray($params['assignees']));
181 }
182
183 MCPHelper::loadTaskDetails($task);
184
185 return [
186 'task' => MCPHelper::formatTask($task),
187 'message' => __('Task has been updated', 'fluent-boards'),
188 ];
189 }
190
191 public static function moveTask($params = [])
192 {
193 $task = MCPHelper::resolveTask($params);
194 if (is_wp_error($task)) {
195 return $task;
196 }
197
198 $sourceBoardId = (int) $task->board_id;
199 $targetBoardId = !empty($params['target_board_id']) ? absint($params['target_board_id']) : $sourceBoardId;
200 $targetStageId = !empty($params['target_stage_id']) ? absint($params['target_stage_id']) : 0;
201
202 if (!MCPHelper::canWriteBoard($sourceBoardId) || !MCPHelper::canWriteBoard($targetBoardId)) {
203 return MCPHelper::error('forbidden', __('You do not have permission to move this task', 'fluent-boards'));
204 }
205
206 $stage = MCPHelper::assertStageBelongsToBoard($targetStageId, $targetBoardId);
207 if (is_wp_error($stage)) {
208 return $stage;
209 }
210
211 $service = new TaskService();
212 $oldStageId = (int) $task->stage_id;
213
214 if ($targetBoardId !== $sourceBoardId) {
215 $task = $service->changeBoardByTask($task, $targetBoardId);
216 }
217
218 if ($oldStageId !== $targetStageId) {
219 \FluentBoards\App\Models\TaskMeta::where('task_id', $task->id)
220 ->where('key', Constant::META_KEY_ARCHIVED_BY_STAGE)
221 ->delete();
222 }
223
224 $task->stage_id = $targetStageId;
225 $task->save();
226
227 $previousTaskId = !empty($params['previous_task_id']) ? absint($params['previous_task_id']) : 0;
228 $nextTaskId = !empty($params['next_task_id']) ? absint($params['next_task_id']) : 0;
229 if ($previousTaskId || $nextTaskId) {
230 $task = $task->moveBetweenTasks($previousTaskId, $nextTaskId);
231 } else {
232 $position = !empty($params['position']) ? absint($params['position']) : 1;
233 $task = $task->moveToNewPosition($position);
234 }
235
236 if ($oldStageId !== $targetStageId) {
237 $service->manageDefaultAssignees($task, $targetStageId);
238
239 if ($stage->defaultTaskStatus() === 'closed' && $task->status !== 'closed') {
240 $task = $task->close();
241 } elseif ($stage->defaultTaskStatus() === 'open' && $task->status === 'closed') {
242 $task = $task->reopen();
243 }
244
245 do_action('fluent_boards/task_stage_updated', $task, $oldStageId);
246 }
247
248 do_action('fluent_boards/task_updated', $task, 'position');
249 MCPHelper::loadTaskDetails($task);
250
251 return [
252 'task' => MCPHelper::formatTask($task),
253 'message' => __('Task has been moved', 'fluent-boards'),
254 ];
255 }
256
257 public static function archiveTask($params = [])
258 {
259 $task = MCPHelper::resolveTask($params);
260 if (is_wp_error($task)) {
261 return $task;
262 }
263
264 if (!MCPHelper::canWriteBoard($task->board_id)) {
265 return MCPHelper::error('forbidden', __('You do not have permission to archive this task', 'fluent-boards'));
266 }
267
268 $archived = array_key_exists('archived', $params) ? (bool) $params['archived'] : true;
269 $value = $archived ? current_time('mysql') : null;
270
271 $task = (new TaskService())->updateTaskProperty('archived_at', $value, $task);
272 MCPHelper::loadTaskDetails($task);
273
274 return [
275 'task' => MCPHelper::formatTask($task),
276 'message' => $archived ? __('Task has been archived', 'fluent-boards') : __('Task has been restored', 'fluent-boards'),
277 ];
278 }
279
280 private static function sanitizeUpdateValue($value, $type)
281 {
282 if ($type === 'html') {
283 return wp_kses_post((string) $value);
284 }
285
286 if ($type === 'array') {
287 return is_array($value) ? self::sanitizeArray($value) : [];
288 }
289
290 if ($type === 'nullable_int') {
291 return $value === null || $value === '' ? null : absint($value);
292 }
293
294 if ($type === 'nullable_text') {
295 return $value === null ? null : sanitize_text_field((string) $value);
296 }
297
298 return sanitize_text_field((string) $value);
299 }
300
301 private static function validateUpdateField($field, $value)
302 {
303 if ($field === 'status' && !in_array($value, ['open', 'closed'], true)) {
304 return MCPHelper::error('invalid_param', __('Invalid task status', 'fluent-boards'), [
305 'allowed' => ['open', 'closed'],
306 ]);
307 }
308
309 if ($field === 'priority' && $value !== '' && $value !== null && !in_array($value, ['low', 'medium', 'high'], true)) {
310 return MCPHelper::error('invalid_param', __('Invalid task priority', 'fluent-boards'), [
311 'allowed' => ['low', 'medium', 'high'],
312 ]);
313 }
314
315 return true;
316 }
317
318 private static function sanitizeArray($value)
319 {
320 $sanitized = [];
321 foreach ((array) $value as $key => $item) {
322 $safeKey = sanitize_key((string) $key);
323 if ($safeKey === '') {
324 continue;
325 }
326
327 if (is_array($item)) {
328 $sanitized[$safeKey] = self::sanitizeArray($item);
329 } elseif (is_bool($item) || is_int($item) || is_float($item) || $item === null) {
330 $sanitized[$safeKey] = $item;
331 } else {
332 $sanitized[$safeKey] = sanitize_text_field((string) $item);
333 }
334 }
335
336 return $sanitized;
337 }
338
339 private static function syncAssignees($task, $newAssigneeIds)
340 {
341 $task->load('assignees');
342 $currentIds = [];
343 foreach ($task->assignees as $assignee) {
344 $currentIds[] = (int) $assignee->ID;
345 }
346
347 $service = new TaskService();
348 foreach (array_diff($currentIds, $newAssigneeIds) as $removeId) {
349 $service->updateAssignee($removeId, $task);
350 $task->load('assignees');
351 }
352
353 foreach (array_diff($newAssigneeIds, $currentIds) as $addId) {
354 $service->updateAssignee($addId, $task);
355 $task->load('assignees');
356 }
357
358 return $task;
359 }
360 }
361