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

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