| @@ -62,8 +62,17 @@ | ||
| 62 | 62 | $type = $request->getSafe('type', 'sanitize_text_field', 'to-do'); |
| 63 | 63 | |
| 64 | 64 | $order = $request->getSafe('order', 'sanitize_text_field', 'created_at'); |
| 65 | 65 | $orderBy = $request->getSafe('orderBy', 'sanitize_text_field', 'DESC'); |
| 66 | + $order = is_string($order) ? sanitize_sql_orderby($order) : false; | |
| 67 | + if (!in_array($order, ['created_at', 'title', 'updated_at', 'id'], true)) { | |
| 68 | + $order = 'created_at'; | |
| 69 | + } | |
| 70 | + $orderBy = is_string($orderBy) ? strtoupper($orderBy) : 'DESC'; | |
| 71 | + if (!in_array($orderBy, ['ASC', 'DESC'], true)) { | |
| 72 | + $orderBy = 'DESC'; | |
| 73 | + } | |
| 74 | + | |
| 66 | 75 | $searchInput = $request->getSafe('searchInput', 'sanitize_text_field'); |
| 67 | 76 | |
| 68 | 77 | $option = $request->getSafe('option', 'sanitize_text_field'); |
| 69 | 78 | $folderId = $request->getSafe('fid', 'intval'); // Get folder ID from request |
| @@ -85,10 +94,10 @@ | ||
| 85 | 94 | |
| 86 | 95 | // Scope pinned before pagination, from the same id source as getBoardCounts(), |
| 87 | 96 | // so the pinned page and board_counts.pinned always agree. Filtering pinned |
| 88 | 97 | // after pagination would drop pinned boards that fall on a later active page. |
| 98 | + $pinnedIds = $this->boardService->getPinnedBoardIds(); | |
| 89 | 99 | if ($option == 'pinned') { |
| 90 | - $pinnedIds = $this->boardService->getPinnedBoardIds(); | |
| 91 | 100 | |
| 92 | 101 | $relatedBoardsQuery = $pinnedIds |
| 93 | 102 | ? $relatedBoardsQuery->whereIn('id', $pinnedIds) |
| 94 | 103 | : $relatedBoardsQuery->where('id', 0); |
| @@ -110,15 +119,15 @@ | ||
| 110 | 119 | } |
| 111 | 120 | |
| 112 | 121 | $relatedBoards = $relatedBoardsQuery->orderBy($order, $orderBy) |
| 113 | 122 | ->withCount('completedTasks') |
| 114 | - ->with('stages', 'users') | |
| 123 | + ->with('stages', 'users', 'boardMeta', 'viewerMembership') | |
| 115 | 124 | ->paginate($per_page); |
| 116 | 125 | |
| 117 | 126 | foreach ($relatedBoards as $relatedBoard) { |
| 118 | 127 | $relatedBoard->description = DescriptionMarkdownConverter::normalize($relatedBoard->description); |
| 119 | 128 | $relatedBoard->users = Helper::sanitizeUserCollections($relatedBoard->users); |
| 120 | - $relatedBoard->is_pinned = $this->boardService->isPinned($relatedBoard->id); | |
| 129 | + $relatedBoard->is_pinned = in_array((int) $relatedBoard->id, $pinnedIds, true); | |
| 121 | 130 | } |
| 122 | 131 | |
| 123 | 132 | $response = [ |
| 124 | 133 | 'boards' => $relatedBoards, |
| @@ -228,9 +237,12 @@ | ||
| 228 | 237 | { |
| 229 | 238 | $boards = $this->boardService->getRecentBoards(); |
| 230 | 239 | |
| 231 | 240 | if (!$boards || $boards->isEmpty()) { |
| 232 | - $boards = Board::where('type', 'to-do')->excludeTemplates()->byAccessUser(get_current_user_id()) | |
| 241 | + $boards = Board::whereNull('archived_at') | |
| 242 | + ->excludeTemplates() | |
| 243 | + ->availableInCurrentInstall() | |
| 244 | + ->byAccessUser(get_current_user_id()) | |
| 233 | 245 | ->limit(4) |
| 234 | 246 | ->withCount('completedTasks') |
| 235 | 247 | ->with(['stages', 'users']) |
| 236 | 248 | ->get(); |
| @@ -347,8 +359,10 @@ | ||
| 347 | 359 | if (!empty($backgroundData)) { |
| 348 | 360 | $boardData['background'] = $backgroundData; |
| 349 | 361 | } |
| 350 | 362 | |
| 363 | + $this->validateRequestedLabelPresets($request->get('labels')); | |
| 364 | + | |
| 351 | 365 | $board = $this->boardService->createBoard($boardData); |
| 352 | 366 | $this->createBoardLabelsFromRequest($request, $board->id); |
| 353 | 367 | $this->addBoardMembersFromRequest($request, $board->id); |
| 354 | 368 | $type = ucfirst($boardData['type']); |
| @@ -442,20 +456,53 @@ | ||
| 442 | 456 | |
| 443 | 457 | foreach ($labels as $label) { |
| 444 | 458 | $labelData = Helper::sanitizeLabel((array) $label); |
| 445 | 459 | |
| 446 | - if (empty($labelData['label']) && empty($labelData['bg_color'])) { | |
| 460 | + if (empty($labelData['label']) && empty($labelData['bg_color']) && empty($labelData['color_preset'])) { | |
| 447 | 461 | continue; |
| 448 | 462 | } |
| 449 | 463 | |
| 450 | - $this->labelService->createLabel([ | |
| 464 | + $labelPayload = [ | |
| 451 | 465 | 'label' => $labelData['label'] ?? '', |
| 452 | 466 | 'bg_color' => $labelData['bg_color'] ?? '#f3f4f6', |
| 453 | 467 | 'color' => $labelData['color'] ?? '#1B2533', |
| 454 | - ], $boardId); | |
| 468 | + ]; | |
| 469 | + | |
| 470 | + if (array_key_exists('color_preset', $labelData)) { | |
| 471 | + $labelPayload['color_preset'] = $labelData['color_preset']; | |
| 472 | + } | |
| 473 | + | |
| 474 | + $this->labelService->createLabel($labelPayload, $boardId); | |
| 455 | 475 | } |
| 456 | 476 | } |
| 457 | 477 | |
| 478 | + /** | |
| 479 | + * Reject unsupported label preset ids before creating any board records. | |
| 480 | + * | |
| 481 | + * @param mixed $labels | |
| 482 | + * @return void | |
| 483 | + * @throws \Exception | |
| 484 | + */ | |
| 485 | + private function validateRequestedLabelPresets($labels) | |
| 486 | + { | |
| 487 | + if (!is_array($labels)) { | |
| 488 | + return; | |
| 489 | + } | |
| 490 | + | |
| 491 | + foreach ($labels as $label) { | |
| 492 | + $labelData = Helper::sanitizeLabel((array) $label); | |
| 493 | + $presetId = $labelData[Constant::LABEL_COLOR_PRESET_SETTING] ?? null; | |
| 494 | + | |
| 495 | + if ($presetId === null || $presetId === '') { | |
| 496 | + continue; | |
| 497 | + } | |
| 498 | + | |
| 499 | + if (!is_string($presetId) || !Constant::getLabelColorPreset($presetId)) { | |
| 500 | + throw new \Exception(esc_html__('Invalid label color preset', 'fluent-boards')); | |
| 501 | + } | |
| 502 | + } | |
| 503 | + } | |
| 504 | + | |
| 458 | 505 | private function addBoardMembersFromRequest(Request $request, $boardId) |
| 459 | 506 | { |
| 460 | 507 | $memberIds = $request->get('member_ids'); |
| 461 | 508 | |
| @@ -497,10 +544,12 @@ | ||
| 497 | 544 | return $this->sendError($e->getMessage(), 404); |
| 498 | 545 | } |
| 499 | 546 | } |
| 500 | 547 | |
| 548 | + /** Return board details with a cursor covering changes made during retrieval. */ | |
| 501 | 549 | public function find(Request $request, $board_id) |
| 502 | 550 | { |
| 551 | + $syncStartedAt = current_time('mysql'); | |
| 503 | 552 | $board = Board::findOrFail($board_id); |
| 504 | 553 | $board->description = DescriptionMarkdownConverter::normalize($board->description); |
| 505 | 554 | $includeArchived = filter_var($request->get('include_archived', false), FILTER_VALIDATE_BOOLEAN); |
| 506 | 555 | $board->background = maybe_unserialize($board->background); |
| @@ -529,8 +578,9 @@ | ||
| 529 | 578 | $this->boardService->updateRecentBoards($board_id); |
| 530 | 579 | |
| 531 | 580 | $board->labelColor = Constant::TRELLO_COLOR_MAP; |
| 532 | 581 | $board->labelColorText = Constant::TEXT_COLOR_MAP; |
| 582 | + $board->labelColorPresets = Constant::LABEL_COLOR_PRESETS; | |
| 533 | 583 | |
| 534 | 584 | $board->users = Helper::sanitizeUserCollections($board->users); |
| 535 | 585 | $board->owner = Helper::sanitizeUserCollections($board->owner); |
| 536 | 586 | |
| @@ -539,14 +589,24 @@ | ||
| 539 | 589 | $board = apply_filters('fluent_boards/board_find', $board); |
| 540 | 590 | |
| 541 | 591 | return [ |
| 542 | 592 | 'board' => $board, |
| 543 | - 'synced_at' => current_time('mysql') | |
| 593 | + 'synced_at' => $syncStartedAt | |
| 544 | 594 | ]; |
| 545 | 595 | } |
| 546 | 596 | |
| 547 | 597 | public function update(Request $request, $board_id) |
| 548 | 598 | { |
| 599 | + // Board identity (title/description) is manager-only. This action shares the | |
| 600 | + // `update` name with CommentController@update under the same policy group, so the | |
| 601 | + // guard lives here rather than in a SingleBoardPolicy::update() method that would | |
| 602 | + // also block ordinary members from editing their own comments. | |
| 603 | + if (!PermissionManager::isBoardManager(absint($board_id))) { | |
| 604 | + return $this->sendError([ | |
| 605 | + 'message' => __('You do not have permission to edit this board.', 'fluent-boards'), | |
| 606 | + ], 403); | |
| 607 | + } | |
| 608 | + | |
| 549 | 609 | $boardData = $this->boardSanitizeAndValidate($request->only(['title', 'description']), [ |
| 550 | 610 | 'title' => 'required|string', |
| 551 | 611 | 'description' => 'nullable|string', |
| 552 | 612 | ]); |
| @@ -709,9 +769,8 @@ | ||
| 709 | 769 | |
| 710 | 770 | $formattedUsers[] = [ |
| 711 | 771 | 'ID' => $user->ID, |
| 712 | 772 | 'display_name' => $name, |
| 713 | - 'user_login' => $user->user_login, | |
| 714 | 773 | 'email' => $user->user_email, |
| 715 | 774 | 'photo' => fluent_boards_user_avatar($user->user_email, $name), |
| 716 | 775 | 'role' => $this->boardUserRole($boardRelation), |
| 717 | 776 | 'is_super' => in_array($user->ID, $superAdminIds), |
| @@ -800,20 +859,28 @@ | ||
| 800 | 859 | } |
| 801 | 860 | |
| 802 | 861 | public function addMembersInBoard(Request $request, $board_id) |
| 803 | 862 | { |
| 804 | - $memberId = $request->getSafe('memberId'); | |
| 805 | - $isViewerOnly = $request->getSafe('isViewerOnly'); | |
| 863 | + $memberId = $request->getSafe('memberId', 'intval'); | |
| 864 | + $isViewerOnly = $request->getSafe('isViewerOnly', 'sanitize_text_field'); | |
| 806 | 865 | $member = $this->boardService->addMembersInBoard($board_id, $memberId, $isViewerOnly); |
| 866 | + | |
| 867 | + if ($member === null) { | |
| 868 | + return $this->sendError([ | |
| 869 | + 'message' => __('User not found.', 'fluent-boards'), | |
| 870 | + ], 404); | |
| 871 | + } | |
| 872 | + | |
| 807 | 873 | if (!$member) { |
| 808 | 874 | return $this->sendError([ |
| 809 | 875 | 'message' => __('User already a member', 'fluent-boards'), |
| 810 | - ], 304); | |
| 876 | + ], 409); | |
| 811 | 877 | } |
| 812 | 878 | |
| 813 | 879 | |
| 814 | 880 | return [ |
| 815 | 881 | 'message' => __('Member added successfully', 'fluent-boards'), |
| 882 | + 'is_admin' => PermissionManager::isAdmin($memberId), | |
| 816 | 883 | 'member' => Helper::sanitizeUserCollections($member) |
| 817 | 884 | ]; |
| 818 | 885 | } |
| 819 | 886 | |
| @@ -932,9 +999,9 @@ | ||
| 932 | 999 | 'reset' => true, |
| 933 | 1000 | ]; |
| 934 | 1001 | } elseif ($request->image_url) { |
| 935 | 1002 | $backgroundData = $this->boardSanitizeAndValidate($request->all(), [ |
| 936 | - "id" => 'required', | |
| 1003 | + 'id' => 'required|integer', | |
| 937 | 1004 | 'image_url' => 'required|string|url', |
| 938 | 1005 | ]); |
| 939 | 1006 | } elseif ($request->color) { |
| 940 | 1007 | $backgroundData = $this->boardSanitizeAndValidate($request->all(), [ |
| @@ -1166,8 +1233,11 @@ | ||
| 1166 | 1233 | |
| 1167 | 1234 | return (bool) $permissionManager::currentUserCan('fcrm_read_contacts'); |
| 1168 | 1235 | } |
| 1169 | 1236 | |
| 1237 | + /** | |
| 1238 | + * Duplicate a board with the selected board data options. | |
| 1239 | + */ | |
| 1170 | 1240 | public function duplicateBoard(Request $request, $board_id) |
| 1171 | 1241 | { |
| 1172 | 1242 | $boardData = $this->taskSanitizeAndValidate($request->get('board'), [ |
| 1173 | 1243 | 'title' => 'required|string' |
| @@ -1176,8 +1246,10 @@ | ||
| 1176 | 1246 | $boardData['source_board_id'] = $board_id; |
| 1177 | 1247 | |
| 1178 | 1248 | $isWithLabels = $request->getSafe('isWithLabels'); |
| 1179 | 1249 | $isWithTasks = $request->getSafe('isWithTasks'); |
| 1250 | + $isWithMembers = $request->getSafe('isWithMembers'); | |
| 1251 | + $isWithCustomFields = $request->getSafe('isWithCustomFields', 'sanitize_text_field', 'yes'); | |
| 1180 | 1252 | $isWithTemplates = $request->getSafe('isWithTemplates'); |
| 1181 | 1253 | |
| 1182 | 1254 | try { |
| 1183 | 1255 | if(!PermissionManager::isAdmin()) { |
| @@ -1185,9 +1257,14 @@ | ||
| 1185 | 1257 | throw new \Exception(esc_html($errorMessage), 400); |
| 1186 | 1258 | } |
| 1187 | 1259 | //create board |
| 1188 | 1260 | $newBoard = $this->boardService->copyBoard($boardData); |
| 1261 | + $customFieldMap = []; | |
| 1189 | 1262 | |
| 1263 | + if ($isWithCustomFields == 'yes') { | |
| 1264 | + $customFieldMap = $this->boardService->copyCustomFieldsOfBoard($board_id, $newBoard); | |
| 1265 | + } | |
| 1266 | + | |
| 1190 | 1267 | //label copy |
| 1191 | 1268 | $labelMap = []; |
| 1192 | 1269 | |
| 1193 | 1270 | if ($isWithLabels == 'yes') { |
| @@ -1198,9 +1275,13 @@ | ||
| 1198 | 1275 | $stageMapForCopyingTask = $this->stageService->copyStagesOfBoard($newBoard, $board_id, $isWithTemplates); |
| 1199 | 1276 | |
| 1200 | 1277 | //copy tasks of selected stages |
| 1201 | 1278 | if ($isWithTasks == 'yes') { |
| 1202 | - $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap,$isWithTemplates); | |
| 1279 | + $this->taskService->copyTasks($board_id, $stageMapForCopyingTask, $newBoard, $labelMap, $isWithTemplates, $customFieldMap); | |
| 1280 | + } | |
| 1281 | + | |
| 1282 | + if ($isWithMembers == 'yes') { | |
| 1283 | + $this->boardService->copyMembersOfBoard($board_id, $newBoard); | |
| 1203 | 1284 | } |
| 1204 | 1285 | |
| 1205 | 1286 | return $this->sendSuccess([ |
| 1206 | 1287 | 'board' => $newBoard, |