PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.0.4
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.0.4
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 / Modules / MCP / Helpers / MCPHelper.php

MCPHelper.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.0.4, at app/Modules/MCP/Helpers/MCPHelper.php

384 lines 13.6 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\Helpers;
4
5 use FluentBoards\App\Models\Board;
6 use FluentBoards\App\Models\Stage;
7 use FluentBoards\App\Models\Task;
8 use FluentBoards\App\Services\Constant;
9 use FluentBoards\App\Services\DescriptionMarkdownConverter;
10 use FluentBoards\App\Services\Helper;
11 use FluentBoards\App\Services\PermissionManager;
12
13 /**
14 * Shared utilities for Fluent Boards MCP tools.
15 */
16 class MCPHelper
17 {
18 const TASK_HISTORY_LIMIT = 20;
19 const MAX_MARKDOWN_DESCRIPTION_LENGTH = 65535;
20
21 public static function error($code, $message, $data = [])
22 {
23 return new \WP_Error($code, $message, $data);
24 }
25
26 public static function normalizePagination($params, $defaultPerPage = 20, $maxPerPage = 100)
27 {
28 $page = isset($params['page']) ? absint($params['page']) : 1;
29 $perPage = isset($params['per_page']) ? absint($params['per_page']) : $defaultPerPage;
30
31 return [
32 'page' => max(1, $page),
33 'per_page' => max(1, min($maxPerPage, $perPage)),
34 ];
35 }
36
37 public static function sanitizeMarkdown($value)
38 {
39 $value = wp_check_invalid_utf8((string) $value);
40 $value = str_replace(["\r\n", "\r", "\0"], ["\n", "\n", ""], $value);
41 $value = self::descriptionToMarkdown($value);
42
43 if (strlen($value) <= self::MAX_MARKDOWN_DESCRIPTION_LENGTH) {
44 return $value;
45 }
46
47 if (function_exists('mb_strcut')) {
48 return mb_strcut($value, 0, self::MAX_MARKDOWN_DESCRIPTION_LENGTH);
49 }
50
51 return substr($value, 0, self::MAX_MARKDOWN_DESCRIPTION_LENGTH);
52 }
53
54 public static function resolveBoard($params)
55 {
56 $boardId = isset($params['board_id']) ? absint($params['board_id']) : 0;
57 if (!$boardId) {
58 return self::error('invalid_param', __('Provide board_id', 'fluent-boards'));
59 }
60
61 $board = Board::with(['stages', 'labels', 'users'])->find($boardId);
62 if (!$board) {
63 return self::error('not_found', __('Board not found', 'fluent-boards'), ['board_id' => $boardId]);
64 }
65
66 return $board;
67 }
68
69 public static function resolveTask($params)
70 {
71 $taskId = isset($params['task_id']) ? absint($params['task_id']) : 0;
72 $boardId = isset($params['board_id']) ? absint($params['board_id']) : 0;
73
74 if (!$taskId || !$boardId) {
75 return self::error('invalid_param', __('Provide board_id and task_id', 'fluent-boards'));
76 }
77
78 $task = Task::with(self::taskDetailRelations())->find($taskId);
79 if (!$task || (int) $task->board_id !== $boardId) {
80 return self::error('not_found', __('Task not found on this board', 'fluent-boards'), [
81 'board_id' => $boardId,
82 'task_id' => $taskId,
83 ]);
84 }
85
86 return $task;
87 }
88
89 public static function loadTaskDetails($task)
90 {
91 return $task->load(self::taskDetailRelations());
92 }
93
94 public static function taskDetailRelations()
95 {
96 return [
97 'board',
98 'stage',
99 'labels',
100 'assignees',
101 'watchers',
102 'comments' => function ($query) {
103 $query->without(['images', 'replies'])
104 ->orderBy('id', 'DESC')
105 ->limit(self::TASK_HISTORY_LIMIT);
106 },
107 'activities' => function ($query) {
108 $query->limit(self::TASK_HISTORY_LIMIT);
109 },
110 ];
111 }
112
113 public static function canReadBoard($boardId)
114 {
115 return PermissionManager::userHasBoardPermission(absint($boardId), 'GET');
116 }
117
118 public static function canWriteBoard($boardId)
119 {
120 return PermissionManager::userHasBoardPermission(absint($boardId), 'POST');
121 }
122
123 public static function assertStageBelongsToBoard($stageId, $boardId)
124 {
125 $stage = Stage::where('id', absint($stageId))
126 ->where('board_id', absint($boardId))
127 ->whereNull('archived_at')
128 ->first();
129
130 if (!$stage) {
131 return self::error('not_found', __('Stage not found on this board', 'fluent-boards'), [
132 'board_id' => absint($boardId),
133 'stage_id' => absint($stageId),
134 ]);
135 }
136
137 return $stage;
138 }
139
140 public static function formatBoardSummary($board)
141 {
142 return [
143 'id' => (int) $board->id,
144 'title' => $board->title,
145 'description' => self::descriptionToMarkdown($board->description),
146 'type' => $board->type,
147 'currency' => $board->currency,
148 'created_by' => (int) $board->created_by,
149 'archived_at' => self::toIso8601($board->archived_at),
150 'created_at' => self::toIso8601($board->created_at),
151 'updated_at' => self::toIso8601($board->updated_at),
152 'stages_count' => isset($board->stages) ? count($board->stages) : 0,
153 'labels_count' => isset($board->labels) ? count($board->labels) : 0,
154 'members_count' => isset($board->users) ? count($board->users) : 0,
155 ];
156 }
157
158 public static function formatBoard($board, $includeTasks = false)
159 {
160 $data = self::formatBoardSummary($board);
161 $data['stages'] = self::formatStageList($board->stages ?? []);
162 $data['labels'] = self::formatLabelList($board->labels ?? []);
163 $data['members'] = self::formatUserList($board->users ?? []);
164
165 if ($includeTasks) {
166 $tasks = Task::with(['stage', 'labels', 'assignees'])
167 ->where('board_id', $board->id)
168 ->whereNull('parent_id')
169 ->whereNull('archived_at')
170 ->orderBy('stage_id', 'asc')
171 ->orderBy('position', 'asc')
172 ->limit(100)
173 ->get();
174 $data['tasks'] = self::formatTaskList($tasks);
175 $data['tasks_limited_to'] = 100;
176 }
177
178 return $data;
179 }
180
181 public static function formatTaskSummary($task)
182 {
183 return [
184 'id' => (int) $task->id,
185 'title' => $task->title,
186 'slug' => $task->slug,
187 'board_id' => (int) $task->board_id,
188 'stage_id' => (int) $task->stage_id,
189 'parent_id' => $task->parent_id ? (int) $task->parent_id : null,
190 'status' => $task->status,
191 'priority' => $task->priority,
192 'position' => isset($task->position) ? (float) $task->position : null,
193 'crm_contact_id' => $task->crm_contact_id ? (int) $task->crm_contact_id : null,
194 'comments_count' => isset($task->comments_count) ? (int) $task->comments_count : 0,
195 'due_at' => self::toIso8601($task->due_at),
196 'started_at' => self::toIso8601($task->started_at),
197 'last_completed_at' => self::toIso8601($task->last_completed_at),
198 'archived_at' => self::toIso8601($task->archived_at),
199 'created_at' => self::toIso8601($task->created_at),
200 'updated_at' => self::toIso8601($task->updated_at),
201 'stage' => $task->stage ? self::formatStage($task->stage) : null,
202 'labels' => self::formatLabelList($task->labels ?? []),
203 'assignees' => self::formatUserList($task->assignees ?? []),
204 ];
205 }
206
207 public static function formatTask($task)
208 {
209 $data = self::formatTaskSummary($task);
210 $data['description'] = self::descriptionToMarkdown($task->description);
211 $data['settings'] = $task->settings;
212 $data['board'] = $task->board ? self::formatBoardSummary($task->board) : null;
213 $data['watchers'] = self::formatUserList($task->watchers ?? []);
214 $data['comments'] = self::formatCommentList(self::limitItems($task->comments ?? [], self::TASK_HISTORY_LIMIT));
215 $data['comments_limited_to'] = self::TASK_HISTORY_LIMIT;
216 $data['activities'] = self::formatActivityList(self::limitItems($task->activities ?? [], self::TASK_HISTORY_LIMIT));
217 $data['activities_limited_to'] = self::TASK_HISTORY_LIMIT;
218
219 return $data;
220 }
221
222 public static function descriptionToMarkdown($description)
223 {
224 return DescriptionMarkdownConverter::normalize($description);
225 }
226
227 public static function formatTaskList($tasks)
228 {
229 $items = [];
230 foreach ($tasks as $task) {
231 $items[] = self::formatTaskSummary($task);
232 }
233 return $items;
234 }
235
236 public static function formatStage($stage)
237 {
238 return [
239 'id' => (int) $stage->id,
240 'board_id' => (int) $stage->board_id,
241 'title' => $stage->title,
242 'slug' => $stage->slug,
243 'position' => isset($stage->position) ? (float) $stage->position : null,
244 'default_task_status' => method_exists($stage, 'defaultTaskStatus') ? $stage->defaultTaskStatus() : 'open',
245 'archived_at' => self::toIso8601($stage->archived_at),
246 ];
247 }
248
249 public static function formatStageList($stages)
250 {
251 $items = [];
252 foreach ($stages as $stage) {
253 $items[] = self::formatStage($stage);
254 }
255 return $items;
256 }
257
258 public static function formatLabelList($labels)
259 {
260 $items = [];
261 foreach ($labels as $label) {
262 $items[] = [
263 'id' => (int) $label->id,
264 'board_id' => (int) $label->board_id,
265 'title' => $label->title,
266 'slug' => $label->slug,
267 'color' => $label->color,
268 'bg_color' => $label->bg_color,
269 'position' => isset($label->position) ? (float) $label->position : null,
270 'archived_at' => self::toIso8601($label->archived_at),
271 ];
272 }
273 return $items;
274 }
275
276 public static function formatUserList($users)
277 {
278 $items = [];
279 foreach ($users as $user) {
280 $name = trim((string) ($user->display_name ?? ''));
281 if (!$name) {
282 $name = trim((string) (($user->first_name ?? '') . ' ' . ($user->last_name ?? '')));
283 }
284
285 $items[] = [
286 'id' => isset($user->ID) ? (int) $user->ID : (int) ($user->id ?? 0),
287 'display_name' => $name,
288 'email' => $user->user_email ?? '',
289 'avatar' => !empty($user->user_email) ? fluent_boards_user_avatar($user->user_email, $name) : '',
290 ];
291 }
292 return $items;
293 }
294
295 public static function formatCommentList($comments)
296 {
297 $items = [];
298 foreach ($comments as $comment) {
299 $items[] = [
300 'id' => (int) $comment->id,
301 'task_id' => (int) $comment->task_id,
302 'parent_id' => $comment->parent_id ? (int) $comment->parent_id : null,
303 'type' => $comment->type,
304 'privacy' => $comment->privacy,
305 'status' => $comment->status,
306 'author_name' => $comment->author_name,
307 'description' => $comment->description,
308 'created_by' => $comment->created_by ? (int) $comment->created_by : null,
309 'created_at' => self::toIso8601($comment->created_at),
310 ];
311 }
312 return $items;
313 }
314
315 public static function formatActivityList($activities)
316 {
317 $items = [];
318 foreach ($activities as $activity) {
319 $items[] = [
320 'id' => (int) $activity->id,
321 'object_id' => isset($activity->object_id) ? (int) $activity->object_id : null,
322 'object_type' => $activity->object_type ?? '',
323 'action' => $activity->action ?? '',
324 'description' => $activity->description ?? '',
325 'created_by' => isset($activity->created_by) ? (int) $activity->created_by : null,
326 'created_at' => self::toIso8601($activity->created_at ?? null),
327 ];
328 }
329 return $items;
330 }
331
332 public static function sanitizeIdArray($values)
333 {
334 return array_values(array_filter(array_map('absint', (array) $values)));
335 }
336
337 private static function limitItems($items, $limit)
338 {
339 if (is_array($items)) {
340 return array_slice($items, 0, $limit);
341 }
342
343 $limited = [];
344 foreach ($items as $item) {
345 if (count($limited) >= $limit) {
346 break;
347 }
348
349 $limited[] = $item;
350 }
351
352 return $limited;
353 }
354
355 public static function currentUser()
356 {
357 $userId = get_current_user_id();
358 $user = $userId ? get_user_by('ID', $userId) : null;
359
360 return [
361 'wp_user_id' => (int) $userId,
362 'name' => $user ? $user->display_name : null,
363 'email' => $user ? $user->user_email : null,
364 'is_wp_admin' => $user ? user_can($user, 'manage_options') : false,
365 'is_fluent_boards_admin' => PermissionManager::isAdmin($userId),
366 'can_create_boards' => PermissionManager::userHasBoardCreationPermission($userId),
367 ];
368 }
369
370 public static function toIso8601($value)
371 {
372 if (!$value) {
373 return null;
374 }
375
376 $timestamp = strtotime((string) $value);
377 if (!$timestamp) {
378 return (string) $value;
379 }
380
381 return date('c', $timestamp);
382 }
383 }
384