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 / Modules / MCP / Helpers / MCPHelper.php

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

407 lines 14.8 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 ?? [], $board->id);
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 ?? [], $task->board_id),
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 ?? [], $task->board_id);
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 'color_preset' => \FluentBoards\Framework\Support\Arr::get(
270 (array) $label->settings,
271 \FluentBoards\App\Services\Constant::LABEL_COLOR_PRESET_SETTING
272 ),
273 'position' => isset($label->position) ? (float) $label->position : null,
274 'archived_at' => self::toIso8601($label->archived_at),
275 ];
276 }
277 return $items;
278 }
279
280 public static function formatUserList($users, $boardId)
281 {
282 $items = [];
283 foreach ($users as $user) {
284 $name = trim((string) ($user->display_name ?? ''));
285 if (!$name) {
286 $name = trim((string) (($user->first_name ?? '') . ' ' . ($user->last_name ?? '')));
287 }
288
289 $items[] = [
290 'id' => isset($user->ID) ? (int) $user->ID : (int) ($user->id ?? 0),
291 'display_name' => $name,
292 'email' => $user->user_email ?? '',
293 'avatar' => '',
294 ];
295 }
296 // Reuse the board permission lookup across user lists in this request.
297 static $boardManagerResults = [];
298 $isBoardManager = null;
299 if (!current_user_can('list_users')) {
300 $cacheKey = get_current_blog_id() . ':' . get_current_user_id() . ':' . (int) $boardId;
301 if (!array_key_exists($cacheKey, $boardManagerResults)) {
302 $boardManagerResults[$cacheKey] = PermissionManager::isBoardManager($boardId);
303 }
304 $isBoardManager = $boardManagerResults[$cacheKey];
305 }
306 $sanitizedItems = Helper::sanitizeUsersArray($items, $boardId, $isBoardManager);
307 foreach ($sanitizedItems as $index => &$item) {
308 // Only generate avatars when the policy permits disclosure of the email.
309 if ($item['email'] !== '' && $item['email'] === $items[$index]['email']) {
310 $item['avatar'] = fluent_boards_user_avatar($item['email'], $item['display_name']);
311 }
312 }
313 unset($item);
314
315 return $sanitizedItems;
316 }
317
318 public static function formatCommentList($comments)
319 {
320 $items = [];
321 foreach ($comments as $comment) {
322 $items[] = [
323 'id' => (int) $comment->id,
324 'task_id' => (int) $comment->task_id,
325 'parent_id' => $comment->parent_id ? (int) $comment->parent_id : null,
326 'type' => $comment->type,
327 'privacy' => $comment->privacy,
328 'status' => $comment->status,
329 'author_name' => $comment->author_name,
330 'description' => $comment->description,
331 'created_by' => $comment->created_by ? (int) $comment->created_by : null,
332 'created_at' => self::toIso8601($comment->created_at),
333 ];
334 }
335 return $items;
336 }
337
338 public static function formatActivityList($activities)
339 {
340 $items = [];
341 foreach ($activities as $activity) {
342 $items[] = [
343 'id' => (int) $activity->id,
344 'object_id' => isset($activity->object_id) ? (int) $activity->object_id : null,
345 'object_type' => $activity->object_type ?? '',
346 'action' => $activity->action ?? '',
347 'description' => $activity->description ?? '',
348 'created_by' => isset($activity->created_by) ? (int) $activity->created_by : null,
349 'created_at' => self::toIso8601($activity->created_at ?? null),
350 ];
351 }
352 return $items;
353 }
354
355 public static function sanitizeIdArray($values)
356 {
357 return array_values(array_filter(array_map('absint', (array) $values)));
358 }
359
360 private static function limitItems($items, $limit)
361 {
362 if (is_array($items)) {
363 return array_slice($items, 0, $limit);
364 }
365
366 $limited = [];
367 foreach ($items as $item) {
368 if (count($limited) >= $limit) {
369 break;
370 }
371
372 $limited[] = $item;
373 }
374
375 return $limited;
376 }
377
378 public static function currentUser()
379 {
380 $userId = get_current_user_id();
381 $user = $userId ? get_user_by('ID', $userId) : null;
382
383 return [
384 'wp_user_id' => (int) $userId,
385 'name' => $user ? $user->display_name : null,
386 'email' => $user ? $user->user_email : null,
387 'is_wp_admin' => $user ? user_can($user, 'manage_options') : false,
388 'is_fluent_boards_admin' => PermissionManager::isAdmin($userId),
389 'can_create_boards' => PermissionManager::userHasBoardCreationPermission($userId),
390 ];
391 }
392
393 public static function toIso8601($value)
394 {
395 if (!$value) {
396 return null;
397 }
398
399 $timestamp = strtotime((string) $value);
400 if (!$timestamp) {
401 return (string) $value;
402 }
403
404 return date('c', $timestamp);
405 }
406 }
407