| @@ -5,8 +5,9 @@ | ||
| 5 | 5 | use FluentBoards\App\Models\Board; |
| 6 | 6 | use FluentBoards\App\Models\Stage; |
| 7 | 7 | use FluentBoards\App\Models\Task; |
| 8 | 8 | use FluentBoards\App\Services\Constant; |
| 9 | +use FluentBoards\App\Services\DescriptionMarkdownConverter; | |
| 9 | 10 | use FluentBoards\App\Services\Helper; |
| 10 | 11 | use FluentBoards\App\Services\PermissionManager; |
| 11 | 12 | |
| 12 | 13 | /** |
| @@ -14,8 +15,9 @@ | ||
| 14 | 15 | */ |
| 15 | 16 | class MCPHelper |
| 16 | 17 | { |
| 17 | 18 | const TASK_HISTORY_LIMIT = 20; |
| 19 | + const MAX_MARKDOWN_DESCRIPTION_LENGTH = 65535; | |
| 18 | 20 | |
| 19 | 21 | public static function error($code, $message, $data = []) |
| 20 | 22 | { |
| 21 | 23 | return new \WP_Error($code, $message, $data); |
| @@ -31,8 +33,25 @@ | ||
| 31 | 33 | 'per_page' => max(1, min($maxPerPage, $perPage)), |
| 32 | 34 | ]; |
| 33 | 35 | } |
| 34 | 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 | + | |
| 35 | 54 | public static function resolveBoard($params) |
| 36 | 55 | { |
| 37 | 56 | $boardId = isset($params['board_id']) ? absint($params['board_id']) : 0; |
| 38 | 57 | if (!$boardId) { |
| @@ -122,9 +141,9 @@ | ||
| 122 | 141 | { |
| 123 | 142 | return [ |
| 124 | 143 | 'id' => (int) $board->id, |
| 125 | 144 | 'title' => $board->title, |
| 126 | - 'description' => $board->description, | |
| 145 | + 'description' => self::descriptionToMarkdown($board->description), | |
| 127 | 146 | 'type' => $board->type, |
| 128 | 147 | 'currency' => $board->currency, |
| 129 | 148 | 'created_by' => (int) $board->created_by, |
| 130 | 149 | 'archived_at' => self::toIso8601($board->archived_at), |
| @@ -140,9 +159,9 @@ | ||
| 140 | 159 | { |
| 141 | 160 | $data = self::formatBoardSummary($board); |
| 142 | 161 | $data['stages'] = self::formatStageList($board->stages ?? []); |
| 143 | 162 | $data['labels'] = self::formatLabelList($board->labels ?? []); |
| 144 | - $data['members'] = self::formatUserList($board->users ?? []); | |
| 163 | + $data['members'] = self::formatUserList($board->users ?? [], $board->id); | |
| 145 | 164 | |
| 146 | 165 | if ($includeTasks) { |
| 147 | 166 | $tasks = Task::with(['stage', 'labels', 'assignees']) |
| 148 | 167 | ->where('board_id', $board->id) |
| @@ -180,9 +199,9 @@ | ||
| 180 | 199 | 'created_at' => self::toIso8601($task->created_at), |
| 181 | 200 | 'updated_at' => self::toIso8601($task->updated_at), |
| 182 | 201 | 'stage' => $task->stage ? self::formatStage($task->stage) : null, |
| 183 | 202 | 'labels' => self::formatLabelList($task->labels ?? []), |
| 184 | - 'assignees' => self::formatUserList($task->assignees ?? []), | |
| 203 | + 'assignees' => self::formatUserList($task->assignees ?? [], $task->board_id), | |
| 185 | 204 | ]; |
| 186 | 205 | } |
| 187 | 206 | |
| 188 | 207 | public static function formatTask($task) |
| @@ -187,12 +206,12 @@ | ||
| 187 | 206 | |
| 188 | 207 | public static function formatTask($task) |
| 189 | 208 | { |
| 190 | 209 | $data = self::formatTaskSummary($task); |
| 191 | - $data['description'] = $task->description; | |
| 210 | + $data['description'] = self::descriptionToMarkdown($task->description); | |
| 192 | 211 | $data['settings'] = $task->settings; |
| 193 | 212 | $data['board'] = $task->board ? self::formatBoardSummary($task->board) : null; |
| 194 | - $data['watchers'] = self::formatUserList($task->watchers ?? []); | |
| 213 | + $data['watchers'] = self::formatUserList($task->watchers ?? [], $task->board_id); | |
| 195 | 214 | $data['comments'] = self::formatCommentList(self::limitItems($task->comments ?? [], self::TASK_HISTORY_LIMIT)); |
| 196 | 215 | $data['comments_limited_to'] = self::TASK_HISTORY_LIMIT; |
| 197 | 216 | $data['activities'] = self::formatActivityList(self::limitItems($task->activities ?? [], self::TASK_HISTORY_LIMIT)); |
| 198 | 217 | $data['activities_limited_to'] = self::TASK_HISTORY_LIMIT; |
| @@ -199,8 +218,13 @@ | ||
| 199 | 218 | |
| 200 | 219 | return $data; |
| 201 | 220 | } |
| 202 | 221 | |
| 222 | + public static function descriptionToMarkdown($description) | |
| 223 | + { | |
| 224 | + return DescriptionMarkdownConverter::normalize($description); | |
| 225 | + } | |
| 226 | + | |
| 203 | 227 | public static function formatTaskList($tasks) |
| 204 | 228 | { |
| 205 | 229 | $items = []; |
| 206 | 230 | foreach ($tasks as $task) { |
| @@ -241,8 +265,12 @@ | ||
| 241 | 265 | 'title' => $label->title, |
| 242 | 266 | 'slug' => $label->slug, |
| 243 | 267 | 'color' => $label->color, |
| 244 | 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 | + ), | |
| 245 | 273 | 'position' => isset($label->position) ? (float) $label->position : null, |
| 246 | 274 | 'archived_at' => self::toIso8601($label->archived_at), |
| 247 | 275 | ]; |
| 248 | 276 | } |
| @@ -248,9 +276,9 @@ | ||
| 248 | 276 | } |
| 249 | 277 | return $items; |
| 250 | 278 | } |
| 251 | 279 | |
| 252 | - public static function formatUserList($users) | |
| 280 | + public static function formatUserList($users, $boardId) | |
| 253 | 281 | { |
| 254 | 282 | $items = []; |
| 255 | 283 | foreach ($users as $user) { |
| 256 | 284 | $name = trim((string) ($user->display_name ?? '')); |
| @@ -261,12 +289,31 @@ | ||
| 261 | 289 | $items[] = [ |
| 262 | 290 | 'id' => isset($user->ID) ? (int) $user->ID : (int) ($user->id ?? 0), |
| 263 | 291 | 'display_name' => $name, |
| 264 | 292 | 'email' => $user->user_email ?? '', |
| 265 | - 'avatar' => !empty($user->user_email) ? fluent_boards_user_avatar($user->user_email, $name) : '', | |
| 293 | + 'avatar' => '', | |
| 266 | 294 | ]; |
| 267 | 295 | } |
| 268 | - return $items; | |
| 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; | |
| 269 | 316 | } |
| 270 | 317 | |
| 271 | 318 | public static function formatCommentList($comments) |
| 272 | 319 | { |